Issues (847)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

inc/Mailer.class.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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 <[email protected]>
10
 */
11
12
use dokuwiki\Extension\Event;
13
14
// end of line for mail lines - RFC822 says CRLF but postfix (and other MTAs?)
15
// think different
16
if(!defined('MAILHEADER_EOL')) define('MAILHEADER_EOL', "\n");
17
#define('MAILHEADER_ASCIIONLY',1);
18
19
/**
20
 * Mail Handling
21
 */
22
class Mailer {
23
24
    protected $headers   = array();
25
    protected $attach    = array();
26
    protected $html      = '';
27
    protected $text      = '';
28
29
    protected $boundary  = '';
30
    protected $partid    = '';
31
    protected $sendparam = null;
32
33
    protected $allowhtml = true;
34
35
    protected $replacements = array('text'=> array(), 'html' => array());
36
37
    /**
38
     * Constructor
39
     *
40
     * Initializes the boundary strings, part counters and token replacements
41
     */
42
    public function __construct() {
43
        global $conf;
44
        /* @var Input $INPUT */
45
        global $INPUT;
46
47
        $server = parse_url(DOKU_URL, PHP_URL_HOST);
48
        if(strpos($server,'.') === false) $server .= '.localhost';
49
50
        $this->partid   = substr(md5(uniqid(mt_rand(), true)),0, 8).'@'.$server;
51
        $this->boundary = '__________'.md5(uniqid(mt_rand(), true));
52
53
        $listid = implode('.', array_reverse(explode('/', DOKU_BASE))).$server;
54
        $listid = strtolower(trim($listid, '.'));
55
56
        $this->allowhtml = (bool)$conf['htmlmail'];
57
58
        // add some default headers for mailfiltering FS#2247
59
        if(!empty($conf['mailreturnpath'])) {
60
            $this->setHeader('Return-Path', $conf['mailreturnpath']);
61
        }
62
        $this->setHeader('X-Mailer', 'DokuWiki');
63
        $this->setHeader('X-DokuWiki-User', $INPUT->server->str('REMOTE_USER'));
64
        $this->setHeader('X-DokuWiki-Title', $conf['title']);
65
        $this->setHeader('X-DokuWiki-Server', $server);
0 ignored issues
show
It seems like $server defined by parse_url(DOKU_URL, PHP_URL_HOST) on line 47 can also be of type false; however, Mailer::setHeader() does only seem to accept string|array<integer,string>, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

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