source: trunk/library/mime/mimePart.php @ 7655

Revision 7655, 42.3 KB checked in by douglasz, 11 years ago (diff)

Ticket #3236 - Melhorias de performance no codigo do Expresso.

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