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