xref: /dokuwiki/inc/Mailer.class.php (revision 73dc0a8919857718a3b64a4c0741b57580a34b2a)
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