1<?php 2 3/** 4 * A class to build and send multi part mails (with HTML content and embedded 5 * attachments). All mails are assumed to be in UTF-8 encoding. 6 * 7 * Attachments are handled in memory so this shouldn't be used to send huge 8 * files, but then again mail shouldn't be used to send huge files either. 9 * 10 * @author Andreas Gohr <andi@splitbrain.org> 11 */ 12 13use dokuwiki\Utf8\PhpString; 14use dokuwiki\Utf8\Clean; 15use dokuwiki\Extension\Event; 16 17/** 18 * Mail Handling 19 */ 20class Mailer 21{ 22 protected $headers = []; 23 protected $attach = []; 24 protected $html = ''; 25 protected $text = ''; 26 27 protected $boundary = ''; 28 protected $partid = ''; 29 protected $sendparam; 30 31 protected $allowhtml = true; 32 33 protected $replacements = ['text' => [], 'html' => []]; 34 35 /** 36 * Constructor 37 * 38 * Initializes the boundary strings, part counters and token replacements 39 */ 40 public function __construct() 41 { 42 global $conf; 43 /* @var Input $INPUT */ 44 global $INPUT; 45 46 $server = parse_url(DOKU_URL, PHP_URL_HOST); 47 if (strpos($server, '.') === false) $server .= '.localhost'; 48 49 $this->partid = substr(md5(uniqid(random_int(0, mt_getrandmax()), true)), 0, 8) . '@' . $server; 50 $this->boundary = '__________' . md5(uniqid(random_int(0, mt_getrandmax()), true)); 51 52 $listid = implode('.', array_reverse(explode('/', DOKU_BASE))) . $server; 53 $listid = strtolower(trim($listid, '.')); 54 55 $messageid = uniqid(random_int(0, mt_getrandmax()), true) . "@$server"; 56 57 $this->allowhtml = (bool)$conf['htmlmail']; 58 59 // add some default headers for mailfiltering FS#2247 60 if (!empty($conf['mailreturnpath'])) { 61 $this->setHeader('Return-Path', $conf['mailreturnpath']); 62 } 63 $this->setHeader('X-Mailer', 'DokuWiki'); 64 $this->setHeader('X-DokuWiki-User', $INPUT->server->str('REMOTE_USER')); 65 $this->setHeader('X-DokuWiki-Title', $conf['title']); 66 $this->setHeader('X-DokuWiki-Server', $server); 67 $this->setHeader('X-Auto-Response-Suppress', 'OOF'); 68 $this->setHeader('List-Id', $conf['title'] . ' <' . $listid . '>'); 69 $this->setHeader('Date', date('r'), false); 70 $this->setHeader('Message-Id', "<$messageid>"); 71 72 $this->prepareTokenReplacements(); 73 } 74 75 /** 76 * Attach a file 77 * 78 * @param string $path Path to the file to attach 79 * @param string $mime Mimetype of the attached file 80 * @param string $name The filename to use 81 * @param string $embed Unique key to reference this file from the HTML part 82 */ 83 public function attachFile($path, $mime, $name = '', $embed = '') 84 { 85 if (!$name) { 86 $name = PhpString::basename($path); 87 } 88 89 $this->attach[] = [ 90 'data' => file_get_contents($path), 91 'mime' => $mime, 92 'name' => $name, 93 'embed' => $embed 94 ]; 95 } 96 97 /** 98 * Attach a file 99 * 100 * @param string $data The file contents to attach 101 * @param string $mime Mimetype of the attached file 102 * @param string $name The filename to use 103 * @param string $embed Unique key to reference this file from the HTML part 104 */ 105 public function attachContent($data, $mime, $name = '', $embed = '') 106 { 107 if (!$name) { 108 [, $ext] = explode('/', $mime); 109 $name = count($this->attach) . ".$ext"; 110 } 111 112 $this->attach[] = [ 113 'data' => $data, 114 'mime' => $mime, 115 'name' => $name, 116 'embed' => $embed 117 ]; 118 } 119 120 /** 121 * Callback function to automatically embed images referenced in HTML templates 122 * 123 * @param array $matches 124 * @return string placeholder 125 */ 126 protected function autoEmbedCallBack($matches) 127 { 128 static $embeds = 0; 129 $embeds++; 130 131 // get file and mime type 132 $media = cleanID($matches[1]); 133 [, $mime] = mimetype($media); 134 $file = mediaFN($media); 135 if (!file_exists($file)) return $matches[0]; //bad reference, keep as is 136 137 // attach it and set placeholder 138 $this->attachFile($file, $mime, '', 'autoembed' . $embeds); 139 return '%%autoembed' . $embeds . '%%'; 140 } 141 142 /** 143 * Add an arbitrary header to the mail 144 * 145 * If an empy value is passed, the header is removed 146 * 147 * @param string $header the header name (no trailing colon!) 148 * @param string|string[] $value the value of the header 149 * @param bool $clean remove all non-ASCII chars and line feeds? 150 */ 151 public function setHeader($header, $value, $clean = true) 152 { 153 $header = str_replace(' ', '-', ucwords(strtolower(str_replace('-', ' ', $header)))); // streamline casing 154 if ($clean) { 155 $header = preg_replace('/[^a-zA-Z0-9_ \-\.\+\@]+/', '', $header); 156 $value = preg_replace('/[^a-zA-Z0-9_ \-\.\+\@<>]+/', '', $value); 157 } 158 159 // empty value deletes 160 if (is_array($value)) { 161 $value = array_map('trim', $value); 162 $value = array_filter($value); 163 if (!$value) $value = ''; 164 } else { 165 $value = trim($value); 166 } 167 if ($value === '') { 168 if (isset($this->headers[$header])) unset($this->headers[$header]); 169 } else { 170 $this->headers[$header] = $value; 171 } 172 } 173 174 /** 175 * Set additional parameters to be passed to sendmail 176 * 177 * Whatever is set here is directly passed to PHP's mail() command as last 178 * parameter. Depending on the PHP setup this might break mailing alltogether 179 * 180 * @param string $param 181 */ 182 public function setParameters($param) 183 { 184 $this->sendparam = $param; 185 } 186 187 /** 188 * Set the text and HTML body and apply replacements 189 * 190 * This function applies a whole bunch of default replacements in addition 191 * to the ones specified as parameters 192 * 193 * If you pass the HTML part or HTML replacements yourself you have to make 194 * sure you encode all HTML special chars correctly 195 * 196 * @param string $text plain text body 197 * @param array $textrep replacements to apply on the text part 198 * @param array $htmlrep replacements to apply on the HTML part, null to use $textrep (urls wrapped in <a> tags) 199 * @param string $html the HTML body, leave null to create it from $text 200 * @param bool $wrap wrap the HTML in the default header/Footer 201 */ 202 public function setBody($text, $textrep = null, $htmlrep = null, $html = null, $wrap = true) 203 { 204 205 $htmlrep = (array)$htmlrep; 206 $textrep = (array)$textrep; 207 208 // create HTML from text if not given 209 if ($html === null) { 210 $html = $text; 211 $html = hsc($html); 212 $html = preg_replace('/^----+$/m', '<hr >', $html); 213 $html = nl2br($html); 214 } 215 if ($wrap) { 216 $wrapper = rawLocale('mailwrap', 'html'); 217 $html = preg_replace('/\n-- <br \/>.*$/s', '', $html); //strip signature 218 $html = str_replace('@EMAILSIGNATURE@', '', $html); //strip @EMAILSIGNATURE@ 219 $html = str_replace('@HTMLBODY@', $html, $wrapper); 220 } 221 222 if (strpos($text, '@EMAILSIGNATURE@') === false) { 223 $text .= '@EMAILSIGNATURE@'; 224 } 225 226 // copy over all replacements missing for HTML (autolink URLs) 227 foreach ($textrep as $key => $value) { 228 if (isset($htmlrep[$key])) continue; 229 if (media_isexternal($value)) { 230 $htmlrep[$key] = '<a href="' . hsc($value) . '">' . hsc($value) . '</a>'; 231 } else { 232 $htmlrep[$key] = hsc($value); 233 } 234 } 235 236 // embed media from templates 237 $html = preg_replace_callback( 238 '/@MEDIA\(([^\)]+)\)@/', 239 [$this, 'autoEmbedCallBack'], 240 $html 241 ); 242 243 // add default token replacements 244 $trep = array_merge($this->replacements['text'], $textrep); 245 $hrep = array_merge($this->replacements['html'], $htmlrep); 246 247 // Apply replacements 248 foreach ($trep as $key => $substitution) { 249 $text = str_replace('@' . strtoupper($key) . '@', $substitution, $text); 250 } 251 foreach ($hrep as $key => $substitution) { 252 $html = str_replace('@' . strtoupper($key) . '@', $substitution, $html); 253 } 254 255 $this->setHTML($html); 256 $this->setText($text); 257 } 258 259 /** 260 * Set the HTML part of the mail 261 * 262 * Placeholders can be used to reference embedded attachments 263 * 264 * You probably want to use setBody() instead 265 * 266 * @param string $html 267 */ 268 public function setHTML($html) 269 { 270 $this->html = $html; 271 } 272 273 /** 274 * Set the plain text part of the mail 275 * 276 * You probably want to use setBody() instead 277 * 278 * @param string $text 279 */ 280 public function setText($text) 281 { 282 $this->text = $text; 283 } 284 285 /** 286 * Add the To: recipients 287 * 288 * @see cleanAddress 289 * @param string|string[] $address Multiple adresses separated by commas or as array 290 */ 291 public function to($address) 292 { 293 $this->setHeader('To', $address, false); 294 } 295 296 /** 297 * Add the Cc: recipients 298 * 299 * @see cleanAddress 300 * @param string|string[] $address Multiple adresses separated by commas or as array 301 */ 302 public function cc($address) 303 { 304 $this->setHeader('Cc', $address, false); 305 } 306 307 /** 308 * Add the Bcc: recipients 309 * 310 * @see cleanAddress 311 * @param string|string[] $address Multiple adresses separated by commas or as array 312 */ 313 public function bcc($address) 314 { 315 $this->setHeader('Bcc', $address, false); 316 } 317 318 /** 319 * Add the From: address 320 * 321 * This is set to $conf['mailfrom'] when not specified so you shouldn't need 322 * to call this function 323 * 324 * @see cleanAddress 325 * @param string $address from address 326 */ 327 public function from($address) 328 { 329 $this->setHeader('From', $address, false); 330 } 331 332 /** 333 * Add the mail's Subject: header 334 * 335 * @param string $subject the mail subject 336 */ 337 public function subject($subject) 338 { 339 $this->headers['Subject'] = $subject; 340 } 341 342 /** 343 * Return a clean name which can be safely used in mail address 344 * fields. That means the name will be enclosed in '"' if it includes 345 * a '"' or a ','. Also a '"' will be escaped as '\"'. 346 * 347 * @param string $name the name to clean-up 348 * @see cleanAddress 349 */ 350 public function getCleanName($name) 351 { 352 $name = trim($name, " \t\""); 353 $name = str_replace('"', '\"', $name, $count); 354 if ($count > 0 || strpos($name, ',') !== false) { 355 $name = '"' . $name . '"'; 356 } 357 return $name; 358 } 359 360 /** 361 * Sets an email address header with correct encoding 362 * 363 * Unicode characters will be deaccented and encoded base64 364 * for headers. Addresses may not contain Non-ASCII data! 365 * 366 * If @$addresses is a string then it will be split into multiple 367 * addresses. Addresses must be separated by a comma. If the display 368 * name includes a comma then it MUST be properly enclosed by '"' to 369 * prevent spliting at the wrong point. 370 * 371 * Example: 372 * cc("föö <foo@bar.com>, me@somewhere.com","TBcc"); 373 * to("foo, Dr." <foo@bar.com>, me@somewhere.com"); 374 * 375 * @param string|string[] $addresses Multiple adresses separated by commas or as array 376 * @return false|string the prepared header (can contain multiple lines) 377 */ 378 public function cleanAddress($addresses) 379 { 380 $headers = ''; 381 if (!is_array($addresses)) { 382 $count = preg_match_all('/\s*(?:("[^"]*"[^,]+),*)|([^,]+)\s*,*/', $addresses, $matches, PREG_SET_ORDER); 383 $addresses = []; 384 if ($count !== false && is_array($matches)) { 385 foreach ($matches as $match) { 386 $addresses[] = rtrim($match[0], ','); 387 } 388 } 389 } 390 391 foreach ($addresses as $part) { 392 $part = preg_replace('/[\r\n\0]+/', ' ', $part); // remove attack vectors 393 $part = trim($part); 394 395 // parse address 396 if (preg_match('#(.*?)<(.*?)>#', $part, $matches)) { 397 $text = trim($matches[1]); 398 $addr = $matches[2]; 399 } else { 400 $text = ''; 401 $addr = $part; 402 } 403 // skip empty ones 404 if (empty($addr)) { 405 continue; 406 } 407 408 // FIXME: is there a way to encode the localpart of a emailaddress? 409 if (!Clean::isASCII($addr)) { 410 msg(hsc("E-Mail address <$addr> is not ASCII"), -1, __LINE__, __FILE__, MSG_ADMINS_ONLY); 411 continue; 412 } 413 414 if (!mail_isvalid($addr)) { 415 msg(hsc("E-Mail address <$addr> is not valid"), -1, __LINE__, __FILE__, MSG_ADMINS_ONLY); 416 continue; 417 } 418 419 // text was given 420 if (!empty($text) && !isWindows()) { // No named recipients for To: in Windows (see FS#652) 421 // add address quotes 422 $addr = "<$addr>"; 423 424 if (defined('MAILHEADER_ASCIIONLY')) { 425 $text = Clean::deaccent($text); 426 $text = Clean::strip($text); 427 } 428 429 if (strpos($text, ',') !== false || !Clean::isASCII($text)) { 430 $text = '=?UTF-8?B?' . base64_encode($text) . '?='; 431 } 432 } else { 433 $text = ''; 434 } 435 436 // add to header comma seperated 437 if ($headers != '') { 438 $headers .= ', '; 439 } 440 $headers .= $text . ' ' . $addr; 441 } 442 443 $headers = trim($headers); 444 if (empty($headers)) return false; 445 446 return $headers; 447 } 448 449 450 /** 451 * Prepare the mime multiparts for all attachments 452 * 453 * Replaces placeholders in the HTML with the correct CIDs 454 * 455 * @return string mime multiparts 456 */ 457 protected function prepareAttachments() 458 { 459 $mime = ''; 460 $part = 1; 461 // embedded attachments 462 foreach ($this->attach as $media) { 463 $media['name'] = str_replace(':', '_', cleanID($media['name'], true)); 464 465 // create content id 466 $cid = 'part' . $part . '.' . $this->partid; 467 468 // replace wildcards 469 if ($media['embed']) { 470 $this->html = str_replace('%%' . $media['embed'] . '%%', 'cid:' . $cid, $this->html); 471 } 472 473 $mime .= '--' . $this->boundary . MAILHEADER_EOL; 474 $mime .= $this->wrappedHeaderLine('Content-Type', $media['mime'] . '; id="' . $cid . '"'); 475 $mime .= $this->wrappedHeaderLine('Content-Transfer-Encoding', 'base64'); 476 $mime .= $this->wrappedHeaderLine('Content-ID', "<$cid>"); 477 if ($media['embed']) { 478 $mime .= $this->wrappedHeaderLine('Content-Disposition', 'inline; filename=' . $media['name']); 479 } else { 480 $mime .= $this->wrappedHeaderLine('Content-Disposition', 'attachment; filename=' . $media['name']); 481 } 482 $mime .= MAILHEADER_EOL; //end of headers 483 $mime .= chunk_split(base64_encode($media['data']), 74, MAILHEADER_EOL); 484 485 $part++; 486 } 487 return $mime; 488 } 489 490 /** 491 * Build the body and handles multi part mails 492 * 493 * Needs to be called before prepareHeaders! 494 * 495 * @return string the prepared mail body, false on errors 496 */ 497 protected function prepareBody() 498 { 499 500 // no HTML mails allowed? remove HTML body 501 if (!$this->allowhtml) { 502 $this->html = ''; 503 } 504 505 // check for body 506 if (!$this->text && !$this->html) { 507 return false; 508 } 509 510 // add general headers 511 $this->headers['MIME-Version'] = '1.0'; 512 513 $body = ''; 514 515 if (!$this->html && !count($this->attach)) { // we can send a simple single part message 516 $this->headers['Content-Type'] = 'text/plain; charset=UTF-8'; 517 $this->headers['Content-Transfer-Encoding'] = 'base64'; 518 $body .= chunk_split(base64_encode($this->text), 72, MAILHEADER_EOL); 519 } else { // multi part it is 520 $body .= "This is a multi-part message in MIME format." . MAILHEADER_EOL; 521 522 // prepare the attachments 523 $attachments = $this->prepareAttachments(); 524 525 // do we have alternative text content? 526 if ($this->text && $this->html) { 527 $this->headers['Content-Type'] = 'multipart/alternative;' . MAILHEADER_EOL . 528 ' boundary="' . $this->boundary . 'XX"'; 529 $body .= '--' . $this->boundary . 'XX' . MAILHEADER_EOL; 530 $body .= 'Content-Type: text/plain; charset=UTF-8' . MAILHEADER_EOL; 531 $body .= 'Content-Transfer-Encoding: base64' . MAILHEADER_EOL; 532 $body .= MAILHEADER_EOL; 533 $body .= chunk_split(base64_encode($this->text), 72, MAILHEADER_EOL); 534 $body .= '--' . $this->boundary . 'XX' . MAILHEADER_EOL; 535 $body .= 'Content-Type: multipart/related;' . MAILHEADER_EOL . 536 ' boundary="' . $this->boundary . '";' . MAILHEADER_EOL . 537 ' type="text/html"' . MAILHEADER_EOL; 538 $body .= MAILHEADER_EOL; 539 } 540 541 $body .= '--' . $this->boundary . MAILHEADER_EOL; 542 $body .= 'Content-Type: text/html; charset=UTF-8' . MAILHEADER_EOL; 543 $body .= 'Content-Transfer-Encoding: base64' . MAILHEADER_EOL; 544 $body .= MAILHEADER_EOL; 545 $body .= chunk_split(base64_encode($this->html), 72, MAILHEADER_EOL); 546 $body .= MAILHEADER_EOL; 547 $body .= $attachments; 548 $body .= '--' . $this->boundary . '--' . MAILHEADER_EOL; 549 550 // close open multipart/alternative boundary 551 if ($this->text && $this->html) { 552 $body .= '--' . $this->boundary . 'XX--' . MAILHEADER_EOL; 553 } 554 } 555 556 return $body; 557 } 558 559 /** 560 * Cleanup and encode the headers array 561 */ 562 protected function cleanHeaders() 563 { 564 global $conf; 565 566 // clean up addresses 567 if (empty($this->headers['From'])) $this->from($conf['mailfrom']); 568 $addrs = ['To', 'From', 'Cc', 'Bcc', 'Reply-To', 'Sender']; 569 foreach ($addrs as $addr) { 570 if (isset($this->headers[$addr])) { 571 $this->headers[$addr] = $this->cleanAddress($this->headers[$addr]); 572 } 573 } 574 575 if (isset($this->headers['Subject'])) { 576 // add prefix to subject 577 if (empty($conf['mailprefix'])) { 578 if (PhpString::strlen($conf['title']) < 20) { 579 $prefix = '[' . $conf['title'] . ']'; 580 } else { 581 $prefix = '[' . PhpString::substr($conf['title'], 0, 20) . '...]'; 582 } 583 } else { 584 $prefix = '[' . $conf['mailprefix'] . ']'; 585 } 586 if (!str_starts_with($this->headers['Subject'], $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 * 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 ( 745 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