source: branches/version-2_5-dev/data/module/Mail/mimePart.php @ 19941

Revision 19941, 41.9 KB checked in by Seasoft, 13 years ago (diff)

#403(インクルードしているライブラリ群をバージョンアップする)

  • 一部に不正と思われる文字エンコードがあり開発に支障があるため、バージョンアップで解決を図る。
  • Mail_Mime 1.8.1
  • Mail_mimeDecode 1.5.5
  • Property svn:keywords set to Id
  • Property svn:mime-type set to text/x-httpd-php; charset=UTF-8
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$
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    *     dfilename         - 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        }
190
191        // Default content-type
192        if (empty($params['content_type'])) {
193            $params['content_type'] = 'text/plain';
194        }
195
196        // Content-Type
197        $headers['Content-Type'] = $params['content_type'];
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 . ' ';
205            }
206            $headers['Content-Type'] .= $charset;
207
208            // Default headers charset
209            if (!isset($params['headers_charset'])) {
210                $params['headers_charset'] = $params['charset'];
211            }
212        }
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            );
221        }
222
223        // Content-Disposition
224        if (!empty($params['disposition'])) {
225            $headers['Content-Disposition'] = $params['disposition'];
226            if (!empty($params['filename'])) {
227                $headers['Content-Disposition'] .= ';' . $this->_eol;
228                $headers['Content-Disposition'] .= $this->_buildHeaderParam(
229                    'filename', $params['filename'],
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
233                );
234            }
235        }
236
237        if (!empty($params['description'])) {
238            $headers['Content-Description'] = $this->encodeHeader(
239                'Content-Description', $params['description'],
240                !empty($params['headers_charset']) ? $params['headers_charset'] : 'US-ASCII',
241                !empty($params['name_encoding']) ? $params['name_encoding'] : 'quoted-printable',
242                $this->_eol
243            );
244        }
245
246        // Default encoding
247        if (!isset($this->_encoding)) {
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     *
261     * @param string $boundary Pre-defined boundary string
262     *
263     * @return An associative array containing two elements,
264     *         body and headers. The headers element is itself
265     *         an indexed array. On error returns PEAR error object.
266     * @access public
267     */
268    function encode($boundary=null)
269    {
270        $encoded =& $this->_encoded;
271
272        if (count($this->_subparts)) {
273            $boundary = $boundary ? $boundary : '=_' . md5(rand() . microtime());
274            $eol = $this->_eol;
275
276            $this->_headers['Content-Type'] .= ";$eol boundary=\"$boundary\"";
277
278            $encoded['body'] = '';
279
280            for ($i = 0; $i < count($this->_subparts); $i++) {
281                $encoded['body'] .= '--' . $boundary . $eol;
282                $tmp = $this->_subparts[$i]->encode();
283                if (PEAR::isError($tmp)) {
284                    return $tmp;
285                }
286                foreach ($tmp['headers'] as $key => $value) {
287                    $encoded['body'] .= $key . ': ' . $value . $eol;
288                }
289                $encoded['body'] .= $eol . $tmp['body'] . $eol;
290            }
291
292            $encoded['body'] .= '--' . $boundary . '--' . $eol;
293
294        } else if ($this->_body) {
295            $encoded['body'] = $this->_getEncodedData($this->_body, $this->_encoding);
296        } else if ($this->_body_file) {
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'] = '';
312        }
313
314        // Add headers to $encoded
315        $encoded['headers'] =& $this->_headers;
316
317        return $encoded;
318    }
319
320    /**
321     * Encodes and saves the email into file. File must exist.
322     * Data will be appended to the file.
323     *
324     * @param string  $filename  Output file location
325     * @param string  $boundary  Pre-defined boundary string
326     * @param boolean $skip_head True if you don't want to save headers
327     *
328     * @return array An associative array containing message headers
329     *               or PEAR error object
330     * @access public
331     * @since 1.6.0
332     */
333    function encodeToFile($filename, $boundary=null, $skip_head=false)
334    {
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
350        $res = $this->_encodePartToFile($fh, $boundary, $skip_head);
351
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     *
364     * @param string  $fh        Output file handle
365     * @param string  $boundary  Pre-defined boundary string
366     * @param boolean $skip_head True if you don't want to save headers
367     *
368     * @return array True on sucess or PEAR error object
369     * @access private
370     */
371    function _encodePartToFile($fh, $boundary=null, $skip_head=false)
372    {
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
380        if (!$skip_head) {
381            foreach ($this->_headers as $key => $value) {
382                fwrite($fh, $key . ': ' . $value . $eol);
383            }
384            $f_eol = $eol;
385        } else {
386            $f_eol = '';
387        }
388
389        if (count($this->_subparts)) {
390            for ($i = 0; $i < count($this->_subparts); $i++) {
391                fwrite($fh, $f_eol . '--' . $boundary . $eol);
392                $res = $this->_subparts[$i]->_encodePartToFile($fh);
393                if (PEAR::isError($res)) {
394                    return $res;
395                }
396                $f_eol = $eol;
397            }
398
399            fwrite($fh, $eol . '--' . $boundary . '--' . $eol);
400
401        } else if ($this->_body) {
402            fwrite($fh, $f_eol . $this->_getEncodedData($this->_body, $this->_encoding));
403        } else if ($this->_body_file) {
404            fwrite($fh, $f_eol);
405            $res = $this->_getEncodedDataFromFile(
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    /**
417     * Adds a subpart to current mime part and returns
418     * a reference to it
419     *
420     * @param string $body   The body of the subpart, if any.
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.
428     * @access public
429     */
430    function &addSubpart($body, $params)
431    {
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     *
439     * @param string $data     The data to encode.
440     * @param string $encoding The encoding type to use, 7bit, base64,
441     *                         or quoted-printable.
442     *
443     * @return string
444     * @access private
445     */
446    function _getEncodedData($data, $encoding)
447    {
448        switch ($encoding) {
449        case 'quoted-printable':
450            return $this->_quotedPrintableEncode($data);
451            break;
452
453        case 'base64':
454            return rtrim(chunk_split(base64_encode($data), 76, $this->_eol));
455            break;
456
457        case '8bit':
458        case '7bit':
459        default:
460            return $data;
461        }
462    }
463
464    /**
465     * Returns encoded data based upon encoding passed to it
466     *
467     * @param string   $filename Data file location
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    /**
543     * Encodes data to quoted-printable standard.
544     *
545     * @param string $input    The data to encode
546     * @param int    $line_max Optional max line length. Should
547     *                         not be more than 76 chars
548     *
549     * @return string Encoded data
550     *
551     * @access private
552     */
553    function _quotedPrintableEncode($input , $line_max = 76)
554    {
555        $eol = $this->_eol;
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        */
567        $lines  = preg_split("/\r?\n/", $input);
568        $escape = '=';
569        $output = '';
570
571        while (list($idx, $line) = each($lines)) {
572            $newline = '';
573            $i = 0;
574
575            while (isset($line[$i])) {
576                $char = $line[$i];
577                $dec  = ord($char);
578                $i++;
579
580                if (($dec == 32) && (!isset($line[$i]))) {
581                    // convert space at eol only
582                    $char = '=20';
583                } elseif ($dec == 9 && isset($line[$i])) {
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.
594                    $char = '=2E';
595                }
596
597                // Note, when changing this line, also change the ($dec == 46)
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;
603                    $newline  = '';
604                }
605                $newline .= $char;
606            } // end of for
607            $output .= $newline . $eol;
608            unset($lines[$idx]);
609        }
610        // Don't want last crlf
611        $output = substr($output, 0, -1 * strlen($eol));
612        return $output;
613    }
614
615    /**
616     * Encodes the paramater of a header.
617     *
618     * @param string $name      The name of the header-parameter
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
627     *
628     * @access private
629     */
630    function _buildHeaderParam($name, $value, $charset=null, $language=null,
631        $encoding=null, $maxLength=75
632    ) {
633        // RFC 2045:
634        // value needs encoding if contains non-ASCII chars or is longer than 78 chars
635        if (!preg_match('#[^\x20-\x7E]#', $value)) {
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
645                $quoted = addcslashes($value, '\\"');
646                if (strlen($name) + strlen($quoted) + 5 <= $maxLength) {
647                    return " {$name}=\"{$quoted}\"";
648                }
649            }
650        }
651
652        // RFC2047: use quoted-printable/base64 encoding
653        if ($encoding == 'quoted-printable' || $encoding == 'base64') {
654            return $this->_buildRFC2047Param($name, $value, $charset, $encoding);
655        }
656
657        // RFC2231:
658        $encValue = preg_replace_callback(
659            '/([^\x21,\x23,\x24,\x26,\x2B,\x2D,\x2E,\x30-\x39,\x41-\x5A,\x5E-\x7E])/',
660            array($this, '_encodeReplaceCallback'), $value
661        );
662        $value = "$charset'$language'$encValue";
663
664        $header = " {$name}*={$value}";
665        if (strlen($header) <= $maxLength) {
666            return $header;
667        }
668
669        $preLength = strlen(" {$name}*0*=");
670        $maxLength = max(16, $maxLength - $preLength - 3);
671        $maxLengthReg = "|(.{0,$maxLength}[^\%][^\%])|";
672
673        $headers = array();
674        $headCount = 0;
675        while ($value) {
676            $matches = array();
677            $found = preg_match($maxLengthReg, $value, $matches);
678            if ($found) {
679                $headers[] = " {$name}*{$headCount}*={$matches[0]}";
680                $value = substr($value, strlen($matches[0]));
681            } else {
682                $headers[] = " {$name}*{$headCount}*={$value}";
683                $value = '';
684            }
685            $headCount++;
686        }
687
688        $headers = implode(';' . $this->_eol, $headers);
689        return $headers;
690    }
691
692    /**
693     * Encodes header parameter as per RFC2047 if needed
694     *
695     * @param string $name      The parameter name
696     * @param string $value     The parameter value
697     * @param string $charset   The parameter charset
698     * @param string $encoding  Encoding type (quoted-printable or base64)
699     * @param int    $maxLength Encoded parameter max length. Default: 76
700     *
701     * @return string Parameter line
702     * @access private
703     */
704    function _buildRFC2047Param($name, $value, $charset,
705        $encoding='quoted-printable', $maxLength=76
706    ) {
707        // WARNING: RFC 2047 says: "An 'encoded-word' MUST NOT be used in
708        // parameter of a MIME Content-Type or Content-Disposition field",
709        // but... it's supported by many clients/servers
710        $quoted = '';
711
712        if ($encoding == 'base64') {
713            $value = base64_encode($value);
714            $prefix = '=?' . $charset . '?B?';
715            $suffix = '?=';
716
717            // 2 x SPACE, 2 x '"', '=', ';'
718            $add_len = strlen($prefix . $suffix) + strlen($name) + 6;
719            $len = $add_len + strlen($value);
720
721            while ($len > $maxLength) {
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);
726
727                $quoted .= $prefix . $_quote . $suffix . $this->_eol . ' ';
728                $add_len = strlen($prefix . $suffix) + 4; // 2 x SPACE, '"', ';'
729                $len = strlen($value) + $add_len;
730            }
731            $quoted .= $prefix . $value . $suffix;
732
733        } else {
734            // quoted-printable
735            $value = $this->encodeQP($value);
736            $prefix = '=?' . $charset . '?Q?';
737            $suffix = '?=';
738
739            // 2 x SPACE, 2 x '"', '=', ';'
740            $add_len = strlen($prefix . $suffix) + strlen($name) + 6;
741            $len = $add_len + strlen($value);
742
743            while ($len > $maxLength) {
744                $length = $maxLength - $add_len;
745                // don't break any encoded letters
746                if (preg_match("/^(.{0,$length}[^\=][^\=])/", $value, $matches)) {
747                    $_quote = $matches[1];
748                }
749
750                $quoted .= $prefix . $_quote . $suffix . $this->_eol . ' ';
751                $value = substr($value, strlen($_quote));
752                $add_len = strlen($prefix . $suffix) + 4; // 2 x SPACE, '"', ';'
753                $len = strlen($value) + $add_len;
754            }
755
756            $quoted .= $prefix . $value . $suffix;
757        }
758
759        return " {$name}=\"{$quoted}\"";
760    }
761
762    /**
763     * Encodes a header as per RFC2047
764     *
765     * @param string $name     The header name
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"
770     *
771     * @return string          Encoded header data (without a name)
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
908     * @access private
909     */
910    function _explodeQuotedString($delimiter, $string)
911    {
912        $result = array();
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    {
945        // #17311: Use multibyte aware method (requires mbstring extension)
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') {
960            // Base64 encode the entire string
961            $value = base64_encode($value);
962
963            // We can cut base64 every 4 characters, so the real max
964            // we can get must be rounded down.
965            $maxLength = $maxLength - ($maxLength % 4);
966            $maxLength1stLine = $maxLength1stLine - ($maxLength1stLine % 4);
967
968            $cutpoint = $maxLength1stLine;
969            $output = '';
970
971            while ($value) {
972                // Split translated string at every $maxLength
973                $part = substr($value, 0, $cutpoint);
974                $value = substr($value, $cutpoint);
975                $cutpoint = $maxLength;
976                // RFC 2047 specifies that any split header should
977                // be seperated by a CRLF SPACE.
978                if ($output) {
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
993            if (strlen($value) > $maxLength1stLine) {
994                // Begin with the regexp for the first line.
995                $reg = $reg1st;
996                $output = '';
997                while ($value) {
998                    // Split translated string at every $maxLength
999                    // But make sure not to break any translated chars.
1000                    $found = preg_match($reg, $value, $matches);
1001
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]);
1012                        $value = substr($value, $len);
1013                    } else {
1014                        $part = $value;
1015                        $value = '';
1016                    }
1017
1018                    // RFC 2047 specifies that any split header should
1019                    // be seperated by a CRLF SPACE
1020                    if ($output) {
1021                        $output .= $eol . ' ';
1022                    }
1023                    $output .= $prefix . $part . $suffix;
1024                }
1025                $value = $output;
1026            } else {
1027                $value = $prefix . $value . $suffix;
1028            }
1029        }
1030
1031        return $value;
1032    }
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    {
1045        // Bug #17226 RFC 2047 restricts some characters
1046        // if the word is inside a phrase, permitted chars are only:
1047        // ASCII letters, decimal digits, "!", "*", "+", "-", "/", "=", and "_"
1048
1049        // "=",  "_",  "?" must be encoded
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
1053        );
1054
1055        return str_replace(' ', '_', $str);
1056    }
1057
1058    /**
1059     * Encodes the given string using base64 or quoted-printable.
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    /**
1163     * Callback function to replace extended characters (\x80-xFF) with their
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]));
1174    }
1175
1176    /**
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
1190} // End of class
Note: See TracBrowser for help on using the repository browser.