source: subversion/trunk/roundcubemail/program/lib/Mail/mimePart.php @ 3837

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