]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - extlib/Mail/mimeDecode.php
PEAR's Mail_mimeDecode updated
[quix0rs-gnu-social.git] / extlib / Mail / mimeDecode.php
1 <?php
2 /**
3  * The Mail_mimeDecode class is used to decode mail/mime messages
4  *
5  * This class will parse a raw mime email and return
6  * the structure. Returned structure is similar to
7  * that returned by imap_fetchstructure().
8  *
9  *  +----------------------------- IMPORTANT ------------------------------+
10  *  | Usage of this class compared to native php extensions such as        |
11  *  | mailparse or imap, is slow and may be feature deficient. If available|
12  *  | you are STRONGLY recommended to use the php extensions.              |
13  *  +----------------------------------------------------------------------+
14  *
15  * Compatible with PHP versions 4 and 5
16  *
17  * LICENSE: This LICENSE is in the BSD license style.
18  * Copyright (c) 2002-2003, Richard Heyes <richard@phpguru.org>
19  * Copyright (c) 2003-2006, PEAR <pear-group@php.net>
20  * All rights reserved.
21  *
22  * Redistribution and use in source and binary forms, with or
23  * without modification, are permitted provided that the following
24  * conditions are met:
25  *
26  * - Redistributions of source code must retain the above copyright
27  *   notice, this list of conditions and the following disclaimer.
28  * - Redistributions in binary form must reproduce the above copyright
29  *   notice, this list of conditions and the following disclaimer in the
30  *   documentation and/or other materials provided with the distribution.
31  * - Neither the name of the authors, nor the names of its contributors 
32  *   may be used to endorse or promote products derived from this 
33  *   software without specific prior written permission.
34  *
35  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
36  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
37  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
38  * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
39  * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
40  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
41  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
42  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
43  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
44  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
45  * THE POSSIBILITY OF SUCH DAMAGE.
46  *
47  * @category   Mail
48  * @package    Mail_Mime
49  * @author     Richard Heyes  <richard@phpguru.org>
50  * @author     George Schlossnagle <george@omniti.com>
51  * @author     Cipriano Groenendal <cipri@php.net>
52  * @author     Sean Coates <sean@php.net>
53  * @copyright  2003-2006 PEAR <pear-group@php.net>
54  * @license    http://www.opensource.org/licenses/bsd-license.php BSD License
55  * @version    CVS: $Id: mimeDecode.php 305875 2010-12-01 07:17:10Z alan_k $
56  * @link       http://pear.php.net/package/Mail_mime
57  */
58
59
60 /**
61  * require PEAR
62  *
63  * This package depends on PEAR to raise errors.
64  */
65 require_once 'PEAR.php';
66
67
68 /**
69  * The Mail_mimeDecode class is used to decode mail/mime messages
70  *
71  * This class will parse a raw mime email and return the structure.
72  * Returned structure is similar to that returned by imap_fetchstructure().
73  *
74  *  +----------------------------- IMPORTANT ------------------------------+
75  *  | Usage of this class compared to native php extensions such as        |
76  *  | mailparse or imap, is slow and may be feature deficient. If available|
77  *  | you are STRONGLY recommended to use the php extensions.              |
78  *  +----------------------------------------------------------------------+
79  *
80  * @category   Mail
81  * @package    Mail_Mime
82  * @author     Richard Heyes  <richard@phpguru.org>
83  * @author     George Schlossnagle <george@omniti.com>
84  * @author     Cipriano Groenendal <cipri@php.net>
85  * @author     Sean Coates <sean@php.net>
86  * @copyright  2003-2006 PEAR <pear-group@php.net>
87  * @license    http://www.opensource.org/licenses/bsd-license.php BSD License
88  * @version    Release: @package_version@
89  * @link       http://pear.php.net/package/Mail_mime
90  */
91 class Mail_mimeDecode extends PEAR
92 {
93     /**
94      * The raw email to decode
95      *
96      * @var    string
97      * @access private
98      */
99     var $_input;
100
101     /**
102      * The header part of the input
103      *
104      * @var    string
105      * @access private
106      */
107     var $_header;
108
109     /**
110      * The body part of the input
111      *
112      * @var    string
113      * @access private
114      */
115     var $_body;
116
117     /**
118      * If an error occurs, this is used to store the message
119      *
120      * @var    string
121      * @access private
122      */
123     var $_error;
124
125     /**
126      * Flag to determine whether to include bodies in the
127      * returned object.
128      *
129      * @var    boolean
130      * @access private
131      */
132     var $_include_bodies;
133
134     /**
135      * Flag to determine whether to decode bodies
136      *
137      * @var    boolean
138      * @access private
139      */
140     var $_decode_bodies;
141
142     /**
143      * Flag to determine whether to decode headers
144      *
145      * @var    boolean
146      * @access private
147      */
148     var $_decode_headers;
149
150     /**
151      * Flag to determine whether to include attached messages
152      * as body in the returned object. Depends on $_include_bodies
153      *
154      * @var    boolean
155      * @access private
156      */
157     var $_rfc822_bodies;
158
159     /**
160      * Constructor.
161      *
162      * Sets up the object, initialise the variables, and splits and
163      * stores the header and body of the input.
164      *
165      * @param string The input to decode
166      * @access public
167      */
168     function Mail_mimeDecode($input)
169     {
170         list($header, $body)   = $this->_splitBodyHeader($input);
171
172         $this->_input          = $input;
173         $this->_header         = $header;
174         $this->_body           = $body;
175         $this->_decode_bodies  = false;
176         $this->_include_bodies = true;
177         $this->_rfc822_bodies  = false;
178     }
179
180     /**
181      * Begins the decoding process. If called statically
182      * it will create an object and call the decode() method
183      * of it.
184      *
185      * @param array An array of various parameters that determine
186      *              various things:
187      *              include_bodies - Whether to include the body in the returned
188      *                               object.
189      *              decode_bodies  - Whether to decode the bodies
190      *                               of the parts. (Transfer encoding)
191      *              decode_headers - Whether to decode headers
192      *              input          - If called statically, this will be treated
193      *                               as the input
194      * @return object Decoded results
195      * @access public
196      */
197     function decode($params = null)
198     {
199         // determine if this method has been called statically
200         $isStatic = empty($this) || !is_a($this, __CLASS__);
201
202         // Have we been called statically?
203         // If so, create an object and pass details to that.
204         if ($isStatic AND isset($params['input'])) {
205
206             $obj = new Mail_mimeDecode($params['input']);
207             $structure = $obj->decode($params);
208
209         // Called statically but no input
210         } elseif ($isStatic) {
211             return PEAR::raiseError('Called statically and no input given');
212
213         // Called via an object
214         } else {
215             $this->_include_bodies = isset($params['include_bodies']) ?
216                                      $params['include_bodies'] : false;
217             $this->_decode_bodies  = isset($params['decode_bodies']) ?
218                                      $params['decode_bodies']  : false;
219             $this->_decode_headers = isset($params['decode_headers']) ?
220                                      $params['decode_headers'] : false;
221             $this->_rfc822_bodies  = isset($params['rfc_822bodies']) ?
222                                      $params['rfc_822bodies']  : false;
223
224             $structure = $this->_decode($this->_header, $this->_body);
225             if ($structure === false) {
226                 $structure = $this->raiseError($this->_error);
227             }
228         }
229
230         return $structure;
231     }
232
233     /**
234      * Performs the decoding. Decodes the body string passed to it
235      * If it finds certain content-types it will call itself in a
236      * recursive fashion
237      *
238      * @param string Header section
239      * @param string Body section
240      * @return object Results of decoding process
241      * @access private
242      */
243     function _decode($headers, $body, $default_ctype = 'text/plain')
244     {
245         $return = new stdClass;
246         $return->headers = array();
247         $headers = $this->_parseHeaders($headers);
248
249         foreach ($headers as $value) {
250             $value['value'] = $this->_decode_headers ? $this->_decodeHeader($value['value']) : $value['value'];
251             if (isset($return->headers[strtolower($value['name'])]) AND !is_array($return->headers[strtolower($value['name'])])) {
252                 $return->headers[strtolower($value['name'])]   = array($return->headers[strtolower($value['name'])]);
253                 $return->headers[strtolower($value['name'])][] = $value['value'];
254
255             } elseif (isset($return->headers[strtolower($value['name'])])) {
256                 $return->headers[strtolower($value['name'])][] = $value['value'];
257
258             } else {
259                 $return->headers[strtolower($value['name'])] = $value['value'];
260             }
261         }
262
263
264         foreach ($headers as $key => $value) {
265             $headers[$key]['name'] = strtolower($headers[$key]['name']);
266             switch ($headers[$key]['name']) {
267
268                 case 'content-type':
269                     $content_type = $this->_parseHeaderValue($headers[$key]['value']);
270
271                     if (preg_match('/([0-9a-z+.-]+)\/([0-9a-z+.-]+)/i', $content_type['value'], $regs)) {
272                         $return->ctype_primary   = $regs[1];
273                         $return->ctype_secondary = $regs[2];
274                     }
275
276                     if (isset($content_type['other'])) {
277                         foreach($content_type['other'] as $p_name => $p_value) {
278                             $return->ctype_parameters[$p_name] = $p_value;
279                         }
280                     }
281                     break;
282
283                 case 'content-disposition':
284                     $content_disposition = $this->_parseHeaderValue($headers[$key]['value']);
285                     $return->disposition   = $content_disposition['value'];
286                     if (isset($content_disposition['other'])) {
287                         foreach($content_disposition['other'] as $p_name => $p_value) {
288                             $return->d_parameters[$p_name] = $p_value;
289                         }
290                     }
291                     break;
292
293                 case 'content-transfer-encoding':
294                     $content_transfer_encoding = $this->_parseHeaderValue($headers[$key]['value']);
295                     break;
296             }
297         }
298
299         if (isset($content_type)) {
300             switch (strtolower($content_type['value'])) {
301                 case 'text/plain':
302                     $encoding = isset($content_transfer_encoding) ? $content_transfer_encoding['value'] : '7bit';
303                     $this->_include_bodies ? $return->body = ($this->_decode_bodies ? $this->_decodeBody($body, $encoding) : $body) : null;
304                     break;
305
306                 case 'text/html':
307                     $encoding = isset($content_transfer_encoding) ? $content_transfer_encoding['value'] : '7bit';
308                     $this->_include_bodies ? $return->body = ($this->_decode_bodies ? $this->_decodeBody($body, $encoding) : $body) : null;
309                     break;
310
311                 case 'multipart/parallel':
312                 case 'multipart/appledouble': // Appledouble mail
313                 case 'multipart/report': // RFC1892
314                 case 'multipart/signed': // PGP
315                 case 'multipart/digest':
316                 case 'multipart/alternative':
317                 case 'multipart/related':
318                 case 'multipart/mixed':
319                 case 'application/vnd.wap.multipart.related':
320                     if(!isset($content_type['other']['boundary'])){
321                         $this->_error = 'No boundary found for ' . $content_type['value'] . ' part';
322                         return false;
323                     }
324
325                     $default_ctype = (strtolower($content_type['value']) === 'multipart/digest') ? 'message/rfc822' : 'text/plain';
326
327                     $parts = $this->_boundarySplit($body, $content_type['other']['boundary']);
328                     for ($i = 0; $i < count($parts); $i++) {
329                         list($part_header, $part_body) = $this->_splitBodyHeader($parts[$i]);
330                         $part = $this->_decode($part_header, $part_body, $default_ctype);
331                         if($part === false)
332                             $part = $this->raiseError($this->_error);
333                         $return->parts[] = $part;
334                     }
335                     break;
336
337                 case 'message/rfc822':
338                                         if ($this->_rfc822_bodies) {
339                                                 $encoding = isset($content_transfer_encoding) ? $content_transfer_encoding['value'] : '7bit';
340                                                 $return->body = ($this->_decode_bodies ? $this->_decodeBody($body, $encoding) : $body);
341                                         }
342                     $obj = new Mail_mimeDecode($body);
343                     $return->parts[] = $obj->decode(array('include_bodies' => $this->_include_bodies,
344                                                                               'decode_bodies'  => $this->_decode_bodies,
345                                                                                                                   'decode_headers' => $this->_decode_headers));
346                     unset($obj);
347                     break;
348
349                 default:
350                     if(!isset($content_transfer_encoding['value']))
351                         $content_transfer_encoding['value'] = '7bit';
352                     $this->_include_bodies ? $return->body = ($this->_decode_bodies ? $this->_decodeBody($body, $content_transfer_encoding['value']) : $body) : null;
353                     break;
354             }
355
356         } else {
357             $ctype = explode('/', $default_ctype);
358             $return->ctype_primary   = $ctype[0];
359             $return->ctype_secondary = $ctype[1];
360             $this->_include_bodies ? $return->body = ($this->_decode_bodies ? $this->_decodeBody($body) : $body) : null;
361         }
362
363         return $return;
364     }
365
366     /**
367      * Given the output of the above function, this will return an
368      * array of references to the parts, indexed by mime number.
369      *
370      * @param  object $structure   The structure to go through
371      * @param  string $mime_number Internal use only.
372      * @return array               Mime numbers
373      */
374     function &getMimeNumbers(&$structure, $no_refs = false, $mime_number = '', $prepend = '')
375     {
376         $return = array();
377         if (!empty($structure->parts)) {
378             if ($mime_number != '') {
379                 $structure->mime_id = $prepend . $mime_number;
380                 $return[$prepend . $mime_number] = &$structure;
381             }
382             for ($i = 0; $i < count($structure->parts); $i++) {
383
384             
385                 if (!empty($structure->headers['content-type']) AND substr(strtolower($structure->headers['content-type']), 0, 8) == 'message/') {
386                     $prepend      = $prepend . $mime_number . '.';
387                     $_mime_number = '';
388                 } else {
389                     $_mime_number = ($mime_number == '' ? $i + 1 : sprintf('%s.%s', $mime_number, $i + 1));
390                 }
391
392                 $arr = &Mail_mimeDecode::getMimeNumbers($structure->parts[$i], $no_refs, $_mime_number, $prepend);
393                 foreach ($arr as $key => $val) {
394                     $no_refs ? $return[$key] = '' : $return[$key] = &$arr[$key];
395                 }
396             }
397         } else {
398             if ($mime_number == '') {
399                 $mime_number = '1';
400             }
401             $structure->mime_id = $prepend . $mime_number;
402             $no_refs ? $return[$prepend . $mime_number] = '' : $return[$prepend . $mime_number] = &$structure;
403         }
404         
405         return $return;
406     }
407
408     /**
409      * Given a string containing a header and body
410      * section, this function will split them (at the first
411      * blank line) and return them.
412      *
413      * @param string Input to split apart
414      * @return array Contains header and body section
415      * @access private
416      */
417     function _splitBodyHeader($input)
418     {
419         if (preg_match("/^(.*?)\r?\n\r?\n(.*)/s", $input, $match)) {
420             return array($match[1], $match[2]);
421         }
422         // bug #17325 - empty bodies are allowed. - we just check that at least one line 
423         // of headers exist..
424         if (count(explode("\n",$input))) {
425             return array($input, '');
426         }
427         $this->_error = 'Could not split header and body';
428         return false;
429     }
430
431     /**
432      * Parse headers given in $input and return
433      * as assoc array.
434      *
435      * @param string Headers to parse
436      * @return array Contains parsed headers
437      * @access private
438      */
439     function _parseHeaders($input)
440     {
441
442         if ($input !== '') {
443             // Unfold the input
444             $input   = preg_replace("/\r?\n/", "\r\n", $input);
445             //#7065 - wrapping.. with encoded stuff.. - probably not needed,
446             // wrapping space should only get removed if the trailing item on previous line is a 
447             // encoded character
448             $input   = preg_replace("/=\r\n(\t| )+/", '=', $input);
449             $input   = preg_replace("/\r\n(\t| )+/", ' ', $input);
450             
451             $headers = explode("\r\n", trim($input));
452
453             foreach ($headers as $value) {
454                 $hdr_name = substr($value, 0, $pos = strpos($value, ':'));
455                 $hdr_value = substr($value, $pos+1);
456                 if($hdr_value[0] == ' ')
457                     $hdr_value = substr($hdr_value, 1);
458
459                 $return[] = array(
460                                   'name'  => $hdr_name,
461                                   'value' =>  $hdr_value
462                                  );
463             }
464         } else {
465             $return = array();
466         }
467
468         return $return;
469     }
470
471     /**
472      * Function to parse a header value,
473      * extract first part, and any secondary
474      * parts (after ;) This function is not as
475      * robust as it could be. Eg. header comments
476      * in the wrong place will probably break it.
477      *
478      * @param string Header value to parse
479      * @return array Contains parsed result
480      * @access private
481      */
482     function _parseHeaderValue($input)
483     {
484
485         if (($pos = strpos($input, ';')) === false) {
486             $input = $this->_decode_headers ? $this->_decodeHeader($input) : $input;
487             $return['value'] = trim($input);
488             return $return;
489         }
490
491
492
493         $value = substr($input, 0, $pos);
494         $value = $this->_decode_headers ? $this->_decodeHeader($value) : $value;
495         $return['value'] = trim($value);
496         $input = trim(substr($input, $pos+1));
497
498         if (!strlen($input) > 0) {
499             return $return;
500         }
501         // at this point input contains xxxx=".....";zzzz="...."
502         // since we are dealing with quoted strings, we need to handle this properly..
503         $i = 0;
504         $l = strlen($input);
505         $key = '';
506         $val = false; // our string - including quotes..
507         $q = false; // in quote..
508         $lq = ''; // last quote..
509
510         while ($i < $l) {
511             
512             $c = $input[$i];
513             //var_dump(array('i'=>$i,'c'=>$c,'q'=>$q, 'lq'=>$lq, 'key'=>$key, 'val' =>$val));
514
515             $escaped = false;
516             if ($c == '\\') {
517                 $i++;
518                 if ($i == $l-1) { // end of string.
519                     break;
520                 }
521                 $escaped = true;
522                 $c = $input[$i];
523             }            
524
525
526             // state - in key..
527             if ($val === false) {
528                 if (!$escaped && $c == '=') {
529                     $val = '';
530                     $key = trim($key);
531                     $i++;
532                     continue;
533                 }
534                 if (!$escaped && $c == ';') {
535                     if ($key) { // a key without a value..
536                         $key= trim($key);
537                         $return['other'][$key] = '';
538                         $return['other'][strtolower($key)] = '';
539                     }
540                     $key = '';
541                 }
542                 $key .= $c;
543                 $i++;
544                 continue;
545             }
546                      
547             // state - in value.. (as $val is set..)
548
549             if ($q === false) {
550                 // not in quote yet.
551                 if ((!strlen($val) || $lq !== false) && $c == ' ' ||  $c == "\t") {
552                     $i++;
553                     continue; // skip leading spaces after '=' or after '"'
554                 }
555                 if (!$escaped && ($c == '"' || $c == "'")) {
556                     // start quoted area..
557                     $q = $c;
558                     // in theory should not happen raw text in value part..
559                     // but we will handle it as a merged part of the string..
560                     $val = !strlen(trim($val)) ? '' : trim($val);
561                     $i++;
562                     continue;
563                 }
564                 // got end....
565                 if (!$escaped && $c == ';') {
566
567                     $val = trim($val);
568                     $added = false;
569                     if (preg_match('/\*[0-9]+$/', $key)) {
570                         // this is the extended aaa*0=...;aaa*1=.... code
571                         // it assumes the pieces arrive in order, and are valid...
572                         $key = preg_replace('/\*[0-9]+$/', '', $key);
573                         if (isset($return['other'][$key])) {
574                             $return['other'][$key] .= $val;
575                             if (strtolower($key) != $key) {
576                                 $return['other'][strtolower($key)] .= $val;
577                             }
578                             $added = true;
579                         }
580                         // continue and use standard setters..
581                     }
582                     if (!$added) {
583                         $return['other'][$key] = $val;
584                         $return['other'][strtolower($key)] = $val;
585                     }
586                     $val = false;
587                     $key = '';
588                     $lq = false;
589                     $i++;
590                     continue;
591                 }
592
593                 $val .= $c;
594                 $i++;
595                 continue;
596             }
597             
598             // state - in quote..
599             if (!$escaped && $c == $q) {  // potential exit state..
600
601                 // end of quoted string..
602                 $lq = $q;
603                 $q = false;
604                 $i++;
605                 continue;
606             }
607                 
608             // normal char inside of quoted string..
609             $val.= $c;
610             $i++;
611         }
612         
613         // do we have anything left..
614         if (strlen(trim($key)) || $val !== false) {
615            
616             $val = trim($val);
617             $added = false;
618             if ($val !== false && preg_match('/\*[0-9]+$/', $key)) {
619                 // no dupes due to our crazy regexp.
620                 $key = preg_replace('/\*[0-9]+$/', '', $key);
621                 if (isset($return['other'][$key])) {
622                     $return['other'][$key] .= $val;
623                     if (strtolower($key) != $key) {
624                         $return['other'][strtolower($key)] .= $val;
625                     }
626                     $added = true;
627                 }
628                 // continue and use standard setters..
629             }
630             if (!$added) {
631                 $return['other'][$key] = $val;
632                 $return['other'][strtolower($key)] = $val;
633             }
634         }
635         // decode values.
636         foreach($return['other'] as $key =>$val) {
637             $return['other'][$key] = $this->_decode_headers ? $this->_decodeHeader($val) : $val;
638         }
639        //print_r($return);
640         return $return;
641     }
642
643     /**
644      * This function splits the input based
645      * on the given boundary
646      *
647      * @param string Input to parse
648      * @return array Contains array of resulting mime parts
649      * @access private
650      */
651     function _boundarySplit($input, $boundary)
652     {
653         $parts = array();
654
655         $bs_possible = substr($boundary, 2, -2);
656         $bs_check = '\"' . $bs_possible . '\"';
657
658         if ($boundary == $bs_check) {
659             $boundary = $bs_possible;
660         }
661         $tmp = preg_split("/--".preg_quote($boundary, '/')."((?=\s)|--)/", $input);
662
663         $len = count($tmp) -1;
664         for ($i = 1; $i < $len; $i++) {
665             if (strlen(trim($tmp[$i]))) {
666                 $parts[] = $tmp[$i];
667             }
668         }
669         
670         // add the last part on if it does not end with the 'closing indicator'
671         if (!empty($tmp[$len]) && strlen(trim($tmp[$len])) && $tmp[$len][0] != '-') {
672             $parts[] = $tmp[$len];
673         }
674         return $parts;
675     }
676
677     /**
678      * Given a header, this function will decode it
679      * according to RFC2047. Probably not *exactly*
680      * conformant, but it does pass all the given
681      * examples (in RFC2047).
682      *
683      * @param string Input header value to decode
684      * @return string Decoded header value
685      * @access private
686      */
687     function _decodeHeader($input)
688     {
689         // Remove white space between encoded-words
690         $input = preg_replace('/(=\?[^?]+\?(q|b)\?[^?]*\?=)(\s)+=\?/i', '\1=?', $input);
691
692         // For each encoded-word...
693         while (preg_match('/(=\?([^?]+)\?(q|b)\?([^?]*)\?=)/i', $input, $matches)) {
694
695             $encoded  = $matches[1];
696             $charset  = $matches[2];
697             $encoding = $matches[3];
698             $text     = $matches[4];
699
700             switch (strtolower($encoding)) {
701                 case 'b':
702                     $text = base64_decode($text);
703                     break;
704
705                 case 'q':
706                     $text = str_replace('_', ' ', $text);
707                     preg_match_all('/=([a-f0-9]{2})/i', $text, $matches);
708                     foreach($matches[1] as $value)
709                         $text = str_replace('='.$value, chr(hexdec($value)), $text);
710                     break;
711             }
712
713             $input = str_replace($encoded, $text, $input);
714         }
715
716         return $input;
717     }
718
719     /**
720      * Given a body string and an encoding type,
721      * this function will decode and return it.
722      *
723      * @param  string Input body to decode
724      * @param  string Encoding type to use.
725      * @return string Decoded body
726      * @access private
727      */
728     function _decodeBody($input, $encoding = '7bit')
729     {
730         switch (strtolower($encoding)) {
731             case '7bit':
732                 return $input;
733                 break;
734
735             case 'quoted-printable':
736                 return $this->_quotedPrintableDecode($input);
737                 break;
738
739             case 'base64':
740                 return base64_decode($input);
741                 break;
742
743             default:
744                 return $input;
745         }
746     }
747
748     /**
749      * Given a quoted-printable string, this
750      * function will decode and return it.
751      *
752      * @param  string Input body to decode
753      * @return string Decoded body
754      * @access private
755      */
756     function _quotedPrintableDecode($input)
757     {
758         // Remove soft line breaks
759         $input = preg_replace("/=\r?\n/", '', $input);
760
761         // Replace encoded characters
762                 $input = preg_replace('/=([a-f0-9]{2})/ie', "chr(hexdec('\\1'))", $input);
763
764         return $input;
765     }
766
767     /**
768      * Checks the input for uuencoded files and returns
769      * an array of them. Can be called statically, eg:
770      *
771      * $files =& Mail_mimeDecode::uudecode($some_text);
772      *
773      * It will check for the begin 666 ... end syntax
774      * however and won't just blindly decode whatever you
775      * pass it.
776      *
777      * @param  string Input body to look for attahcments in
778      * @return array  Decoded bodies, filenames and permissions
779      * @access public
780      * @author Unknown
781      */
782     function &uudecode($input)
783     {
784         // Find all uuencoded sections
785         preg_match_all("/begin ([0-7]{3}) (.+)\r?\n(.+)\r?\nend/Us", $input, $matches);
786
787         for ($j = 0; $j < count($matches[3]); $j++) {
788
789             $str      = $matches[3][$j];
790             $filename = $matches[2][$j];
791             $fileperm = $matches[1][$j];
792
793             $file = '';
794             $str = preg_split("/\r?\n/", trim($str));
795             $strlen = count($str);
796
797             for ($i = 0; $i < $strlen; $i++) {
798                 $pos = 1;
799                 $d = 0;
800                 $len=(int)(((ord(substr($str[$i],0,1)) -32) - ' ') & 077);
801
802                 while (($d + 3 <= $len) AND ($pos + 4 <= strlen($str[$i]))) {
803                     $c0 = (ord(substr($str[$i],$pos,1)) ^ 0x20);
804                     $c1 = (ord(substr($str[$i],$pos+1,1)) ^ 0x20);
805                     $c2 = (ord(substr($str[$i],$pos+2,1)) ^ 0x20);
806                     $c3 = (ord(substr($str[$i],$pos+3,1)) ^ 0x20);
807                     $file .= chr(((($c0 - ' ') & 077) << 2) | ((($c1 - ' ') & 077) >> 4));
808
809                     $file .= chr(((($c1 - ' ') & 077) << 4) | ((($c2 - ' ') & 077) >> 2));
810
811                     $file .= chr(((($c2 - ' ') & 077) << 6) |  (($c3 - ' ') & 077));
812
813                     $pos += 4;
814                     $d += 3;
815                 }
816
817                 if (($d + 2 <= $len) && ($pos + 3 <= strlen($str[$i]))) {
818                     $c0 = (ord(substr($str[$i],$pos,1)) ^ 0x20);
819                     $c1 = (ord(substr($str[$i],$pos+1,1)) ^ 0x20);
820                     $c2 = (ord(substr($str[$i],$pos+2,1)) ^ 0x20);
821                     $file .= chr(((($c0 - ' ') & 077) << 2) | ((($c1 - ' ') & 077) >> 4));
822
823                     $file .= chr(((($c1 - ' ') & 077) << 4) | ((($c2 - ' ') & 077) >> 2));
824
825                     $pos += 3;
826                     $d += 2;
827                 }
828
829                 if (($d + 1 <= $len) && ($pos + 2 <= strlen($str[$i]))) {
830                     $c0 = (ord(substr($str[$i],$pos,1)) ^ 0x20);
831                     $c1 = (ord(substr($str[$i],$pos+1,1)) ^ 0x20);
832                     $file .= chr(((($c0 - ' ') & 077) << 2) | ((($c1 - ' ') & 077) >> 4));
833
834                 }
835             }
836             $files[] = array('filename' => $filename, 'fileperm' => $fileperm, 'filedata' => $file);
837         }
838
839         return $files;
840     }
841
842     /**
843      * getSendArray() returns the arguments required for Mail::send()
844      * used to build the arguments for a mail::send() call 
845      *
846      * Usage:
847      * $mailtext = Full email (for example generated by a template)
848      * $decoder = new Mail_mimeDecode($mailtext);
849      * $parts =  $decoder->getSendArray();
850      * if (!PEAR::isError($parts) {
851      *     list($recipents,$headers,$body) = $parts;
852      *     $mail = Mail::factory('smtp');
853      *     $mail->send($recipents,$headers,$body);
854      * } else {
855      *     echo $parts->message;
856      * }
857      * @return mixed   array of recipeint, headers,body or Pear_Error
858      * @access public
859      * @author Alan Knowles <alan@akbkhome.com>
860      */
861     function getSendArray()
862     {
863         // prevent warning if this is not set
864         $this->_decode_headers = FALSE;
865         $headerlist =$this->_parseHeaders($this->_header);
866         $to = "";
867         if (!$headerlist) {
868             return $this->raiseError("Message did not contain headers");
869         }
870         foreach($headerlist as $item) {
871             $header[$item['name']] = $item['value'];
872             switch (strtolower($item['name'])) {
873                 case "to":
874                 case "cc":
875                 case "bcc":
876                     $to .= ",".$item['value'];
877                 default:
878                    break;
879             }
880         }
881         if ($to == "") {
882             return $this->raiseError("Message did not contain any recipents");
883         }
884         $to = substr($to,1);
885         return array($to,$header,$this->_body);
886     } 
887
888     /**
889      * Returns a xml copy of the output of
890      * Mail_mimeDecode::decode. Pass the output in as the
891      * argument. This function can be called statically. Eg:
892      *
893      * $output = $obj->decode();
894      * $xml    = Mail_mimeDecode::getXML($output);
895      *
896      * The DTD used for this should have been in the package. Or
897      * alternatively you can get it from cvs, or here:
898      * http://www.phpguru.org/xmail/xmail.dtd.
899      *
900      * @param  object Input to convert to xml. This should be the
901      *                output of the Mail_mimeDecode::decode function
902      * @return string XML version of input
903      * @access public
904      */
905     function getXML($input)
906     {
907         $crlf    =  "\r\n";
908         $output  = '<?xml version=\'1.0\'?>' . $crlf .
909                    '<!DOCTYPE email SYSTEM "http://www.phpguru.org/xmail/xmail.dtd">' . $crlf .
910                    '<email>' . $crlf .
911                    Mail_mimeDecode::_getXML($input) .
912                    '</email>';
913
914         return $output;
915     }
916
917     /**
918      * Function that does the actual conversion to xml. Does a single
919      * mimepart at a time.
920      *
921      * @param  object  Input to convert to xml. This is a mimepart object.
922      *                 It may or may not contain subparts.
923      * @param  integer Number of tabs to indent
924      * @return string  XML version of input
925      * @access private
926      */
927     function _getXML($input, $indent = 1)
928     {
929         $htab    =  "\t";
930         $crlf    =  "\r\n";
931         $output  =  '';
932         $headers = @(array)$input->headers;
933
934         foreach ($headers as $hdr_name => $hdr_value) {
935
936             // Multiple headers with this name
937             if (is_array($headers[$hdr_name])) {
938                 for ($i = 0; $i < count($hdr_value); $i++) {
939                     $output .= Mail_mimeDecode::_getXML_helper($hdr_name, $hdr_value[$i], $indent);
940                 }
941
942             // Only one header of this sort
943             } else {
944                 $output .= Mail_mimeDecode::_getXML_helper($hdr_name, $hdr_value, $indent);
945             }
946         }
947
948         if (!empty($input->parts)) {
949             for ($i = 0; $i < count($input->parts); $i++) {
950                 $output .= $crlf . str_repeat($htab, $indent) . '<mimepart>' . $crlf .
951                            Mail_mimeDecode::_getXML($input->parts[$i], $indent+1) .
952                            str_repeat($htab, $indent) . '</mimepart>' . $crlf;
953             }
954         } elseif (isset($input->body)) {
955             $output .= $crlf . str_repeat($htab, $indent) . '<body><![CDATA[' .
956                        $input->body . ']]></body>' . $crlf;
957         }
958
959         return $output;
960     }
961
962     /**
963      * Helper function to _getXML(). Returns xml of a header.
964      *
965      * @param  string  Name of header
966      * @param  string  Value of header
967      * @param  integer Number of tabs to indent
968      * @return string  XML version of input
969      * @access private
970      */
971     function _getXML_helper($hdr_name, $hdr_value, $indent)
972     {
973         $htab   = "\t";
974         $crlf   = "\r\n";
975         $return = '';
976
977         $new_hdr_value = ($hdr_name != 'received') ? Mail_mimeDecode::_parseHeaderValue($hdr_value) : array('value' => $hdr_value);
978         $new_hdr_name  = str_replace(' ', '-', ucwords(str_replace('-', ' ', $hdr_name)));
979
980         // Sort out any parameters
981         if (!empty($new_hdr_value['other'])) {
982             foreach ($new_hdr_value['other'] as $paramname => $paramvalue) {
983                 $params[] = str_repeat($htab, $indent) . $htab . '<parameter>' . $crlf .
984                             str_repeat($htab, $indent) . $htab . $htab . '<paramname>' . htmlspecialchars($paramname) . '</paramname>' . $crlf .
985                             str_repeat($htab, $indent) . $htab . $htab . '<paramvalue>' . htmlspecialchars($paramvalue) . '</paramvalue>' . $crlf .
986                             str_repeat($htab, $indent) . $htab . '</parameter>' . $crlf;
987             }
988
989             $params = implode('', $params);
990         } else {
991             $params = '';
992         }
993
994         $return = str_repeat($htab, $indent) . '<header>' . $crlf .
995                   str_repeat($htab, $indent) . $htab . '<headername>' . htmlspecialchars($new_hdr_name) . '</headername>' . $crlf .
996                   str_repeat($htab, $indent) . $htab . '<headervalue>' . htmlspecialchars($new_hdr_value['value']) . '</headervalue>' . $crlf .
997                   $params .
998                   str_repeat($htab, $indent) . '</header>' . $crlf;
999
1000         return $return;
1001     }
1002
1003 } // End of class