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