[ Index ]

PHP Cross Reference of DokuWiki

title

Body

[close]

/inc/ -> Mailer.class.php (source)

   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  
  13  use dokuwiki\Utf8\PhpString;
  14  use dokuwiki\Utf8\Clean;
  15  use dokuwiki\Extension\Event;
  16  
  17  /**
  18   * Mail Handling
  19   */
  20  class 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  }