xref: /dokuwiki/inc/mail.php (revision 868bf7c9ce8ea9f2e433c53e37c74f8140127a1e)
1<?php
2/**
3 * Mail functions
4 *
5 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
6 * @author     Andreas Gohr <andi@splitbrain.org>
7 */
8
9// end of line for mail lines - RFC822 says CRLF but postfix (and other MTAs?)
10// think different
11use dokuwiki\Extension\Event;
12
13if(!defined('MAILHEADER_EOL')) define('MAILHEADER_EOL',"\n");
14#define('MAILHEADER_ASCIIONLY',1);
15
16/**
17 * Patterns for use in email detection and validation
18 *
19 * NOTE: there is an unquoted '/' in RFC2822_ATEXT, it must remain unquoted to be used in the parser
20 * the pattern uses non-capturing groups as captured groups aren't allowed in the parser
21 * select pattern delimiters with care!
22 *
23 * May not be completly RFC conform!
24 * @link http://www.faqs.org/rfcs/rfc2822.html (paras 3.4.1 & 3.2.4)
25 *
26 * @author Chris Smith <chris@jalakai.co.uk>
27 * Check if a given mail address is valid
28 */
29if (!defined('RFC2822_ATEXT')) define('RFC2822_ATEXT',"0-9a-zA-Z!#$%&'*+/=?^_`{|}~-");
30if (!defined('PREG_PATTERN_VALID_EMAIL')) define(
31    'PREG_PATTERN_VALID_EMAIL',
32    '['.RFC2822_ATEXT.']+(?:\.['.RFC2822_ATEXT.']+)*@(?i:[0-9a-z][0-9a-z-]*\.)+(?i:[a-z]{2,63})'
33);
34
35/**
36 * Prepare mailfrom replacement patterns
37 *
38 * Also prepares a mailfromnobody config that contains an autoconstructed address
39 * if the mailfrom one is userdependent and this might not be wanted (subscriptions)
40 *
41 * @author Andreas Gohr <andi@splitbrain.org>
42 */
43function mail_setup(){
44    global $conf;
45    global $USERINFO;
46    /** @var Input $INPUT */
47    global $INPUT;
48
49    // auto constructed address
50    $host = @parse_url(DOKU_URL,PHP_URL_HOST);
51    if(!$host) $host = 'example.com';
52    $noreply = 'noreply@'.$host;
53
54    $replace = array();
55    if(!empty($USERINFO['mail'])){
56        $replace['@MAIL@'] = $USERINFO['mail'];
57    }else{
58        $replace['@MAIL@'] = $noreply;
59    }
60
61    // use 'noreply' if no user
62    $replace['@USER@'] = $INPUT->server->str('REMOTE_USER', 'noreply', true);
63
64    if(!empty($USERINFO['name'])){
65        $replace['@NAME@'] = $USERINFO['name'];
66    }else{
67        $replace['@NAME@'] = '';
68    }
69
70    // apply replacements
71    $from = str_replace(array_keys($replace),
72                        array_values($replace),
73                        $conf['mailfrom']);
74
75    // any replacements done? set different mailfromnone
76    if($from != $conf['mailfrom']){
77        $conf['mailfromnobody'] = $noreply;
78    }else{
79        $conf['mailfromnobody'] = $from;
80    }
81    $conf['mailfrom'] = $from;
82}
83
84/**
85 * UTF-8 autoencoding replacement for PHPs mail function
86 *
87 * Email address fields (To, From, Cc, Bcc can contain a textpart and an address
88 * like this: 'Andreas Gohr <andi@splitbrain.org>' - the text part is encoded
89 * automatically. You can seperate receivers by commas.
90 *
91 * @param string $to      Receiver of the mail (multiple seperated by commas)
92 * @param string $subject Mailsubject
93 * @param string $body    Messagebody
94 * @param string $from    Sender address
95 * @param string $cc      CarbonCopy receiver (multiple seperated by commas)
96 * @param string $bcc     BlindCarbonCopy receiver (multiple seperated by commas)
97 * @param string $headers Additional Headers (seperated by MAILHEADER_EOL
98 * @param string $params  Additonal Sendmail params (passed to mail())
99 *
100 * @author Andreas Gohr <andi@splitbrain.org>
101 * @see    mail()
102 *
103 * @deprecated User the Mailer:: class instead
104 */
105function mail_send($to, $subject, $body, $from='', $cc='', $bcc='', $headers=null, $params=null){
106    dbg_deprecated('class Mailer::');
107    $message = compact('to','subject','body','from','cc','bcc','headers','params');
108    return Event::createAndTrigger('MAIL_MESSAGE_SEND',$message,'_mail_send_action');
109}
110
111/**
112 * @param $data
113 * @return bool
114 *
115 * @deprecated User the Mailer:: class instead
116 */
117function _mail_send_action($data) {
118    dbg_deprecated('class Mailer::');
119    // retrieve parameters from event data, $to, $subject, $body, $from, $cc, $bcc, $headers, $params
120    $to = $data['to'];
121    $subject = $data['subject'];
122    $body = $data['body'];
123
124    // add robustness in case plugin removes any of these optional values
125    $from = isset($data['from']) ? $data['from'] : '';
126    $cc = isset($data['cc']) ? $data['cc'] : '';
127    $bcc = isset($data['bcc']) ? $data['bcc'] : '';
128    $headers = isset($data['headers']) ? $data['headers'] : null;
129    $params = isset($data['params']) ? $data['params'] : null;
130
131    // discard mail request if no recipients are available
132    if(trim($to) === '' && trim($cc) === '' && trim($bcc) === '') return false;
133
134    // end additional code to support event ... original mail_send() code from here
135
136    if(defined('MAILHEADER_ASCIIONLY')){
137        $subject = \dokuwiki\Utf8\Clean::deaccent($subject);
138        $subject = \dokuwiki\Utf8\Clean::strip($subject);
139    }
140
141    if(!\dokuwiki\Utf8\Clean::isASCII($subject)) {
142        $enc_subj = '=?UTF-8?Q?'.mail_quotedprintable_encode($subject,0).'?=';
143        // Spaces must be encoded according to rfc2047. Use the "_" shorthand
144        $enc_subj = preg_replace('/ /', '_', $enc_subj);
145
146        // quoted printable has length restriction, use base64 if needed
147        if(strlen($subject) > 74){
148            $enc_subj = '=?UTF-8?B?'.base64_encode($subject).'?=';
149        }
150
151        $subject = $enc_subj;
152    }
153
154    $header  = '';
155
156    // No named recipients for To: in Windows (see FS#652)
157    $usenames = (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') ? false : true;
158
159    $to = mail_encode_address($to,'',$usenames);
160    $header .= mail_encode_address($from,'From');
161    $header .= mail_encode_address($cc,'Cc');
162    $header .= mail_encode_address($bcc,'Bcc');
163    $header .= 'MIME-Version: 1.0'.MAILHEADER_EOL;
164    $header .= 'Content-Type: text/plain; charset=UTF-8'.MAILHEADER_EOL;
165    $header .= 'Content-Transfer-Encoding: quoted-printable'.MAILHEADER_EOL;
166    $header .= $headers;
167    $header  = trim($header);
168
169    $body = mail_quotedprintable_encode($body);
170
171    if($params == null){
172        return @mail($to,$subject,$body,$header);
173    }else{
174        return @mail($to,$subject,$body,$header,$params);
175    }
176}
177
178/**
179 * Encodes an email address header
180 *
181 * Unicode characters will be deaccented and encoded
182 * quoted_printable for headers.
183 * Addresses may not contain Non-ASCII data!
184 *
185 * Example:
186 *   mail_encode_address("föö <foo@bar.com>, me@somewhere.com","TBcc");
187 *
188 * @param string  $string Multiple adresses separated by commas
189 * @param string  $header Name of the header (To,Bcc,Cc,...)
190 * @param boolean $names  Allow named Recipients?
191 *
192 * @deprecated User the Mailer:: class instead
193 */
194function mail_encode_address($string,$header='',$names=true){
195    dbg_deprecated('class Mailer::');
196    $headers = '';
197    $parts = explode(',',$string);
198    foreach ($parts as $part){
199        $part = trim($part);
200
201        // parse address
202        if(preg_match('#(.*?)<(.*?)>#',$part,$matches)){
203            $text = trim($matches[1]);
204            $addr = $matches[2];
205        }else{
206            $addr = $part;
207        }
208
209        // skip empty ones
210        if(empty($addr)){
211            continue;
212        }
213
214        // FIXME: is there a way to encode the localpart of a emailaddress?
215        if(!\dokuwiki\Utf8\Clean::isASCII($addr)){
216            msg(hsc("E-Mail address <$addr> is not ASCII"),-1);
217            continue;
218        }
219
220        if(!mail_isvalid($addr)){
221            msg(hsc("E-Mail address <$addr> is not valid"),-1);
222            continue;
223        }
224
225        // text was given
226        if(!empty($text) && $names){
227            // add address quotes
228            $addr = "<$addr>";
229
230            if(defined('MAILHEADER_ASCIIONLY')){
231                $text = \dokuwiki\Utf8\Clean::deaccent($text);
232                $text = \dokuwiki\Utf8\Clean::strip($text);
233            }
234
235            if(!\dokuwiki\Utf8\Clean::isASCII($text)){
236                // put the quotes outside as in =?UTF-8?Q?"Elan Ruusam=C3=A4e"?= vs "=?UTF-8?Q?Elan Ruusam=C3=A4e?="
237                if (preg_match('/^"(.+)"$/', $text, $matches)) {
238                    $text = '"=?UTF-8?Q?'.mail_quotedprintable_encode($matches[1], 0).'?="';
239                } else {
240                    $text = '=?UTF-8?Q?'.mail_quotedprintable_encode($text, 0).'?=';
241                }
242                // additionally the space character should be encoded as =20 (or each
243                // word QP encoded separately).
244                // however this is needed only in mail headers, not globally in mail_quotedprintable_encode().
245                $text = str_replace(" ", "=20", $text);
246            }
247        }else{
248            $text = '';
249        }
250
251        // add to header comma seperated
252        if($headers != ''){
253            $headers .= ',';
254            if($header) $headers .= MAILHEADER_EOL.' '; // avoid overlong mail headers
255        }
256        $headers .= $text.' '.$addr;
257    }
258
259    if(empty($headers)) return null;
260
261    //if headername was given add it and close correctly
262    if($header) $headers = $header.': '.$headers.MAILHEADER_EOL;
263
264    return $headers;
265}
266
267/**
268 * Check if a given mail address is valid
269 *
270 * @param   string $email the address to check
271 * @return  bool          true if address is valid
272 */
273function mail_isvalid($email) {
274    return EmailAddressValidator::checkEmailAddress($email, true);
275}
276
277/**
278 * Quoted printable encoding
279 *
280 * @author umu <umuAThrz.tu-chemnitz.de>
281 * @link   http://php.net/manual/en/function.imap-8bit.php#61216
282 *
283 * @param string $sText
284 * @param int $maxlen
285 * @param bool $bEmulate_imap_8bit
286 *
287 * @return string
288 */
289function mail_quotedprintable_encode($sText,$maxlen=74,$bEmulate_imap_8bit=true) {
290    // split text into lines
291    $aLines= preg_split("/(?:\r\n|\r|\n)/", $sText);
292    $cnt = count($aLines);
293
294    for ($i=0;$i<$cnt;$i++) {
295        $sLine =& $aLines[$i];
296        if (strlen($sLine)===0) continue; // do nothing, if empty
297
298        $sRegExp = '/[^\x09\x20\x21-\x3C\x3E-\x7E]/e';
299
300        // imap_8bit encodes x09 everywhere, not only at lineends,
301        // for EBCDIC safeness encode !"#$@[\]^`{|}~,
302        // for complete safeness encode every character :)
303        if ($bEmulate_imap_8bit)
304            $sRegExp = '/[^\x20\x21-\x3C\x3E-\x7E]/';
305
306        $sLine = preg_replace_callback( $sRegExp, 'mail_quotedprintable_encode_callback', $sLine );
307
308        // encode x09,x20 at lineends
309        {
310            $iLength = strlen($sLine);
311            $iLastChar = ord($sLine[$iLength-1]);
312
313            //              !!!!!!!!
314            // imap_8_bit does not encode x20 at the very end of a text,
315            // here is, where I don't agree with imap_8_bit,
316            // please correct me, if I'm wrong,
317            // or comment next line for RFC2045 conformance, if you like
318            if (!($bEmulate_imap_8bit && ($i==count($aLines)-1))){
319                if (($iLastChar==0x09)||($iLastChar==0x20)) {
320                    $sLine[$iLength-1]='=';
321                    $sLine .= ($iLastChar==0x09)?'09':'20';
322                }
323            }
324        }    // imap_8bit encodes x20 before chr(13), too
325        // although IMHO not requested by RFC2045, why not do it safer :)
326        // and why not encode any x20 around chr(10) or chr(13)
327        if ($bEmulate_imap_8bit) {
328            $sLine=str_replace(' =0D','=20=0D',$sLine);
329            //$sLine=str_replace(' =0A','=20=0A',$sLine);
330            //$sLine=str_replace('=0D ','=0D=20',$sLine);
331            //$sLine=str_replace('=0A ','=0A=20',$sLine);
332        }
333
334        // finally split into softlines no longer than $maxlen chars,
335        // for even more safeness one could encode x09,x20
336        // at the very first character of the line
337        // and after soft linebreaks, as well,
338        // but this wouldn't be caught by such an easy RegExp
339        if($maxlen){
340            preg_match_all( '/.{1,'.($maxlen - 2).'}([^=]{0,2})?/', $sLine, $aMatch );
341            $sLine = implode( '=' . MAILHEADER_EOL, $aMatch[0] ); // add soft crlf's
342        }
343    }
344
345    // join lines into text
346    return implode(MAILHEADER_EOL,$aLines);
347}
348
349function mail_quotedprintable_encode_callback($matches){
350    return sprintf( "=%02X", ord ( $matches[0] ) ) ;
351}
352