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