xref: /dokuwiki/inc/mail.php (revision 8cbc5ee84fe788597ede5266255a74af6da47555)
1ed7b5f09Sandi<?php
244f669e9Sandi/**
344f669e9Sandi * Mail functions
444f669e9Sandi *
544f669e9Sandi * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
644f669e9Sandi * @author     Andreas Gohr <andi@splitbrain.org>
744f669e9Sandi */
844f669e9Sandi
929f3a5faSAndreas Gohr// end of line for mail lines - RFC822 says CRLF but postfix (and other MTAs?)
1029f3a5faSAndreas Gohr// think different
11cbb44eabSAndreas Gohruse dokuwiki\Extension\Event;
12cbb44eabSAndreas Gohr
1329f3a5faSAndreas Gohrif(!defined('MAILHEADER_EOL')) define('MAILHEADER_EOL',"\n");
14e1906e6eSandi#define('MAILHEADER_ASCIIONLY',1);
1544f669e9Sandi
1644f669e9Sandi/**
17d530a62aSAndreas Gohr * Patterns for use in email detection and validation
18d530a62aSAndreas Gohr *
19d530a62aSAndreas Gohr * NOTE: there is an unquoted '/' in RFC2822_ATEXT, it must remain unquoted to be used in the parser
20d530a62aSAndreas Gohr * the pattern uses non-capturing groups as captured groups aren't allowed in the parser
21d530a62aSAndreas Gohr * select pattern delimiters with care!
22d530a62aSAndreas Gohr *
23d530a62aSAndreas Gohr * May not be completly RFC conform!
24d530a62aSAndreas Gohr * @link http://www.faqs.org/rfcs/rfc2822.html (paras 3.4.1 & 3.2.4)
25d530a62aSAndreas Gohr *
26d530a62aSAndreas Gohr * @author Chris Smith <chris@jalakai.co.uk>
27d530a62aSAndreas Gohr * Check if a given mail address is valid
28d530a62aSAndreas Gohr */
29d530a62aSAndreas Gohrif (!defined('RFC2822_ATEXT')) define('RFC2822_ATEXT',"0-9a-zA-Z!#$%&'*+/=?^_`{|}~-");
3064159a61SAndreas Gohrif (!defined('PREG_PATTERN_VALID_EMAIL')) define(
3164159a61SAndreas Gohr    'PREG_PATTERN_VALID_EMAIL',
3264159a61SAndreas Gohr    '['.RFC2822_ATEXT.']+(?:\.['.RFC2822_ATEXT.']+)*@(?i:[0-9a-z][0-9a-z-]*\.)+(?i:[a-z]{2,63})'
3364159a61SAndreas Gohr);
34d530a62aSAndreas Gohr
355ec3fefcSAndreas Gohr/**
365ec3fefcSAndreas Gohr * Prepare mailfrom replacement patterns
375ec3fefcSAndreas Gohr *
38465e809bSAndreas Gohr * Also prepares a mailfromnobody config that contains an autoconstructed address
39f7cefc02SAndreas Gohr * if the mailfrom one is userdependent and this might not be wanted (subscriptions)
40f7cefc02SAndreas Gohr *
415ec3fefcSAndreas Gohr * @author Andreas Gohr <andi@splitbrain.org>
425ec3fefcSAndreas Gohr */
435ec3fefcSAndreas Gohrfunction mail_setup(){
445ec3fefcSAndreas Gohr    global $conf;
45609c41e4SElan Ruusamäe    global $USERINFO;
46585bf44eSChristopher Smith    /** @var Input $INPUT */
47585bf44eSChristopher Smith    global $INPUT;
48d530a62aSAndreas Gohr
49f7cefc02SAndreas Gohr    // auto constructed address
50f7cefc02SAndreas Gohr    $host = @parse_url(DOKU_URL,PHP_URL_HOST);
51f7cefc02SAndreas Gohr    if(!$host) $host = 'example.com';
52f7cefc02SAndreas Gohr    $noreply = 'noreply@'.$host;
535ec3fefcSAndreas Gohr
54f7cefc02SAndreas Gohr    $replace = array();
55609c41e4SElan Ruusamäe    if(!empty($USERINFO['mail'])){
56609c41e4SElan Ruusamäe        $replace['@MAIL@'] = $USERINFO['mail'];
575ec3fefcSAndreas Gohr    }else{
58f7cefc02SAndreas Gohr        $replace['@MAIL@'] = $noreply;
595ec3fefcSAndreas Gohr    }
605ec3fefcSAndreas Gohr
61585bf44eSChristopher Smith    // use 'noreply' if no user
62585bf44eSChristopher Smith    $replace['@USER@'] = $INPUT->server->str('REMOTE_USER', 'noreply', true);
635ec3fefcSAndreas Gohr
64609c41e4SElan Ruusamäe    if(!empty($USERINFO['name'])){
65609c41e4SElan Ruusamäe        $replace['@NAME@'] = $USERINFO['name'];
665ec3fefcSAndreas Gohr    }else{
675ec3fefcSAndreas Gohr        $replace['@NAME@'] = '';
685ec3fefcSAndreas Gohr    }
695ec3fefcSAndreas Gohr
70f7cefc02SAndreas Gohr    // apply replacements
71f7cefc02SAndreas Gohr    $from = str_replace(array_keys($replace),
725ec3fefcSAndreas Gohr                        array_values($replace),
735ec3fefcSAndreas Gohr                        $conf['mailfrom']);
74f7cefc02SAndreas Gohr
75f7cefc02SAndreas Gohr    // any replacements done? set different mailfromnone
76f7cefc02SAndreas Gohr    if($from != $conf['mailfrom']){
77465e809bSAndreas Gohr        $conf['mailfromnobody'] = $noreply;
78f7cefc02SAndreas Gohr    }else{
79465e809bSAndreas Gohr        $conf['mailfromnobody'] = $from;
80f7cefc02SAndreas Gohr    }
81f7cefc02SAndreas Gohr    $conf['mailfrom'] = $from;
825ec3fefcSAndreas Gohr}
83d530a62aSAndreas Gohr
84d530a62aSAndreas Gohr/**
8544f669e9Sandi * UTF-8 autoencoding replacement for PHPs mail function
8644f669e9Sandi *
8744f669e9Sandi * Email address fields (To, From, Cc, Bcc can contain a textpart and an address
8844f669e9Sandi * like this: 'Andreas Gohr <andi@splitbrain.org>' - the text part is encoded
8944f669e9Sandi * automatically. You can seperate receivers by commas.
9044f669e9Sandi *
9144f669e9Sandi * @param string $to      Receiver of the mail (multiple seperated by commas)
9244f669e9Sandi * @param string $subject Mailsubject
9344f669e9Sandi * @param string $body    Messagebody
9444f669e9Sandi * @param string $from    Sender address
9544f669e9Sandi * @param string $cc      CarbonCopy receiver (multiple seperated by commas)
9644f669e9Sandi * @param string $bcc     BlindCarbonCopy receiver (multiple seperated by commas)
9744f669e9Sandi * @param string $headers Additional Headers (seperated by MAILHEADER_EOL
9844f669e9Sandi * @param string $params  Additonal Sendmail params (passed to mail())
9944f669e9Sandi *
10044f669e9Sandi * @author Andreas Gohr <andi@splitbrain.org>
10144f669e9Sandi * @see    mail()
1020a71e551SMichael Große *
1030a71e551SMichael Große * @deprecated User the Mailer:: class instead
10444f669e9Sandi */
10544f669e9Sandifunction mail_send($to, $subject, $body, $from='', $cc='', $bcc='', $headers=null, $params=null){
1060a71e551SMichael Große    dbg_deprecated('class Mailer::');
1071986aa9eSChris Smith    $message = compact('to','subject','body','from','cc','bcc','headers','params');
108cbb44eabSAndreas Gohr    return Event::createAndTrigger('MAIL_MESSAGE_SEND',$message,'_mail_send_action');
1091986aa9eSChris Smith}
1101986aa9eSChris Smith
1110a71e551SMichael Große/**
1120a71e551SMichael Große * @param $data
1130a71e551SMichael Große * @return bool
1140a71e551SMichael Große *
1150a71e551SMichael Große * @deprecated User the Mailer:: class instead
1160a71e551SMichael Große */
117348e3c03SChris Smithfunction _mail_send_action($data) {
1180a71e551SMichael Große    dbg_deprecated('class Mailer::');
119348e3c03SChris Smith    // retrieve parameters from event data, $to, $subject, $body, $from, $cc, $bcc, $headers, $params
120348e3c03SChris Smith    $to = $data['to'];
121348e3c03SChris Smith    $subject = $data['subject'];
122348e3c03SChris Smith    $body = $data['body'];
123348e3c03SChris Smith
124348e3c03SChris Smith    // add robustness in case plugin removes any of these optional values
125348e3c03SChris Smith    $from = isset($data['from']) ? $data['from'] : '';
126348e3c03SChris Smith    $cc = isset($data['cc']) ? $data['cc'] : '';
127348e3c03SChris Smith    $bcc = isset($data['bcc']) ? $data['bcc'] : '';
128348e3c03SChris Smith    $headers = isset($data['headers']) ? $data['headers'] : null;
129348e3c03SChris Smith    $params = isset($data['params']) ? $data['params'] : null;
130348e3c03SChris Smith
13196569d48SMatthias Schulte    // discard mail request if no recipients are available
13296569d48SMatthias Schulte    if(trim($to) === '' && trim($cc) === '' && trim($bcc) === '') return false;
13396569d48SMatthias Schulte
134348e3c03SChris Smith    // end additional code to support event ... original mail_send() code from here
135348e3c03SChris Smith
136e1906e6eSandi    if(defined('MAILHEADER_ASCIIONLY')){
137*8cbc5ee8SAndreas Gohr        $subject = \dokuwiki\Utf8\Clean::deaccent($subject);
138*8cbc5ee8SAndreas Gohr        $subject = \dokuwiki\Utf8\Clean::strip($subject);
139e1906e6eSandi    }
140e1906e6eSandi
141*8cbc5ee8SAndreas Gohr    if(!\dokuwiki\Utf8\Clean::isASCII($subject)) {
1427e8e923fSAndreas Gohr        $enc_subj = '=?UTF-8?Q?'.mail_quotedprintable_encode($subject,0).'?=';
143265f02e3SGuy Brand        // Spaces must be encoded according to rfc2047. Use the "_" shorthand
144ddcd5ab6SKazutaka Miyasaka        $enc_subj = preg_replace('/ /', '_', $enc_subj);
1457e8e923fSAndreas Gohr
1467e8e923fSAndreas Gohr        // quoted printable has length restriction, use base64 if needed
1477e8e923fSAndreas Gohr        if(strlen($subject) > 74){
1487e8e923fSAndreas Gohr            $enc_subj = '=?UTF-8?B?'.base64_encode($subject).'?=';
1497e8e923fSAndreas Gohr        }
1507e8e923fSAndreas Gohr
1517e8e923fSAndreas Gohr        $subject = $enc_subj;
152265f02e3SGuy Brand    }
15344f669e9Sandi
15444f669e9Sandi    $header  = '';
155e1906e6eSandi
156f95e691eSAndreas Gohr    // No named recipients for To: in Windows (see FS#652)
157f95e691eSAndreas Gohr    $usenames = (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') ? false : true;
158f95e691eSAndreas Gohr
159f95e691eSAndreas Gohr    $to = mail_encode_address($to,'',$usenames);
16044f669e9Sandi    $header .= mail_encode_address($from,'From');
16144f669e9Sandi    $header .= mail_encode_address($cc,'Cc');
16244f669e9Sandi    $header .= mail_encode_address($bcc,'Bcc');
16344f669e9Sandi    $header .= 'MIME-Version: 1.0'.MAILHEADER_EOL;
16444f669e9Sandi    $header .= 'Content-Type: text/plain; charset=UTF-8'.MAILHEADER_EOL;
16544f669e9Sandi    $header .= 'Content-Transfer-Encoding: quoted-printable'.MAILHEADER_EOL;
16644f669e9Sandi    $header .= $headers;
167e1906e6eSandi    $header  = trim($header);
16844f669e9Sandi
16944f669e9Sandi    $body = mail_quotedprintable_encode($body);
17044f669e9Sandi
171d7785cceSandi    if($params == null){
172d7785cceSandi        return @mail($to,$subject,$body,$header);
173d7785cceSandi    }else{
174e1906e6eSandi        return @mail($to,$subject,$body,$header,$params);
17544f669e9Sandi    }
176d7785cceSandi}
17744f669e9Sandi
17844f669e9Sandi/**
17944f669e9Sandi * Encodes an email address header
18044f669e9Sandi *
181e1906e6eSandi * Unicode characters will be deaccented and encoded
182e1906e6eSandi * quoted_printable for headers.
18344f669e9Sandi * Addresses may not contain Non-ASCII data!
18444f669e9Sandi *
18544f669e9Sandi * Example:
186a2021ad8Sandi *   mail_encode_address("föö <foo@bar.com>, me@somewhere.com","TBcc");
18744f669e9Sandi *
188f95e691eSAndreas Gohr * @param string  $string Multiple adresses separated by commas
189f95e691eSAndreas Gohr * @param string  $header Name of the header (To,Bcc,Cc,...)
190f95e691eSAndreas Gohr * @param boolean $names  Allow named Recipients?
1910a71e551SMichael Große *
1920a71e551SMichael Große * @deprecated User the Mailer:: class instead
19344f669e9Sandi */
194f95e691eSAndreas Gohrfunction mail_encode_address($string,$header='',$names=true){
1950a71e551SMichael Große    dbg_deprecated('class Mailer::');
19644f669e9Sandi    $headers = '';
1974b7f9e70STom N Harris    $parts = explode(',',$string);
19844f669e9Sandi    foreach ($parts as $part){
19944f669e9Sandi        $part = trim($part);
20044f669e9Sandi
20144f669e9Sandi        // parse address
20244f669e9Sandi        if(preg_match('#(.*?)<(.*?)>#',$part,$matches)){
20344f669e9Sandi            $text = trim($matches[1]);
20444f669e9Sandi            $addr = $matches[2];
20544f669e9Sandi        }else{
20644f669e9Sandi            $addr = $part;
20744f669e9Sandi        }
20844f669e9Sandi
20944f669e9Sandi        // skip empty ones
21044f669e9Sandi        if(empty($addr)){
21144f669e9Sandi            continue;
21244f669e9Sandi        }
21344f669e9Sandi
21444f669e9Sandi        // FIXME: is there a way to encode the localpart of a emailaddress?
215*8cbc5ee8SAndreas Gohr        if(!\dokuwiki\Utf8\Clean::isASCII($addr)){
21665cc1598SPhy            msg(hsc("E-Mail address <$addr> is not ASCII"),-1);
21744f669e9Sandi            continue;
21844f669e9Sandi        }
21944f669e9Sandi
22044f669e9Sandi        if(!mail_isvalid($addr)){
22165cc1598SPhy            msg(hsc("E-Mail address <$addr> is not valid"),-1);
22244f669e9Sandi            continue;
22344f669e9Sandi        }
22444f669e9Sandi
225a2021ad8Sandi        // text was given
226f95e691eSAndreas Gohr        if(!empty($text) && $names){
227a2021ad8Sandi            // add address quotes
228a2021ad8Sandi            $addr = "<$addr>";
22944f669e9Sandi
230e1906e6eSandi            if(defined('MAILHEADER_ASCIIONLY')){
231*8cbc5ee8SAndreas Gohr                $text = \dokuwiki\Utf8\Clean::deaccent($text);
232*8cbc5ee8SAndreas Gohr                $text = \dokuwiki\Utf8\Clean::strip($text);
233e1906e6eSandi            }
234e1906e6eSandi
235*8cbc5ee8SAndreas Gohr            if(!\dokuwiki\Utf8\Clean::isASCII($text)){
2360667123fSElan Ruusamäe                // put the quotes outside as in =?UTF-8?Q?"Elan Ruusam=C3=A4e"?= vs "=?UTF-8?Q?Elan Ruusam=C3=A4e?="
2370667123fSElan Ruusamäe                if (preg_match('/^"(.+)"$/', $text, $matches)) {
2380667123fSElan Ruusamäe                    $text = '"=?UTF-8?Q?'.mail_quotedprintable_encode($matches[1], 0).'?="';
2390667123fSElan Ruusamäe                } else {
24091275a65SAndreas Gohr                    $text = '=?UTF-8?Q?'.mail_quotedprintable_encode($text, 0).'?=';
24144f669e9Sandi                }
2420667123fSElan Ruusamäe                // additionally the space character should be encoded as =20 (or each
2430667123fSElan Ruusamäe                // word QP encoded separately).
2440667123fSElan Ruusamäe                // however this is needed only in mail headers, not globally in mail_quotedprintable_encode().
2450667123fSElan Ruusamäe                $text = str_replace(" ", "=20", $text);
2460667123fSElan Ruusamäe            }
247628e6ba7SAndreas Gohr        }else{
248628e6ba7SAndreas Gohr            $text = '';
24944f669e9Sandi        }
25044f669e9Sandi
2512300b3e6SAndreas Gohr        // add to header comma seperated
2522300b3e6SAndreas Gohr        if($headers != ''){
2532300b3e6SAndreas Gohr            $headers .= ',';
2542300b3e6SAndreas Gohr            if($header) $headers .= MAILHEADER_EOL.' '; // avoid overlong mail headers
2552300b3e6SAndreas Gohr        }
256ba36e50eSAndreas Gohr        $headers .= $text.' '.$addr;
257a2021ad8Sandi    }
258a2021ad8Sandi
259a2021ad8Sandi    if(empty($headers)) return null;
260a2021ad8Sandi
261a2021ad8Sandi    //if headername was given add it and close correctly
262a2021ad8Sandi    if($header) $headers = $header.': '.$headers.MAILHEADER_EOL;
263a2021ad8Sandi
26444f669e9Sandi    return $headers;
26544f669e9Sandi}
26644f669e9Sandi
26744f669e9Sandi/**
268e8f8d645SAndreas Gohr * Check if a given mail address is valid
26944f669e9Sandi *
27044f669e9Sandi * @param   string $email the address to check
27144f669e9Sandi * @return  bool          true if address is valid
27244f669e9Sandi */
27344f669e9Sandifunction mail_isvalid($email) {
2743d4e3335SAndreas Gohr    return EmailAddressValidator::checkEmailAddress($email, true);
27544f669e9Sandi}
27644f669e9Sandi
27744f669e9Sandi/**
27844f669e9Sandi * Quoted printable encoding
27944f669e9Sandi *
28091275a65SAndreas Gohr * @author umu <umuAThrz.tu-chemnitz.de>
28159752844SAnders Sandblad * @link   http://php.net/manual/en/function.imap-8bit.php#61216
282f50a239bSTakamura *
283f50a239bSTakamura * @param string $sText
284f50a239bSTakamura * @param int $maxlen
285f50a239bSTakamura * @param bool $bEmulate_imap_8bit
286f50a239bSTakamura *
287f50a239bSTakamura * @return string
28844f669e9Sandi */
28991275a65SAndreas Gohrfunction mail_quotedprintable_encode($sText,$maxlen=74,$bEmulate_imap_8bit=true) {
29091275a65SAndreas Gohr    // split text into lines
29191275a65SAndreas Gohr    $aLines= preg_split("/(?:\r\n|\r|\n)/", $sText);
292db959ae3SAndreas Gohr    $cnt = count($aLines);
29391275a65SAndreas Gohr
294db959ae3SAndreas Gohr    for ($i=0;$i<$cnt;$i++) {
29591275a65SAndreas Gohr        $sLine =& $aLines[$i];
29691275a65SAndreas Gohr        if (strlen($sLine)===0) continue; // do nothing, if empty
29791275a65SAndreas Gohr
29891275a65SAndreas Gohr        $sRegExp = '/[^\x09\x20\x21-\x3C\x3E-\x7E]/e';
29991275a65SAndreas Gohr
30091275a65SAndreas Gohr        // imap_8bit encodes x09 everywhere, not only at lineends,
30191275a65SAndreas Gohr        // for EBCDIC safeness encode !"#$@[\]^`{|}~,
30291275a65SAndreas Gohr        // for complete safeness encode every character :)
30391275a65SAndreas Gohr        if ($bEmulate_imap_8bit)
3049c107bd1SChristopher Smith            $sRegExp = '/[^\x20\x21-\x3C\x3E-\x7E]/';
30591275a65SAndreas Gohr
3069c107bd1SChristopher Smith        $sLine = preg_replace_callback( $sRegExp, 'mail_quotedprintable_encode_callback', $sLine );
30791275a65SAndreas Gohr
30891275a65SAndreas Gohr        // encode x09,x20 at lineends
30991275a65SAndreas Gohr        {
31091275a65SAndreas Gohr            $iLength = strlen($sLine);
31191275a65SAndreas Gohr            $iLastChar = ord($sLine{$iLength-1});
31291275a65SAndreas Gohr
31391275a65SAndreas Gohr            //              !!!!!!!!
31491275a65SAndreas Gohr            // imap_8_bit does not encode x20 at the very end of a text,
31591275a65SAndreas Gohr            // here is, where I don't agree with imap_8_bit,
31691275a65SAndreas Gohr            // please correct me, if I'm wrong,
31791275a65SAndreas Gohr            // or comment next line for RFC2045 conformance, if you like
318db959ae3SAndreas Gohr            if (!($bEmulate_imap_8bit && ($i==count($aLines)-1))){
31991275a65SAndreas Gohr                if (($iLastChar==0x09)||($iLastChar==0x20)) {
32091275a65SAndreas Gohr                    $sLine{$iLength-1}='=';
32191275a65SAndreas Gohr                    $sLine .= ($iLastChar==0x09)?'09':'20';
32244f669e9Sandi                }
323db959ae3SAndreas Gohr            }
32491275a65SAndreas Gohr        }    // imap_8bit encodes x20 before chr(13), too
32591275a65SAndreas Gohr        // although IMHO not requested by RFC2045, why not do it safer :)
32691275a65SAndreas Gohr        // and why not encode any x20 around chr(10) or chr(13)
32791275a65SAndreas Gohr        if ($bEmulate_imap_8bit) {
32891275a65SAndreas Gohr            $sLine=str_replace(' =0D','=20=0D',$sLine);
32991275a65SAndreas Gohr            //$sLine=str_replace(' =0A','=20=0A',$sLine);
33091275a65SAndreas Gohr            //$sLine=str_replace('=0D ','=0D=20',$sLine);
33191275a65SAndreas Gohr            //$sLine=str_replace('=0A ','=0A=20',$sLine);
33244f669e9Sandi        }
33344f669e9Sandi
33491275a65SAndreas Gohr        // finally split into softlines no longer than $maxlen chars,
33591275a65SAndreas Gohr        // for even more safeness one could encode x09,x20
33691275a65SAndreas Gohr        // at the very first character of the line
33791275a65SAndreas Gohr        // and after soft linebreaks, as well,
33891275a65SAndreas Gohr        // but this wouldn't be caught by such an easy RegExp
33991275a65SAndreas Gohr        if($maxlen){
34091275a65SAndreas Gohr            preg_match_all( '/.{1,'.($maxlen - 2).'}([^=]{0,2})?/', $sLine, $aMatch );
3411a6a1c04SAndreas Gohr            $sLine = implode( '=' . MAILHEADER_EOL, $aMatch[0] ); // add soft crlf's
34291275a65SAndreas Gohr        }
34391275a65SAndreas Gohr    }
34491275a65SAndreas Gohr
34591275a65SAndreas Gohr    // join lines into text
3461a6a1c04SAndreas Gohr    return implode(MAILHEADER_EOL,$aLines);
34791275a65SAndreas Gohr}
34844f669e9Sandi
3499c107bd1SChristopher Smithfunction mail_quotedprintable_encode_callback($matches){
3509c107bd1SChristopher Smith    return sprintf( "=%02X", ord ( $matches[0] ) ) ;
3519c107bd1SChristopher Smith}
352