You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

798 lines
26 KiB

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