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'], 239 $html 240 ); 241 242 // add default token replacements 243 $trep = array_merge($this->replacements['text'], $textrep); 244 $hrep = array_merge($this->replacements['html'], $htmlrep); 245 246 // Apply replacements 247 foreach ($trep as $key => $substitution) { 248 $text = str_replace('@'.strtoupper($key).'@', $substitution, $text); 249 } 250 foreach ($hrep as $key => $substitution) { 251 $html = str_replace('@'.strtoupper($key).'@', $substitution, $html); 252 } 253 254 $this->setHTML($html); 255 $this->setText($text); 256 } 257 258 /** 259 * Set the HTML part of the mail 260 * 261 * Placeholders can be used to reference embedded attachments 262 * 263 * You probably want to use setBody() instead 264 * 265 * @param string $html 266 */ 267 public function setHTML($html) 268 { 269 $this->html = $html; 270 } 271 272 /** 273 * Set the plain text part of the mail 274 * 275 * You probably want to use setBody() instead 276 * 277 * @param string $text 278 */ 279 public function setText($text) 280 { 281 $this->text = $text; 282 } 283 284 /** 285 * Add the To: recipients 286 * 287 * @see cleanAddress 288 * @param string|string[] $address Multiple adresses separated by commas or as array 289 */ 290 public function to($address) 291 { 292 $this->setHeader('To', $address, false); 293 } 294 295 /** 296 * Add the Cc: recipients 297 * 298 * @see cleanAddress 299 * @param string|string[] $address Multiple adresses separated by commas or as array 300 */ 301 public function cc($address) 302 { 303 $this->setHeader('Cc', $address, false); 304 } 305 306 /** 307 * Add the Bcc: recipients 308 * 309 * @see cleanAddress 310 * @param string|string[] $address Multiple adresses separated by commas or as array 311 */ 312 public function bcc($address) 313 { 314 $this->setHeader('Bcc', $address, false); 315 } 316 317 /** 318 * Add the From: address 319 * 320 * This is set to $conf['mailfrom'] when not specified so you shouldn't need 321 * to call this function 322 * 323 * @see cleanAddress 324 * @param string $address from address 325 */ 326 public function from($address) 327 { 328 $this->setHeader('From', $address, false); 329 } 330 331 /** 332 * Add the mail's Subject: header 333 * 334 * @param string $subject the mail subject 335 */ 336 public function subject($subject) 337 { 338 $this->headers['Subject'] = $subject; 339 } 340 341 /** 342 * Return a clean name which can be safely used in mail address 343 * fields. That means the name will be enclosed in '"' if it includes 344 * a '"' or a ','. Also a '"' will be escaped as '\"'. 345 * 346 * @param string $name the name to clean-up 347 * @see cleanAddress 348 */ 349 public function getCleanName($name) 350 { 351 $name = trim($name, " \t\""); 352 $name = str_replace('"', '\"', $name, $count); 353 if ($count > 0 || strpos($name, ',') !== false) { 354 $name = '"'.$name.'"'; 355 } 356 return $name; 357 } 358 359 /** 360 * Sets an email address header with correct encoding 361 * 362 * Unicode characters will be deaccented and encoded base64 363 * for headers. Addresses may not contain Non-ASCII data! 364 * 365 * If @$addresses is a string then it will be split into multiple 366 * addresses. Addresses must be separated by a comma. If the display 367 * name includes a comma then it MUST be properly enclosed by '"' to 368 * prevent spliting at the wrong point. 369 * 370 * Example: 371 * cc("föö <foo@bar.com>, me@somewhere.com","TBcc"); 372 * to("foo, Dr." <foo@bar.com>, me@somewhere.com"); 373 * 374 * @param string|string[] $addresses Multiple adresses separated by commas or as array 375 * @return false|string the prepared header (can contain multiple lines) 376 */ 377 public function cleanAddress($addresses) 378 { 379 $headers = ''; 380 if (!is_array($addresses)) { 381 $count = preg_match_all('/\s*(?:("[^"]*"[^,]+),*)|([^,]+)\s*,*/', $addresses, $matches, PREG_SET_ORDER); 382 $addresses = []; 383 if ($count !== false && is_array($matches)) { 384 foreach ($matches as $match) { 385 $addresses[] = rtrim($match[0], ','); 386 } 387 } 388 } 389 390 foreach ($addresses as $part) { 391 $part = preg_replace('/[\r\n\0]+/', ' ', $part); // remove attack vectors 392 $part = trim($part); 393 394 // parse address 395 if (preg_match('#(.*?)<(.*?)>#', $part, $matches)) { 396 $text = trim($matches[1]); 397 $addr = $matches[2]; 398 } else { 399 $text = ''; 400 $addr = $part; 401 } 402 // skip empty ones 403 if (empty($addr)) { 404 continue; 405 } 406 407 // FIXME: is there a way to encode the localpart of a emailaddress? 408 if (!Clean::isASCII($addr)) { 409 msg(hsc("E-Mail address <$addr> is not ASCII"), -1, __LINE__, __FILE__, MSG_ADMINS_ONLY); 410 continue; 411 } 412 413 if (!mail_isvalid($addr)) { 414 msg(hsc("E-Mail address <$addr> is not valid"), -1, __LINE__, __FILE__, MSG_ADMINS_ONLY); 415 continue; 416 } 417 418 // text was given 419 if (!empty($text) && !isWindows()) { // No named recipients for To: in Windows (see FS#652) 420 // add address quotes 421 $addr = "<$addr>"; 422 423 if (defined('MAILHEADER_ASCIIONLY')) { 424 $text = Clean::deaccent($text); 425 $text = Clean::strip($text); 426 } 427 428 if (strpos($text, ',') !== false || !Clean::isASCII($text)) { 429 $text = '=?UTF-8?B?'.base64_encode($text).'?='; 430 } 431 } else { 432 $text = ''; 433 } 434 435 // add to header comma seperated 436 if ($headers != '') { 437 $headers .= ', '; 438 } 439 $headers .= $text.' '.$addr; 440 } 441 442 $headers = trim($headers); 443 if (empty($headers)) return false; 444 445 return $headers; 446 } 447 448 449 /** 450 * Prepare the mime multiparts for all attachments 451 * 452 * Replaces placeholders in the HTML with the correct CIDs 453 * 454 * @return string mime multiparts 455 */ 456 protected function prepareAttachments() 457 { 458 $mime = ''; 459 $part = 1; 460 // embedded attachments 461 foreach ($this->attach as $media) { 462 $media['name'] = str_replace(':', '_', cleanID($media['name'], true)); 463 464 // create content id 465 $cid = 'part'.$part.'.'.$this->partid; 466 467 // replace wildcards 468 if ($media['embed']) { 469 $this->html = str_replace('%%'.$media['embed'].'%%', 'cid:'.$cid, $this->html); 470 } 471 472 $mime .= '--'.$this->boundary.MAILHEADER_EOL; 473 $mime .= $this->wrappedHeaderLine('Content-Type', $media['mime'].'; id="'.$cid.'"'); 474 $mime .= $this->wrappedHeaderLine('Content-Transfer-Encoding', 'base64'); 475 $mime .= $this->wrappedHeaderLine('Content-ID', "<$cid>"); 476 if ($media['embed']) { 477 $mime .= $this->wrappedHeaderLine('Content-Disposition', 'inline; filename='.$media['name']); 478 } else { 479 $mime .= $this->wrappedHeaderLine('Content-Disposition', 'attachment; filename='.$media['name']); 480 } 481 $mime .= MAILHEADER_EOL; //end of headers 482 $mime .= chunk_split(base64_encode($media['data']), 74, MAILHEADER_EOL); 483 484 $part++; 485 } 486 return $mime; 487 } 488 489 /** 490 * Build the body and handles multi part mails 491 * 492 * Needs to be called before prepareHeaders! 493 * 494 * @return string the prepared mail body, false on errors 495 */ 496 protected function prepareBody() 497 { 498 499 // no HTML mails allowed? remove HTML body 500 if (!$this->allowhtml) { 501 $this->html = ''; 502 } 503 504 // check for body 505 if (!$this->text && !$this->html) { 506 return false; 507 } 508 509 // add general headers 510 $this->headers['MIME-Version'] = '1.0'; 511 512 $body = ''; 513 514 if (!$this->html && !count($this->attach)) { // we can send a simple single part message 515 $this->headers['Content-Type'] = 'text/plain; charset=UTF-8'; 516 $this->headers['Content-Transfer-Encoding'] = 'base64'; 517 $body .= chunk_split(base64_encode($this->text), 72, MAILHEADER_EOL); 518 } else { // multi part it is 519 $body .= "This is a multi-part message in MIME format.".MAILHEADER_EOL; 520 521 // prepare the attachments 522 $attachments = $this->prepareAttachments(); 523 524 // do we have alternative text content? 525 if ($this->text && $this->html) { 526 $this->headers['Content-Type'] = 'multipart/alternative;'.MAILHEADER_EOL. 527 ' boundary="'.$this->boundary.'XX"'; 528 $body .= '--'.$this->boundary.'XX'.MAILHEADER_EOL; 529 $body .= 'Content-Type: text/plain; charset=UTF-8'.MAILHEADER_EOL; 530 $body .= 'Content-Transfer-Encoding: base64'.MAILHEADER_EOL; 531 $body .= MAILHEADER_EOL; 532 $body .= chunk_split(base64_encode($this->text), 72, MAILHEADER_EOL); 533 $body .= '--'.$this->boundary.'XX'.MAILHEADER_EOL; 534 $body .= 'Content-Type: multipart/related;'.MAILHEADER_EOL. 535 ' boundary="'.$this->boundary.'";'.MAILHEADER_EOL. 536 ' type="text/html"'.MAILHEADER_EOL; 537 $body .= MAILHEADER_EOL; 538 } 539 540 $body .= '--'.$this->boundary.MAILHEADER_EOL; 541 $body .= 'Content-Type: text/html; charset=UTF-8'.MAILHEADER_EOL; 542 $body .= 'Content-Transfer-Encoding: base64'.MAILHEADER_EOL; 543 $body .= MAILHEADER_EOL; 544 $body .= chunk_split(base64_encode($this->html), 72, MAILHEADER_EOL); 545 $body .= MAILHEADER_EOL; 546 $body .= $attachments; 547 $body .= '--'.$this->boundary.'--'.MAILHEADER_EOL; 548 549 // close open multipart/alternative boundary 550 if ($this->text && $this->html) { 551 $body .= '--'.$this->boundary.'XX--'.MAILHEADER_EOL; 552 } 553 } 554 555 return $body; 556 } 557 558 /** 559 * Cleanup and encode the headers array 560 */ 561 protected function cleanHeaders() 562 { 563 global $conf; 564 565 // clean up addresses 566 if (empty($this->headers['From'])) $this->from($conf['mailfrom']); 567 $addrs = ['To', 'From', 'Cc', 'Bcc', 'Reply-To', 'Sender']; 568 foreach ($addrs as $addr) { 569 if (isset($this->headers[$addr])) { 570 $this->headers[$addr] = $this->cleanAddress($this->headers[$addr]); 571 } 572 } 573 574 if (isset($this->headers['Subject'])) { 575 // add prefix to subject 576 if (empty($conf['mailprefix'])) { 577 if (PhpString::strlen($conf['title']) < 20) { 578 $prefix = '['.$conf['title'].']'; 579 } else { 580 $prefix = '['.PhpString::substr($conf['title'], 0, 20).'...]'; 581 } 582 } else { 583 $prefix = '['.$conf['mailprefix'].']'; 584 } 585 $len = strlen($prefix); 586 if (substr($this->headers['Subject'], 0, $len) !== $prefix) { 587 $this->headers['Subject'] = $prefix.' '.$this->headers['Subject']; 588 } 589 590 // encode subject 591 if (defined('MAILHEADER_ASCIIONLY')) { 592 $this->headers['Subject'] = Clean::deaccent($this->headers['Subject']); 593 $this->headers['Subject'] = Clean::strip($this->headers['Subject']); 594 } 595 if (!Clean::isASCII($this->headers['Subject'])) { 596 $this->headers['Subject'] = '=?UTF-8?B?'.base64_encode($this->headers['Subject']).'?='; 597 } 598 } 599 600 } 601 602 /** 603 * Returns a complete, EOL terminated header line, wraps it if necessary 604 * 605 * @param string $key 606 * @param string $val 607 * @return string line 608 */ 609 protected function wrappedHeaderLine($key, $val) 610 { 611 return wordwrap("$key: $val", 78, MAILHEADER_EOL.' ').MAILHEADER_EOL; 612 } 613 614 /** 615 * Create a string from the headers array 616 * 617 * @returns string the headers 618 */ 619 protected function prepareHeaders() 620 { 621 $headers = ''; 622 foreach ($this->headers as $key => $val) { 623 if ($val === '' || $val === null) continue; 624 $headers .= $this->wrappedHeaderLine($key, $val); 625 } 626 return $headers; 627 } 628 629 /** 630 * return a full email with all headers 631 * 632 * This is mainly intended for debugging and testing but could also be 633 * used for MHT exports 634 * 635 * @return string the mail, false on errors 636 */ 637 public function dump() 638 { 639 $this->cleanHeaders(); 640 $body = $this->prepareBody(); 641 if ($body === false) return false; 642 $headers = $this->prepareHeaders(); 643 644 return $headers.MAILHEADER_EOL.$body; 645 } 646 647 /** 648 * Prepare default token replacement strings 649 * 650 * Populates the '$replacements' property. 651 * Should be called by the class constructor 652 */ 653 protected function prepareTokenReplacements() 654 { 655 global $INFO; 656 global $conf; 657 /* @var Input $INPUT */ 658 global $INPUT; 659 global $lang; 660 661 $ip = clientIP(); 662 $cip = gethostsbyaddrs($ip); 663 $name = $INFO['userinfo']['name'] ?? ''; 664 $mail = $INFO['userinfo']['mail'] ?? ''; 665 666 $this->replacements['text'] = [ 667 'DATE' => dformat(), 668 'BROWSER' => $INPUT->server->str('HTTP_USER_AGENT'), 669 'IPADDRESS' => $ip, 670 'HOSTNAME' => $cip, 671 'TITLE' => $conf['title'], 672 'DOKUWIKIURL' => DOKU_URL, 673 'USER' => $INPUT->server->str('REMOTE_USER'), 674 'NAME' => $name, 675 'MAIL' => $mail 676 ]; 677 678 $signature = str_replace( 679 '@DOKUWIKIURL@', 680 $this->replacements['text']['DOKUWIKIURL'], 681 $lang['email_signature_text'] 682 ); 683 $this->replacements['text']['EMAILSIGNATURE'] = "\n-- \n" . $signature . "\n"; 684 685 $this->replacements['html'] = [ 686 'DATE' => '<i>' . hsc(dformat()) . '</i>', 687 'BROWSER' => hsc($INPUT->server->str('HTTP_USER_AGENT')), 688 'IPADDRESS' => '<code>' . hsc($ip) . '</code>', 689 'HOSTNAME' => '<code>' . hsc($cip) . '</code>', 690 'TITLE' => hsc($conf['title']), 691 'DOKUWIKIURL' => '<a href="' . DOKU_URL . '">' . DOKU_URL . '</a>', 692 'USER' => hsc($INPUT->server->str('REMOTE_USER')), 693 'NAME' => hsc($name), 694 'MAIL' => '<a href="mailto:"' . hsc($mail) . '">' . hsc($mail) . '</a>' 695 ]; 696 $signature = $lang['email_signature_text']; 697 if (!empty($lang['email_signature_html'])) { 698 $signature = $lang['email_signature_html']; 699 } 700 $signature = str_replace( 701 ['@DOKUWIKIURL@', "\n"], 702 [$this->replacements['html']['DOKUWIKIURL'], '<br />'], 703 $signature 704 ); 705 $this->replacements['html']['EMAILSIGNATURE'] = $signature; 706 } 707 708 /** 709 * Send the mail 710 * 711 * Call this after all data was set 712 * 713 * @triggers MAIL_MESSAGE_SEND 714 * @return bool true if the mail was successfully passed to the MTA 715 */ 716 public function send() 717 { 718 global $lang; 719 $success = false; 720 721 // prepare hook data 722 $data = [ 723 // pass the whole mail class to plugin 724 'mail' => $this, 725 // pass references for backward compatibility 726 'to' => &$this->headers['To'], 727 'cc' => &$this->headers['Cc'], 728 'bcc' => &$this->headers['Bcc'], 729 'from' => &$this->headers['From'], 730 'subject' => &$this->headers['Subject'], 731 'body' => &$this->text, 732 'params' => &$this->sendparam, 733 'headers' => '', // plugins shouldn't use this 734 // signal if we mailed successfully to AFTER event 735 'success' => &$success, 736 ]; 737 738 // do our thing if BEFORE hook approves 739 $evt = new Event('MAIL_MESSAGE_SEND', $data); 740 if ($evt->advise_before(true)) { 741 // clean up before using the headers 742 $this->cleanHeaders(); 743 744 // any recipients? 745 if (trim($this->headers['To']) === '' && 746 trim($this->headers['Cc']) === '' && 747 trim($this->headers['Bcc']) === '' 748 ) return false; 749 750 // The To: header is special 751 if (array_key_exists('To', $this->headers)) { 752 $to = (string)$this->headers['To']; 753 unset($this->headers['To']); 754 } else { 755 $to = ''; 756 } 757 758 // so is the subject 759 if (array_key_exists('Subject', $this->headers)) { 760 $subject = (string)$this->headers['Subject']; 761 unset($this->headers['Subject']); 762 } else { 763 $subject = ''; 764 } 765 766 // make the body 767 $body = $this->prepareBody(); 768 if ($body === false) return false; 769 770 // cook the headers 771 $headers = $this->prepareHeaders(); 772 // add any headers set by legacy plugins 773 if (trim($data['headers'])) { 774 $headers .= MAILHEADER_EOL.trim($data['headers']); 775 } 776 777 if (!function_exists('mail')) { 778 $emsg = $lang['email_fail'] . $subject; 779 error_log($emsg); 780 msg(hsc($emsg), -1, __LINE__, __FILE__, MSG_MANAGERS_ONLY); 781 $evt->advise_after(); 782 return false; 783 } 784 785 // send the thing 786 if ($to === '') $to = '(undisclosed-recipients)'; // #1422 787 if ($this->sendparam === null) { 788 $success = @mail($to, $subject, $body, $headers); 789 } else { 790 $success = @mail($to, $subject, $body, $headers, $this->sendparam); 791 } 792 } 793 // any AFTER actions? 794 $evt->advise_after(); 795 return $success; 796 } 797} 798