thomascube
2012-03-14 a621a9d7ecf334c4894ef8f5168eb6208e5ae0e4
commit | author | age
4e17e6 1 <?php
ab6f80 2 /**
T 3  * The Mail_mimePart class is used to create MIME E-mail messages
4  *
5  * This class enables you to manipulate and build a mime email
6  * from the ground up. The Mail_Mime class is a userfriendly api
7  * to this class for people who aren't interested in the internals
8  * of mime mail.
9  * This class however allows full control over the email.
10  *
11  * Compatible with PHP versions 4 and 5
12  *
13  * LICENSE: This LICENSE is in the BSD license style.
14  * Copyright (c) 2002-2003, Richard Heyes <richard@phpguru.org>
15  * Copyright (c) 2003-2006, PEAR <pear-group@php.net>
16  * All rights reserved.
17  *
18  * Redistribution and use in source and binary forms, with or
19  * without modification, are permitted provided that the following
20  * conditions are met:
21  *
22  * - Redistributions of source code must retain the above copyright
23  *   notice, this list of conditions and the following disclaimer.
24  * - Redistributions in binary form must reproduce the above copyright
25  *   notice, this list of conditions and the following disclaimer in the
26  *   documentation and/or other materials provided with the distribution.
27  * - Neither the name of the authors, nor the names of its contributors 
28  *   may be used to endorse or promote products derived from this 
29  *   software without specific prior written permission.
30  *
31  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
32  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
33  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
34  * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
35  * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
36  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
37  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
38  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
39  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
40  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
41  * THE POSSIBILITY OF SUCH DAMAGE.
42  *
091735 43  * @category  Mail
A 44  * @package   Mail_Mime
45  * @author    Richard Heyes  <richard@phpguru.org>
46  * @author    Cipriano Groenendal <cipri@php.net>
47  * @author    Sean Coates <sean@php.net>
48  * @author    Aleksander Machniak <alec@php.net>
49  * @copyright 2003-2006 PEAR <pear-group@php.net>
50  * @license   http://www.opensource.org/licenses/bsd-license.php BSD License
51  * @version   CVS: $Id$
52  * @link      http://pear.php.net/package/Mail_mime
ab6f80 53  */
T 54
4e17e6 55
T 56 /**
ab6f80 57  * The Mail_mimePart class is used to create MIME E-mail messages
T 58  *
59  * This class enables you to manipulate and build a mime email
60  * from the ground up. The Mail_Mime class is a userfriendly api
61  * to this class for people who aren't interested in the internals
62  * of mime mail.
63  * This class however allows full control over the email.
64  *
091735 65  * @category  Mail
A 66  * @package   Mail_Mime
67  * @author    Richard Heyes  <richard@phpguru.org>
68  * @author    Cipriano Groenendal <cipri@php.net>
69  * @author    Sean Coates <sean@php.net>
70  * @author    Aleksander Machniak <alec@php.net>
71  * @copyright 2003-2006 PEAR <pear-group@php.net>
72  * @license   http://www.opensource.org/licenses/bsd-license.php BSD License
73  * @version   Release: @package_version@
74  * @link      http://pear.php.net/package/Mail_mime
ab6f80 75  */
091735 76 class Mail_mimePart
A 77 {
78     /**
4e17e6 79     * The encoding type of this part
ab6f80 80     *
4e17e6 81     * @var string
ab6f80 82     * @access private
4e17e6 83     */
T 84     var $_encoding;
85
091735 86     /**
4e17e6 87     * An array of subparts
ab6f80 88     *
4e17e6 89     * @var array
ab6f80 90     * @access private
4e17e6 91     */
T 92     var $_subparts;
93
091735 94     /**
4e17e6 95     * The output of this part after being built
ab6f80 96     *
4e17e6 97     * @var string
ab6f80 98     * @access private
4e17e6 99     */
T 100     var $_encoded;
101
091735 102     /**
4e17e6 103     * Headers for this part
ab6f80 104     *
4e17e6 105     * @var array
ab6f80 106     * @access private
4e17e6 107     */
T 108     var $_headers;
109
091735 110     /**
4e17e6 111     * The body of this part (not encoded)
ab6f80 112     *
4e17e6 113     * @var string
ab6f80 114     * @access private
4e17e6 115     */
T 116     var $_body;
117
118     /**
091735 119     * The location of file with body of this part (not encoded)
A 120     *
121     * @var string
122     * @access private
123     */
124     var $_body_file;
125
126     /**
127     * The end-of-line sequence
128     *
129     * @var string
130     * @access private
131     */
132     var $_eol = "\r\n";
133
6d0ada 134
091735 135     /**
A 136     * Constructor.
137     *
138     * Sets up the object.
139     *
140     * @param string $body   The body of the mime part if any.
141     * @param array  $params An associative array of optional parameters:
142     *     content_type      - The content type for this part eg multipart/mixed
143     *     encoding          - The encoding to use, 7bit, 8bit,
144     *                         base64, or quoted-printable
53604a 145     *     charset           - Content character set
091735 146     *     cid               - Content ID to apply
A 147     *     disposition       - Content disposition, inline or attachment
4c2424 148     *     filename          - Filename parameter for content disposition
091735 149     *     description       - Content description
53604a 150     *     name_encoding     - Encoding of the attachment name (Content-Type)
091735 151     *                         By default filenames are encoded using RFC2231
A 152     *                         Here you can set RFC2047 encoding (quoted-printable
153     *                         or base64) instead
53604a 154     *     filename_encoding - Encoding of the attachment filename (Content-Disposition)
091735 155     *                         See 'name_encoding'
53604a 156     *     headers_charset   - Charset of the headers e.g. filename, description.
A 157     *                         If not set, 'charset' will be used
091735 158     *     eol               - End of line sequence. Default: "\r\n"
A 159     *     body_file         - Location of file with part's body (instead of $body)
160     *
161     * @access public
162     */
4e17e6 163     function Mail_mimePart($body = '', $params = array())
T 164     {
091735 165         if (!empty($params['eol'])) {
A 166             $this->_eol = $params['eol'];
167         } else if (defined('MAIL_MIMEPART_CRLF')) { // backward-copat.
168             $this->_eol = MAIL_MIMEPART_CRLF;
4e17e6 169         }
T 170
171         foreach ($params as $key => $value) {
172             switch ($key) {
091735 173             case 'encoding':
A 174                 $this->_encoding = $value;
175                 $headers['Content-Transfer-Encoding'] = $value;
176                 break;
4e17e6 177
091735 178             case 'cid':
A 179                 $headers['Content-ID'] = '<' . $value . '>';
180                 break;
ee289d 181
091735 182             case 'location':
A 183                 $headers['Content-Location'] = $value;
184                 break;
ee289d 185
091735 186             case 'body_file':
A 187                 $this->_body_file = $value;
4c2424 188                 break;
A 189
190             // for backward compatibility
191             case 'dfilename':
192                 $params['filename'] = $value;
091735 193                 break;
ee289d 194             }
A 195         }
f7f934 196
8fc810 197         // Default content-type
53604a 198         if (empty($params['content_type'])) {
A 199             $params['content_type'] = 'text/plain';
8fc810 200         }
A 201
091735 202         // Content-Type
53604a 203         $headers['Content-Type'] = $params['content_type'];
A 204         if (!empty($params['charset'])) {
205             $charset = "charset={$params['charset']}";
206             // place charset parameter in the same line, if possible
207             if ((strlen($headers['Content-Type']) + strlen($charset) + 16) <= 76) {
208                 $headers['Content-Type'] .= '; ';
209             } else {
210                 $headers['Content-Type'] .= ';' . $this->_eol . ' ';
8fc810 211             }
53604a 212             $headers['Content-Type'] .= $charset;
A 213
214             // Default headers charset
215             if (!isset($params['headers_charset'])) {
216                 $params['headers_charset'] = $params['charset'];
781f34 217             }
53604a 218         }
A 219         if (!empty($params['filename'])) {
220             $headers['Content-Type'] .= ';' . $this->_eol;
221             $headers['Content-Type'] .= $this->_buildHeaderParam(
222                 'name', $params['filename'],
223                 !empty($params['headers_charset']) ? $params['headers_charset'] : 'US-ASCII',
224                 !empty($params['language']) ? $params['language'] : null,
225                 !empty($params['name_encoding']) ? $params['name_encoding'] : null
226             );
4e17e6 227         }
T 228
091735 229         // Content-Disposition
53604a 230         if (!empty($params['disposition'])) {
A 231             $headers['Content-Disposition'] = $params['disposition'];
232             if (!empty($params['filename'])) {
091735 233                 $headers['Content-Disposition'] .= ';' . $this->_eol;
A 234                 $headers['Content-Disposition'] .= $this->_buildHeaderParam(
53604a 235                     'filename', $params['filename'],
A 236                     !empty($params['headers_charset']) ? $params['headers_charset'] : 'US-ASCII',
237                     !empty($params['language']) ? $params['language'] : null,
238                     !empty($params['filename_encoding']) ? $params['filename_encoding'] : null
091735 239                 );
ee289d 240             }
A 241         }
ffae15 242
53604a 243         if (!empty($params['description'])) {
be5133 244             $headers['Content-Description'] = $this->encodeHeader(
53604a 245                 'Content-Description', $params['description'],
A 246                 !empty($params['headers_charset']) ? $params['headers_charset'] : 'US-ASCII',
247                 !empty($params['name_encoding']) ? $params['name_encoding'] : 'quoted-printable',
be5133 248                 $this->_eol
A 249             );
4e17e6 250         }
T 251
091735 252         // Default encoding
4e17e6 253         if (!isset($this->_encoding)) {
T 254             $this->_encoding = '7bit';
255         }
256
257         // Assign stuff to member variables
258         $this->_encoded  = array();
259         $this->_headers  = $headers;
260         $this->_body     = $body;
261     }
262
263     /**
264      * Encodes and returns the email. Also stores
265      * it in the encoded member variable
266      *
091735 267      * @param string $boundary Pre-defined boundary string
A 268      *
4e17e6 269      * @return An associative array containing two elements,
T 270      *         body and headers. The headers element is itself
091735 271      *         an indexed array. On error returns PEAR error object.
4e17e6 272      * @access public
T 273      */
091735 274     function encode($boundary=null)
4e17e6 275     {
T 276         $encoded =& $this->_encoded;
277
ee289d 278         if (count($this->_subparts)) {
091735 279             $boundary = $boundary ? $boundary : '=_' . md5(rand() . microtime());
A 280             $eol = $this->_eol;
4e17e6 281
091735 282             $this->_headers['Content-Type'] .= ";$eol boundary=\"$boundary\"";
A 283
284             $encoded['body'] = ''; 
285
4e17e6 286             for ($i = 0; $i < count($this->_subparts); $i++) {
091735 287                 $encoded['body'] .= '--' . $boundary . $eol;
4e17e6 288                 $tmp = $this->_subparts[$i]->encode();
091735 289                 if (PEAR::isError($tmp)) {
A 290                     return $tmp;
4e17e6 291                 }
091735 292                 foreach ($tmp['headers'] as $key => $value) {
A 293                     $encoded['body'] .= $key . ': ' . $value . $eol;
294                 }
295                 $encoded['body'] .= $eol . $tmp['body'] . $eol;
4e17e6 296             }
T 297
091735 298             $encoded['body'] .= '--' . $boundary . '--' . $eol;
4e17e6 299
091735 300         } else if ($this->_body) {
ee289d 301             $encoded['body'] = $this->_getEncodedData($this->_body, $this->_encoding);
091735 302         } else if ($this->_body_file) {
A 303             // Temporarily reset magic_quotes_runtime for file reads and writes
304             if ($magic_quote_setting = get_magic_quotes_runtime()) {
305                 @ini_set('magic_quotes_runtime', 0);
306             }
307             $body = $this->_getEncodedDataFromFile($this->_body_file, $this->_encoding);
308             if ($magic_quote_setting) {
309                 @ini_set('magic_quotes_runtime', $magic_quote_setting);
310             }
311
312             if (PEAR::isError($body)) {
313                 return $body;
314             }
315             $encoded['body'] = $body;
316         } else {
317             $encoded['body'] = '';
4e17e6 318         }
T 319
320         // Add headers to $encoded
321         $encoded['headers'] =& $this->_headers;
322
323         return $encoded;
324     }
325
326     /**
091735 327      * Encodes and saves the email into file. File must exist.
A 328      * Data will be appended to the file.
4e17e6 329      *
90fe6c 330      * @param string  $filename  Output file location
A 331      * @param string  $boundary  Pre-defined boundary string
332      * @param boolean $skip_head True if you don't want to save headers
091735 333      *
A 334      * @return array An associative array containing message headers
335      *               or PEAR error object
336      * @access public
337      * @since 1.6.0
338      */
90fe6c 339     function encodeToFile($filename, $boundary=null, $skip_head=false)
091735 340     {
A 341         if (file_exists($filename) && !is_writable($filename)) {
342             $err = PEAR::raiseError('File is not writeable: ' . $filename);
343             return $err;
344         }
345
346         if (!($fh = fopen($filename, 'ab'))) {
347             $err = PEAR::raiseError('Unable to open file: ' . $filename);
348             return $err;
349         }
350
351         // Temporarily reset magic_quotes_runtime for file reads and writes
352         if ($magic_quote_setting = get_magic_quotes_runtime()) {
353             @ini_set('magic_quotes_runtime', 0);
354         }
355
90fe6c 356         $res = $this->_encodePartToFile($fh, $boundary, $skip_head);
091735 357
A 358         fclose($fh);
359
360         if ($magic_quote_setting) {
361             @ini_set('magic_quotes_runtime', $magic_quote_setting);
362         }
363
364         return PEAR::isError($res) ? $res : $this->_headers;
365     }
366
367     /**
368      * Encodes given email part into file
369      *
90fe6c 370      * @param string  $fh        Output file handle
A 371      * @param string  $boundary  Pre-defined boundary string
372      * @param boolean $skip_head True if you don't want to save headers
091735 373      *
A 374      * @return array True on sucess or PEAR error object
375      * @access private
376      */
90fe6c 377     function _encodePartToFile($fh, $boundary=null, $skip_head=false)
091735 378     {
A 379         $eol = $this->_eol;
380
381         if (count($this->_subparts)) {
382             $boundary = $boundary ? $boundary : '=_' . md5(rand() . microtime());
383             $this->_headers['Content-Type'] .= ";$eol boundary=\"$boundary\"";
384         }
385
90fe6c 386         if (!$skip_head) {
A 387             foreach ($this->_headers as $key => $value) {
388                 fwrite($fh, $key . ': ' . $value . $eol);
389             }
390             $f_eol = $eol;
391         } else {
392             $f_eol = '';
091735 393         }
A 394
395         if (count($this->_subparts)) {
396             for ($i = 0; $i < count($this->_subparts); $i++) {
90fe6c 397                 fwrite($fh, $f_eol . '--' . $boundary . $eol);
091735 398                 $res = $this->_subparts[$i]->_encodePartToFile($fh);
A 399                 if (PEAR::isError($res)) {
400                     return $res;
401                 }
90fe6c 402                 $f_eol = $eol;
091735 403             }
A 404
405             fwrite($fh, $eol . '--' . $boundary . '--' . $eol);
406
407         } else if ($this->_body) {
90fe6c 408             fwrite($fh, $f_eol . $this->_getEncodedData($this->_body, $this->_encoding));
091735 409         } else if ($this->_body_file) {
90fe6c 410             fwrite($fh, $f_eol);
091735 411             $res = $this->_getEncodedDataFromFile(
A 412                 $this->_body_file, $this->_encoding, $fh
413             );
414             if (PEAR::isError($res)) {
415                 return $res;
416             }
417         }
418
419         return true;
420     }
421
422     /**
4e17e6 423      * Adds a subpart to current mime part and returns
T 424      * a reference to it
425      *
091735 426      * @param string $body   The body of the subpart, if any.
A 427      * @param array  $params The parameters for the subpart, same
428      *                       as the $params argument for constructor.
429      *
430      * @return Mail_mimePart A reference to the part you just added. It is
431      *                       crucial if using multipart/* in your subparts that
432      *                       you use =& in your script when calling this function,
433      *                       otherwise you will not be able to add further subparts.
4e17e6 434      * @access public
T 435      */
e62346 436     function &addSubpart($body, $params)
4e17e6 437     {
T 438         $this->_subparts[] = new Mail_mimePart($body, $params);
439         return $this->_subparts[count($this->_subparts) - 1];
440     }
441
442     /**
443      * Returns encoded data based upon encoding passed to it
444      *
091735 445      * @param string $data     The data to encode.
A 446      * @param string $encoding The encoding type to use, 7bit, base64,
447      *                         or quoted-printable.
448      *
449      * @return string
4e17e6 450      * @access private
T 451      */
452     function _getEncodedData($data, $encoding)
453     {
454         switch ($encoding) {
091735 455         case 'quoted-printable':
A 456             return $this->_quotedPrintableEncode($data);
457             break;
4e17e6 458
091735 459         case 'base64':
A 460             return rtrim(chunk_split(base64_encode($data), 76, $this->_eol));
461             break;
4e17e6 462
091735 463         case '8bit':
A 464         case '7bit':
465         default:
466             return $data;
4e17e6 467         }
T 468     }
469
470     /**
091735 471      * Returns encoded data based upon encoding passed to it
4e17e6 472      *
091735 473      * @param string   $filename Data file location
A 474      * @param string   $encoding The encoding type to use, 7bit, base64,
475      *                           or quoted-printable.
476      * @param resource $fh       Output file handle. If set, data will be
477      *                           stored into it instead of returning it
478      *
479      * @return string Encoded data or PEAR error object
480      * @access private
481      */
482     function _getEncodedDataFromFile($filename, $encoding, $fh=null)
483     {
484         if (!is_readable($filename)) {
485             $err = PEAR::raiseError('Unable to read file: ' . $filename);
486             return $err;
487         }
488
489         if (!($fd = fopen($filename, 'rb'))) {
490             $err = PEAR::raiseError('Could not open file: ' . $filename);
491             return $err;
492         }
493
494         $data = '';
495
496         switch ($encoding) {
497         case 'quoted-printable':
498             while (!feof($fd)) {
499                 $buffer = $this->_quotedPrintableEncode(fgets($fd));
500                 if ($fh) {
501                     fwrite($fh, $buffer);
502                 } else {
503                     $data .= $buffer;
504                 }
505             }
506             break;
507
508         case 'base64':
509             while (!feof($fd)) {
510                 // Should read in a multiple of 57 bytes so that
511                 // the output is 76 bytes per line. Don't use big chunks
512                 // because base64 encoding is memory expensive
513                 $buffer = fread($fd, 57 * 9198); // ca. 0.5 MB
514                 $buffer = base64_encode($buffer);
515                 $buffer = chunk_split($buffer, 76, $this->_eol);
516                 if (feof($fd)) {
517                     $buffer = rtrim($buffer);
518                 }
519
520                 if ($fh) {
521                     fwrite($fh, $buffer);
522                 } else {
523                     $data .= $buffer;
524                 }
525             }
526             break;
527
528         case '8bit':
529         case '7bit':
530         default:
531             while (!feof($fd)) {
532                 $buffer = fread($fd, 1048576); // 1 MB
533                 if ($fh) {
534                     fwrite($fh, $buffer);
535                 } else {
536                     $data .= $buffer;
537                 }
538             }
539         }
540
541         fclose($fd);
542
543         if (!$fh) {
544             return $data;
545         }
546     }
547
548     /**
4e17e6 549      * Encodes data to quoted-printable standard.
T 550      *
091735 551      * @param string $input    The data to encode
A 552      * @param int    $line_max Optional max line length. Should
553      *                         not be more than 76 chars
554      *
555      * @return string Encoded data
4e17e6 556      *
T 557      * @access private
558      */
559     function _quotedPrintableEncode($input , $line_max = 76)
560     {
091735 561         $eol = $this->_eol;
A 562         /*
563         // imap_8bit() is extremely fast, but doesn't handle properly some characters
564         if (function_exists('imap_8bit') && $line_max == 76) {
565             $input = preg_replace('/\r?\n/', "\r\n", $input);
566             $input = imap_8bit($input);
567             if ($eol != "\r\n") {
568                 $input = str_replace("\r\n", $eol, $input);
569             }
570             return $input;
571         }
572         */
4e17e6 573         $lines  = preg_split("/\r?\n/", $input);
T 574         $escape = '=';
575         $output = '';
576
091735 577         while (list($idx, $line) = each($lines)) {
4e17e6 578             $newline = '';
091735 579             $i = 0;
4e17e6 580
091735 581             while (isset($line[$i])) {
ab6f80 582                 $char = $line[$i];
4e17e6 583                 $dec  = ord($char);
091735 584                 $i++;
4e17e6 585
091735 586                 if (($dec == 32) && (!isset($line[$i]))) {
A 587                     // convert space at eol only
4e17e6 588                     $char = '=20';
091735 589                 } elseif ($dec == 9 && isset($line[$i])) {
A 590                     ; // Do nothing if a TAB is not on eol
591                 } elseif (($dec == 61) || ($dec < 32) || ($dec > 126)) {
592                     $char = $escape . sprintf('%02X', $dec);
593                 } elseif (($dec == 46) && (($newline == '')
594                     || ((strlen($newline) + strlen("=2E")) >= $line_max))
595                 ) {
596                     // Bug #9722: convert full-stop at bol,
597                     // some Windows servers need this, won't break anything (cipri)
598                     // Bug #11731: full-stop at bol also needs to be encoded
599                     // if this line would push us over the line_max limit.
ee289d 600                     $char = '=2E';
4e17e6 601                 }
T 602
091735 603                 // Note, when changing this line, also change the ($dec == 46)
A 604                 // check line, as it mimics this line due to Bug #11731
605                 // EOL is not counted
606                 if ((strlen($newline) + strlen($char)) >= $line_max) {
607                     // soft line break; " =\r\n" is okay
608                     $output  .= $newline . $escape . $eol;
4e17e6 609                     $newline  = '';
T 610                 }
611                 $newline .= $char;
612             } // end of for
613             $output .= $newline . $eol;
091735 614             unset($lines[$idx]);
4e17e6 615         }
091735 616         // Don't want last crlf
A 617         $output = substr($output, 0, -1 * strlen($eol));
4e17e6 618         return $output;
T 619     }
ee289d 620
A 621     /**
622      * Encodes the paramater of a header.
623      *
091735 624      * @param string $name      The name of the header-parameter
A 625      * @param string $value     The value of the paramter
626      * @param string $charset   The characterset of $value
627      * @param string $language  The language used in $value
628      * @param string $encoding  Parameter encoding. If not set, parameter value
629      *                          is encoded according to RFC2231
630      * @param int    $maxLength The maximum length of a line. Defauls to 75
631      *
632      * @return string
ee289d 633      *
A 634      * @access private
635      */
091735 636     function _buildHeaderParam($name, $value, $charset=null, $language=null,
A 637         $encoding=null, $maxLength=75
638     ) {
639         // RFC 2045:
f7f934 640         // value needs encoding if contains non-ASCII chars or is longer than 78 chars
091735 641         if (!preg_match('#[^\x20-\x7E]#', $value)) {
fe3a1d 642             $token_regexp = '#([^\x21\x23-\x27\x2A\x2B\x2D'
A 643                 . '\x2E\x30-\x39\x41-\x5A\x5E-\x7E])#';
091735 644             if (!preg_match($token_regexp, $value)) {
A 645                 // token
646                 if (strlen($name) + strlen($value) + 3 <= $maxLength) {
647                     return " {$name}={$value}";
648                 }
649             } else {
650                 // quoted-string
f7f934 651                 $quoted = addcslashes($value, '\\"');
091735 652                 if (strlen($name) + strlen($quoted) + 5 <= $maxLength) {
A 653                     return " {$name}=\"{$quoted}\"";
654                 }
f7f934 655             }
A 656         }
ffae15 657
f7f934 658         // RFC2047: use quoted-printable/base64 encoding
091735 659         if ($encoding == 'quoted-printable' || $encoding == 'base64') {
A 660             return $this->_buildRFC2047Param($name, $value, $charset, $encoding);
661         }
e8a1b7 662
f7f934 663         // RFC2231:
091735 664         $encValue = preg_replace_callback(
fe3a1d 665             '/([^\x21\x23\x24\x26\x2B\x2D\x2E\x30-\x39\x41-\x5A\x5E-\x7E])/',
091735 666             array($this, '_encodeReplaceCallback'), $value
A 667         );
e8a1b7 668         $value = "$charset'$language'$encValue";
A 669
091735 670         $header = " {$name}*={$value}";
ee289d 671         if (strlen($header) <= $maxLength) {
A 672             return $header;
673         }
674
93c0be 675         $preLength = strlen(" {$name}*0*=");
A 676         $maxLength = max(16, $maxLength - $preLength - 3);
ee289d 677         $maxLengthReg = "|(.{0,$maxLength}[^\%][^\%])|";
A 678
679         $headers = array();
680         $headCount = 0;
681         while ($value) {
682             $matches = array();
683             $found = preg_match($maxLengthReg, $value, $matches);
684             if ($found) {
93c0be 685                 $headers[] = " {$name}*{$headCount}*={$matches[0]}";
ee289d 686                 $value = substr($value, strlen($matches[0]));
A 687             } else {
93c0be 688                 $headers[] = " {$name}*{$headCount}*={$value}";
A 689                 $value = '';
ee289d 690             }
A 691             $headCount++;
692         }
091735 693
A 694         $headers = implode(';' . $this->_eol, $headers);
ee289d 695         return $headers;
A 696     }
ffae15 697
A 698     /**
091735 699      * Encodes header parameter as per RFC2047 if needed
ffae15 700      *
091735 701      * @param string $name      The parameter name
A 702      * @param string $value     The parameter value
703      * @param string $charset   The parameter charset
ffae15 704      * @param string $encoding  Encoding type (quoted-printable or base64)
091735 705      * @param int    $maxLength Encoded parameter max length. Default: 76
ffae15 706      *
A 707      * @return string Parameter line
708      * @access private
709      */
091735 710     function _buildRFC2047Param($name, $value, $charset,
A 711         $encoding='quoted-printable', $maxLength=76
712     ) {
f7f934 713         // WARNING: RFC 2047 says: "An 'encoded-word' MUST NOT be used in
091735 714         // parameter of a MIME Content-Type or Content-Disposition field",
f7f934 715         // but... it's supported by many clients/servers
091735 716         $quoted = '';
f7f934 717
091735 718         if ($encoding == 'base64') {
f7f934 719             $value = base64_encode($value);
ffae15 720             $prefix = '=?' . $charset . '?B?';
A 721             $suffix = '?=';
722
091735 723             // 2 x SPACE, 2 x '"', '=', ';'
A 724             $add_len = strlen($prefix . $suffix) + strlen($name) + 6;
f7f934 725             $len = $add_len + strlen($value);
31ddb5 726
f7f934 727             while ($len > $maxLength) { 
A 728                 // We can cut base64-encoded string every 4 characters
729                 $real_len = floor(($maxLength - $add_len) / 4) * 4;
730                 $_quote = substr($value, 0, $real_len);
731                 $value = substr($value, $real_len);
31ddb5 732
091735 733                 $quoted .= $prefix . $_quote . $suffix . $this->_eol . ' ';
f7f934 734                 $add_len = strlen($prefix . $suffix) + 4; // 2 x SPACE, '"', ';'
A 735                 $len = strlen($value) + $add_len;
736             }
31ddb5 737             $quoted .= $prefix . $value . $suffix;
A 738
091735 739         } else {
A 740             // quoted-printable
741             $value = $this->encodeQP($value);
ffae15 742             $prefix = '=?' . $charset . '?Q?';
A 743             $suffix = '?=';
744
091735 745             // 2 x SPACE, 2 x '"', '=', ';'
A 746             $add_len = strlen($prefix . $suffix) + strlen($name) + 6;
f7f934 747             $len = $add_len + strlen($value);
A 748
749             while ($len > $maxLength) {
750                 $length = $maxLength - $add_len;
091735 751                 // don't break any encoded letters
A 752                 if (preg_match("/^(.{0,$length}[^\=][^\=])/", $value, $matches)) {
f7f934 753                     $_quote = $matches[1];
091735 754                 }
f7f934 755
091735 756                 $quoted .= $prefix . $_quote . $suffix . $this->_eol . ' ';
f7f934 757                 $value = substr($value, strlen($_quote));
A 758                 $add_len = strlen($prefix . $suffix) + 4; // 2 x SPACE, '"', ';'
759                 $len = strlen($value) + $add_len;
760             }
761
762             $quoted .= $prefix . $value . $suffix;
ffae15 763         }
A 764
091735 765         return " {$name}=\"{$quoted}\"";
A 766     }
767
768     /**
be5133 769      * Encodes a header as per RFC2047
091735 770      *
be5133 771      * @param string $name     The header name
A 772      * @param string $value    The header data to encode
773      * @param string $charset  Character set name
774      * @param string $encoding Encoding name (base64 or quoted-printable)
775      * @param string $eol      End-of-line sequence. Default: "\r\n"
091735 776      *
be5133 777      * @return string          Encoded header data (without a name)
A 778      * @access public
779      * @since 1.6.1
780      */
781     function encodeHeader($name, $value, $charset='ISO-8859-1',
782         $encoding='quoted-printable', $eol="\r\n"
783     ) {
784         // Structured headers
785         $comma_headers = array(
786             'from', 'to', 'cc', 'bcc', 'sender', 'reply-to',
787             'resent-from', 'resent-to', 'resent-cc', 'resent-bcc',
788             'resent-sender', 'resent-reply-to',
789             'return-receipt-to', 'disposition-notification-to',
790         );
791         $other_headers = array(
792             'references', 'in-reply-to', 'message-id', 'resent-message-id',
793         );
794
795         $name = strtolower($name);
796
797         if (in_array($name, $comma_headers)) {
798             $separator = ',';
799         } else if (in_array($name, $other_headers)) {
800             $separator = ' ';
801         }
802
803         if (!$charset) {
804             $charset = 'ISO-8859-1';
805         }
806
807         // Structured header (make sure addr-spec inside is not encoded)
808         if (!empty($separator)) {
6d0ada 809             // Simple e-mail address regexp
4c2424 810             $email_regexp = '(\S+|("[^\r\n"]+"))@\S+';
6d0ada 811
be5133 812             $parts = Mail_mimePart::_explodeQuotedString($separator, $value);
A 813             $value = '';
814
815             foreach ($parts as $part) {
816                 $part = preg_replace('/\r?\n[\s\t]*/', $eol . ' ', $part);
817                 $part = trim($part);
818
819                 if (!$part) {
820                     continue;
821                 }
822                 if ($value) {
823                     $value .= $separator==',' ? $separator.' ' : ' ';
824                 } else {
825                     $value = $name . ': ';
826                 }
827
828                 // let's find phrase (name) and/or addr-spec
6d0ada 829                 if (preg_match('/^<' . $email_regexp . '>$/', $part)) {
be5133 830                     $value .= $part;
6d0ada 831                 } else if (preg_match('/^' . $email_regexp . '$/', $part)) {
be5133 832                     // address without brackets and without name
A 833                     $value .= $part;
6d0ada 834                 } else if (preg_match('/<*' . $email_regexp . '>*$/', $part, $matches)) {
be5133 835                     // address with name (handle name)
A 836                     $address = $matches[0];
837                     $word = str_replace($address, '', $part);
838                     $word = trim($word);
839                     // check if phrase requires quoting
840                     if ($word) {
841                         // non-ASCII: require encoding
842                         if (preg_match('#([\x80-\xFF]){1}#', $word)) {
843                             if ($word[0] == '"' && $word[strlen($word)-1] == '"') {
844                                 // de-quote quoted-string, encoding changes
845                                 // string to atom
846                                 $search = array("\\\"", "\\\\");
847                                 $replace = array("\"", "\\");
848                                 $word = str_replace($search, $replace, $word);
849                                 $word = substr($word, 1, -1);
850                             }
851                             // find length of last line
852                             if (($pos = strrpos($value, $eol)) !== false) {
853                                 $last_len = strlen($value) - $pos;
854                             } else {
855                                 $last_len = strlen($value);
856                             }
857                             $word = Mail_mimePart::encodeHeaderValue(
858                                 $word, $charset, $encoding, $last_len, $eol
859                             );
860                         } else if (($word[0] != '"' || $word[strlen($word)-1] != '"')
861                             && preg_match('/[\(\)\<\>\\\.\[\]@,;:"]/', $word)
862                         ) {
863                             // ASCII: quote string if needed
864                             $word = '"'.addcslashes($word, '\\"').'"';
865                         }
866                     }
867                     $value .= $word.' '.$address;
868                 } else {
869                     // addr-spec not found, don't encode (?)
870                     $value .= $part;
871                 }
872
873                 // RFC2822 recommends 78 characters limit, use 76 from RFC2047
874                 $value = wordwrap($value, 76, $eol . ' ');
875             }
876
877             // remove header name prefix (there could be EOL too)
878             $value = preg_replace(
879                 '/^'.$name.':('.preg_quote($eol, '/').')* /', '', $value
880             );
881
882         } else {
883             // Unstructured header
884             // non-ASCII: require encoding
885             if (preg_match('#([\x80-\xFF]){1}#', $value)) {
886                 if ($value[0] == '"' && $value[strlen($value)-1] == '"') {
887                     // de-quote quoted-string, encoding changes
888                     // string to atom
889                     $search = array("\\\"", "\\\\");
890                     $replace = array("\"", "\\");
891                     $value = str_replace($search, $replace, $value);
892                     $value = substr($value, 1, -1);
893                 }
894                 $value = Mail_mimePart::encodeHeaderValue(
895                     $value, $charset, $encoding, strlen($name) + 2, $eol
896                 );
897             } else if (strlen($name.': '.$value) > 78) {
898                 // ASCII: check if header line isn't too long and use folding
899                 $value = preg_replace('/\r?\n[\s\t]*/', $eol . ' ', $value);
900                 $tmp = wordwrap($name.': '.$value, 78, $eol . ' ');
901                 $value = preg_replace('/^'.$name.':\s*/', '', $tmp);
902                 // hard limit 998 (RFC2822)
903                 $value = wordwrap($value, 998, $eol . ' ', true);
904             }
905         }
906
907         return $value;
908     }
909
910     /**
911      * Explode quoted string
912      *
913      * @param string $delimiter Delimiter expression string for preg_match()
914      * @param string $string    Input string
915      *
916      * @return array            String tokens array
091735 917      * @access private
A 918      */
be5133 919     function _explodeQuotedString($delimiter, $string)
091735 920     {
be5133 921         $result = array();
A 922         $strlen = strlen($string);
923
924         for ($q=$p=$i=0; $i < $strlen; $i++) {
925             if ($string[$i] == "\""
926                 && (empty($string[$i-1]) || $string[$i-1] != "\\")
927             ) {
928                 $q = $q ? false : true;
929             } else if (!$q && preg_match("/$delimiter/", $string[$i])) {
930                 $result[] = substr($string, $p, $i - $p);
931                 $p = $i + 1;
932             }
933         }
934
935         $result[] = substr($string, $p);
936         return $result;
937     }
938
939     /**
940      * Encodes a header value as per RFC2047
941      *
942      * @param string $value      The header data to encode
943      * @param string $charset    Character set name
944      * @param string $encoding   Encoding name (base64 or quoted-printable)
945      * @param int    $prefix_len Prefix length. Default: 0
946      * @param string $eol        End-of-line sequence. Default: "\r\n"
947      *
948      * @return string            Encoded header data
949      * @access public
950      * @since 1.6.1
951      */
952     function encodeHeaderValue($value, $charset, $encoding, $prefix_len=0, $eol="\r\n")
953     {
8fc810 954         // #17311: Use multibyte aware method (requires mbstring extension)
A 955         if ($result = Mail_mimePart::encodeMB($value, $charset, $encoding, $prefix_len, $eol)) {
956             return $result;
957         }
958
959         // Generate the header using the specified params and dynamicly
960         // determine the maximum length of such strings.
961         // 75 is the value specified in the RFC.
962         $encoding = $encoding == 'base64' ? 'B' : 'Q';
963         $prefix = '=?' . $charset . '?' . $encoding .'?';
964         $suffix = '?=';
965         $maxLength = 75 - strlen($prefix . $suffix);
966         $maxLength1stLine = $maxLength - $prefix_len;
967
968         if ($encoding == 'B') {
be5133 969             // Base64 encode the entire string
A 970             $value = base64_encode($value);
971
8fc810 972             // We can cut base64 every 4 characters, so the real max
be5133 973             // we can get must be rounded down.
A 974             $maxLength = $maxLength - ($maxLength % 4);
975             $maxLength1stLine = $maxLength1stLine - ($maxLength1stLine % 4);
976
977             $cutpoint = $maxLength1stLine;
978             $output = '';
8fc810 979
A 980             while ($value) {
be5133 981                 // Split translated string at every $maxLength
8fc810 982                 $part = substr($value, 0, $cutpoint);
A 983                 $value = substr($value, $cutpoint);
be5133 984                 $cutpoint = $maxLength;
A 985                 // RFC 2047 specifies that any split header should
8fc810 986                 // be seperated by a CRLF SPACE.
be5133 987                 if ($output) {
A 988                     $output .= $eol . ' ';
989                 }
990                 $output .= $prefix . $part . $suffix;
991             }
992             $value = $output;
993         } else {
994             // quoted-printable encoding has been selected
995             $value = Mail_mimePart::encodeQP($value);
996
997             // This regexp will break QP-encoded text at every $maxLength
998             // but will not break any encoded letters.
999             $reg1st = "|(.{0,$maxLength1stLine}[^\=][^\=])|";
1000             $reg2nd = "|(.{0,$maxLength}[^\=][^\=])|";
1001
8fc810 1002             if (strlen($value) > $maxLength1stLine) {
be5133 1003                 // Begin with the regexp for the first line.
A 1004                 $reg = $reg1st;
1005                 $output = '';
8fc810 1006                 while ($value) {
be5133 1007                     // Split translated string at every $maxLength
A 1008                     // But make sure not to break any translated chars.
8fc810 1009                     $found = preg_match($reg, $value, $matches);
be5133 1010
A 1011                     // After this first line, we need to use a different
1012                     // regexp for the first line.
1013                     $reg = $reg2nd;
1014
1015                     // Save the found part and encapsulate it in the
1016                     // prefix & suffix. Then remove the part from the
1017                     // $value_out variable.
1018                     if ($found) {
1019                         $part = $matches[0];
1020                         $len = strlen($matches[0]);
8fc810 1021                         $value = substr($value, $len);
be5133 1022                     } else {
8fc810 1023                         $part = $value;
A 1024                         $value = '';
be5133 1025                     }
A 1026
8fc810 1027                     // RFC 2047 specifies that any split header should
be5133 1028                     // be seperated by a CRLF SPACE
A 1029                     if ($output) {
1030                         $output .= $eol . ' ';
1031                     }
1032                     $output .= $prefix . $part . $suffix;
1033                 }
8fc810 1034                 $value = $output;
be5133 1035             } else {
8fc810 1036                 $value = $prefix . $value . $suffix;
be5133 1037             }
A 1038         }
1039
1040         return $value;
091735 1041     }
A 1042
1043     /**
1044      * Encodes the given string using quoted-printable
1045      *
1046      * @param string $str String to encode
1047      *
1048      * @return string     Encoded string
1049      * @access public
1050      * @since 1.6.0
1051      */
1052     function encodeQP($str)
1053     {
9e8d85 1054         // Bug #17226 RFC 2047 restricts some characters
8fc810 1055         // if the word is inside a phrase, permitted chars are only:
9e8d85 1056         // ASCII letters, decimal digits, "!", "*", "+", "-", "/", "=", and "_"
091735 1057
9e8d85 1058         // "=",  "_",  "?" must be encoded
A 1059         $regexp = '/([\x22-\x29\x2C\x2E\x3A-\x40\x5B-\x60\x7B-\x7E\x80-\xFF])/';
1060         $str = preg_replace_callback(
1061             $regexp, array('Mail_mimePart', '_qpReplaceCallback'), $str
091735 1062         );
9e8d85 1063
A 1064         return str_replace(' ', '_', $str);
091735 1065     }
A 1066
1067     /**
8fc810 1068      * Encodes the given string using base64 or quoted-printable.
A 1069      * This method makes sure that encoded-word represents an integral
1070      * number of characters as per RFC2047.
1071      *
1072      * @param string $str        String to encode
1073      * @param string $charset    Character set name
1074      * @param string $encoding   Encoding name (base64 or quoted-printable)
1075      * @param int    $prefix_len Prefix length. Default: 0
1076      * @param string $eol        End-of-line sequence. Default: "\r\n"
1077      *
1078      * @return string     Encoded string
1079      * @access public
1080      * @since 1.8.0
1081      */
1082     function encodeMB($str, $charset, $encoding, $prefix_len=0, $eol="\r\n")
1083     {
1084         if (!function_exists('mb_substr') || !function_exists('mb_strlen')) {
1085             return;
1086         }
1087
1088         $encoding = $encoding == 'base64' ? 'B' : 'Q';
1089         // 75 is the value specified in the RFC
1090         $prefix = '=?' . $charset . '?'.$encoding.'?';
1091         $suffix = '?=';
1092         $maxLength = 75 - strlen($prefix . $suffix);
1093
1094         // A multi-octet character may not be split across adjacent encoded-words
1095         // So, we'll loop over each character
1096         // mb_stlen() with wrong charset will generate a warning here and return null
1097         $length      = mb_strlen($str, $charset);
1098         $result      = '';
1099         $line_length = $prefix_len;
1100
1101         if ($encoding == 'B') {
1102             // base64
1103             $start = 0;
1104             $prev  = '';
1105
1106             for ($i=1; $i<=$length; $i++) {
1107                 // See #17311
1108                 $chunk = mb_substr($str, $start, $i-$start, $charset);
1109                 $chunk = base64_encode($chunk);
1110                 $chunk_len = strlen($chunk);
1111
1112                 if ($line_length + $chunk_len == $maxLength || $i == $length) {
1113                     if ($result) {
1114                         $result .= "\n";
1115                     }
1116                     $result .= $chunk;
1117                     $line_length = 0;
1118                     $start = $i;
1119                 } else if ($line_length + $chunk_len > $maxLength) {
1120                     if ($result) {
1121                         $result .= "\n";
1122                     }
1123                     if ($prev) {
1124                         $result .= $prev;
1125                     }
1126                     $line_length = 0;
1127                     $start = $i - 1;
1128                 } else {
1129                     $prev = $chunk;
1130                 }
1131             }
1132         } else {
1133             // quoted-printable
1134             // see encodeQP()
1135             $regexp = '/([\x22-\x29\x2C\x2E\x3A-\x40\x5B-\x60\x7B-\x7E\x80-\xFF])/';
1136
1137             for ($i=0; $i<=$length; $i++) {
1138                 $char = mb_substr($str, $i, 1, $charset);
1139                 // RFC recommends underline (instead of =20) in place of the space
1140                 // that's one of the reasons why we're not using iconv_mime_encode()
1141                 if ($char == ' ') {
1142                     $char = '_';
1143                     $char_len = 1;
1144                 } else {
1145                     $char = preg_replace_callback(
1146                         $regexp, array('Mail_mimePart', '_qpReplaceCallback'), $char
1147                     );
1148                     $char_len = strlen($char);
1149                 }
1150
1151                 if ($line_length + $char_len > $maxLength) {
1152                     if ($result) {
1153                         $result .= "\n";
1154                     }
1155                     $line_length = 0;
1156                 }
1157
1158                 $result      .= $char;
1159                 $line_length += $char_len;
1160             }
1161         }
1162
1163         if ($result) {
1164             $result = $prefix
1165                 .str_replace("\n", $suffix.$eol.' '.$prefix, $result).$suffix;
1166         }
1167
1168         return $result;
1169     }
1170
1171     /**
091735 1172      * Callback function to replace extended characters (\x80-xFF) with their
A 1173      * ASCII values (RFC2047: quoted-printable)
1174      *
1175      * @param array $matches Preg_replace's matches array
1176      *
1177      * @return string        Encoded character string
1178      * @access private
1179      */
1180     function _qpReplaceCallback($matches)
1181     {
1182         return sprintf('=%02X', ord($matches[1]));
ffae15 1183     }
A 1184
be5133 1185     /**
A 1186      * Callback function to replace extended characters (\x80-xFF) with their
1187      * ASCII values (RFC2231)
1188      *
1189      * @param array $matches Preg_replace's matches array
1190      *
1191      * @return string        Encoded character string
1192      * @access private
1193      */
1194     function _encodeReplaceCallback($matches)
1195     {
1196         return sprintf('%%%02X', ord($matches[1]));
1197     }
1198
4e17e6 1199 } // End of class