xref: /dokuwiki/inc/Mailer.class.php (revision bb01c27c6c2c8e83091764cebadeef0489985b4b)
1<?php
2/**
3 * @author Andreas Gohr <andi@splitbrain.org>
4 */
5
6
7// end of line for mail lines - RFC822 says CRLF but postfix (and other MTAs?)
8// think different
9if(!defined('MAILHEADER_EOL')) define('MAILHEADER_EOL',"\n");
10#define('MAILHEADER_ASCIIONLY',1);
11
12
13class Mailer {
14
15    private $headers = array();
16    private $attach  = array();
17    private $html    = '';
18    private $text    = '';
19
20    private $boundary = '';
21    private $partid   = '';
22    private $sendparam= '';
23
24    function __construct(){
25        $this->partid = md5(uniqid(rand(),true)).'@'.$_SERVER['SERVER_NAME'];
26        $this->boundary = '----------'.md5(uniqid(rand(),true));
27    }
28
29    /**
30     * Attach a file
31     *
32     * @param $path  Path to the file to attach
33     * @param $mime  Mimetype of the attached file
34     * @param $name  The filename to use
35     * @param $embed Unique key to reference this file from the HTML part
36     */
37    public function attachFile($path,$mime,$name='',$embed=''){
38        if(!$name){
39            $name = basename($path);
40        }
41
42        $this->attach[] = array(
43            'data'  => file_get_contents($path),
44            'mime'  => $mime,
45            'name'  => $name,
46            'embed' => $embed
47        );
48    }
49
50    /**
51     * Attach a file
52     *
53     * @param $path  The file contents to attach
54     * @param $mime  Mimetype of the attached file
55     * @param $name  The filename to use
56     * @param $embed Unique key to reference this file from the HTML part
57     */
58    public function attachContent($data,$mime,$name='',$embed=''){
59        if(!$name){
60            list($junk,$ext) = split('/',$mime);
61            $name = count($this->attach).".$ext";
62        }
63
64        $this->attach[] = array(
65            'data'  => $data,
66            'mime'  => $mime,
67            'name'  => $name,
68            'embed' => $embed
69        );
70    }
71
72    /**
73     * Set the HTML part of the mail
74     *
75     * Placeholders can be used to reference embedded attachments
76     */
77    public function setHTMLBody($html){
78        $this->html = $html;
79    }
80
81    /**
82     * Set the plain text part of the mail
83     */
84    public function setTextBody($text){
85        $this->text = $text;
86    }
87
88    /**
89     * Ses an email address header with correct encoding
90     *
91     * Unicode characters will be deaccented and encoded base64
92     * for headers. Addresses may not contain Non-ASCII data!
93     *
94     * Example:
95     *   setAddress("föö <foo@bar.com>, me@somewhere.com","TBcc");
96     *
97     * @param string  $address Multiple adresses separated by commas
98     * @param string  $header  Name of the header (To,Bcc,Cc,...)
99     */
100    function mail_encode_address($address,$header){
101        // No named recipients for To: in Windows (see FS#652)
102        $names = (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') ? false : true;
103
104        $headers = '';
105        $parts = explode(',',$address);
106        foreach ($parts as $part){
107            $part = trim($part);
108
109            // parse address
110            if(preg_match('#(.*?)<(.*?)>#',$part,$matches)){
111                $text = trim($matches[1]);
112                $addr = $matches[2];
113            }else{
114                $addr = $part;
115            }
116            // skip empty ones
117            if(empty($addr)){
118                continue;
119            }
120
121            // FIXME: is there a way to encode the localpart of a emailaddress?
122            if(!utf8_isASCII($addr)){
123                msg(htmlspecialchars("E-Mail address <$addr> is not ASCII"),-1);
124                continue;
125            }
126
127            if(!mail_isvalid($addr)){
128                msg(htmlspecialchars("E-Mail address <$addr> is not valid"),-1);
129                continue;
130            }
131
132            // text was given
133            if(!empty($text) && $names){
134                // add address quotes
135                $addr = "<$addr>";
136
137                if(defined('MAILHEADER_ASCIIONLY')){
138                    $text = utf8_deaccent($text);
139                    $text = utf8_strip($text);
140                }
141
142                if(!utf8_isASCII($text)){
143                    //FIXME
144                    // put the quotes outside as in =?UTF-8?Q?"Elan Ruusam=C3=A4e"?= vs "=?UTF-8?Q?Elan Ruusam=C3=A4e?="
145                    /*
146                    if (preg_match('/^"(.+)"$/', $text, $matches)) {
147                      $text = '"=?UTF-8?Q?'.mail_quotedprintable_encode($matches[1], 0).'?="';
148                    } else {
149                      $text = '=?UTF-8?Q?'.mail_quotedprintable_encode($text, 0).'?=';
150                    }
151                    */
152                    $text = '=?UTF-8?B?'.base64_encode($text).'?=';
153                }
154            }else{
155                $text = '';
156            }
157
158            // add to header comma seperated
159            if($headers != ''){
160                $headers .= ',';
161                $headers .= MAILHEADER_EOL.' '; // avoid overlong mail headers
162            }
163            $headers .= $text.' '.$addr;
164        }
165
166        if(empty($headers)) return false;
167
168        $this->headers[$header] = $headers;
169        return $headers;
170    }
171
172    /**
173     * Add the To: recipients
174     *
175     * @see setAddress
176     * @param string  $address Multiple adresses separated by commas
177     */
178    public function to($address){
179        $this->setAddress($address, 'To');
180    }
181
182    /**
183     * Add the Cc: recipients
184     *
185     * @see setAddress
186     * @param string  $address Multiple adresses separated by commas
187     */
188    public function cc($address){
189        $this->setAddress($address, 'Cc');
190    }
191
192    /**
193     * Add the Bcc: recipients
194     *
195     * @see setAddress
196     * @param string  $address Multiple adresses separated by commas
197     */
198    public function bcc($address){
199        $this->setAddress($address, 'Bcc');
200    }
201
202    /**
203     * Add the mail's Subject: header
204     *
205     * @param string $subject the mail subject
206     */
207    public function subject($subject){
208        if(!utf8_isASCII($subject)){
209            $subject = '=?UTF-8?B?'.base64_encode($subject).'?=';
210        }
211        $this->headers['Subject'] = $subject;
212    }
213
214    /**
215     * Prepare the mime multiparts for all attachments
216     *
217     * Replaces placeholders in the HTML with the correct CIDs
218     */
219    protected function prepareAttachments(){
220        $mime = '';
221        $part = 1;
222        // embedded attachments
223        foreach($this->attach as $media){
224            // create content id
225            $cid = 'part'.$part.'.'.$this->partid;
226
227            // replace wildcards
228            if($media['embed']){
229                $this->html = str_replace('%%'.$media['embed'].'%%','cid:'.$cid,$this->html);
230            }
231
232            $mime .= '--'.$this->boundary.MAILHEADER_EOL;
233            $mime .= 'Content-Type: '.$media['mime'].';'.MAILHEADER_EOL;
234            $mime .= 'Content-Transfer-Encoding: base64'.MAILHEADER_EOL;
235            $mime .= "Content-ID: <$cid>".MAILHEADER_EOL;
236            if($media['embed']){
237                $mime .= 'Content-Disposition: inline; filename="'.$media['name'].'"'.MAILHEADER_EOL;
238            }else{
239                $mime .= 'Content-Disposition: attachment; filename="'.$media['name'].'"'.MAILHEADER_EOL;
240            }
241            $mime .= MAILHEADER_EOL; //end of headers
242            $mime .= chunk_split(base64_encode($media['data']),74,MAILHEADER_EOL);
243
244            $part++;
245        }
246        return $mime;
247    }
248
249    protected function createBody(){
250        // check for body
251        if(!$this->text && !$this->html){
252            return false;
253        }
254
255        // add general headers
256        $this->headers['MIME-Version'] = '1.0';
257
258        if(!$this->html && !count($this->attach)){ // we can send a simple single part message
259            $this->headers['Content-Type'] = 'text/plain; charset=UTF-8';
260            $this->headers['Content-Transfer-Encoding'] = 'base64';
261            $body = chunk_split(base64_encode($this->text),74,MAILHEADER_EOL);
262        }else{ // multi part it is
263
264            // prepare the attachments
265            $attachments = $this->prepareAttachments();
266
267            // do we have alternative text content?
268            if($this->text && $this->html){
269                $this->headers['Content-Type'] = 'multipart/alternative; boundary="'.$this->boundary.'XX"';
270                $body  = "This is a multi-part message in MIME format.".MAILHEADER_EOL;
271                $body .= '--'.$this->boundary.'XX'.MAILHEADER_EOL;
272                $body .= MAILHEADER_EOL;
273                $body .= 'Content-Type: text/plain; charset=UTF-8';
274                $body .= 'Content-Transfer-Encoding: base64';
275                $body .= chunk_split(base64_encode($this->text),74,MAILHEADER_EOL);
276                $body .= '--'.$this->boundary.'XX'.MAILHEADER_EOL;
277                $body .= 'Content-Type: multipart/related; boundary="'.$this->boundary.'"'.MAILHEADER_EOL;
278                $body .= MAILHEADER_EOL;
279            }else{
280                $this->headers['Content-Type'] = 'multipart/related; boundary="'.$this->boundary.'"';
281                $body  = "This is a multi-part message in MIME format.".MAILHEADER_EOL;
282            }
283
284            $body .= '--'.$this->boundary."\n";
285            $body .= "Content-Type: text/html; charset=UTF-8\n";
286            $body .= "Content-Transfer-Encoding: base64\n";
287            $body .= MAILHEADER_EOL;
288            $body = chunk_split(base64_encode($this->html),74,MAILHEADER_EOL);
289            $body .= MAILHEADER_EOL;
290            $body .= $attachments;
291            $body .= '--'.$this->boundary.'--'.MAILHEADER_EOL;
292
293            // close open multipart/alternative boundary
294            if($this->text && $this->html){
295                $body .= '--'.$this->boundary.'XX--'.MAILHEADER_EOL;
296            }
297        }
298
299        return $body;
300    }
301
302    /**
303     * Create a string from the headers array
304     */
305    protected function prepareHeaders(){
306        $headers = '';
307        foreach($this->headers as $key => $val){
308            $headers .= "$key: $val".MAILHEADER_EOL;
309        }
310        return $headers;
311    }
312
313    /**
314     * return a full email with all headers
315     *
316     * This is mainly for debugging and testing
317     */
318    public function dump(){
319        $headers = $this->prepareHeaders();
320        $body    = $this->prepareBody();
321
322        return $headers.MAILHEADER_EOL.$body;
323    }
324}
325