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