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