1<?php 2/** 3 * A class to build and send multi part mails (with HTML content and embedded 4 * attachments). All mails are assumed to be in UTF-8 encoding. 5 * 6 * Attachments are handled in memory so this shouldn't be used to send huge 7 * files, but then again mail shouldn't be used to send huge files either. 8 * 9 * @author Andreas Gohr <andi@splitbrain.org> 10 */ 11use dokuwiki\Utf8\PhpString; 12use dokuwiki\Utf8\Clean; 13use dokuwiki\Extension\Event; 14 15/** 16 * Mail Handling 17 */ 18class Mailer 19{ 20 21 protected $headers = []; 22 protected $attach = []; 23 protected $html = ''; 24 protected $text = ''; 25 26 protected $boundary = ''; 27 protected $partid = ''; 28 protected $sendparam; 29 30 protected $allowhtml = true; 31 32 protected $replacements = ['text'=> [], 'html' => []]; 33 34 /** 35 * Constructor 36 * 37 * Initializes the boundary strings, part counters and token replacements 38 */ 39 public function __construct() 40 { 41 global $conf; 42 /* @var Input $INPUT */ 43 global $INPUT; 44 45 $server = parse_url(DOKU_URL, PHP_URL_HOST); 46 if(strpos($server, '.') === false) $server .= '.localhost'; 47 48 $this->partid = substr(md5(uniqid(random_int(0, mt_getrandmax()), true)), 0, 8).'@'.$server; 49 $this->boundary = '__________'.md5(uniqid(random_int(0, mt_getrandmax()), true)); 50 51 $listid = implode('.', array_reverse(explode('/', DOKU_BASE))).$server; 52 $listid = strtolower(trim($listid, '.')); 53 54 $messageid = uniqid(random_int(0, mt_getrandmax()), true) . "@$server"; 55 56 $this->allowhtml = (bool)$conf['htmlmail']; 57 58 // add some default headers for mailfiltering FS#2247 59 if(!empty($conf['mailreturnpath'])) { 60 $this->setHeader('Return-Path', $conf['mailreturnpath']); 61 } 62 $this->setHeader('X-Mailer', 'DokuWiki'); 63 $this->setHeader('X-DokuWiki-User', $INPUT->server->str('REMOTE_USER')); 64 $this->setHeader('X-DokuWiki-Title', $conf['title']); 65 $this->setHeader('X-DokuWiki-Server', $server); 66 $this->setHeader('X-Auto-Response-Suppress', 'OOF'); 67 $this->setHeader('List-Id', $conf['title'].' <'.$listid.'>'); 68 $this->setHeader('Date', date('r'), false); 69 $this->setHeader('Message-Id', "<$messageid>"); 70 71 $this->prepareTokenReplacements(); 72 } 73 74 /** 75 * Attach a file 76 * 77 * @param string $path Path to the file to attach 78 * @param string $mime Mimetype of the attached file 79 * @param string $name The filename to use 80 * @param string $embed Unique key to reference this file from the HTML part 81 */ 82 public function attachFile($path, $mime, $name = '', $embed = '') 83 { 84 if(!$name) { 85 $name = PhpString::basename($path); 86 } 87 88 $this->attach[] = [ 89 'data' => file_get_contents($path), 90 'mime' => $mime, 91 'name' => $name, 92 'embed' => $embed 93 ]; 94 } 95 96 /** 97 * Attach a file 98 * 99 * @param string $data The file contents to attach 100 * @param string $mime Mimetype of the attached file 101 * @param string $name The filename to use 102 * @param string $embed Unique key to reference this file from the HTML part 103 */ 104 public function attachContent($data, $mime, $name = '', $embed = '') 105 { 106 if(!$name) { 107 [, $ext] = explode('/', $mime); 108 $name = count($this->attach).".$ext"; 109 } 110 111 $this->attach[] = [ 112 'data' => $data, 113 'mime' => $mime, 114 'name' => $name, 115 'embed' => $embed 116 ]; 117 } 118 119 /** 120 * Callback function to automatically embed images referenced in HTML templates 121 * 122 * @param array $matches 123 * @return string placeholder 124 */ 125 protected function autoEmbedCallBack($matches) 126 { 127 static $embeds = 0; 128 $embeds++; 129 130 // get file and mime type 131 $media = cleanID($matches[1]); 132 [, $mime] = mimetype($media); 133 $file = mediaFN($media); 134 if(!file_exists($file)) return $matches[0]; //bad reference, keep as is 135 136 // attach it and set placeholder 137 $this->attachFile($file, $mime, '', 'autoembed'.$embeds); 138 return '%%autoembed'.$embeds.'%%'; 139 } 140 141 /** 142 * Add an arbitrary header to the mail 143 * 144 * If an empy value is passed, the header is removed 145 * 146 * @param string $header the header name (no trailing colon!) 147 * @param string|string[] $value the value of the header 148 * @param bool $clean remove all non-ASCII chars and line feeds? 149 */ 150 public function setHeader($header, $value, $clean = true) 151 { 152 $header = str_replace(' ', '-', ucwords(strtolower(str_replace('-', ' ', $header)))); // streamline casing 153 if($clean) { 154 $header = preg_replace('/[^a-zA-Z0-9_ \-\.\+\@]+/', '', $header); 155 $value = preg_replace('/[^a-zA-Z0-9_ \-\.\+\@<>]+/', '', $value); 156 } 157 158 // empty value deletes 159 if(is_array($value)){ 160 $value = array_map('trim', $value); 161 $value = array_filter($value); 162 if(!$value) $value = ''; 163 }else{ 164 $value = trim($value); 165 } 166 if($value === '') { 167 if(isset($this->headers[$header])) unset($this->headers[$header]); 168 } else { 169 $this->headers[$header] = $value; 170 } 171 } 172 173 /** 174 * Set additional parameters to be passed to sendmail 175 * 176 * Whatever is set here is directly passed to PHP's mail() command as last 177 * parameter. Depending on the PHP setup this might break mailing alltogether 178 * 179 * @param string $param 180 */ 181 public function setParameters($param) 182 { 183 $this->sendparam = $param; 184 } 185 186 /** 187 * Set the text and HTML body and apply replacements 188 * 189 * This function applies a whole bunch of default replacements in addition 190 * to the ones specified as parameters 191 * 192 * If you pass the HTML part or HTML replacements yourself you have to make 193 * sure you encode all HTML special chars correctly 194 * 195 * @param string $text plain text body 196 * @param array $textrep replacements to apply on the text part 197 * @param array $htmlrep replacements to apply on the HTML part, null to use $textrep (urls wrapped in <a> tags) 198 * @param string $html the HTML body, leave null to create it from $text 199 * @param bool $wrap wrap the HTML in the default header/Footer 200 */ 201 public function setBody($text, $textrep = null, $htmlrep = null, $html = null, $wrap = true) 202 { 203 204 $htmlrep = (array)$htmlrep; 205 $textrep = (array)$textrep; 206 207 // create HTML from text if not given 208 if($html === null) { 209 $html = $text; 210 $html = hsc($html); 211 $html = preg_replace('/^----+$/m', '<hr >', $html); 212 $html = nl2br($html); 213 } 214 if($wrap) { 215 $wrapper = rawLocale('mailwrap', 'html'); 216 $html = preg_replace('/\n-- <br \/>.*$/s', '', $html); //strip signature 217 $html = str_replace('@EMAILSIGNATURE@', '', $html); //strip @EMAILSIGNATURE@ 218 $html = str_replace('@HTMLBODY@', $html, $wrapper); 219 } 220 221 if(strpos($text, '@EMAILSIGNATURE@') === false) { 222 $text .= '@EMAILSIGNATURE@'; 223 } 224 225 // copy over all replacements missing for HTML (autolink URLs) 226 foreach($textrep as $key => $value) { 227 if(isset($htmlrep[$key])) continue; 228 if(media_isexternal($value)) { 229 $htmlrep[$key] = '<a href="'.hsc($value).'">'.hsc($value).'</a>'; 230 } else { 231 $htmlrep[$key] = hsc($value); 232 } 233 } 234 235 // embed media from templates 236 $html = preg_replace_callback( 237 '/@MEDIA\(([^\)]+)\)@/', 238 [$this, 'autoEmbedCallBack'], $html 239 ); 240 241 // add default token replacements 242 $trep = array_merge($this->replacements['text'], $textrep); 243 $hrep = array_merge($this->replacements['html'], $htmlrep); 244 245 // Apply replacements 246 foreach($trep as $key => $substitution) { 247 $text = str_replace('@'.strtoupper($key).'@', $substitution, $text); 248 } 249 foreach($hrep as $key => $substitution) { 250 $html = str_replace('@'.strtoupper($key).'@', $substitution, $html); 251 } 252 253 $this->setHTML($html); 254 $this->setText($text); 255 } 256 257 /** 258 * Set the HTML part of the mail 259 * 260 * Placeholders can be used to reference embedded attachments 261 * 262 * You probably want to use setBody() instead 263 * 264 * @param string $html 265 */ 266 public function setHTML($html) 267 { 268 $this->html = $html; 269 } 270 271 /** 272 * Set the plain text part of the mail 273 * 274 * You probably want to use setBody() instead 275 * 276 * @param string $text 277 */ 278 public function setText($text) 279 { 280 $this->text = $text; 281 } 282 283 /** 284 * Add the To: recipients 285 * 286 * @see cleanAddress 287 * @param string|string[] $address Multiple adresses separated by commas or as array 288 */ 289 public function to($address) 290 { 291 $this->setHeader('To', $address, false); 292 } 293 294 /** 295 * Add the Cc: recipients 296 * 297 * @see cleanAddress 298 * @param string|string[] $address Multiple adresses separated by commas or as array 299 */ 300 public function cc($address) 301 { 302 $this->setHeader('Cc', $address, false); 303 } 304 305 /** 306 * Add the Bcc: recipients 307 * 308 * @see cleanAddress 309 * @param string|string[] $address Multiple adresses separated by commas or as array 310 */ 311 public function bcc($address) 312 { 313 $this->setHeader('Bcc', $address, false); 314 } 315 316 /** 317 * Add the From: address 318 * 319 * This is set to $conf['mailfrom'] when not specified so you shouldn't need 320 * to call this function 321 * 322 * @see cleanAddress 323 * @param string $address from address 324 */ 325 public function from($address) 326 { 327 $this->setHeader('From', $address, false); 328 } 329 330 /** 331 * Add the mail's Subject: header 332 * 333 * @param string $subject the mail subject 334 */ 335 public function subject($subject) 336 { 337 $this->headers['Subject'] = $subject; 338 } 339 340 /** 341 * Return a clean name which can be safely used in mail address 342 * fields. That means the name will be enclosed in '"' if it includes 343 * a '"' or a ','. Also a '"' will be escaped as '\"'. 344 * 345 * @param string $name the name to clean-up 346 * @see cleanAddress 347 */ 348 public function getCleanName($name) 349 { 350 $name = trim($name, " \t\""); 351 $name = str_replace('"', '\"', $name, $count); 352 if ($count > 0 || strpos($name, ',') !== false) { 353 $name = '"'.$name.'"'; 354 } 355 return $name; 356 } 357 358 /** 359 * Sets an email address header with correct encoding 360 * 361 * Unicode characters will be deaccented and encoded base64 362 * for headers. Addresses may not contain Non-ASCII data! 363 * 364 * If @$addresses is a string then it will be split into multiple 365 * addresses. Addresses must be separated by a comma. If the display 366 * name includes a comma then it MUST be properly enclosed by '"' to 367 * prevent spliting at the wrong point. 368 * 369 * Example: 370 * cc("föö <foo@bar.com>, me@somewhere.com","TBcc"); 371 * to("foo, Dr." <foo@bar.com>, me@somewhere.com"); 372 * 373 * @param string|string[] $addresses Multiple adresses separated by commas or as array 374 * @return false|string the prepared header (can contain multiple lines) 375 */ 376 public function cleanAddress($addresses) 377 { 378 $headers = ''; 379 if(!is_array($addresses)){ 380 $count = preg_match_all('/\s*(?:("[^"]*"[^,]+),*)|([^,]+)\s*,*/', $addresses, $matches, PREG_SET_ORDER); 381 $addresses = []; 382 if ($count !== false && is_array($matches)) { 383 foreach ($matches as $match) { 384 $addresses[] = rtrim($match[0], ','); 385 } 386 } 387 } 388 389 foreach($addresses as $part) { 390 $part = preg_replace('/[\r\n\0]+/', ' ', $part); // remove attack vectors 391 $part = trim($part); 392 393 // parse address 394 if(preg_match('#(.*?)<(.*?)>#', $part, $matches)) { 395 $text = trim($matches[1]); 396 $addr = $matches[2]; 397 } else { 398 $text = ''; 399 $addr = $part; 400 } 401 // skip empty ones 402 if(empty($addr)) { 403 continue; 404 } 405 406 // FIXME: is there a way to encode the localpart of a emailaddress? 407 if(!Clean::isASCII($addr)) { 408 msg(hsc("E-Mail address <$addr> is not ASCII"), -1, __LINE__, __FILE__, MSG_ADMINS_ONLY); 409 continue; 410 } 411 412 if(!mail_isvalid($addr)) { 413 msg(hsc("E-Mail address <$addr> is not valid"), -1, __LINE__, __FILE__, MSG_ADMINS_ONLY); 414 continue; 415 } 416 417 // text was given 418 if(!empty($text) && !isWindows()) { // No named recipients for To: in Windows (see FS#652) 419 // add address quotes 420 $addr = "<$addr>"; 421 422 if(defined('MAILHEADER_ASCIIONLY')) { 423 $text = Clean::deaccent($text); 424 $text = Clean::strip($text); 425 } 426 427 if(strpos($text, ',') !== false || !Clean::isASCII($text)) { 428 $text = '=?UTF-8?B?'.base64_encode($text).'?='; 429 } 430 } else { 431 $text = ''; 432 } 433 434 // add to header comma seperated 435 if($headers != '') { 436 $headers .= ', '; 437 } 438 $headers .= $text.' '.$addr; 439 } 440 441 $headers = trim($headers); 442 if(empty($headers)) return false; 443 444 return $headers; 445 } 446 447 448 /** 449 * Prepare the mime multiparts for all attachments 450 * 451 * Replaces placeholders in the HTML with the correct CIDs 452 * 453 * @return string mime multiparts 454 */ 455 protected function prepareAttachments() 456 { 457 $mime = ''; 458 $part = 1; 459 // embedded attachments 460 foreach($this->attach as $media) { 461 $media['name'] = str_replace(':', '_', cleanID($media['name'], true)); 462 463 // create content id 464 $cid = 'part'.$part.'.'.$this->partid; 465 466 // replace wildcards 467 if($media['embed']) { 468 $this->html = str_replace('%%'.$media['embed'].'%%', 'cid:'.$cid, $this->html); 469 } 470 471 $mime .= '--'.$this->boundary.MAILHEADER_EOL; 472 $mime .= $this->wrappedHeaderLine('Content-Type', $media['mime'].'; id="'.$cid.'"'); 473 $mime .= $this->wrappedHeaderLine('Content-Transfer-Encoding', 'base64'); 474 $mime .= $this->wrappedHeaderLine('Content-ID', "<$cid>"); 475 if($media['embed']) { 476 $mime .= $this->wrappedHeaderLine('Content-Disposition', 'inline; filename='.$media['name']); 477 } else { 478 $mime .= $this->wrappedHeaderLine('Content-Disposition', 'attachment; filename='.$media['name']); 479 } 480 $mime .= MAILHEADER_EOL; //end of headers 481 $mime .= chunk_split(base64_encode($media['data']), 74, MAILHEADER_EOL); 482 483 $part++; 484 } 485 return $mime; 486 } 487 488 /** 489 * Build the body and handles multi part mails 490 * 491 * Needs to be called before prepareHeaders! 492 * 493 * @return string the prepared mail body, false on errors 494 */ 495 protected function prepareBody() 496 { 497 498 // no HTML mails allowed? remove HTML body 499 if(!$this->allowhtml) { 500 $this->html = ''; 501 } 502 503 // check for body 504 if(!$this->text && !$this->html) { 505 return false; 506 } 507 508 // add general headers 509 $this->headers['MIME-Version'] = '1.0'; 510 511 $body = ''; 512 513 if(!$this->html && !count($this->attach)) { // we can send a simple single part message 514 $this->headers['Content-Type'] = 'text/plain; charset=UTF-8'; 515 $this->headers['Content-Transfer-Encoding'] = 'base64'; 516 $body .= chunk_split(base64_encode($this->text), 72, MAILHEADER_EOL); 517 } else { // multi part it is 518 $body .= "This is a multi-part message in MIME format.".MAILHEADER_EOL; 519 520 // prepare the attachments 521 $attachments = $this->prepareAttachments(); 522 523 // do we have alternative text content? 524 if($this->text && $this->html) { 525 $this->headers['Content-Type'] = 'multipart/alternative;'.MAILHEADER_EOL. 526 ' boundary="'.$this->boundary.'XX"'; 527 $body .= '--'.$this->boundary.'XX'.MAILHEADER_EOL; 528 $body .= 'Content-Type: text/plain; charset=UTF-8'.MAILHEADER_EOL; 529 $body .= 'Content-Transfer-Encoding: base64'.MAILHEADER_EOL; 530 $body .= MAILHEADER_EOL; 531 $body .= chunk_split(base64_encode($this->text), 72, MAILHEADER_EOL); 532 $body .= '--'.$this->boundary.'XX'.MAILHEADER_EOL; 533 $body .= 'Content-Type: multipart/related;'.MAILHEADER_EOL. 534 ' boundary="'.$this->boundary.'";'.MAILHEADER_EOL. 535 ' type="text/html"'.MAILHEADER_EOL; 536 $body .= MAILHEADER_EOL; 537 } 538 539 $body .= '--'.$this->boundary.MAILHEADER_EOL; 540 $body .= 'Content-Type: text/html; charset=UTF-8'.MAILHEADER_EOL; 541 $body .= 'Content-Transfer-Encoding: base64'.MAILHEADER_EOL; 542 $body .= MAILHEADER_EOL; 543 $body .= chunk_split(base64_encode($this->html), 72, MAILHEADER_EOL); 544 $body .= MAILHEADER_EOL; 545 $body .= $attachments; 546 $body .= '--'.$this->boundary.'--'.MAILHEADER_EOL; 547 548 // close open multipart/alternative boundary 549 if($this->text && $this->html) { 550 $body .= '--'.$this->boundary.'XX--'.MAILHEADER_EOL; 551 } 552 } 553 554 return $body; 555 } 556 557 /** 558 * Cleanup and encode the headers array 559 */ 560 protected function cleanHeaders() 561 { 562 global $conf; 563 564 // clean up addresses 565 if(empty($this->headers['From'])) $this->from($conf['mailfrom']); 566 $addrs = ['To', 'From', 'Cc', 'Bcc', 'Reply-To', 'Sender']; 567 foreach($addrs as $addr) { 568 if(isset($this->headers[$addr])) { 569 $this->headers[$addr] = $this->cleanAddress($this->headers[$addr]); 570 } 571 } 572 573 if(isset($this->headers['Subject'])) { 574 // add prefix to subject 575 if(empty($conf['mailprefix'])) { 576 if(PhpString::strlen($conf['title']) < 20) { 577 $prefix = '['.$conf['title'].']'; 578 } else { 579 $prefix = '['.PhpString::substr($conf['title'], 0, 20).'...]'; 580 } 581 } else { 582 $prefix = '['.$conf['mailprefix'].']'; 583 } 584 $len = strlen($prefix); 585 if(substr($this->headers['Subject'], 0, $len) !== $prefix) { 586 $this->headers['Subject'] = $prefix.' '.$this->headers['Subject']; 587 } 588 589 // encode subject 590 if(defined('MAILHEADER_ASCIIONLY')) { 591 $this->headers['Subject'] = Clean::deaccent($this->headers['Subject']); 592 $this->headers['Subject'] = Clean::strip($this->headers['Subject']); 593 } 594 if(!Clean::isASCII($this->headers['Subject'])) { 595 $this->headers['Subject'] = '=?UTF-8?B?'.base64_encode($this->headers['Subject']).'?='; 596 } 597 } 598 599 } 600 601 /** 602 * Returns a complete, EOL terminated header line, wraps it if necessary 603 * 604 * @param string $key 605 * @param string $val 606 * @return string line 607 */ 608 protected function wrappedHeaderLine($key, $val) 609 { 610 return wordwrap("$key: $val", 78, MAILHEADER_EOL.' ').MAILHEADER_EOL; 611 } 612 613 /** 614 * Create a string from the headers array 615 * 616 * @returns string the headers 617 */ 618 protected function prepareHeaders() 619 { 620 $headers = ''; 621 foreach($this->headers as $key => $val) { 622 if ($val === '' || $val === null) continue; 623 $headers .= $this->wrappedHeaderLine($key, $val); 624 } 625 return $headers; 626 } 627 628 /** 629 * return a full email with all headers 630 * 631 * This is mainly intended for debugging and testing but could also be 632 * used for MHT exports 633 * 634 * @return string the mail, false on errors 635 */ 636 public function dump() 637 { 638 $this->cleanHeaders(); 639 $body = $this->prepareBody(); 640 if($body === false) return false; 641 $headers = $this->prepareHeaders(); 642 643 return $headers.MAILHEADER_EOL.$body; 644 } 645 646 /** 647 * Prepare default token replacement strings 648 * 649 * Populates the '$replacements' property. 650 * Should be called by the class constructor 651 */ 652 protected function prepareTokenReplacements() 653 { 654 global $INFO; 655 global $conf; 656 /* @var Input $INPUT */ 657 global $INPUT; 658 global $lang; 659 660 $ip = clientIP(); 661 $cip = gethostsbyaddrs($ip); 662 $name = $INFO['userinfo']['name'] ?? ''; 663 $mail = $INFO['userinfo']['mail'] ?? ''; 664 665 $this->replacements['text'] = [ 666 'DATE' => dformat(), 667 'BROWSER' => $INPUT->server->str('HTTP_USER_AGENT'), 668 'IPADDRESS' => $ip, 669 'HOSTNAME' => $cip, 670 'TITLE' => $conf['title'], 671 'DOKUWIKIURL' => DOKU_URL, 672 'USER' => $INPUT->server->str('REMOTE_USER'), 673 'NAME' => $name, 674 'MAIL' => $mail 675 ]; 676 677 $signature = str_replace( 678 '@DOKUWIKIURL@', 679 $this->replacements['text']['DOKUWIKIURL'], 680 $lang['email_signature_text'] 681 ); 682 $this->replacements['text']['EMAILSIGNATURE'] = "\n-- \n" . $signature . "\n"; 683 684 $this->replacements['html'] = [ 685 'DATE' => '<i>' . hsc(dformat()) . '</i>', 686 'BROWSER' => hsc($INPUT->server->str('HTTP_USER_AGENT')), 687 'IPADDRESS' => '<code>' . hsc($ip) . '</code>', 688 'HOSTNAME' => '<code>' . hsc($cip) . '</code>', 689 'TITLE' => hsc($conf['title']), 690 'DOKUWIKIURL' => '<a href="' . DOKU_URL . '">' . DOKU_URL . '</a>', 691 'USER' => hsc($INPUT->server->str('REMOTE_USER')), 692 'NAME' => hsc($name), 693 'MAIL' => '<a href="mailto:"' . hsc($mail) . '">' . hsc($mail) . '</a>' 694 ]; 695 $signature = $lang['email_signature_text']; 696 if(!empty($lang['email_signature_html'])) { 697 $signature = $lang['email_signature_html']; 698 } 699 $signature = str_replace( 700 ['@DOKUWIKIURL@', "\n"], 701 [$this->replacements['html']['DOKUWIKIURL'], '<br />'], 702 $signature 703 ); 704 $this->replacements['html']['EMAILSIGNATURE'] = $signature; 705 } 706 707 /** 708 * Send the mail 709 * 710 * Call this after all data was set 711 * 712 * @triggers MAIL_MESSAGE_SEND 713 * @return bool true if the mail was successfully passed to the MTA 714 */ 715 public function send() 716 { 717 global $lang; 718 $success = false; 719 720 // prepare hook data 721 $data = [ 722 // pass the whole mail class to plugin 723 'mail' => $this, 724 // pass references for backward compatibility 725 'to' => &$this->headers['To'], 726 'cc' => &$this->headers['Cc'], 727 'bcc' => &$this->headers['Bcc'], 728 'from' => &$this->headers['From'], 729 'subject' => &$this->headers['Subject'], 730 'body' => &$this->text, 731 'params' => &$this->sendparam, 732 'headers' => '', // plugins shouldn't use this 733 // signal if we mailed successfully to AFTER event 734 'success' => &$success, 735 ]; 736 737 // do our thing if BEFORE hook approves 738 $evt = new Event('MAIL_MESSAGE_SEND', $data); 739 if($evt->advise_before(true)) { 740 // clean up before using the headers 741 $this->cleanHeaders(); 742 743 // any recipients? 744 if(trim($this->headers['To']) === '' && 745 trim($this->headers['Cc']) === '' && 746 trim($this->headers['Bcc']) === '' 747 ) return false; 748 749 // The To: header is special 750 if(array_key_exists('To', $this->headers)) { 751 $to = (string)$this->headers['To']; 752 unset($this->headers['To']); 753 } else { 754 $to = ''; 755 } 756 757 // so is the subject 758 if(array_key_exists('Subject', $this->headers)) { 759 $subject = (string)$this->headers['Subject']; 760 unset($this->headers['Subject']); 761 } else { 762 $subject = ''; 763 } 764 765 // make the body 766 $body = $this->prepareBody(); 767 if($body === false) return false; 768 769 // cook the headers 770 $headers = $this->prepareHeaders(); 771 // add any headers set by legacy plugins 772 if(trim($data['headers'])) { 773 $headers .= MAILHEADER_EOL.trim($data['headers']); 774 } 775 776 if(!function_exists('mail')){ 777 $emsg = $lang['email_fail'] . $subject; 778 error_log($emsg); 779 msg(hsc($emsg), -1, __LINE__, __FILE__, MSG_MANAGERS_ONLY); 780 $evt->advise_after(); 781 return false; 782 } 783 784 // send the thing 785 if($to === '') $to = '(undisclosed-recipients)'; // #1422 786 if($this->sendparam === null) { 787 $success = @mail($to, $subject, $body, $headers); 788 } else { 789 $success = @mail($to, $subject, $body, $headers, $this->sendparam); 790 } 791 } 792 // any AFTER actions? 793 $evt->advise_after(); 794 return $success; 795 } 796} 797