1<?php
2/*
3 * e107 website system
4 *
5 * Copyright (C) 2008-2013 e107 Inc (e107.org)
6 * Released under the terms and conditions of the
7 * GNU General Public License (http://www.gnu.org/licenses/gpl.txt)
8 *
9 * e107 Mailout - mail database API and utility routines
10 *
11 * $URL: https://e107.svn.sourceforge.net/svnroot/e107/trunk/e107_0.8/e107_handlers/redirection_class.php $
12 * $Id: redirection_class.php 11922 2010-10-27 11:31:18Z secretr $
13 * $Revision: 12125 $
14*/
15
16/**
17 *
18 *	@package     e107
19 *	@subpackage	e107_handlers
20 *	@version 	$Id: mail_manager_class.php 12125 2011-04-08 05:11:38Z e107coders $;
21 *
22 *	@todo - consider whether to extract links in text-only emails
23 *	@todo - support separate template for the text part of emails
24
25This class isolates the caller from the underlying database used to buffer and send emails.
26Also includes a number of useful routines
27
28This is the 'day to day' module - there's an admin class which extends this one.
29
30There are two parts to the database:
31	a) Email body (including attachments etc)
32	b) Target recipients - potentially including target-specific values to substitute
33
34There is an option to override the style information sent if the email is to include
35theme-related information. Create file 'emailstyle.css' in the current theme directory, and this
36will be included in preference to the current theme style.
37
38
39
40Event Triggers generated
41------------------------
42	mailbounce - when an email bounce is received
43	maildone - when the sending of a complete bulk email is complete (also does 'Notify' event)
44
45
46Database tables
47---------------
48mail_recipients			- Details of individual recipients (targets) of an email
49	mail_target_id		Unique ID for this target/email combination
50	mail_recipient_id	User ID (if registered user), else zero
51	mail_recipient_email Email address of recipient
52	mail_recipient_name	Name of recipient
53	mail_status			Status of this  entry - see define() statements below
54	mail_detail_id		Email body link
55	mail_send_date		Earliest date/time when email may be sent. Once mail sent, actual time/date of sending (or time of failure to send)
56	mail_target_info	Array of target-specific info for substitution into email. Key is the code in the email body, value is the substitution
57
58mail_content			- Details of the email to be sent to a number of people
59	mail_source_id
60	mail_content_status	Overall status of mailshot record - See define() statements below
61	mail_togo_count		Number of recipients to go
62	mail_sent_count		Number of successful sends (including bounces)
63	mail_fail_count		Number of unsuccessful sends
64	mail_bounce_count	Number of bounced emails
65	mail_start_send		Time/date of sending first email
66	mail_end_send		Time/date of sending last email
67	mail_create_date
68	mail_creator		User ID
69	mail_create_app		ID string for application/plugin creating mail
70	mail_e107_priority	Our internal priority - generally high for single emails, low for bulk emails
71	mail_notify_complete Notify options when email complete
72	mail_last_date		Don't send after this date/time
73	mail_title			A description of the mailout - not sent
74	mail_subject		Subject line
75	mail_body			Body text - the 'raw' text as entered/specified by the user
76	mail_body_templated	Complete body text after applying the template, but before any variable substitutions
77	mail_other			Evaluates to an array of misc info - cc, bcc, attachments etc
78
79mail_other constituents:
80	mail_sender_email	Sender's email address
81	mail_sender_name	Sender's name
82	mail_copy_to		Any recipients to copy
83	mail_bcopy_to		Any recipients to BCC
84	mail_attach			Comma-separated list of attachments
85	mail_send_style		Send style -  HTML, text, template name etc
86	mail_selectors		Details of the selection criteria used for recipients (Only used internally)
87	mail_include_images	TRUE if to embed images, FALSE to add link to them
88	mail_body_alt		If non-empty, use for alternate email text (generally the 'plain text' alternative)
89	mail_overrides		If non-empty, any overrides for the mailer, set by the template
90
91
92
93Within internal arrays, a flat structure is adopted, with 'mail_other' merged with the rest of the 'mail_content' values.
94Variables relating to DB values all begin 'mail_' - others are internal (volatile) control variables
95
96*/
97
98if (!defined('e107_INIT')) { exit; }
99
100e107::includeLan(e_LANGUAGEDIR.e_LANGUAGE.'/admin/lan_mailout.php');		// May be needed by anything loading this class
101
102define('MAIL_STATUS_SENT', 0);			// Mail sent. Email handler happy, but may have bounced (or may be yet to bounce)
103define('MAIL_STATUS_BOUNCED', 1);
104define('MAIL_STATUS_CANCELLED', 2);
105define('MAIL_STATUS_PARTIAL', 3);		// A run which was abandoned - errors, out of time etc
106define('MAIL_STATUS_FAILED', 5);		// Failure on initial send - rejected by selected email handler
107										// This must be the numerically highest 'processing complete' code
108define('MAIL_STATUS_PENDING', 10);		// Mail which is in the sending list (even if outside valid sending window)
109										// This must be the numerically lowest 'not sent' code
110										// E107_EMAIL_MAX_TRIES values used in here for retry counting
111define('MAIL_STATUS_MAX_ACTIVE', 19);	// Highest allowable 'not sent or processed' code
112define('MAIL_STATUS_SAVED', 20);		// Identifies an email which is just saved (or in process of update)
113define('MAIL_STATUS_HELD',21);			// Held pending release
114define('MAIL_STATUS_TEMP', 22);			// Tags entries which aren't yet in any list
115
116
117class e107MailManager
118{
119	const	E107_EMAIL_PRIORITY_LOW = 1;		// 'E107' priorities, to determine what to do next.
120	const	E107_EMAIL_PRIORITY_MED = 3;		// Distinct from the priority which can be assigned to the...
121	const	E107_EMAIL_PRIORITY_HIGH = 5;		// actual email when sending. Use LOW or MED for bulk mail, HIGH for individual emails.
122
123	const	E107_EMAIL_MAX_TRIES = 3;			// Maximum number of tries by us (mail server may do more)
124												// - max allowable value is MAIL_STATUS_MAX_ACTIVE - MAIL_STATUS_PENDING
125
126	private		$debugMode = false;
127	protected	$e107;
128
129	/** @var e_db_pdo  */
130	protected	$db = NULL;					// Use our own database object - this one for reading data
131
132	/** @var e_db_pdo  */
133	protected	$db2 = NULL;				// Use our own database object - this one for updates
134	protected	$queryActive = FALSE;		// Keeps track of unused records in currently active query
135	protected	$mailCounters = array();	// Counters to track adding recipients
136	protected	$queryCount = array();		// Stores total number of records if SQL_CALC_ROWS is used (index = db object #)
137	protected	$currentBatchInfo = array();	// Used during batch send to hold info about current mailout
138	protected	$currentMailBody = '';			// Buffers current mail body
139	protected	$currentTextBody = '';			// Alternative text body (if required)
140
141	/** @var e107Email  */
142	protected	$mailer = NULL;				// Mailer class when required
143	protected	$mailOverrides = FALSE;		// Any overrides to be passed to the mailer
144
145
146	// Array defines DB types to be used
147	protected	$dbTypes = array(
148		'mail_recipients' => array
149		(
150			'mail_target_id'  	=> 'int',
151			'mail_recipient_id' => 'int',
152			'mail_recipient_email' => 'todb',
153			'mail_recipient_name' => 'todb',
154			'mail_status' 		=> 'int',
155			'mail_detail_id' 	=> 'int',
156			'mail_send_date' 	=> 'int',
157			'mail_target_info'	=> 'string'			// Don't want entities here!
158		),
159		'mail_content' => array(
160			'mail_source_id' 	=> 'int',
161			'mail_content_status' => 'int',
162			'mail_total_count' 	=> 'int',
163			'mail_togo_count' 	=> 'int',
164			'mail_sent_count' 	=> 'int',
165			'mail_fail_count' 	=> 'int',
166			'mail_bounce_count' => 'int',
167			'mail_start_send' 	=> 'int',
168			'mail_end_send' 	=> 'int',
169			'mail_create_date' 	=> 'int',
170			'mail_creator' 		=> 'int',
171			'mail_create_app' 	=> 'todb',
172			'mail_e107_priority' => 'int',
173			'mail_notify_complete' => 'int',
174			'mail_last_date' 	=> 'int',
175			'mail_title' 		=> 'todb',
176			'mail_subject' 		=> 'todb',
177			'mail_body' 		=> 'todb',
178			'mail_body_templated' => 'todb',
179			'mail_other' 		=> 'string',		// Don't want entities here!
180			'mail_media'        => 'string'
181		)
182	);
183
184	// Array defines defaults for 'NOT NULL' fields where a default can't be set in the field definition
185	protected	$dbNull = array('mail_recipients' => array
186		(
187			'mail_target_info' => ''
188		),
189		'mail_content' => array(
190			'mail_body' => '',
191			'mail_body_templated' => '',
192			'mail_other' => ''
193		)
194	);
195
196	// List of fields which are combined into the 'mail_other' field of the email
197	protected	$dbOther = array(
198					'mail_sender_email' => 1,
199					'mail_sender_name'	=> 1,
200					'mail_copy_to'		=> 1,
201					'mail_bcopy_to'		=> 1,
202					'mail_attach'		=> 1,
203					'mail_send_style'	=> 1,			// HTML, text, template name etc
204					'mail_selectors'	=> 1,			// Only used internally
205					'mail_include_images' => 1,			// Used to determine whether to embed images, or link to them
206					'mail_body_alt'		=> 1,			// If non-empty, use for alternate email text (generally the 'plain text' alternative)
207					'mail_overrides'	=> 1
208		);
209
210	// List of fields which are the status counts of an email, and their titles
211	protected	$mailCountFields = array(
212			'mail_togo_count' 	=> LAN_MAILOUT_83,
213			'mail_sent_count' 	=> LAN_MAILOUT_82,
214			'mail_fail_count' 	=> LAN_MAILOUT_128,
215			'mail_bounce_count' => LAN_MAILOUT_144,
216		);
217
218	/**
219	 * Constructor
220	 *
221	 *
222	 * @return void
223	 */
224	public function __construct($overrides = array())
225	{
226		$this->e107 = e107::getInstance();
227
228		$pref = e107::pref('core');
229
230		$bulkmailer = (!empty($pref['bulkmailer'])) ? $pref['bulkmailer'] : $pref['mailer'];
231
232	//	if($overrides === false)
233	//	{
234			$overrides['mailer'] = $bulkmailer;
235	//	}
236
237		$this->mailOverrides = $overrides;
238
239		if(deftrue('e_DEBUG_BULKMAIL'))
240		{
241			$this->debugMode = true;
242		}
243
244		if($this->debugMode === true)
245		{
246			e107::getMessage()->addWarning('Debug Mode is active. Emailing will only be simulated!');
247		}
248
249
250	}
251
252
253	/**
254	 * Generate an array of data which can be passed directly to the DB routines.
255	 * Only valid DB fields are copied
256	 * Combining/splitting of fields is done as necessary
257	 * (This is essentially the translation between internal storage format and db storage format. If
258	 * the DB format changes, only this routine and its counterpart should need changing)
259	 *
260	 * @param $data - array of email-related data in internal format
261	 * @param $addMissing - if TRUE, undefined fields are added
262	 *
263	 * @return void
264	 */
265	public function mailToDb(&$data, $addMissing = false)
266	{
267		$res = array();
268		$res1 = array();
269		// Generate the 'mail_other' array first
270		foreach ($this->dbOther as $f => $v)
271		{
272			if (isset($data[$f]))
273			{
274				$res1[$f] = $data[$f];
275			}
276			elseif ($addMissing)
277			{
278				$res1[$f] = '';
279			}
280		}
281
282		// Now do the main email array
283		foreach ($this->dbTypes['mail_content'] as $f => $v)
284		{
285			if (isset($data[$f]))
286			{
287				$res[$f] = $data[$f];
288			}
289			elseif ($addMissing)
290			{
291				$res[$f] = '';
292			}
293		}
294
295		$res['mail_other'] = e107::serialize($res1,false);	// Ready to write to DB
296
297		if (!empty($res['mail_media']))
298		{
299			$res['mail_media'] = e107::serialize($res['mail_media']);
300		}
301
302		return $res;
303	}
304
305
306	/**
307	 * Given an array (row) of data retrieved from the DB table, converts to internal format.
308	 * Combining/splitting of fields is done as necessary
309	 * (This is essentially the translation between internal storage format and db storage format. If
310	 * the DB format changes, only this routine and its counterpart should need changing)
311	 *
312	 * @param $data - array of DB-sourced email-related data
313	 * @param $addMissing - if TRUE, undefined fields are added
314	 *
315	 * @return array of data
316	 */
317	public function dbToMail(&$data, $addMissing = FALSE)
318	{
319		$res = array();
320
321		foreach ($this->dbTypes['mail_content'] as $f => $v)
322		{
323			if (isset($data[$f]))
324			{
325				$res[$f] = $data[$f];
326			}
327			elseif ($addMissing)
328			{
329				$res[$f] = '';
330			}
331		}
332		if (isset($data['mail_other']))
333		{
334
335			$tmp = e107::unserialize(str_replace('\\\'', '\'',$data['mail_other']));	// May have escaped data
336			if (is_array($tmp))
337			{
338				$res = array_merge($res,$tmp);
339			}
340			else
341			{
342				$res['Array_ERROR'] = 'No array found';
343			}
344			unset($res['mail_other']);
345		}
346		if ($addMissing)
347		{
348			foreach ($this->dbOther as $f => $v)
349			{
350				$res[$f] = '';
351			}
352		}
353
354		if (isset($data['mail_media']))
355		{
356			$res['mail_media'] = e107::unserialize($data['mail_media']);
357		}
358
359		return $res;
360	}
361
362
363
364	/**
365	 * Generate an array of mail recipient data which can be passed directly to the DB routines.
366	 * Only valid DB fields are copied
367	 * Combining/splitting of fields is done as necessary
368	 * (This is essentially the translation between internal storage format and db storage format. If
369	 * the DB format changes, only this routine and its counterpart should need changing)
370	 *
371	 * @param $data - array of email target-related data in internal format
372	 * @param $addMissing - if TRUE, undefined fields are added
373	 *
374	 * @return void
375	 */
376	public function targetToDb(&$data, $addMissing = FALSE)
377	{	// Direct correspondence at present (apart from needing to convert potential array $data['mail_target_info']) - but could change
378		$res = array();
379		foreach ($this->dbTypes['mail_recipients'] as $f => $v)
380		{
381			if (isset($data[$f]))
382			{
383				$res[$f] = $data[$f];
384			}
385			elseif ($addMissing)
386			{
387				$res[$f] = '';
388			}
389		}
390		if (isset($data['mail_target_info']) && is_array($data['mail_target_info']))
391		{
392			$tmp = e107::serialize($data['mail_target_info'], TRUE);
393			$res['mail_target_info'] = $tmp;
394		}
395		return $res;
396	}
397
398
399
400	/**
401	 * Given an array (row) of data retrieved from the DB table, converts to internal format.
402	 * Combining/splitting of fields is done as necessary
403	 * (This is essentially the translation between internal storage format and db storage format. If
404	 * the DB format changes, only this routine and its counterpart should need changing)
405	 *
406	 * @param $data - array of DB-sourced target-related data
407	 * @param $addMissing - if TRUE, undefined fields are added
408	 *
409	 * @return void
410	 */
411	public function dbToTarget(&$data, $addMissing = FALSE)
412	{	// Direct correspondence at present - but could change
413		$res = array();
414		foreach ($this->dbTypes['mail_recipients'] as $f => $v)
415		{
416			if (isset($data[$f]))
417			{
418				$res[$f] = $data[$f];
419			}
420			elseif ($addMissing)
421			{
422				$res[$f] = '';
423			}
424		}
425		if (isset($data['mail_target_info']))
426		{
427			$tmp = e107::unserialize($data['mail_target_info']);
428			$res['mail_target_info'] = $tmp;
429		}
430		return $res;
431	}
432
433
434
435
436	/**
437	 * Given an array (row) of data retrieved from the DB table, converts to internal format.
438	 * Combining/splitting of fields is done as necessary
439	 * This version intended for 'Joined' reads which have both recipient and content data
440	 *
441	 * @param $data - array of DB-sourced target-related data
442	 * @param $addMissing - if TRUE, undefined fields are added
443	 *
444	 * @return array
445	 */
446	public function dbToBoth(&$data, $addMissing = FALSE)
447	{
448		$res = array();
449		$oneToOne = array_merge($this->dbTypes['mail_content'], $this->dbTypes['mail_recipients']);		// List of valid elements
450
451
452		// Start with simple 'one to one' fields
453		foreach ($oneToOne as $f => $v)
454		{
455			if (isset($data[$f]))
456			{
457				$res[$f] = $data[$f];
458			}
459			elseif ($addMissing)
460			{
461				$res[$f] = '';
462			}
463		}
464
465		// Now array fields
466		if (isset($data['mail_other']))
467		{
468			$tmp = e107::unserialize(str_replace('\\\'', '\'',$data['mail_other']));	// May have escaped data
469			if (is_array($tmp))
470			{
471				$res = array_merge($res,$tmp);
472			}
473			unset($res['mail_other']);
474		}
475		elseif ($addMissing)
476		{
477			foreach ($this->dbOther as $f => $v)
478			{
479				$res[$f] = '';
480			}
481		}
482		if (isset($data['mail_target_info']))
483		{
484			$clean = stripslashes($data['mail_target_info']);
485			$tmp = e107::unserialize($clean);	// May have escaped data
486
487			$res['mail_target_info'] = $tmp;
488		}
489
490		if (isset($data['mail_media']))
491		{
492			$res['mail_media'] = e107::unserialize($data['mail_media']);
493		}
494
495		return $res;
496	}
497
498
499
500
501	/**
502	 * Set the internal debug/logging level
503	 *
504	 * @return void
505	 */
506	public function controlDebug($level = 0)
507	{
508		$this->debugMode = $level;
509	}
510
511
512
513	/**
514	 *	Internal function to create a db object for our use if none exists
515	 */
516	protected function checkDB($which = 1)
517	{
518		if (($which == 1) && ($this->db == null))
519		{
520			$this->db = e107::getDb('mail1');
521		}
522		if (($which == 2) && ($this->db2 == null))
523		{
524			$this->db2 = e107::getDb('mail2');;
525		}
526	}
527
528
529	/**
530	 * Internal function to create a mailer object for our use if none exists
531	 */
532	protected function checkMailer()
533	{
534		if ($this->mailer != NULL) return;
535		if (!class_exists('e107Email'))
536		{
537			require_once(e_HANDLER.'mail.php');
538		}
539		$this->mailer = new e107Email($this->mailOverrides);
540	}
541
542
543
544	/**
545	 *	Set the override values for the mailer object.
546	 *
547	 *	@param array $overrides - see mail.php for details of accepted values
548	 *
549	 *	@return boolean TRUE if accepted, FALSE if rejected
550	 */
551	public function setMailOverrides($overrides)
552	{
553		if ($this->mailer != NULL) return FALSE;		// Mailer already created - it's too late!
554		$this->mailOverrides = $overrides;
555	}
556
557
558
559
560	/**
561	 * Convert numeric representation of mail status to a text string
562	 *
563	 * @param integer $status - numeric value of status
564	 * @return string text value
565	 */
566	public function statusToText($status)
567	{
568		switch (intval($status))
569		{
570			case MAIL_STATUS_SENT :
571				return LAN_MAILOUT_211;
572			case MAIL_STATUS_BOUNCED :
573				return LAN_MAILOUT_213;
574			case MAIL_STATUS_CANCELLED :
575				return LAN_MAILOUT_218;
576			case MAIL_STATUS_PARTIAL :
577				return LAN_MAILOUT_219;
578			case MAIL_STATUS_FAILED :
579				return LAN_MAILOUT_212;
580			case MAIL_STATUS_PENDING :
581				return LAN_MAILOUT_214;
582			case MAIL_STATUS_SAVED :
583				return LAN_MAILOUT_215;
584			case MAIL_STATUS_HELD :
585				return LAN_MAILOUT_217;
586			default :
587				if (($status > MAIL_STATUS_PENDING) && ($status <= MAIL_STATUS_ACTIVE)) return LAN_MAILOUT_214;
588		}
589		return LAN_MAILOUT_216.' ('.$status.')';		// General coding error
590	}
591
592
593
594	/**
595	 * Select the next $count emails in the send queue
596	 * $count gives the maximum number. '*' does 'select all'
597	 * @return boolean|handle Returns FALSE on error.
598	 * 		 Returns a 'handle' on success (actually the ID in the DB of the email)
599	 */
600	public function selectEmails($count = 1)
601	{
602		if (is_numeric($count))
603		{
604			if ($count < 1) $count = 1;
605			$count = ' LIMIT '.$count;
606		}
607		else
608		{
609			$count = '';
610		}
611		$this->checkDB(1);			// Make sure DB object created
612		$query = "SELECT mt.*, ms.* FROM `#mail_recipients` AS mt
613							LEFT JOIN `#mail_content` AS ms ON mt.`mail_detail_id` = ms.`mail_source_id`
614							WHERE ms.`mail_content_status` = ".MAIL_STATUS_PENDING."
615							AND mt.`mail_status` >= ".MAIL_STATUS_PENDING."
616							AND mt.`mail_status` <= ".MAIL_STATUS_MAX_ACTIVE."
617							AND mt.`mail_send_date` <= ".time()."
618							AND (ms.`mail_last_date` >= ".time()." OR ms.`mail_last_date`=0)
619							ORDER BY ms.`mail_e107_priority` DESC, mt.mail_target_id ASC {$count}";
620//		echo $query.'<br />';
621		$result = $this->db->gen($query);
622
623		if ($result !== FALSE)
624		{
625			$this->queryActive = $result;		// Note number of emails to go
626		}
627		return $result;
628	}
629
630
631	/**
632	 * Get next email from selection (usually from selectEmails() )
633	 * @return Returns array of email data if available - FALSE if no further data, no active query, or other error
634	 */
635	public function getNextEmail()
636	{
637		if (!$this->queryActive)
638		{
639			return false;
640		}
641		if ($result = $this->db->fetch())
642		{
643			$this->queryActive--;
644			return $this->dbToBoth($result);
645		}
646		else
647		{
648			$this->queryActive = false;		// Make sure no further attempts to read emails
649			return false;
650		}
651	}
652
653
654	/**
655	 * Call to see whether any emails left to try in current selection
656	 * @return Returns number left unread in query - FALSE if no active query
657	 */
658	public function emailsToGo()
659	{
660		return $this->queryActive;			// Just return saved number
661	}
662
663
664	/**
665	 * Call to send next email from selection
666	 *
667	 * @return Returns TRUE if successful, FALSE on fail (or no more to go)
668	 *
669	 *	@todo Could maybe save parsed page in cache if more than one email to go
670	 */
671	public function sendNextEmail()
672	{
673		$counterList = array('mail_source_id','mail_togo_count', 'mail_sent_count', 'mail_fail_count', 'mail_start_send');
674
675		if (($email = $this->getNextEmail()) === false)
676		{
677			return false;
678		}
679
680
681		/**
682		 *	The $email variable has all the email data in 'flat' form, including that of the current recipient.
683		 *	field $email['mail_target_info'] has variable substitution information relating to the current recipient
684		 */
685		if (count($this->currentBatchInfo))
686		{
687			//print_a($this->currentBatchInfo);
688			if ($this->currentBatchInfo['mail_source_id'] != $email['mail_source_id'])
689			{	// New email body etc started
690				//echo "New email body: {$this->currentBatchInfo['mail_source_id']} != {$email['mail_source_id']}<br />";
691				$this->currentBatchInfo = array();		// New source email - clear stored info
692				$this->currentMailBody = '';			// ...and clear cache for message body
693				$this->currentTextBody = '';
694			}
695		}
696		if (count($this->currentBatchInfo) == 0)
697		{
698			//echo "First email of batch: {$email['mail_source_id']}<br />";
699			foreach ($counterList as $k)
700			{
701				$this->currentBatchInfo[$k] = $email[$k];		// This copies across all the counts
702			}
703		}
704
705		if (($this->currentBatchInfo['mail_sent_count'] > 0) || ($this->currentBatchInfo['mail_fail_count'] > 0))
706		{	// Only send these on first email - otherwise someone could get inundated!
707			unset($email['mail_copy_to']);
708			unset($email['mail_bcopy_to']);
709		}
710
711		$targetData = array();		// Arrays for updated data
712
713		$this->checkMailer();		// Make sure we have a mailer object to play with
714
715		if ($this->currentBatchInfo['mail_start_send'] == 0)
716		{
717			$this->currentBatchInfo['mail_start_send'] = time();			// Log when we started processing this email
718		}
719
720		if (!$this->currentMailBody)
721		{
722			if (!empty($email['mail_body_templated']))
723			{
724				$this->currentMailBody = $email['mail_body_templated'];
725			}
726			else
727			{
728				$this->currentMailBody = $email['mail_body'];
729			}
730
731			$this->currentTextBody = $email['mail_body_alt'];		// May be null
732		}
733
734
735		$mailToSend = $this->makeEmailBlock($email);			// Substitute mail-specific variables, attachments etc
736
737
738
739		if($this->debugMode)
740		{
741
742			echo "<h3>Preview</h3>";
743			$preview = $this->mailer->preview($mailToSend);
744			echo $preview;
745			echo "<h3>Preview (HTML)</h3>";
746			print_a($preview);
747			$logName = "mailout_simulation_".$email['mail_source_id'];
748			e107::getLog()->addDebug("Sending Email to <".$email['mail_recipient_name']."> ".$email['mail_recipient_email'])->toFile($logName,'Mailout Simulation Log',true);
749			$result = true;
750
751
752			$this->mailer->setDebug(true);
753			echo "<h2>SendEmail()->Body</h2>";
754			print_a($this->mailer->Body);
755			echo "<h2>SendEmail()->AltBody</h2>";
756			print_a($this->mailer->AltBody);
757			echo "<h1>_________________________________________________________________________</h1>";
758			return;
759
760
761		}
762
763
764		$result = $this->mailer->sendEmail($email['mail_recipient_email'], $email['mail_recipient_name'], $mailToSend, TRUE);
765
766
767		if($this->debugMode)
768		{
769			return true;
770		}
771
772		// Try and send
773
774
775//		return;			// ************************************************** Temporarily stop DB being updated when line active *****************************
776
777		$addons = array_keys($email['mail_selectors']); // trigger e_mailout.php addons. 'sent' method.
778
779		foreach($addons as $plug)
780		{
781			if($plug === 'core')
782			{
783				continue;
784			}
785
786			if($cls = e107::getAddon($plug,'e_mailout'))
787			{
788				$email['status'] = $result;
789
790				if(e107::callMethod($cls, 'sent', $email) === false)
791				{
792					e107::getAdminLog()->add($plug.' sent process failed', $email, E_LOG_FATAL, 'SENT');
793				}
794			}
795		}
796		// --------------------------
797
798
799
800		$this->checkDB(2);			// Make sure DB object created
801
802		// Now update email status in DB. We just create new arrays of changed data
803		if ($result === TRUE)
804		{	// Success!
805			$targetData['mail_status'] = MAIL_STATUS_SENT;
806			$targetData['mail_send_date'] = time();
807			$this->currentBatchInfo['mail_togo_count']--;
808			$this->currentBatchInfo['mail_sent_count']++;
809		}
810		else
811		{	// Failure
812		// If fail and still retries, downgrade priority
813			if ($targetData['mail_status'] > MAIL_STATUS_PENDING)
814			{
815				$targetData['mail_status'] = max($targetData['mail_status'] - 1, MAIL_STATUS_PENDING);		// One off retry count
816				$targetData['mail_e107_priority'] = max($email['mail_e107_priority'] - 1, 1); 	// Downgrade priority to avoid clag-ups
817			}
818			else
819			{
820				$targetData['mail_status'] = MAIL_STATUS_FAILED;
821				$this->currentBatchInfo['mail_togo_count'] = max($this->currentBatchInfo['mail_togo_count'] - 1, 0);
822				$this->currentBatchInfo['mail_fail_count']++;
823				$targetData['mail_send_date'] = time();
824			}
825		}
826
827		if (isset($this->currentBatchInfo['mail_togo_count']) && ($this->currentBatchInfo['mail_togo_count'] == 0))
828		{
829			$this->currentBatchInfo['mail_end_send'] = time();
830			$this->currentBatchInfo['mail_content_status'] = MAIL_STATUS_SENT;
831		}
832
833		// Update DB record, mail record with status (if changed). Must use different sql object
834		if (count($targetData))
835		{
836			//print_a($targetData);
837			$this->db2->update('mail_recipients', array('data' => $targetData, '_FIELD_TYPES' => $this->dbTypes['mail_recipients'], 'WHERE' => '`mail_target_id` = '.intval($email['mail_target_id'])));
838		}
839
840		if (count($this->currentBatchInfo))
841		{
842			//print_a($this->currentBatchInfo);
843			$this->db2->update('mail_content', array('data' => $this->currentBatchInfo,
844														'_FIELD_TYPES' => $this->dbTypes['mail_content'],
845														'WHERE' => '`mail_source_id` = '.intval($email['mail_source_id'])));
846		}
847
848		if (($this->currentBatchInfo['mail_togo_count'] == 0) && ($email['mail_notify_complete'] > 0)) // Need to notify completion
849		{
850			$email = array_merge($email, $this->currentBatchInfo);		// This should ensure the counters are up to date
851			$mailInfo = LAN_MAILOUT_247.'<br />'.LAN_TITLE.': '.$email['mail_title'].'<br />'.LAN_MAILOUT_248.$this->statusToText($email['mail_content_status']).'<br />';
852			$mailInfo .= '<br />'.LAN_MAILOUT_249.'<br />';
853			foreach ($this->mailCountFields as $f => $t)
854			{
855				$mailInfo .= $t.' => '.$email[$f].'<br />';
856			}
857			$mailInfo .= LAN_MAILOUT_250;
858			$message = array(				// Use same structure for email and notify
859					'mail_subject' => LAN_MAILOUT_244.$email['mail_subject'],
860					'mail_body' => $mailInfo.'<br />'
861				);
862
863			if ($email['mail_notify_complete'] & 1) // Notify email initiator
864			{
865				if ($this->db2->select('user', 'user_name, user_email', '`user_id`='.intval($email['mail_creator'])))
866				{
867					$row = $this->db2->fetch();
868					e107::getEmail()->sendEmail($row['user_name'], $row['user_email'], $message,FALSE);
869				}
870			}
871			if ($email['mail_notify_complete'] & 2) // Do e107 notify
872			{
873				require_once(e_HANDLER."notify_class.php");
874			//	notify_maildone($message); // FIXME
875			}
876			e107::getEvent()->trigger('maildone', $email);
877		}
878
879		return $result;
880	}
881
882
883
884	/**
885	 *	Given an email block, creates an array of data compatible with PHPMailer, including any necessary substitutions
886	 * $eml['subject']
887		$eml['sender_email']	- 'From' email address
888		$eml['sender_name']		- 'From' name
889		$eml['replyto']			- Optional 'reply to' field
890		$eml['replytonames']	- Name(s) corresponding to 'reply to' field  - only used if 'replyto' used
891		$eml['send_html']		- if TRUE, includes HTML part in messages (only those added after this flag)
892		$eml['add_html_header'] - if TRUE, adds the 2-line DOCTYPE declaration to the front of the HTML part (but doesn't add <head>...</head>)
893		$eml['body']			- message body. May be HTML or text. Added according to the current state of the HTML enable flag
894		$eml['attach']			- string if one file, array of filenames if one or more.
895		$eml['copy_to']			- comma-separated list of cc addresses.
896		$eml['cc_names']  		- comma-separated list of cc names. Optional, used only if $eml['copy_to'] specified
897		$eml['bcopy_to']		- comma-separated list
898		$eml['bcc_names'] 		- comma-separated list of bcc names. Optional, used only if $eml['copy_to'] specified
899		$eml['bouncepath']		- Sender field (used for bounces)
900		$eml['returnreceipt']	- email address for notification of receipt (reading)
901		$eml['inline_images']	- array of files for inline images
902		$eml['priority']		- Email priority (1 = High, 3 = Normal, 5 = low)
903		$eml['e107_header']		- Adds specific 'X-e107-id:' header
904		$eml['extra_header']	- additional headers (format is name: value
905		$eml['wordwrap']		- Set wordwrap value
906		$eml['split']			- If true, sends an individual email to each recipient
907		$eml['template']		- template to use. 'default'
908		$eml['shortcodes']		- array of shortcode values. eg. array('MY_SHORTCODE'=>'12345');
909	 */
910	protected function makeEmailBlock($email)
911	{
912		$mailSubsInfo = array(
913	 		'subject'       => 'mail_subject',
914			'sender_email'  => 'mail_sender_email',
915			'sender_name'   => 'mail_sender_name',
916			// 'email_replyto'		- Optional 'reply to' field
917			// 'email_replytonames'	- Name(s) corresponding to 'reply to' field  - only used if 'replyto' used
918			'copy_to'	    => 'mail_copy_to', 		// - comma-separated list of cc addresses.
919			//'email_cc_names' - comma-separated list of cc names. Optional, used only if $eml['email_copy_to'] specified
920			'bcopy_to'      => 'mail_bcopy_to',
921			// 'email_bcc_names' - comma-separated list of bcc names. Optional, used only if $eml['email_copy_to'] specified
922			//'bouncepath'		- Sender field (used for bounces)
923			//'returnreceipt'	- email address for notification of receipt (reading)
924			//'email_inline_images'	- array of files for inline images
925			//'priority'		- Email priority (1 = High, 3 = Normal, 5 = low)
926			//'extra_header'	- additional headers (format is name: value
927			//'wordwrap'		- Set wordwrap value
928			//'split'			- If true, sends an individual email to each recipient
929			'template'		=> 'mail_send_style', // required
930			'shortcodes'	=> 'mail_target_info', // required
931			'e107_header'   => 'mail_recipient_id'
932
933			);
934
935
936
937
938		$result = array();
939
940
941		if (!isset($email['mail_source_id'])) $email['mail_source_id'] = 0;
942		if (!isset($email['mail_target_id'])) $email['mail_target_id'] = 0;
943		if (!isset($email['mail_recipient_id'])) $email['mail_recipient_id'] = 0;
944
945
946
947
948
949		foreach ($mailSubsInfo as $k => $v)
950		{
951			if (isset($email[$v]))
952			{
953				$result[$k] = $email[$v];
954				//unset($email[$v]);
955			}
956		}
957
958
959		// Do any substitutions
960		$search = array();
961		$replace = array();
962		foreach ($email['mail_target_info'] as $k => $v)
963		{
964			$search[] = '|'.$k.'|';
965			$replace[] = $v;
966		}
967
968		$result['email_body'] = str_replace($search, $replace, $this->currentMailBody);
969
970		if ($this->currentTextBody)
971		{
972			$result['mail_body_alt'] = str_replace($search, $replace, $this->currentTextBody);
973		}
974
975		$result['send_html'] = ($email['mail_send_style'] != 'textonly');
976		$result['add_html_header'] = FALSE;				// We look after our own headers
977
978
979
980		// Set up any extra mailer parameters that need it
981		if (!vartrue($email['e107_header']))
982		{
983			$temp = intval($email['mail_recipient_id']).'/'.intval($email['mail_source_id']).'/'.intval($email['mail_target_id']).'/';
984			$result['e107_header'] = $temp.md5($temp);		// Set up an ID
985		}
986
987		if (isset($email['mail_attach']) && (trim($email['mail_attach']) || is_array($email['mail_attach'])))
988		{
989			$tp = e107::getParser();
990
991			if (is_array($email['mail_attach']))
992			{
993				foreach ($email['mail_attach'] as $k => $v)
994				{
995					$result['email_attach'][$k] = $tp->replaceConstants($v);
996				}
997			}
998			else
999			{
1000				$result['email_attach'] = $tp->replaceConstants(trim($email['mail_attach']));
1001			}
1002		}
1003
1004		if (isset($email['mail_overrides']) && is_array($email['mail_overrides']))
1005		{
1006			 $result = array_merge($result, $email['mail_overrides']);
1007		}
1008
1009	//	$title = "<h4>".__METHOD__." Line: ".__LINE__."</h4>";
1010	//	e107::getAdminLog()->addDebug($title.print_a($email,true),true);
1011
1012		if(!empty($email['mail_media']))
1013		{
1014			$result['media'] = $email['mail_media'];
1015		}
1016
1017	//	$title2 = "<h4>".__METHOD__." Line: ".__LINE__."</h4>";
1018	//	e107::getAdminLog()->addDebug($title2.print_a($result,true),true);
1019
1020		$result['shortcodes']['MAILREF'] = $email['mail_source_id'];
1021
1022		if($this->debugMode)
1023		{
1024			echo "<h3>makeEmailBlock() : Incoming</h3>";
1025			print_a($email);
1026
1027			echo "<h3>makeEmailBlock(): Outgoing</h3>";
1028			print_a($result);
1029		}
1030
1031		return $result;
1032	}
1033
1034
1035
1036	/**
1037	 * Call to do a number of 'units' of email processing - from a cron job, for example
1038	 * Each 'unit' sends one email from the queue - potentially it could do some other task.
1039	 * @param $limit - number of units of work to do - zero to clear the queue (or do maximum allowed by a hard-coded limit)
1040	 * @param $pauseCount - pause after so many emails
1041	 * @param $pauseTime - time in seconds to pause after 'pauseCount' number of emails.
1042	 * @return None
1043	 */
1044	public function doEmailTask($limit = 0, $pauseCount=null, $pauseTime=1)
1045	{
1046		if ($count = $this->selectEmails($limit))
1047		{
1048			$c=1;
1049			while ($count > 0)
1050			{
1051				$this->sendNextEmail();
1052				$count--;
1053
1054				if(!empty($pauseCount) && ($c === $pauseCount))
1055				{
1056					sleep($pauseTime);
1057					$c=1;
1058				}
1059
1060			}
1061			if ($this->mailer)
1062			{
1063				$this->mailer->allSent();		// Tidy up on completion
1064			}
1065		}
1066		else
1067		{
1068
1069			// e107::getAdminLog()->addDebug("Couldn't select emails", true);
1070		}
1071	}
1072
1073
1074
1075	/**
1076	 * Saves an email to the DB
1077	 * @param $emailData
1078	 * @param $isNew - TRUE if a new email, FALSE if editing
1079	 *
1080	 *
1081	 * @return mail ID for success, FALSE on error
1082	 */
1083	public function saveEmail($emailData, $isNew = FALSE)
1084	{
1085		$this->checkDB(2);						// Make sure we have a DB object to use
1086
1087		$dbData = $this->mailToDb($emailData, FALSE);		// Convert array formats
1088	//	print_a($dbData);
1089
1090
1091		if ($isNew === true)
1092		{
1093			unset($dbData['mail_source_id']);				// Just in case - there are circumstances where might be set
1094			$result = $this->db2->insert('mail_content', array('data' => $dbData,
1095														'_FIELD_TYPES' => $this->dbTypes['mail_content'], 												'_NOTNULL' => $this->dbNull['mail_content']));
1096		}
1097		else
1098		{
1099			if (isset($dbData['mail_source_id']))
1100			{
1101				$result = $this->db2->update('mail_content', array('data' => $dbData,
1102																	'_FIELD_TYPES' => $this->dbTypes['mail_content'],
1103																	'WHERE' => '`mail_source_id` = '.intval($dbData['mail_source_id'])));
1104				if ($result !== FALSE) { $result = $dbData['mail_source_id']; }
1105			}
1106			else
1107			{
1108				echo "Programming bungle! No mail_source_id in function saveEmail()<br />";
1109				$result = FALSE;
1110			}
1111		}
1112		return $result;
1113	}
1114
1115
1116	/**
1117	 * Retrieve an email from the DB
1118	 * @param $mailID - number for email (assumed to be integral)
1119	 * @param $addMissing - if TRUE, any unset fields are added
1120	 *
1121	 * @return FALSE on error. Array of data on success.
1122	 */
1123	public function retrieveEmail($mailID, $addMissing = FALSE)
1124	{
1125		if (!is_numeric($mailID) || ($mailID == 0))
1126		{
1127			return FALSE;
1128		}
1129		$this->checkDB(2);						// Make sure we have a DB object to use
1130		if ($this->db2->select('mail_content', '*', '`mail_source_id`='.$mailID) === FALSE)
1131		{
1132			return FALSE;
1133		}
1134		$mailData = $this->db2->fetch();
1135		return $this->dbToMail($mailData, $addMissing);				// Convert to 'flat array' format
1136	}
1137
1138
1139	/**
1140	 * Delete an email from the DB, including (potential) recipients
1141	 * @param $mailID - number for email (assumed to be integral)
1142	 * @param $actions - allows selection of which DB to delete from
1143	 *
1144	 * @return FALSE on code error. Array of results on success.
1145	 */
1146	public function deleteEmail($mailID, $actions='all')
1147	{
1148		$result = array();
1149		if ($actions == 'all') $actions = 'content,recipients';
1150		$actArray = explode(',', $actions);
1151
1152		if (!is_numeric($mailID) || ($mailID == 0))
1153		{
1154			return FALSE;
1155		}
1156
1157		$this->checkDB(2);						// Make sure we have a DB object to use
1158
1159		if (isset($actArray['content']))
1160		{
1161			$result['content'] = $this->db2->delete('mail_content', '`mail_source_id`='.$mailID);
1162		}
1163		if (isset($actArray['recipients']))
1164		{
1165			$result['recipients'] = $this->db2->delete('mail_recipients', '`mail_detail_id`='.$mailID);
1166		}
1167
1168		return $result;
1169	}
1170
1171
1172
1173	/**
1174	 * Initialise a set of counters prior to adding
1175	 * @param $handle - as returned by makeEmail()
1176	 * @return none
1177	 */
1178	public function mailInitCounters($handle)
1179	{
1180		$this->mailCounters[$handle] = array('add' => 0, 'dups' => 0, 'dberr' => 0);
1181	}
1182
1183
1184
1185	/**
1186	 * Add a recipient to the DB, provide that email not already on the list.
1187	 * @param $handle - as returned by makeEmail()
1188	 * @param $mailRecip is an array of relevant info
1189	 * @param $priority - 'E107' priority for email (different to the priority included in the email)
1190	 * @return mixed - FALSE if error
1191	 *                 'dup' if duplicate of existing email
1192	 *                 integer - number of email recipient in DB
1193	 */
1194	public function mailAddNoDup($handle, $mailRecip, $initStatus = MAIL_STATUS_TEMP, $priority = self::E107_EMAIL_PRIORITY_LOW)
1195	{
1196
1197		if (($handle <= 0) || !is_numeric($handle)) return FALSE;
1198		if (!isset($this->mailCounters[$handle])) return 'nocounter';
1199
1200		$this->checkDB(1);			// Make sure DB object created
1201
1202		if(empty($mailRecip['mail_recipient_email']))
1203		{
1204			e107::getMessage()->addError("Empty Recipient Email");
1205			return false;
1206		}
1207
1208
1209		$result = $this->db->select('mail_recipients', 'mail_target_id', "`mail_detail_id`={$handle} AND `mail_recipient_email`='{$mailRecip['mail_recipient_email']}'");
1210
1211
1212		if ($result === false)
1213		{
1214			return false;
1215		}
1216		elseif ($result != 0)
1217		{
1218			$this->mailCounters[$handle]['dups']++;
1219			return 'dup';
1220		}
1221		$mailRecip['mail_status'] = $initStatus;
1222		$mailRecip['mail_detail_id'] = $handle;
1223		$mailRecip['mail_send_date'] = time();
1224
1225		$data = $this->targetToDb($mailRecip);
1226							// Convert internal types
1227		if ($this->db->insert('mail_recipients', array('data' => $data, '_FIELD_TYPES' => $this->dbTypes['mail_recipients'])))
1228		{
1229			$this->mailCounters[$handle]['add']++;
1230		}
1231		else
1232		{
1233			$this->mailCounters[$handle]['dberr']++;
1234			return FALSE;
1235		}
1236	}
1237
1238
1239	/**
1240	 * Update the mail record with the number of recipients as per counters
1241	 * @param $handle - as returned by makeEmail()
1242	 * @return mixed - FALSE if error
1243	 *					- number set into counter if success
1244	 */
1245	public function mailUpdateCounters($handle)
1246	{
1247		if (($handle <= 0) || !is_numeric($handle)) return FALSE;
1248		if (!isset($this->mailCounters[$handle])) return 'nocounter';
1249		$this->checkDB(2);			// Make sure DB object created
1250
1251
1252
1253
1254
1255		$query = '`mail_togo_count`='.intval($this->mailCounters[$handle]['add']).' WHERE `mail_source_id`='.$handle;
1256		if ($this->db2->db_Update('mail_content', $query))
1257		{
1258			return $this->mailCounters[$handle]['add'];
1259		}
1260		return FALSE;
1261	}
1262
1263
1264	public function updateCounter($id, $type, $count)
1265	{
1266		if(empty($id) || empty($type))
1267		{
1268			return false;
1269		}
1270
1271		$update = array(
1272			'mail_'.$type.'_count'	=> intval($count),
1273			'WHERE'					=> "mail_source_id=".intval($id)
1274		);
1275
1276		return e107::getDb('mail')->update('mail_content', $update) ? $count : false;
1277	}
1278
1279
1280
1281	/**
1282	 * Retrieve the counters for a mail record
1283	 * @param $handle - as returned by makeEmail()
1284	 * @return boolean - FALSE if error
1285	 *					- array of counters if success
1286	 */
1287	public function mailRetrieveCounters($handle)
1288	{
1289		if (isset($this->mailCounters[$handle]))
1290		{
1291			return $this->mailCounters[$handle];
1292		}
1293		return FALSE;
1294	}
1295
1296
1297
1298	/**
1299	 * Update status for email, including all recipient entries (called once all recipients added)
1300	 * @param int $handle - as returned by makeEmail()
1301	 * @param $hold boolean - TRUE to set status to held, false to release for sending
1302	 * @param $notify - value to set in the mail_notify_complete field:
1303	 *			0 - no action on run complete
1304	 *			1 - notify admin who sent email only
1305	 *			2 - notify through e107 notify system only
1306	 *			3 - notify both
1307	 * @param $firstTime int - only valid if $hold === FALSE - earliest time/date when email may be sent
1308	 * @param $lastTime int - only valid if $hold === FALSE - latest time/date when email may be sent
1309	 * @return boolean TRUE on no errors, FALSE on errors
1310	 */
1311	public function activateEmail($handle, $hold = FALSE, $notify = 0, $firstTime = 0, $lastTime = 0)
1312	{
1313		if (($handle <= 0) || !is_numeric($handle)) return FALSE;
1314		$this->checkDB(1);			// Make sure DB object created
1315		$ft = '';
1316		$lt = '';
1317		if (!$hold)
1318		{		// Sending email - set sensible first and last times
1319			if ($lastTime < (time() + 3600))				// Force at least an hour to send emails
1320			{
1321				if ($firstTime < time())
1322				{
1323					$lastTime = time() + 86400;			// Standard delay - 24 hours
1324				}
1325				else
1326				{
1327					$lastTime = $firstTime + 86400;
1328				}
1329			}
1330			if ($firstTime > 0) $ft = ', `mail_send_date` = '.$firstTime;
1331			$lt = ', `mail_end_send` = '.$lastTime;
1332		}
1333		$query = '';
1334		if (!$hold) $query = '`mail_creator` = '.USERID.', `mail_create_date` = '.time().', ';		// Update when we send - might be someone different
1335		$query .= '`mail_notify_complete`='.intval($notify).', `mail_content_status` = '.($hold ? MAIL_STATUS_HELD : MAIL_STATUS_PENDING).$lt.' WHERE `mail_source_id` = '.intval($handle);
1336		//	echo "Update mail body: {$query}<br />";
1337		// Set status of email body first
1338
1339		if (!$this->db->update('mail_content',$query))
1340		{
1341			e107::getLog()->addEvent(10,-1,'MAIL','Activate/hold mail','mail_content: '.$query.'[!br!]Fail: '.$this->db->getLastErrorText(),FALSE,LOG_TO_ROLLING);
1342			return FALSE;
1343		}
1344
1345		// Now set status of individual emails
1346		$query = '`mail_status` = '.($hold ? MAIL_STATUS_HELD : (MAIL_STATUS_PENDING + e107MailManager::E107_EMAIL_MAX_TRIES)).$ft.' WHERE `mail_detail_id` = '.intval($handle);
1347		//	echo "Update individual emails: {$query}<br />";
1348		if (FALSE === $this->db->update('mail_recipients',$query))
1349		{
1350			e107::getLog()->addEvent(10,-1,'MAIL','Activate/hold mail','mail_recipient: '.$query.'[!br!]Fail: '.$this->db->getLastErrorText(),FALSE,LOG_TO_ROLLING);
1351			return FALSE;
1352		}
1353		return TRUE;
1354	}
1355
1356
1357	/**
1358	 * Cancel sending of an email, including marking all unsent recipient entries
1359	 * $handle - as returned by makeEmail()
1360	 * @return boolean - TRUE on success, FALSE on failure
1361	 */
1362	public function cancelEmail($handle)
1363	{
1364		if (($handle <= 0) || !is_numeric($handle)) return FALSE;
1365		$this->checkDB(1);			// Make sure DB object created
1366		// Set status of individual emails first, so we can get a count
1367		if (FALSE === ($count = $this->db->update('mail_recipients','`mail_status` = '.MAIL_STATUS_CANCELLED.' WHERE `mail_detail_id` = '.intval($handle).' AND `mail_status` >'.MAIL_STATUS_FAILED)))
1368		{
1369			return FALSE;
1370		}
1371		// Now do status of email body - no emails to go, add those not sent to fail count
1372		if (!$this->db->update('mail_content','`mail_content_status` = '.MAIL_STATUS_PARTIAL.', `mail_togo_count`=0, `mail_fail_count` = `mail_fail_count` + '.intval($count).' WHERE `mail_source_id` = '.intval($handle)))
1373		{
1374			return FALSE;
1375		}
1376		return TRUE;
1377	}
1378
1379
1380	/**
1381	 * Put email on hold, including marking all unsent recipient entries
1382	 * @param integer $handle - as returned by makeEmail()
1383	 * @return boolean - TRUE on success, FALSE on failure
1384	 */
1385	public function holdEmail($handle)
1386	{
1387		if (($handle <= 0) || !is_numeric($handle)) return FALSE;
1388		$this->checkDB(1);			// Make sure DB object created
1389		// Set status of individual emails first, so we can get a count
1390		if (FALSE === ($count = $this->db->update('mail_recipients','`mail_status` = '.MAIL_STATUS_HELD.' WHERE `mail_detail_id` = '.intval($handle).' AND `mail_status` >'.MAIL_STATUS_FAILED)))
1391		{
1392			return FALSE;
1393		}
1394		if ($count == 0) return TRUE;		// If zero count, must have held email just as queue being emptied, so don't touch main status
1395
1396		if (!$this->db->update('mail_content','`mail_content_status` = '.MAIL_STATUS_HELD.' WHERE `mail_source_id` = '.intval($handle)))
1397		{
1398			return FALSE;
1399		}
1400		return TRUE;
1401	}
1402
1403
1404	/**
1405	 * Handle a bounce report.
1406	 * @param string $bounceString - the string from header X-e107-id
1407	 * @param string $emailAddress - optional email address string for checks
1408	 * @return boolean - TRUE on success, FALSE on failure
1409	 */
1410	public function markBounce($bounceString, $emailAddress = '')
1411	{
1412
1413		$bounceString = trim($bounceString);
1414
1415		$bounceInfo 	= array('mail_bounce_string' => $bounceString, 'mail_recipient_email' => $emailAddress);		// Ready for event data
1416		$errors 		= array();						// Log all errors, at least until proven
1417		$vals 			= explode('/', $bounceString);		// Should get one or four fields
1418
1419		if($this->debugMode)
1420		{
1421			echo "<h4>Bounce String</h4>";
1422			print_a($bounceString);
1423			echo "<h4>Vals</h4>";
1424			print_a($vals);
1425		}
1426
1427		if (!is_numeric($vals[0])) 				// Email recipient user id number (may be zero)
1428		{
1429			$errors[] = 'Bad user ID: '.$vals[0];
1430		}
1431
1432		$uid = intval($vals[0]);				// User ID (zero is valid)
1433
1434		if (count($vals) == 4) // Admin->Mailout format.
1435		{
1436
1437			if (!is_numeric($vals[1])) 		// Email body record number
1438			{
1439				$errors[] = 'Bad body record: '.$vals[1];
1440			}
1441
1442			if (!is_numeric($vals[2])) 		// Email recipient table record number
1443			{
1444				$errors[] = 'Bad recipient record: '.$vals[2];
1445			}
1446
1447			$vals[0] = intval($vals[0]);
1448			$vals[1] = intval($vals[1]);
1449			$vals[2] = intval($vals[2]);
1450			$vals[3] = trim($vals[3]);
1451
1452
1453			$hash = ($vals[0].'/'.$vals[1].'/'.$vals[2].'/');
1454
1455			if (md5($hash) != $vals[3]) // 'Extended' ID has md5 validation
1456			{
1457				$errors[] = 'Bad md5';
1458				$errors[] = print_r($vals,true);
1459				$errors[] = 'hash:'.md5($hash);
1460			}
1461
1462			if (empty($errors))
1463			{
1464				$this->checkDB(1); // Look up in mailer DB if no errors so far
1465
1466				if (false === ($this->db->gen(
1467					"SELECT mr.`mail_recipient_id`, mr.`mail_recipient_email`, mr.`mail_recipient_name`, mr.mail_target_info,
1468					mc.mail_create_date, mc.mail_start_send, mc.mail_end_send, mc.`mail_title`, mc.`mail_subject`, mc.`mail_creator`, mc.`mail_other` FROM `#mail_recipients` AS mr
1469					LEFT JOIN `#mail_content` as mc ON mr.`mail_detail_id` = mc.`mail_source_id`
1470						WHERE mr.`mail_target_id` = {$vals[2]} AND mc.`mail_source_id` = {$vals[1]}")))
1471				{	// Invalid mailer record
1472					$errors[] = 'Not found in DB: '.$vals[1].'/'.$vals[2];
1473				}
1474
1475				$row = $this->db->fetch();
1476
1477				$row = $this->dbToBoth($row);
1478
1479				$bounceInfo = $row;
1480
1481				if ($emailAddress && ($emailAddress != $row['mail_recipient_email'])) // Email address mismatch
1482				{
1483					$errors[] = 'Email address mismatch: '.$emailAddress.'/'.$row['mail_recipient_email'];
1484				}
1485
1486				if ($uid != $row['mail_recipient_id']) 	// User ID mismatch
1487				{
1488					$errors[] = 'User ID mismatch: '.$uid.'/'.$row['mail_recipient_id'];
1489				}
1490
1491				if (count($errors) == 0) // All passed - can update mailout databases
1492				{
1493					$bounceInfo['mail_source_id'] 		= $vals[1];
1494					$bounceInfo['mail_target_id'] 		= $vals[2];
1495					$bounceInfo['mail_recipient_id'] 	= $uid;
1496					$bounceInfo['mail_recipient_name'] 	= $row['mail_recipient_name'];
1497
1498
1499					if(!$this->db->update('mail_content', '`mail_bounce_count` = `mail_bounce_count` + 1 WHERE `mail_source_id` = '.$vals[1]))
1500					{
1501						e107::getAdminLog()->add('Unable to increment bounce-count on mail_source_id='.$vals[1],$bounceInfo, E_LOG_FATAL, 'BOUNCE', LOG_TO_ROLLING);
1502					}
1503
1504
1505					if(!$this->db->update('mail_recipients', '`mail_status` = '.MAIL_STATUS_BOUNCED.' WHERE `mail_target_id` = '.$vals[2]))
1506					{
1507						e107::getAdminLog()->add('Unable to update recipient mail_status to bounce on mail_target_id = '.$vals[2],$bounceInfo, E_LOG_FATAL, 'BOUNCE', LOG_TO_ROLLING);
1508					}
1509
1510					$addons = array_keys($row['mail_selectors']); // trigger e_mailout.php addons. 'bounce' method.
1511					foreach($addons as $plug)
1512					{
1513						if($plug == 'core')
1514						{
1515							if($err = e107::getUserSession()->userStatusUpdate('bounce', $uid, $emailAddress));
1516							{
1517								$errors[] = $err;
1518							}
1519
1520						}
1521						else
1522						{
1523							if($cls = e107::getAddon($plug,'e_mailout'))
1524							{
1525								if(e107::callMethod($cls, 'bounce', $bounceInfo)===false)
1526								{
1527									e107::getAdminLog()->add($plug.' bounce process failed',$bounceInfo, E_LOG_FATAL, 'BOUNCE',LOG_TO_ROLLING);
1528								}
1529							}
1530
1531						}
1532					}
1533				}
1534
1535
1536			//	echo e107::getMessage()->render();
1537			//	print_a($bounceInfo);
1538
1539
1540			}
1541		}
1542		elseif ((count($vals) != 1) && (count($vals) != 4)) // invalid e107-id header.
1543		{
1544			$errors[] = 'Bad element count: '.count($vals);
1545		}
1546		elseif (!empty($uid) || !empty($emailAddress)) // Update the user table for user_id = $uid;
1547		{
1548			// require_once(e_HANDLER.'user_handler.php');
1549			$err = e107::getUserSession()->userStatusUpdate('bounce', $uid, $emailAddress);
1550			if($err)
1551			{
1552				$errors[] = $err;
1553			}
1554		}
1555
1556		if (!empty($errors))
1557		{
1558			$logErrors =$bounceInfo;
1559			$logErrors['user_id'] = $uid;
1560			$logErrors['mailshot'] = $vals[1];
1561			$logErrors['mailshot_recipient'] = $vals[2];
1562			$logErrors['errors'] = $errors;
1563			$logErrors['email'] = $emailAddress;
1564			$logErrors['bounceString'] = $bounceString;
1565			$logString = $bounceString.' ('.$emailAddress.')[!br!]'.implode('[!br!]',$errors).implode('[!br!]',$bounceInfo);
1566		//	e107::getAdminLog()->e_log_event(10,-1,'BOUNCE','Bounce receive error',$logString, FALSE,LOG_TO_ROLLING);
1567			e107::getAdminLog()->add('Bounce receive error',$logErrors, E_LOG_WARNING, 'BOUNCE', LOG_TO_ROLLING);
1568			return $errors;
1569		}
1570		else
1571		{
1572			//	e107::getAdminLog()->e_log_event(10,-1,'BOUNCE','Bounce received/logged',$bounceInfo, FALSE,LOG_TO_ROLLING);
1573			e107::getAdminLog()->add('Bounce received/logged',$bounceInfo, E_LOG_INFORMATIVE, 'BOUNCE',LOG_TO_ROLLING);
1574		}
1575
1576
1577		e107::getEvent()->trigger('mailbounce', $bounceInfo);
1578
1579		return false;
1580	}
1581
1582
1583
1584	/**
1585	 * Does a query to select one or more emails for which status is required.
1586	 * @param $start - sets the offset of the first email to return based on the search criteria
1587	 * @param $count - sets the maximum number of emails to return
1588	 * @param $fields - allows selection of which db fields are returned in each result
1589	 * @param $filters - array contains filter/selection criteria - basically setting limits on each field
1590	 * @return Returns number of records found (maximum $count); FALSE on error
1591	 */
1592	public function selectEmailStatus($start = 0, $count = 0, $fields = '*', $filters = FALSE, $orderField = 'mail_source_id', $sortOrder = 'asc')
1593	{
1594		$this->checkDB(1);			// Make sure DB object created
1595		if (!is_array($filters) && $filters)
1596		{	// Assume a textual email type
1597			switch ($filters)
1598			{
1599				case 'pending' :
1600					$filters = array('`mail_content_status` = '.MAIL_STATUS_PENDING);
1601					break;
1602				case 'held' :
1603					$filters = array('`mail_content_status` = '.MAIL_STATUS_HELD);
1604					break;
1605				case 'pendingheld' :
1606					$filters = array('((`mail_content_status` = '.MAIL_STATUS_PENDING.') OR (`mail_content_status` = '.MAIL_STATUS_HELD.'))');
1607					break;
1608				case 'sent' :
1609					$filters = array('`mail_content_status` = '.MAIL_STATUS_SENT);
1610					break;
1611				case 'allcomplete' :
1612					$filters = array('((`mail_content_status` = '.MAIL_STATUS_SENT.') OR (`mail_content_status` = '.MAIL_STATUS_PARTIAL.') OR (`mail_content_status` = '.MAIL_STATUS_CANCELLED.'))');
1613					break;
1614				case 'failed' :
1615					$filters = array('`mail_content_status` = '.MAIL_STATUS_FAILED);
1616					break;
1617				case 'saved' :
1618					$filters = array('`mail_content_status` = '.MAIL_STATUS_SAVED);
1619					break;
1620			}
1621		}
1622		if (!is_array($filters))
1623		{
1624			$filters = array();
1625		}
1626		$query = "SELECT SQL_CALC_FOUND_ROWS {$fields} FROM `#mail_content`";
1627		if (count($filters))
1628		{
1629			$query .= ' WHERE '.implode (' AND ', $filters);
1630		}
1631		if ($orderField)
1632		{
1633			$query .= " ORDER BY `{$orderField}`";
1634		}
1635		if ($sortOrder)
1636		{
1637			$sortOrder = strtoupper($sortOrder);
1638			$query .= ($sortOrder == 'DESC') ? ' DESC' : ' ASC';
1639		}
1640		if ($count)
1641		{
1642			$query .= " LIMIT {$start}, {$count}";
1643		}
1644		//echo "{$start}, {$count} Mail query: {$query}<br />";
1645		$result = $this->db->gen($query);
1646		if ($result !== FALSE)
1647		{
1648			$this->queryCount[1] = $this->db->total_results;			// Save number of records found
1649		}
1650		else
1651		{
1652			$this->queryCount[1] = 0;
1653		}
1654		return $result;
1655	}
1656
1657
1658	/**
1659	 * Returns the total number of records matching the search done in the most recent call to selectEmailStatus()
1660	 * @return integer - number of emails matching criteria
1661	 */
1662	public function getEmailCount()
1663	{
1664		return $this->queryCount[1];
1665	}
1666
1667
1668
1669	/**
1670	 * Returns the detail of the next email which satisfies the query done in selectEmailStatus()
1671	 * @return bool Returns an array of data relating to a single email if available (in 'flat' format). FALSE on no data or error
1672	 */
1673	public function getNextEmailStatus()
1674	{
1675		$result = $this->db->db_Fetch();
1676		if (is_array($result)) { return $this->dbToMail($result); }
1677		return FALSE;
1678	}
1679
1680
1681
1682	/**
1683	 * Does a query to select from the list of email targets which have been used
1684	 * @param $start - sets the offset of the first email to return based on the search criteria
1685	 * @param $count - sets the maximum number of emails to return
1686	 * @param $fields - allows selection of which db fields are returned in each result
1687	 * @param $filters - array contains filter/selection criteria
1688	 *				'handle=nn' picks out a specific email
1689	 * @return Returns number of records found; FALSE on error
1690	 */
1691	public function selectTargetStatus($handle, $start = 0, $count = 0, $fields = '*', $filters = FALSE, $orderField = 'mail_target_id', $sortOrder = 'asc')
1692	{
1693		$handle = intval($handle);
1694		if ($filters === FALSE) { $filters = array(); }		// Might not need this line
1695
1696		$this->checkDB(2);			// Make sure DB object created
1697
1698		// TODO: Implement filters if needed
1699		$query = "SELECT SQL_CALC_FOUND_ROWS {$fields} FROM `#mail_recipients` WHERE `mail_detail_id`={$handle}";
1700		if ($orderField)
1701		{
1702			$query .= " ORDER BY `{$orderField}`";
1703		}
1704		if ($sortOrder)
1705		{
1706			$sortOrder = strtoupper($sortOrder);
1707			$query .= ($sortOrder == 'DESC') ? ' DESC' : ' ASC';
1708		}
1709		if ($count)
1710		{
1711			$query .= " LIMIT {$start}, {$count}";
1712		}
1713//		echo "{$start}, {$count} Target query: {$query}<br />";
1714		$result = $this->db2->gen($query);
1715		if ($result !== FALSE)
1716		{
1717			$this->queryCount[2] = $this->db2->total_results;			// Save number of records found
1718		}
1719		else
1720		{
1721			$this->queryCount[2] = 0;
1722		}
1723//		echo "Result: {$result}.  Total: {$this->queryCount[2]}<br />";
1724		return $result;
1725	}
1726
1727
1728	/**
1729	 * Returns the total number of records matching the search done in the most recent call to selectTargetStatus()
1730	 * @return integer - number of emails matching criteria
1731	 */
1732	public function getTargetCount()
1733	{
1734		return $this->queryCount[2];
1735	}
1736
1737
1738
1739	/**
1740	 * Returns the detail of the next recipient which satisfies the query done in selectTargetStatus()
1741	 * @return Returns an array of data relating to a single email if available (in 'flat' format). FALSE on no data or error
1742	 */
1743	public function getNextTargetStatus()
1744	{
1745		$result = $this->db2->db_Fetch();
1746		if (is_array($result)) { return $this->dbToTarget($result); }
1747		return FALSE;
1748	}
1749
1750
1751
1752//-----------------------------------------------------
1753//		Function call to send a templated email
1754//-----------------------------------------------------
1755
1756/**
1757 *	Send an email to any number of recipients, using a template
1758 *
1759 *	The template may contain normal shortcodes, which must already have been loaded. @see e107_themes/email_template.php
1760 *
1761 *	The template (or other body text) may also contain field names in the form |USER_NAME| (as used in the bulk mailer edit page). These are
1762 *	filled in from $templateData - field name corresponds to the array index name (case-sensitive)
1763 *
1764 *	The template definition may contain an array $template['email_overrides'] of values which override normal mailer settings.
1765 *
1766 *	The template definition MUST contain a template variable $template['email_body']
1767 *
1768 *	In general, any template definition which isn't overridden uses the default which should be specified in e_THEME.'templates/email_templates.php'
1769 *
1770 *	There is a presumption that the email is being templated because it contains HTML, although this isn't mandatory.
1771 *
1772 *	Any language string constants required in the template must be defined either by loading the requisite language file prior to calling this
1773 *	routine, or by loading them in the template file.
1774 *
1775 *	@param array|string $templateName - if a string, the name of the template - information is loaded from theme and default templates.
1776 *					- if an array, template data as returned by gettemplateInfo() (and defined in the template files)
1777 *			- if empty, sends a simple email using the default template (much as the original sendemail() function in mail.php)
1778 *	@param array $emailData - defines the email information (generally as the 'mail_content' and 'mail_other' info above):
1779 *					$emailData = array(
1780						'mail_create_app' => 'notify',
1781						'mail_title' => 'NOTIFY',
1782						'mail_subject' => $subject,
1783						'mail_sender_email' => $pref['siteadminemail'],
1784						'mail_sender_name'	=> $pref['siteadmin'],
1785						'mail_send_style'	=> 'textonly',
1786						'mail_notify_complete' => 0,			// NEVER notify when this email sent!!!!!
1787						'mail_body' => $message
1788					);
1789 *	@param array|string $recipientData - if a string, its the email address of a single recipient.
1790 *		- if an array, each entry is the data for a single recipient, as the 'mail_recipients' definition above
1791 *									$recipientData = array('mail_recipient_id' => $row['user_id'],
1792											 'mail_recipient_name' => $row['user_name'],
1793											 'mail_recipient_email' => $row['user_email']
1794											 );
1795 *						....and other data as appropriate
1796 *	@param boolean|array $options - any additional parameters to be passed to the mailer - as accepted by arraySet method.
1797 *			These parameters will override any defaults, and any set in the template
1798 *	if ($options['mail_force_queue'] is TRUE, the mail will be added to the queue regardless of the number of recipients
1799 *
1800 *	@return boolean TRUE if either added to queue, or sent, successfully (does NOT indicate receipt). FALSE on any error
1801 *		(Note that with a small number of recipients FALSE indicates that one or more emails weren't sent - some may have been sent successfully)
1802 */
1803
1804	public function sendEmails($templateName, $emailData, $recipientData, $options = false)
1805	{
1806		$log = e107::getAdminLog();
1807		$log->addDebug(print_r($emailData, true),true);
1808		$log->addDebug(print_r($recipientData, true),true);
1809		$log->toFile('mail_manager','Mail Manager Log', true);
1810
1811		if (!is_array($emailData))
1812		{
1813			return false;
1814		}
1815
1816		if (!is_array($recipientData))
1817		{
1818			$recipientData = array(array('mail_recipient_email' => $recipientData, 'mail_recipient_name' => $recipientData));
1819		}
1820
1821		$emailData['mail_content_status'] = MAIL_STATUS_TEMP;
1822
1823		if ($templateName == '')
1824		{
1825			$templateName = varset($emailData['mail_send_style'], 'textonly');		// Safest default if nothing specified
1826		}
1827
1828		$templateName = trim($templateName);
1829		if ($templateName == '') return false;
1830
1831		$this->currentMailBody 				= $emailData['mail_body'];			// In case we send immediately
1832		$this->currentTextBody 				= strip_tags($emailData['mail_body']);
1833
1834		//		$emailData['mail_body_templated'] 	= $ourTemplate->mainBodyText;
1835		//		$emailData['mail_body_alt'] 		= $ourTemplate->altBodyText;
1836
1837		if (!isset($emailData['mail_overrides']))
1838		{
1839			// $emailData['mail_overrides'] = $ourTemplate->lastTemplateData['email_overrides'];
1840		}
1841
1842		if(!empty($emailData['template'])) // Quick Fix for new email template standards.
1843		{
1844			$this->currentMailBody = $emailData['mail_body'];
1845			unset($emailData['mail_body_templated']);
1846
1847			if($this->debugMode)
1848			{
1849				echo "<h4>".$emailData['template']." Template detected</h4>";
1850			}
1851		}
1852
1853
1854		if (is_array($options) && isset($options['mail_force_queue']))
1855		{
1856			$forceQueue = $options['mail_force_queue'];
1857			unset($options['mail_force_queue']);
1858		}
1859
1860		if($this->debugMode)
1861		{
1862
1863			echo "<h4>".__CLASS__." :: ".__METHOD__." - Line ".__LINE__."</h4>";
1864			print_a($emailData);
1865			print_a($recipientData);
1866
1867		}
1868
1869		if ((count($recipientData) <= 5) && !$forceQueue)	// Arbitrary upper limit for sending multiple emails immediately
1870		{
1871			if ($this->mailer == NULL)
1872			{
1873				e107_require_once(e_HANDLER.'mail.php');
1874				$this->mailer = new e107Email($options);
1875			}
1876			$tempResult = TRUE;
1877			$eCount = 0;
1878
1879			// @TODO: Generate alt text etc
1880
1881
1882
1883			foreach ($recipientData as $recip)
1884			{
1885				// Fill in other bits of email
1886			//	$emailData['mail_target_info'] = $recip	;
1887				$merged     = array_merge($emailData,$recip);
1888				$mailToSend = $this->makeEmailBlock($merged);			// Substitute mail-specific variables, attachments etc
1889/*
1890				echo "<h2>MERGED</h2>";
1891				print_a($merged);
1892				echo "<h2>RETURNED</h2>";
1893				print_a($mailToSend);
1894				echo "<hr />";
1895				continue;
1896
1897		*/
1898				if (false == $this->mailer->sendEmail($recip['mail_recipient_email'], $recip['mail_recipient_name'], $mailToSend, true))
1899				{
1900					$tempResult = FALSE;
1901					if($this->debugMode)
1902					{
1903						echo "<h4>Failed to send to: ".$recip['mail_recipient_email']." [". $recip['mail_recipient_name'] ."]</h4>";
1904						print_a($mailToSend);
1905					}
1906				}
1907				else
1908				{	// Success here
1909					if($this->debugMode)
1910					{
1911						echo "<h4>Mail Sent successfully to: ".$recip['mail_recipient_email']." [". $recip['mail_recipient_name'] ."]</h4>";
1912						print_a($mailToSend);
1913					}
1914					if ($eCount == 0)
1915					{	// Only send these on first email - otherwise someone could get inundated!
1916						unset($emailData['mail_copy_to']);
1917						unset($emailData['mail_bcopy_to']);
1918					}
1919					$eCount++;		// Count number of successful emails sent
1920				}
1921			}
1922			return $tempResult;
1923		}
1924
1925
1926		// ----------- Too many recipients to send at once - add to the emailing queue ---------------- //
1927
1928
1929		// @TODO - handle any other relevant $options fields
1930		$emailData['mail_total_count'] = count($recipientData);
1931
1932		$result = $this->saveEmail($emailData, TRUE);
1933
1934		if ($result === FALSE)
1935		{
1936			// TODO: Handle error
1937			return FALSE;			// Probably nothing else we can do
1938		}
1939		elseif (is_numeric($result))
1940		{
1941			$mailMainID = $emailData['mail_source_id'] = $result;
1942		}
1943		else
1944		{
1945			// TODO: Handle strange error
1946			return FALSE;			// Probably nothing else we can do
1947		}
1948		$this->mailInitCounters($mailMainID);			// Initialise counters for emails added
1949
1950		// Now add email addresses to the list
1951		foreach ($recipientData as $email)
1952		{
1953			$result = $this->mailAddNoDup($mailMainID, $email, MAIL_STATUS_TEMP);
1954		}
1955		$this->mailUpdateCounters($mailMainID);			// Update the counters
1956		$counters = $this->mailRetrieveCounters($mailMainID);		// Retrieve the counters
1957		if ($counters['add'] == 0)
1958		{
1959			$this->deleteEmail($mailMainID);			// Probably a fault, but precautionary - delete email
1960			// Don't treat as an error if no recipients
1961		}
1962		else
1963		{
1964			$this->activateEmail($mailMainID, FALSE);					// Actually mark the email for sending
1965		}
1966		return TRUE;
1967	}
1968
1969}
1970
1971
1972