2 /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
3 // +-----------------------------------------------------------------------+
4 // | Copyright (c) 2002-2003 Richard Heyes |
5 // | Copyright (c) 2003-2005 The PHP Group |
6 // | All rights reserved. |
8 // | Redistribution and use in source and binary forms, with or without |
9 // | modification, are permitted provided that the following conditions |
12 // | o Redistributions of source code must retain the above copyright |
13 // | notice, this list of conditions and the following disclaimer. |
14 // | o Redistributions in binary form must reproduce the above copyright |
15 // | notice, this list of conditions and the following disclaimer in the |
16 // | documentation and/or other materials provided with the distribution.|
17 // | o The names of the authors may not be used to endorse or promote |
18 // | products derived from this software without specific prior written |
21 // | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
22 // | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
23 // | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
24 // | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
25 // | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
26 // | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
27 // | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
28 // | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
29 // | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
30 // | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
31 // | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
33 // +-----------------------------------------------------------------------+
34 // | Author: Richard Heyes <richard@phpguru.org> |
35 // | Tomas V.V.Cox <cox@idecnet.com> (port to PEAR) |
36 // +-----------------------------------------------------------------------+
38 // $Id: mime.php 260 2006-06-09 16:47:21Z afladmark $
40 require_once('PEAR.php');
41 require_once('Mail/mimePart.php');
44 * Mime mail composer class. Can handle: text and html bodies, embedded html
45 * images and attachments.
46 * Documentation and examples of this class are avaible here:
47 * http://pear.php.net/manual/
49 * @notes This class is based on HTML Mime Mail class from
50 * Richard Heyes <richard@phpguru.org> which was based also
51 * in the mime_mail.class by Tobias Ratschiller <tobias@dnet.it> and
52 * Sascha Schumann <sascha@schumann.cx>
54 * @author Richard Heyes <richard.heyes@heyes-computing.net>
55 * @author Tomas V.V.Cox <cox@idecnet.com>
62 * Contains the plain text part of the email
67 * Contains the html part of the email
72 * contains the mime encoded text
77 * contains the multipart content
82 * list of the attached images
85 var $_html_images = array();
87 * list of the attachements
90 var $_parts = array();
95 var $_build_params = array();
97 * Headers for the mail
100 var $_headers = array();
102 * End Of Line sequence (for serialize)
109 * Constructor function
113 function Mail_mime($crlf = "\r\n")
115 $this->_setEOL($crlf);
116 $this->_build_params = array(
117 'head_encoding' => 'quoted-printable',
118 'text_encoding' => '7bit',
119 'html_encoding' => 'quoted-printable',
121 'html_charset' => 'ISO-8859-1',
122 'text_charset' => 'ISO-8859-1',
123 'head_charset' => 'ISO-8859-1'
128 * Wakeup (unserialize) - re-sets EOL constant
134 $this->_setEOL($this->_eol);
138 * Accessor function to set the body text. Body text is used if
139 * it's not an html mail being sent or else is used to fill the
140 * text/plain part that emails clients who don't support
143 * @param string $data Either a string or
144 * the file name with the contents
145 * @param bool $isfile If true the first param should be treated
146 * as a file name, else as a string (default)
147 * @param bool $append If true the text or file is appended to
148 * the existing body, else the old body is
150 * @return mixed true on success or PEAR_Error object
153 function setTXTBody($data, $isfile = false, $append = false)
157 $this->_txtbody = $data;
159 $this->_txtbody .= $data;
162 $cont = $this->_file2str($data);
163 if (PEAR::isError($cont)) {
167 $this->_txtbody = $cont;
169 $this->_txtbody .= $cont;
176 * Adds a html part to the mail
178 * @param string $data Either a string or the file name with the
180 * @param bool $isfile If true the first param should be treated
181 * as a file name, else as a string (default)
182 * @return mixed true on success or PEAR_Error object
185 function setHTMLBody($data, $isfile = false)
188 $this->_htmlbody = $data;
190 $cont = $this->_file2str($data);
191 if (PEAR::isError($cont)) {
194 $this->_htmlbody = $cont;
201 * Adds an image to the list of embedded images.
203 * @param string $file The image file name OR image data itself
204 * @param string $c_type The content type
205 * @param string $name The filename of the image.
206 * Only use if $file is the image data
207 * @param bool $isfilename Whether $file is a filename or not
209 * @return mixed true on success or PEAR_Error object
212 function addHTMLImage($file, $c_type='application/octet-stream',
213 $name = '', $isfilename = true)
215 $filedata = ($isfilename === true) ? $this->_file2str($file)
217 if ($isfilename === true) {
218 $filename = ($name == '' ? $file : $name);
222 if (PEAR::isError($filedata)) {
225 $this->_html_images[] = array(
229 'cid' => md5(uniqid(time()))
235 * Adds a file to the list of attachments.
237 * @param string $file The file name of the file to attach
238 * OR the file contents itself
239 * @param string $c_type The content type
240 * @param string $name The filename of the attachment
241 * Only use if $file is the contents
242 * @param bool $isFilename Whether $file is a filename or not
244 * @param string $encoding The type of encoding to use.
245 * Defaults to base64.
246 * Possible values: 7bit, 8bit, base64,
247 * or quoted-printable.
248 * @param string $disposition The content-disposition of this file
249 * Defaults to attachment.
250 * Possible values: attachment, inline.
251 * @param string $charset The character set used in the filename
252 * of this attachment.
253 * @return mixed true on success or PEAR_Error object
256 function addAttachment($file, $c_type = 'application/octet-stream',
257 $name = '', $isfilename = true,
258 $encoding = 'base64',
259 $disposition = 'attachment', $charset = '')
261 $filedata = ($isfilename === true) ? $this->_file2str($file)
263 if ($isfilename === true) {
264 // Force the name the user supplied, otherwise use $file
265 $filename = (!empty($name)) ? $name : $file;
269 if (empty($filename)) {
270 $err = PEAR::raiseError(
271 "The supplied filename for the attachment can't be empty"
275 $filename = basename($filename);
276 if (PEAR::isError($filedata)) {
280 $this->_parts[] = array(
284 'encoding' => $encoding,
285 'charset' => $charset,
286 'disposition' => $disposition
292 * Get the contents of the given file name as string
294 * @param string $file_name path of file to process
295 * @return string contents of $file_name
298 function &_file2str($file_name)
300 if (!is_readable($file_name)) {
301 $err = PEAR::raiseError('File is not readable ' . $file_name);
304 if (!$fd = fopen($file_name, 'rb')) {
305 $err = PEAR::raiseError('Could not open ' . $file_name);
308 $filesize = filesize($file_name);
312 if ($magic_quote_setting = get_magic_quotes_runtime()){
313 set_magic_quotes_runtime(0);
315 $cont = fread($fd, $filesize);
316 if ($magic_quote_setting){
317 set_magic_quotes_runtime($magic_quote_setting);
325 * Adds a text subpart to the mimePart object and
326 * returns it during the build process.
328 * @param mixed The object to add the part to, or
329 * null if a new object is to be created.
330 * @param string The text to add.
331 * @return object The text mimePart object
334 function &_addTextPart(&$obj, $text)
336 $params['content_type'] = 'text/plain';
337 $params['encoding'] = $this->_build_params['text_encoding'];
338 $params['charset'] = $this->_build_params['text_charset'];
339 if (is_object($obj)) {
340 $ret = $obj->addSubpart($text, $params);
343 $ret = new Mail_mimePart($text, $params);
349 * Adds a html subpart to the mimePart object and
350 * returns it during the build process.
352 * @param mixed The object to add the part to, or
353 * null if a new object is to be created.
354 * @return object The html mimePart object
357 function &_addHtmlPart(&$obj)
359 $params['content_type'] = 'text/html';
360 $params['encoding'] = $this->_build_params['html_encoding'];
361 $params['charset'] = $this->_build_params['html_charset'];
362 if (is_object($obj)) {
363 $ret = $obj->addSubpart($this->_htmlbody, $params);
366 $ret = new Mail_mimePart($this->_htmlbody, $params);
372 * Creates a new mimePart object, using multipart/mixed as
373 * the initial content-type and returns it during the
376 * @return object The multipart/mixed mimePart object
379 function &_addMixedPart()
381 $params['content_type'] = 'multipart/mixed';
382 $ret = new Mail_mimePart('', $params);
387 * Adds a multipart/alternative part to a mimePart
388 * object (or creates one), and returns it during
391 * @param mixed The object to add the part to, or
392 * null if a new object is to be created.
393 * @return object The multipart/mixed mimePart object
396 function &_addAlternativePart(&$obj)
398 $params['content_type'] = 'multipart/alternative';
399 if (is_object($obj)) {
400 return $obj->addSubpart('', $params);
402 $ret = new Mail_mimePart('', $params);
408 * Adds a multipart/related part to a mimePart
409 * object (or creates one), and returns it during
412 * @param mixed The object to add the part to, or
413 * null if a new object is to be created
414 * @return object The multipart/mixed mimePart object
417 function &_addRelatedPart(&$obj)
419 $params['content_type'] = 'multipart/related';
420 if (is_object($obj)) {
421 return $obj->addSubpart('', $params);
423 $ret = new Mail_mimePart('', $params);
429 * Adds an html image subpart to a mimePart object
430 * and returns it during the build process.
432 * @param object The mimePart to add the image to
433 * @param array The image information
434 * @return object The image mimePart object
437 function &_addHtmlImagePart(&$obj, $value)
439 $params['content_type'] = $value['c_type'] . '; ' .
440 'name="' . $value['name'] . '"';
441 $params['encoding'] = 'base64';
442 $params['disposition'] = 'inline';
443 $params['dfilename'] = $value['name'];
444 $params['cid'] = $value['cid'];
445 $ret = $obj->addSubpart($value['body'], $params);
451 * Adds an attachment subpart to a mimePart object
452 * and returns it during the build process.
454 * @param object The mimePart to add the image to
455 * @param array The attachment information
456 * @return object The image mimePart object
459 function &_addAttachmentPart(&$obj, $value)
461 $params['dfilename'] = $value['name'];
462 $params['encoding'] = $value['encoding'];
463 if ($value['disposition'] != "inline") {
464 $fname = array("fname" => $value['name']);
465 $fname_enc = $this->_encodeHeaders($fname);
466 $params['dfilename'] = $fname_enc['fname'];
468 if ($value['charset']) {
469 $params['charset'] = $value['charset'];
471 $params['content_type'] = $value['c_type'] . '; ' .
472 'name="' . $params['dfilename'] . '"';
473 $params['disposition'] = isset($value['disposition']) ?
474 $value['disposition'] : 'attachment';
475 $ret = $obj->addSubpart($value['body'], $params);
480 * Returns the complete e-mail, ready to send using an alternative
481 * mail delivery method. Note that only the mailpart that is made
482 * with Mail_Mime is created. This means that,
483 * YOU WILL HAVE NO TO: HEADERS UNLESS YOU SET IT YOURSELF
484 * using the $xtra_headers parameter!
486 * @param string $separation The separation etween these two parts.
487 * @param array $build_params The Build parameters passed to the
488 * &get() function. See &get for more info.
489 * @param array $xtra_headers The extra headers that should be passed
490 * to the &headers() function.
491 * See that function for more info.
492 * @param bool $overwrite Overwrite the existing headers with new.
493 * @return string The complete e-mail.
496 function getMessage($separation = null, $build_params = null, $xtra_headers = null, $overwrite = false)
498 if ($separation === null)
500 $separation = MAIL_MIME_CRLF;
502 $body = $this->get($build_params);
503 $head = $this->txtHeaders($xtra_headers, $overwrite);
504 $mail = $head . $separation . $body;
510 * Builds the multipart message from the list ($this->_parts) and
511 * returns the mime content.
513 * @param array Build parameters that change the way the email
514 * is built. Should be associative. Can contain:
515 * head_encoding - What encoding to use for the headers.
516 * Options: quoted-printable or base64
517 * Default is quoted-printable
518 * text_encoding - What encoding to use for plain text
519 * Options: 7bit, 8bit, base64, or quoted-printable
521 * html_encoding - What encoding to use for html
522 * Options: 7bit, 8bit, base64, or quoted-printable
523 * Default is quoted-printable
524 * 7bit_wrap - Number of characters before text is
525 * wrapped in 7bit encoding
527 * html_charset - The character set to use for html.
528 * Default is iso-8859-1
529 * text_charset - The character set to use for text.
530 * Default is iso-8859-1
531 * head_charset - The character set to use for headers.
532 * Default is iso-8859-1
533 * @return string The mime content
536 function &get($build_params = null)
538 if (isset($build_params)) {
539 while (list($key, $value) = each($build_params)) {
540 $this->_build_params[$key] = $value;
544 if (!empty($this->_html_images) AND isset($this->_htmlbody)) {
545 foreach ($this->_html_images as $key => $value) {
547 $regex[] = '#(\s)((?i)src|background|href(?-i))\s*=\s*(["\']?)' .
548 preg_quote($value['name'], '#') . '\3#';
549 $regex[] = '#(?i)url(?-i)\(\s*(["\']?)' .
550 preg_quote($value['name'], '#') . '\1\s*\)#';
552 $rep[] = '\1\2=\3cid:' . $value['cid'] .'\3';
553 $rep[] = 'url(\1cid:' . $value['cid'] . '\2)';
554 $this->_htmlbody = preg_replace($regex, $rep,
557 $this->_html_images[$key]['name'] = basename($this->_html_images[$key]['name']);
562 $attachments = !empty($this->_parts) ? true : false;
563 $html_images = !empty($this->_html_images) ? true : false;
564 $html = !empty($this->_htmlbody) ? true : false;
565 $text = (!$html AND !empty($this->_txtbody)) ? true : false;
568 case $text AND !$attachments:
569 $message =& $this->_addTextPart($null, $this->_txtbody);
572 case !$text AND !$html AND $attachments:
573 $message =& $this->_addMixedPart();
574 for ($i = 0; $i < count($this->_parts); $i++) {
575 $this->_addAttachmentPart($message, $this->_parts[$i]);
579 case $text AND $attachments:
580 $message =& $this->_addMixedPart();
581 $this->_addTextPart($message, $this->_txtbody);
582 for ($i = 0; $i < count($this->_parts); $i++) {
583 $this->_addAttachmentPart($message, $this->_parts[$i]);
587 case $html AND !$attachments AND !$html_images:
588 if (isset($this->_txtbody)) {
589 $message =& $this->_addAlternativePart($null);
590 $this->_addTextPart($message, $this->_txtbody);
591 $this->_addHtmlPart($message);
593 $message =& $this->_addHtmlPart($null);
597 case $html AND !$attachments AND $html_images:
598 if (isset($this->_txtbody)) {
599 $message =& $this->_addAlternativePart($null);
600 $this->_addTextPart($message, $this->_txtbody);
601 $related =& $this->_addRelatedPart($message);
603 $message =& $this->_addRelatedPart($null);
604 $related =& $message;
606 $this->_addHtmlPart($related);
607 for ($i = 0; $i < count($this->_html_images); $i++) {
608 $this->_addHtmlImagePart($related, $this->_html_images[$i]);
612 case $html AND $attachments AND !$html_images:
613 $message =& $this->_addMixedPart();
614 if (isset($this->_txtbody)) {
615 $alt =& $this->_addAlternativePart($message);
616 $this->_addTextPart($alt, $this->_txtbody);
617 $this->_addHtmlPart($alt);
619 $this->_addHtmlPart($message);
621 for ($i = 0; $i < count($this->_parts); $i++) {
622 $this->_addAttachmentPart($message, $this->_parts[$i]);
626 case $html AND $attachments AND $html_images:
627 $message =& $this->_addMixedPart();
628 if (isset($this->_txtbody)) {
629 $alt =& $this->_addAlternativePart($message);
630 $this->_addTextPart($alt, $this->_txtbody);
631 $rel =& $this->_addRelatedPart($alt);
633 $rel =& $this->_addRelatedPart($message);
635 $this->_addHtmlPart($rel);
636 for ($i = 0; $i < count($this->_html_images); $i++) {
637 $this->_addHtmlImagePart($rel, $this->_html_images[$i]);
639 for ($i = 0; $i < count($this->_parts); $i++) {
640 $this->_addAttachmentPart($message, $this->_parts[$i]);
646 if (isset($message)) {
647 $output = $message->encode();
648 $this->_headers = array_merge($this->_headers,
650 $body = $output['body'];
660 * Returns an array with the headers needed to prepend to the email
661 * (MIME-Version and Content-Type). Format of argument is:
662 * $array['header-name'] = 'header-value';
664 * @param array $xtra_headers Assoc array with any extra headers.
666 * @param bool $overwrite Overwrite already existing headers.
667 * @return array Assoc array with the mime headers
670 function &headers($xtra_headers = null, $overwrite = false)
672 // Content-Type header should already be present,
673 // So just add mime version header
674 $headers['MIME-Version'] = '1.0';
675 if (isset($xtra_headers)) {
676 $headers = array_merge($headers, $xtra_headers);
679 $this->_headers = array_merge($this->_headers, $headers);
681 $this->_headers = array_merge($headers, $this->_headers);
684 $encodedHeaders = $this->_encodeHeaders($this->_headers);
685 return $encodedHeaders;
689 * Get the text version of the headers
690 * (usefull if you want to use the PHP mail() function)
692 * @param array $xtra_headers Assoc array with any extra headers.
694 * @param bool $overwrite Overwrite the existing heaers with new.
695 * @return string Plain text headers
698 function txtHeaders($xtra_headers = null, $overwrite = false)
700 $headers = $this->headers($xtra_headers, $overwrite);
702 foreach ($headers as $key => $val) {
703 $ret .= "$key: $val" . MAIL_MIME_CRLF;
709 * Sets the Subject header
711 * @param string $subject String to set the subject to
714 function setSubject($subject)
716 $this->_headers['Subject'] = $subject;
720 * Set an email to the From (the sender) header
722 * @param string $email The email direction to add
725 function setFrom($email)
727 $this->_headers['From'] = $email;
731 * Add an email to the Cc (carbon copy) header
732 * (multiple calls to this method are allowed)
734 * @param string $email The email direction to add
737 function addCc($email)
739 if (isset($this->_headers['Cc'])) {
740 $this->_headers['Cc'] .= ", $email";
742 $this->_headers['Cc'] = $email;
747 * Add an email to the Bcc (blank carbon copy) header
748 * (multiple calls to this method are allowed)
750 * @param string $email The email direction to add
753 function addBcc($email)
755 if (isset($this->_headers['Bcc'])) {
756 $this->_headers['Bcc'] .= ", $email";
758 $this->_headers['Bcc'] = $email;
763 * Since the PHP send function requires you to specifiy
764 * recipients (To: header) separately from the other
765 * headers, the To: header is not properly encoded.
766 * To fix this, you can use this public method to
767 * encode your recipients before sending to the send
770 * @param string $recipients A comma-delimited list of recipients
771 * @return string Encoded data
774 function encodeRecipients($recipients)
776 $input = array("To" => $recipients);
777 $retval = $this->_encodeHeaders($input);
778 return $retval["To"] ;
782 * Encodes a header as per RFC2047
784 * @param array $input The header data to encode
785 * @return array Encoded data
788 function _encodeHeaders($input)
790 foreach ($input as $hdr_name => $hdr_value) {
791 if (function_exists('iconv_mime_encode') && preg_match('#[\x80-\xFF]{1}#', $hdr_value)){
793 if ($this->_build_params['head_encoding'] == 'base64'){
794 $imePrefs['scheme'] = 'B';
796 $imePrefs['scheme'] = 'Q';
798 $imePrefs['input-charset'] = $this->_build_params['head_charset'];
799 $imePrefs['output-charset'] = $this->_build_params['head_charset'];
800 $hdr_value = iconv_mime_encode($hdr_name, $hdr_value, $imePrefs);
801 $hdr_value = preg_replace("#^{$hdr_name}\:\ #", "", $hdr_value);
802 }elseif (preg_match('#[\x80-\xFF]{1}#', $hdr_value)){
803 //This header contains non ASCII chars and should be encoded.
804 switch ($this->_build_params['head_encoding']) {
806 //Base64 encoding has been selected.
808 //Generate the header using the specified params and dynamicly
809 //determine the maximum length of such strings.
810 //75 is the value specified in the RFC. The -2 is there so
811 //the later regexp doesn't break any of the translated chars.
812 $prefix = '=?' . $this->_build_params['head_charset'] . '?B?';
814 $maxLength = 75 - strlen($prefix . $suffix) - 2;
815 $maxLength1stLine = $maxLength - strlen($hdr_name);
817 //Base64 encode the entire string
818 $hdr_value = base64_encode($hdr_value);
820 //This regexp will break base64-encoded text at every
821 //$maxLength but will not break any encoded letters.
822 $reg1st = "|.{0,$maxLength1stLine}[^\=][^\=]|";
823 $reg2nd = "|.{0,$maxLength}[^\=][^\=]|";
825 case 'quoted-printable':
827 //quoted-printable encoding has been selected
829 //Generate the header using the specified params and dynamicly
830 //determine the maximum length of such strings.
831 //75 is the value specified in the RFC. The -2 is there so
832 //the later regexp doesn't break any of the translated chars.
833 $prefix = '=?' . $this->_build_params['head_charset'] . '?Q?';
835 $maxLength = 75 - strlen($prefix . $suffix) - 2;
836 $maxLength1stLine = $maxLength - strlen($hdr_name);
838 //Replace all special characters used by the encoder.
839 $search = array("=", "_", "?", " ");
840 $replace = array("=3D", "=5F", "=3F", "_");
841 $hdr_value = str_replace($search, $replace, $hdr_value);
843 //Replace all extended characters (\x80-xFF) with their
845 $hdr_value = preg_replace(
847 '"=" . strtoupper(dechex(ord("\1")))',
850 //This regexp will break QP-encoded text at every $maxLength
851 //but will not break any encoded letters.
852 $reg1st = "|(.{0,$maxLength})[^\=]|";
853 $reg2nd = "|(.{0,$maxLength})[^\=]|";
856 //Begin with the regexp for the first line.
860 //Split translated string at every $maxLength
861 //But make sure not to break any translated chars.
862 $found = preg_match($reg, $hdr_value, $matches);
864 //After this first line, we need to use a different
865 //regexp for the first line.
868 //Save the found part and encapsulate it in the
869 //prefix & suffix. Then remove the part from the
870 //$hdr_value variable.
873 $hdr_value = substr($hdr_value, strlen($matches[0]));
879 //RFC 2047 specifies that any split header should be seperated
884 $output .= $prefix . $part . $suffix;
886 $hdr_value = $output;
888 $input[$hdr_name] = $hdr_value;
895 * Set the object's end-of-line and define the constant if applicable
897 * @param string $eol End Of Line sequence
900 function _setEOL($eol)
903 if (!defined('MAIL_MIME_CRLF')) {
904 define('MAIL_MIME_CRLF', $this->_eol, true);