1<?php
2/**
3 * Copyright 2007-2017 Horde LLC (http://www.horde.org/)
4 *
5 * See the enclosed file COPYING for license information (LGPL). If you
6 * did not receive this file, see http://www.horde.org/licenses/lgpl21.
7 *
8 * @category  Horde
9 * @copyright 2007-2017 Horde LLC
10 * @license   http://www.horde.org/licenses/lgpl21 LGPL 2.1
11 * @package   Mime
12 */
13
14/**
15 * The Horde_Mime_Mail:: class wraps around the various MIME library classes
16 * to provide a simple interface for creating and sending MIME messages.
17 *
18 * All content has to be passed UTF-8 encoded. The charset parameters is used
19 * for the generated message only.
20 *
21 * @author    Jan Schneider <jan@horde.org>
22 * @category  Horde
23 * @copyright 2007-2017 Horde LLC
24 * @license   http://www.horde.org/licenses/lgpl21 LGPL 2.1
25 * @package   Mime
26 */
27class Horde_Mime_Mail
28{
29    /**
30     * The message headers.
31     *
32     * @var Horde_Mime_Headers
33     */
34    protected $_headers;
35
36    /**
37     * The base MIME part.
38     *
39     * @var Horde_Mime_Part
40     */
41    protected $_base;
42
43    /**
44     * The main body part.
45     *
46     * @var Horde_Mime_Part
47     */
48    protected $_body;
49
50    /**
51     * The main HTML body part.
52     *
53     * @var Horde_Mime_Part
54     */
55    protected $_htmlBody;
56
57    /**
58     * The message recipients.
59     *
60     * @var Horde_Mail_Rfc822_List
61     */
62    protected $_recipients;
63
64    /**
65     * Bcc recipients.
66     *
67     * @var string
68     */
69    protected $_bcc;
70
71    /**
72     * All MIME parts except the main body part.
73     *
74     * @var array
75     */
76    protected $_parts = array();
77
78    /**
79     * The Mail driver name.
80     *
81     * @link http://pear.php.net/Mail
82     * @var string
83     */
84    protected $_mailer_driver = 'smtp';
85
86    /**
87     * The charset to use for the message.
88     *
89     * @var string
90     */
91    protected $_charset = 'UTF-8';
92
93    /**
94     * The Mail driver parameters.
95     *
96     * @link http://pear.php.net/Mail
97     * @var array
98     */
99    protected $_mailer_params = array();
100
101    /**
102     * Constructor.
103     *
104     * @param array $params  A hash with basic message information. 'charset'
105     *                       is the character set of the message.  'body' is
106     *                       the message body. All other parameters are
107     *                       assumed to be message headers.
108     *
109     * @throws Horde_Mime_Exception
110     */
111    public function __construct($params = array())
112    {
113        /* Set SERVER_NAME. */
114        if (!isset($_SERVER['SERVER_NAME'])) {
115            $_SERVER['SERVER_NAME'] = php_uname('n');
116        }
117
118        $this->_headers = new Horde_Mime_Headers();
119
120        if (isset($params['charset'])) {
121            $this->_charset = $params['charset'];
122            unset($params['charset']);
123        }
124
125        if (isset($params['body'])) {
126            $this->setBody($params['body'], $this->_charset);
127            unset($params['body']);
128        }
129
130        $this->addHeaders($params);
131
132        $this->clearRecipients();
133    }
134
135    /**
136     * Adds several message headers at once.
137     *
138     * @param array $header    Hash with header names as keys and header
139     *                         contents as values.
140     *
141     * @throws Horde_Mime_Exception
142     */
143    public function addHeaders($headers = array())
144    {
145        foreach ($headers as $header => $value) {
146            $this->addHeader($header, $value);
147        }
148    }
149
150    /**
151     * Adds a message header.
152     *
153     * @param string $header      The header name.
154     * @param string $value       The header value.
155     * @param boolean $overwrite  If true, an existing header of the same name
156     *                            is being overwritten; if false, multiple
157     *                            headers are added; if null, the correct
158     *                            behaviour is automatically chosen depending
159     *                            on the header name.
160     *
161     * @throws Horde_Mime_Exception
162     */
163    public function addHeader($header, $value, $overwrite = null)
164    {
165        $lc_header = Horde_String::lower($header);
166
167        if (is_null($overwrite) &&
168            in_array($lc_header, $this->_headers->singleFields(true))) {
169            $overwrite = true;
170        }
171
172        if ($overwrite) {
173            $this->_headers->removeHeader($header);
174        }
175
176        if ($lc_header === 'bcc') {
177            $this->_bcc = $value;
178        } else {
179            $this->_headers->addHeader($header, $value);
180        }
181    }
182
183    /**
184     * Add a Horde_Mime_Headers_Element object to the current header list.
185     *
186     * @since 2.5.0
187     *
188     * @param Horde_Mime_Headers_Element $ob  Header object to add.
189     *
190     * @throws InvalidArgumentException
191     */
192    public function addHeaderOb(Horde_Mime_Headers_Element $ob)
193    {
194        $this->_headers->addHeaderOb($ob, true);
195    }
196
197    /**
198     * Removes a message header.
199     *
200     * @param string $header  The header name.
201     */
202    public function removeHeader($header)
203    {
204        if (Horde_String::lower($header) === 'bcc') {
205            unset($this->_bcc);
206        } else {
207            $this->_headers->removeHeader($header);
208        }
209    }
210
211    /**
212     * Sets the message body text.
213     *
214     * @param string $body             The message content.
215     * @param string $charset          The character set of the message.
216     * @param boolean|integer $wrap    If true, wrap the message at column 76;
217     *                                 If an integer wrap the message at that
218     *                                 column. Don't use wrapping if sending
219     *                                 flowed messages.
220     */
221    public function setBody($body, $charset = null, $wrap = false)
222    {
223        if (!$charset) {
224            $charset = $this->_charset;
225        }
226        $body = Horde_String::convertCharset($body, 'UTF-8', $charset);
227        if ($wrap) {
228            $body = Horde_String::wrap($body, $wrap === true ? 76 : $wrap);
229        }
230        $this->_body = new Horde_Mime_Part();
231        $this->_body->setType('text/plain');
232        $this->_body->setCharset($charset);
233        $this->_body->setContents($body);
234        $this->_base = null;
235    }
236
237    /**
238     * Sets the HTML message body text.
239     *
240     * @param string $body          The message content.
241     * @param string $charset       The character set of the message.
242     * @param boolean $alternative  If true, a multipart/alternative message is
243     *                              created and the text/plain part is
244     *                              generated automatically. If false, a
245     *                              text/html message is generated.
246     */
247    public function setHtmlBody($body, $charset = null, $alternative = true)
248    {
249        if (!$charset) {
250            $charset = $this->_charset;
251        }
252        $this->_htmlBody = new Horde_Mime_Part();
253        $this->_htmlBody->setType('text/html');
254        $this->_htmlBody->setCharset($charset);
255        $this->_htmlBody->setContents($body);
256        if ($alternative) {
257            $this->setBody(Horde_Text_Filter::filter($body, 'Html2text', array('charset' => $charset, 'wrap' => false)), $charset);
258        }
259        $this->_base = null;
260    }
261
262    /**
263     * Adds a message part.
264     *
265     * @param string $mime_type    The content type of the part.
266     * @param string $content      The content of the part.
267     * @param string $charset      The character set of the part.
268     * @param string $disposition  The content disposition of the part.
269     *
270     * @return integer  The part number.
271     */
272    public function addPart($mime_type, $content, $charset = 'us-ascii',
273                            $disposition = null)
274    {
275        $part = new Horde_Mime_Part();
276        $part->setType($mime_type);
277        $part->setCharset($charset);
278        $part->setDisposition($disposition);
279        $part->setContents($content);
280        return $this->addMimePart($part);
281    }
282
283    /**
284     * Adds a MIME message part.
285     *
286     * @param Horde_Mime_Part $part  A Horde_Mime_Part object.
287     *
288     * @return integer  The part number.
289     */
290    public function addMimePart($part)
291    {
292        $this->_parts[] = $part;
293        return count($this->_parts) - 1;
294    }
295
296    /**
297     * Sets the base MIME part.
298     *
299     * If the base part is set, any text bodies will be ignored when building
300     * the message.
301     *
302     * @param Horde_Mime_Part $part  A Horde_Mime_Part object.
303     */
304    public function setBasePart($part)
305    {
306        $this->_base = $part;
307    }
308
309    /**
310     * Adds an attachment.
311     *
312     * @param string $file     The path to the file.
313     * @param string $name     The file name to use for the attachment.
314     * @param string $type     The content type of the file.
315     * @param string $charset  The character set of the part (only relevant for
316     *                         text parts.
317     *
318     * @return integer  The part number.
319     */
320    public function addAttachment($file, $name = null, $type = null,
321                                  $charset = 'us-ascii')
322    {
323        if (empty($name)) {
324            $name = basename($file);
325        }
326
327        if (empty($type)) {
328            $type = Horde_Mime_Magic::filenameToMime($file, false);
329        }
330
331        $num = $this->addPart($type, file_get_contents($file), $charset, 'attachment');
332        $this->_parts[$num]->setName($name);
333        return $num;
334    }
335
336    /**
337     * Removes a message part.
338     *
339     * @param integer $part  The part number.
340     */
341    public function removePart($part)
342    {
343        if (isset($this->_parts[$part])) {
344            unset($this->_parts[$part]);
345        }
346    }
347
348    /**
349     * Removes all (additional) message parts but leaves the body parts
350     * untouched.
351     */
352    public function clearParts()
353    {
354        $this->_parts = array();
355    }
356
357    /**
358     * Adds message recipients.
359     *
360     * Recipients specified by To:, Cc:, or Bcc: headers are added
361     * automatically.
362     *
363     * @param string|array  List of recipients, either as a comma separated
364     *                      list or as an array of email addresses.
365     *
366     * @throws Horde_Mime_Exception
367     */
368    public function addRecipients($recipients)
369    {
370        $this->_recipients->add($recipients);
371    }
372
373    /**
374     * Removes message recipients.
375     *
376     * @param string|array  List of recipients, either as a comma separated
377     *                      list or as an array of email addresses.
378     *
379     * @throws Horde_Mime_Exception
380     */
381    public function removeRecipients($recipients)
382    {
383        $this->_recipients->remove($recipients);
384    }
385
386    /**
387     * Removes all message recipients.
388     */
389    public function clearRecipients()
390    {
391        $this->_recipients = new Horde_Mail_Rfc822_List();
392    }
393
394    /**
395     * Sends this message.
396     *
397     * @param Mail $mailer     A Mail object.
398     * @param boolean $resend  If true, the message id and date are re-used;
399     *                         If false, they will be updated.
400     * @param boolean $flowed  Send message in flowed text format.
401     *
402     * @throws Horde_Mime_Exception
403     */
404    public function send($mailer, $resend = false, $flowed = true)
405    {
406        /* Add mandatory headers if missing. */
407        if (!$resend || !isset($this->_headers['Message-ID'])) {
408            $this->_headers->addHeaderOb(
409                Horde_Mime_Headers_MessageId::create()
410            );
411        }
412        if (!isset($this->_headers['User-Agent'])) {
413            $this->_headers->addHeaderOb(
414                Horde_Mime_Headers_UserAgent::create()
415            );
416        }
417        if (!$resend || !isset($this->_headers['Date'])) {
418            $this->_headers->addHeaderOb(Horde_Mime_Headers_Date::create());
419        }
420
421        if (isset($this->_base)) {
422            $basepart = $this->_base;
423        } else {
424            /* Send in flowed format. */
425            if ($flowed && !empty($this->_body)) {
426                $flowed = new Horde_Text_Flowed($this->_body->getContents(), $this->_body->getCharset());
427                $flowed->setDelSp(true);
428                $this->_body->setContentTypeParameter('format', 'flowed');
429                $this->_body->setContentTypeParameter('DelSp', 'Yes');
430                $this->_body->setContents($flowed->toFlowed());
431            }
432
433            /* Build mime message. */
434            $body = new Horde_Mime_Part();
435            if (!empty($this->_body) && !empty($this->_htmlBody)) {
436                $body->setType('multipart/alternative');
437                $this->_body->setDescription(Horde_Mime_Translation::t("Plaintext Version of Message"));
438                $body[] = $this->_body;
439                $this->_htmlBody->setDescription(Horde_Mime_Translation::t("HTML Version of Message"));
440                $body[] = $this->_htmlBody;
441            } elseif (!empty($this->_htmlBody)) {
442                $body = $this->_htmlBody;
443            } elseif (!empty($this->_body)) {
444                $body = $this->_body;
445            }
446            if (count($this->_parts)) {
447                $basepart = new Horde_Mime_Part();
448                $basepart->setType('multipart/mixed');
449                $basepart->isBasePart(true);
450                if ($body) {
451                    $basepart[] = $body;
452                }
453                foreach ($this->_parts as $mime_part) {
454                    $basepart[] = $mime_part;
455                }
456            } else {
457                $basepart = $body;
458                $basepart->isBasePart(true);
459            }
460        }
461        $basepart->setHeaderCharset($this->_charset);
462
463        /* Build recipients. */
464        $recipients = clone $this->_recipients;
465        foreach (array('to', 'cc') as $header) {
466            if ($h = $this->_headers[$header]) {
467                $recipients->add($h->getAddressList());
468            }
469        }
470        if ($this->_bcc) {
471            $recipients->add($this->_bcc);
472        }
473
474        /* Trick Horde_Mime_Part into re-generating the message headers. */
475        $this->_headers->removeHeader('MIME-Version');
476
477        /* Send message. */
478        $recipients->unique();
479        $basepart->send($recipients->writeAddress(), $this->_headers, $mailer);
480
481        /* Remember the basepart */
482        $this->_base = $basepart;
483    }
484
485    /**
486     * Get the raw email data sent by this object.
487     *
488     * @param  boolean $stream  If true, return a stream resource, otherwise
489     *                          a string is returned.
490     *
491     * @return stream|string  The raw email data.
492     * @since 2.4.0
493     */
494    public function getRaw($stream = true)
495    {
496        if ($stream) {
497            $hdr = new Horde_Stream();
498            $hdr->add($this->_headers->toString(), true);
499            return Horde_Stream_Wrapper_Combine::getStream(
500                array($hdr->stream,
501                      $this->getBasePart()->toString(
502                        array('stream' => true, 'encode' => Horde_Mime_Part::ENCODE_7BIT | Horde_Mime_Part::ENCODE_8BIT | Horde_Mime_Part::ENCODE_BINARY))
503                )
504            );
505        }
506
507        return $this->_headers->toString() . $this->getBasePart()->toString();
508    }
509
510    /**
511     * Return the base MIME part.
512     *
513     * @return Horde_Mime_Part
514     */
515    public function getBasePart()
516    {
517        if (empty($this->_base)) {
518            throw new Horde_Mail_Exception('No base part set.');
519        }
520
521        return $this->_base;
522    }
523
524}
525