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