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 337165 2015-07-15 09:42:08Z 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 */
65require_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 */
91class 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     * (set to UTF8 to iconv convert headers)
145     * @var    mixed
146     * @access private
147     */
148    var $_decode_headers;
149
150
151    /**
152     * Flag to determine whether to include attached messages
153     * as body in the returned object. Depends on $_include_bodies
154     *
155     * @var    boolean
156     * @access private
157     */
158    var $_rfc822_bodies;
159
160    /**
161     * Constructor.
162     *
163     * Sets up the object, initialise the variables, and splits and
164     * stores the header and body of the input.
165     *
166     * @param string The input to decode
167     * @access public
168     */
169    function __construct($input)
170    {
171        list($header, $body)   = $this->_splitBodyHeader($input);
172
173        $this->_input          = $input;
174        $this->_header         = $header;
175        $this->_body           = $body;
176        $this->_decode_bodies  = false;
177        $this->_include_bodies = true;
178        $this->_rfc822_bodies  = false;
179    }
180    // BC
181    function Mail_mimeDecode($input)
182    {
183        $this->__construct($input);
184    }
185
186
187    /**
188     * Begins the decoding process. If called statically
189     * it will create an object and call the decode() method
190     * of it.
191     *
192     * @param array An array of various parameters that determine
193     *              various things:
194     *              include_bodies - Whether to include the body in the returned
195     *                               object.
196     *              decode_bodies  - Whether to decode the bodies
197     *                               of the parts. (Transfer encoding)
198     *              decode_headers - Whether to decode headers,
199     *                             - use "UTF8//IGNORE" to convert charset.
200     *
201     *              input          - If called statically, this will be treated
202     *                               as the input
203     * @return object Decoded results
204     * @access public
205     */
206    function decode($params = null)
207    {
208        // determine if this method has been called statically
209        $isStatic = empty($this) || !is_a($this, __CLASS__);
210
211        // Have we been called statically?
212	// If so, create an object and pass details to that.
213        if ($isStatic AND isset($params['input'])) {
214
215            $obj = new Mail_mimeDecode($params['input']);
216            $structure = $obj->decode($params);
217
218        // Called statically but no input
219        } elseif ($isStatic) {
220            return PEAR::raiseError('Called statically and no input given');
221
222        // Called via an object
223        } else {
224            $this->_include_bodies = isset($params['include_bodies']) ?
225	                             $params['include_bodies'] : false;
226            $this->_decode_bodies  = isset($params['decode_bodies']) ?
227	                             $params['decode_bodies']  : false;
228            $this->_decode_headers = isset($params['decode_headers']) ?
229	                             $params['decode_headers'] : false;
230            $this->_rfc822_bodies  = isset($params['rfc_822bodies']) ?
231	                             $params['rfc_822bodies']  : false;
232
233            if (is_string($this->_decode_headers) && !function_exists('iconv')) {
234                 PEAR::raiseError('header decode conversion requested, however iconv is missing');
235            }
236
237            $structure = $this->_decode($this->_header, $this->_body);
238            if ($structure === false) {
239                $structure = $this->raiseError($this->_error);
240            }
241        }
242
243        return $structure;
244    }
245
246    /**
247     * Performs the decoding. Decodes the body string passed to it
248     * If it finds certain content-types it will call itself in a
249     * recursive fashion
250     *
251     * @param string Header section
252     * @param string Body section
253     * @return object Results of decoding process
254     * @access private
255     */
256    function _decode($headers, $body, $default_ctype = 'text/plain')
257    {
258        $return = new stdClass;
259        $return->headers = array();
260        $headers = $this->_parseHeaders($headers);
261
262        foreach ($headers as $value) {
263            $value['value'] =  $this->_decodeHeader($value['value']);
264            if (isset($return->headers[strtolower($value['name'])]) AND !is_array($return->headers[strtolower($value['name'])])) {
265                $return->headers[strtolower($value['name'])]   = array($return->headers[strtolower($value['name'])]);
266                $return->headers[strtolower($value['name'])][] = $value['value'];
267
268            } elseif (isset($return->headers[strtolower($value['name'])])) {
269                $return->headers[strtolower($value['name'])][] = $value['value'];
270
271            } else {
272                $return->headers[strtolower($value['name'])] = $value['value'];
273            }
274        }
275
276
277        foreach ($headers as $key => $value) {
278            $headers[$key]['name'] = strtolower($headers[$key]['name']);
279            switch ($headers[$key]['name']) {
280
281                case 'content-type':
282                    $content_type = $this->_parseHeaderValue($headers[$key]['value']);
283
284                    if (preg_match('/([0-9a-z+.-]+)\/([0-9a-z+.-]+)/i', $content_type['value'], $regs)) {
285                        $return->ctype_primary   = $regs[1];
286                        $return->ctype_secondary = $regs[2];
287                    }
288
289                    if (isset($content_type['other'])) {
290                        foreach($content_type['other'] as $p_name => $p_value) {
291                            $return->ctype_parameters[$p_name] = $p_value;
292                        }
293                    }
294                    break;
295
296                case 'content-disposition':
297                    $content_disposition = $this->_parseHeaderValue($headers[$key]['value']);
298                    $return->disposition   = $content_disposition['value'];
299                    if (isset($content_disposition['other'])) {
300                        foreach($content_disposition['other'] as $p_name => $p_value) {
301                            $return->d_parameters[$p_name] = $p_value;
302                        }
303                    }
304                    break;
305
306                case 'content-transfer-encoding':
307                    $content_transfer_encoding = $this->_parseHeaderValue($headers[$key]['value']);
308                    break;
309            }
310        }
311
312        if (isset($content_type)) {
313            switch (strtolower($content_type['value'])) {
314                case 'text/plain':
315                    $encoding = isset($content_transfer_encoding) ? $content_transfer_encoding['value'] : '7bit';
316                    $this->_include_bodies ? $return->body = ($this->_decode_bodies ? $this->_decodeBody($body, $encoding) : $body) : null;
317                    break;
318
319                case 'text/html':
320                    $encoding = isset($content_transfer_encoding) ? $content_transfer_encoding['value'] : '7bit';
321                    $this->_include_bodies ? $return->body = ($this->_decode_bodies ? $this->_decodeBody($body, $encoding) : $body) : null;
322                    break;
323
324                case 'multipart/signed': // PGP
325                    $parts = $this->_boundarySplit($body, $content_type['other']['boundary'], true);
326                    $return->parts['msg_body'] = $parts[0];
327                    list($part_header, $part_body) = $this->_splitBodyHeader($parts[1]);
328                    $return->parts['sig_hdr'] = $part_header;
329                    $return->parts['sig_body'] = $part_body;
330                    break;
331
332                case 'multipart/parallel':
333                case 'multipart/appledouble': // Appledouble mail
334                case 'multipart/report': // RFC1892
335                case 'multipart/signed': // PGP
336                case 'multipart/digest':
337                case 'multipart/alternative':
338                case 'multipart/related':
339                case 'multipart/relative': //#20431 - android
340                case 'multipart/mixed':
341                case 'application/vnd.wap.multipart.related':
342                    if(!isset($content_type['other']['boundary'])){
343                        $this->_error = 'No boundary found for ' . $content_type['value'] . ' part';
344                        return false;
345                    }
346
347                    $default_ctype = (strtolower($content_type['value']) === 'multipart/digest') ? 'message/rfc822' : 'text/plain';
348
349                    $parts = $this->_boundarySplit($body, $content_type['other']['boundary']);
350                    for ($i = 0; $i < count($parts); $i++) {
351                        list($part_header, $part_body) = $this->_splitBodyHeader($parts[$i]);
352                        $part = $this->_decode($part_header, $part_body, $default_ctype);
353                        if($part === false)
354                            $part = $this->raiseError($this->_error);
355                        $return->parts[] = $part;
356                    }
357                    break;
358
359                case 'message/rfc822':
360                case 'message/delivery-status': // #bug #18693
361					if ($this->_rfc822_bodies) {
362						$encoding = isset($content_transfer_encoding) ? $content_transfer_encoding['value'] : '7bit';
363						$return->body = ($this->_decode_bodies ? $this->_decodeBody($body, $encoding) : $body);
364					}
365                    $obj = new Mail_mimeDecode($body);
366                    $return->parts[] = $obj->decode(array('include_bodies' => $this->_include_bodies,
367					                                      'decode_bodies'  => $this->_decode_bodies,
368														  'decode_headers' => $this->_decode_headers));
369                    unset($obj);
370                    break;
371
372                default:
373                    if(!isset($content_transfer_encoding['value']))
374                        $content_transfer_encoding['value'] = '7bit';
375                    $this->_include_bodies ? $return->body = ($this->_decode_bodies ? $this->_decodeBody($body, $content_transfer_encoding['value']) : $body) : null;
376                    break;
377            }
378
379        } else {
380            $ctype = explode('/', $default_ctype);
381            $return->ctype_primary   = $ctype[0];
382            $return->ctype_secondary = $ctype[1];
383            $this->_include_bodies ? $return->body = ($this->_decode_bodies ? $this->_decodeBody($body) : $body) : null;
384        }
385
386        return $return;
387    }
388
389    /**
390     * Given the output of the above function, this will return an
391     * array of references to the parts, indexed by mime number.
392     *
393     * @param  object $structure   The structure to go through
394     * @param  string $mime_number Internal use only.
395     * @return array               Mime numbers
396     */
397    function &getMimeNumbers(&$structure, $no_refs = false, $mime_number = '', $prepend = '')
398    {
399        $return = array();
400        if (!empty($structure->parts)) {
401            if ($mime_number != '') {
402                $structure->mime_id = $prepend . $mime_number;
403                $return[$prepend . $mime_number] = &$structure;
404            }
405            for ($i = 0; $i < count($structure->parts); $i++) {
406
407
408                if (!empty($structure->headers['content-type']) AND substr(strtolower($structure->headers['content-type']), 0, 8) == 'message/') {
409                    $prepend      = $prepend . $mime_number . '.';
410                    $_mime_number = '';
411                } else {
412                    $_mime_number = ($mime_number == '' ? $i + 1 : sprintf('%s.%s', $mime_number, $i + 1));
413                }
414
415                $arr = &Mail_mimeDecode::getMimeNumbers($structure->parts[$i], $no_refs, $_mime_number, $prepend);
416                foreach ($arr as $key => $val) {
417                    $no_refs ? $return[$key] = '' : $return[$key] = &$arr[$key];
418                }
419            }
420        } else {
421            if ($mime_number == '') {
422                $mime_number = '1';
423            }
424            $structure->mime_id = $prepend . $mime_number;
425            $no_refs ? $return[$prepend . $mime_number] = '' : $return[$prepend . $mime_number] = &$structure;
426        }
427
428        return $return;
429    }
430
431    /**
432     * Given a string containing a header and body
433     * section, this function will split them (at the first
434     * blank line) and return them.
435     *
436     * @param string Input to split apart
437     * @return array Contains header and body section
438     * @access private
439     */
440    function _splitBodyHeader($input)
441    {
442        if (preg_match("/^(.*?)\r?\n\r?\n(.*)/s", $input, $match)) {
443            return array($match[1], $match[2]);
444        }
445        // bug #17325 - empty bodies are allowed. - we just check that at least one line
446        // of headers exist..
447        if (count(explode("\n",$input))) {
448            return array($input, '');
449        }
450        $this->_error = 'Could not split header and body';
451        return false;
452    }
453
454    /**
455     * Parse headers given in $input and return
456     * as assoc array.
457     *
458     * @param string Headers to parse
459     * @return array Contains parsed headers
460     * @access private
461     */
462    function _parseHeaders($input)
463    {
464
465        if ($input !== '') {
466            // Unfold the input
467            $input   = preg_replace("/\r?\n/", "\r\n", $input);
468            //#7065 - wrapping.. with encoded stuff.. - probably not needed,
469            // wrapping space should only get removed if the trailing item on previous line is a
470            // encoded character
471            $input   = preg_replace("/=\r\n(\t| )+/", '=', $input);
472            $input   = preg_replace("/\r\n(\t| )+/", ' ', $input);
473
474            $headers = explode("\r\n", trim($input));
475            $got_start = false;
476            foreach ($headers as $value) {
477                if (!$got_start) {
478                    // munge headers for mbox style from
479                    if ($value[0] == '>') {
480                        $value = substring($value, 1); // remove mbox >
481                    }
482                    if (substr($value,0,5) == 'From ') {
483                        $value = 'Return-Path: ' . substr($value, 5);
484                    } else {
485                        $got_start = true;
486                    }
487                }
488
489                $hdr_name = substr($value, 0, $pos = strpos($value, ':'));
490                $hdr_value = substr($value, $pos+1);
491                if($hdr_value[0] == ' ') {
492                    $hdr_value = substr($hdr_value, 1);
493                }
494
495                $return[] = array(
496                                  'name'  => $hdr_name,
497                                  'value' =>  $hdr_value
498                                 );
499            }
500        } else {
501            $return = array();
502        }
503
504        return $return;
505    }
506
507    /**
508     * Function to parse a header value,
509     * extract first part, and any secondary
510     * parts (after ;) This function is not as
511     * robust as it could be. Eg. header comments
512     * in the wrong place will probably break it.
513     *
514     * Extra things this can handle
515     *   filename*0=......
516     *   filename*1=......
517     *
518     *  This is where lines are broken in, and need merging.
519     *
520     *   filename*0*=ENC'lang'urlencoded data.
521     *   filename*1*=ENC'lang'urlencoded data.
522     *
523     *
524     *
525     * @param string Header value to parse
526     * @return array Contains parsed result
527     * @access private
528     */
529    function _parseHeaderValue($input)
530    {
531         if (($pos = strpos($input, ';')) === false) {
532            $input = $this->_decodeHeader($input);
533            $return['value'] = trim($input);
534            return $return;
535        }
536
537
538
539        $value = substr($input, 0, $pos);
540        $value = $this->_decodeHeader($value);
541        $return['value'] = trim($value);
542        $input = trim(substr($input, $pos+1));
543
544        if (!strlen($input) > 0) {
545            return $return;
546        }
547        // at this point input contains xxxx=".....";zzzz="...."
548        // since we are dealing with quoted strings, we need to handle this properly..
549        $i = 0;
550        $l = strlen($input);
551        $key = '';
552        $val = false; // our string - including quotes..
553        $q = false; // in quote..
554        $lq = ''; // last quote..
555
556        while ($i < $l) {
557
558            $c = $input[$i];
559            //var_dump(array('i'=>$i,'c'=>$c,'q'=>$q, 'lq'=>$lq, 'key'=>$key, 'val' =>$val));
560
561            $escaped = false;
562            if ($c == '\\') {
563                $i++;
564                if ($i == $l-1) { // end of string.
565                    break;
566                }
567                $escaped = true;
568                $c = $input[$i];
569            }
570
571
572            // state - in key..
573            if ($val === false) {
574                if (!$escaped && $c == '=') {
575                    $val = '';
576                    $key = trim($key);
577                    $i++;
578                    continue;
579                }
580                if (!$escaped && $c == ';') {
581                    if ($key) { // a key without a value..
582                        $key= trim($key);
583                        $return['other'][$key] = '';
584                    }
585                    $key = '';
586                }
587                $key .= $c;
588                $i++;
589                continue;
590            }
591
592            // state - in value.. (as $val is set..)
593
594            if ($q === false) {
595                // not in quote yet.
596                if ((!strlen($val) || $lq !== false) && $c == ' ' ||  $c == "\t") {
597                    $i++;
598                    continue; // skip leading spaces after '=' or after '"'
599                }
600
601                // do not de-quote 'xxx*= itesm..
602                $key_is_trans = $key[strlen($key)-1] == '*';
603
604                if (!$key_is_trans && !$escaped && ($c == '"' || $c == "'")) {
605                    // start quoted area..
606                    $q = $c;
607                    // in theory should not happen raw text in value part..
608                    // but we will handle it as a merged part of the string..
609                    $val = !strlen(trim($val)) ? '' : trim($val);
610                    $i++;
611                    continue;
612                }
613                // got end....
614                if (!$escaped && $c == ';') {
615
616                    $return['other'][$key] = trim($val);
617                    $val = false;
618                    $key = '';
619                    $lq = false;
620                    $i++;
621                    continue;
622                }
623
624                $val .= $c;
625                $i++;
626                continue;
627            }
628
629            // state - in quote..
630            if (!$escaped && $c == $q) {  // potential exit state..
631
632                // end of quoted string..
633                $lq = $q;
634                $q = false;
635                $i++;
636                continue;
637            }
638
639            // normal char inside of quoted string..
640            $val.= $c;
641            $i++;
642        }
643
644        // do we have anything left..
645        if (strlen(trim($key)) || $val !== false) {
646
647            $val = trim($val);
648
649            $return['other'][$key] = $val;
650        }
651
652
653        $clean_others = array();
654        // merge added values. eg. *1[*]
655        foreach($return['other'] as $key =>$val) {
656            if (preg_match('/\*[0-9]+\**$/', $key)) {
657                $key = preg_replace('/(.*)\*[0-9]+(\**)$/', '\1\2', $key);
658                if (isset($clean_others[$key])) {
659                    $clean_others[$key] .= $val;
660                    continue;
661                }
662
663            }
664            $clean_others[$key] = $val;
665
666        }
667
668        // handle language translation of '*' ending others.
669        foreach( $clean_others as $key =>$val) {
670            if ( $key[strlen($key)-1] != '*') {
671                $clean_others[strtolower($key)] = $val;
672                continue;
673            }
674            unset($clean_others[$key]);
675            $key = substr($key,0,-1);
676            //extended-initial-value := [charset] "'" [language] "'"
677            //              extended-other-values
678            $match = array();
679            $info = preg_match("/^([^']+)'([^']*)'(.*)$/", $val, $match);
680
681            $clean_others[$key] = urldecode($match[3]);
682            $clean_others[strtolower($key)] = $clean_others[$key];
683            $clean_others[strtolower($key).'-charset'] = $match[1];
684            $clean_others[strtolower($key).'-language'] = $match[2];
685
686
687        }
688
689
690        $return['other'] = $clean_others;
691
692        // decode values.
693        foreach($return['other'] as $key =>$val) {
694            $charset = isset($return['other'][$key . '-charset']) ?
695                $return['other'][$key . '-charset']  : false;
696
697            $return['other'][$key] = $this->_decodeHeader($val, $charset);
698        }
699
700        return $return;
701    }
702
703    /**
704     * This function splits the input based
705     * on the given boundary
706     *
707     * @param string Input to parse
708     * @return array Contains array of resulting mime parts
709     * @access private
710     */
711    function _boundarySplit($input, $boundary, $eatline = false)
712    {
713        $parts = array();
714
715        $bs_possible = substr($boundary, 2, -2);
716        $bs_check = '\"' . $bs_possible . '\"';
717
718        if ($boundary == $bs_check) {
719            $boundary = $bs_possible;
720        }
721        // eatline is used by multipart/signed.
722        $tmp = $eatline ?
723            preg_split("/\r?\n--".preg_quote($boundary, '/')."(|--)\n/", $input) :
724            preg_split("/--".preg_quote($boundary, '/')."((?=\s)|--)/", $input);
725
726        $len = count($tmp) -1;
727        for ($i = 1; $i < $len; $i++) {
728            if (strlen(trim($tmp[$i]))) {
729                $parts[] = $tmp[$i];
730            }
731        }
732
733        // add the last part on if it does not end with the 'closing indicator'
734        if (!empty($tmp[$len]) && strlen(trim($tmp[$len])) && $tmp[$len][0] != '-') {
735            $parts[] = $tmp[$len];
736        }
737        return $parts;
738    }
739
740    /**
741     * Given a header, this function will decode it
742     * according to RFC2047. Probably not *exactly*
743     * conformant, but it does pass all the given
744     * examples (in RFC2047).
745     *
746     * @param string Input header value to decode
747     * @return string Decoded header value
748     * @access private
749     */
750    function _decodeHeader($input, $default_charset=false)
751    {
752        if (!$this->_decode_headers) {
753            return $input;
754        }
755        // Remove white space between encoded-words
756        $input = preg_replace('/(=\?[^?]+\?(q|b)\?[^?]*\?=)(\s)+=\?/i', '\1=?', $input);
757
758        // For each encoded-word...
759        while (preg_match('/(=\?([^?]+)\?(q|b)\?([^?]*)\?=)/i', $input, $matches)) {
760
761            $encoded  = $matches[1];
762            $charset  = $matches[2];
763            $encoding = $matches[3];
764            $text     = $matches[4];
765
766            switch (strtolower($encoding)) {
767                case 'b':
768                    $text = base64_decode($text);
769                    break;
770
771                case 'q':
772                    $text = str_replace('_', ' ', $text);
773                    preg_match_all('/=([a-f0-9]{2})/i', $text, $matches);
774                    foreach($matches[1] as $value)
775                        $text = str_replace('='.$value, chr(hexdec($value)), $text);
776                    break;
777            }
778            if (is_string($this->_decode_headers)) {
779                $conv = @iconv($charset, $this->_decode_headers, $text);
780                $text = ($conv === false) ? $text : $conv;
781            }
782            $input = str_replace($encoded, $text, $input);
783        }
784
785        if ($default_charset  && is_string($this->_decode_headers)) {
786            $conv = @iconv($charset, $this->_decode_headers, $input);
787            $input = ($conv === false) ? $input : $conv;
788        }
789
790        return $input;
791    }
792
793    /**
794     * Given a body string and an encoding type,
795     * this function will decode and return it.
796     *
797     * @param  string Input body to decode
798     * @param  string Encoding type to use.
799     * @return string Decoded body
800     * @access private
801     */
802    function _decodeBody($input, $encoding = '7bit')
803    {
804        switch (strtolower($encoding)) {
805            case '7bit':
806                return $input;
807                break;
808
809            case 'quoted-printable':
810                return $this->_quotedPrintableDecode($input);
811                break;
812
813            case 'base64':
814                return base64_decode($input);
815                break;
816
817            default:
818                return $input;
819        }
820    }
821
822    /**
823     * Given a quoted-printable string, this
824     * function will decode and return it.
825     *
826     * @param  string Input body to decode
827     * @return string Decoded body
828     * @access private
829     */
830    function _quotedPrintableDecode($input)
831    {
832        // Remove soft line breaks
833        $input = preg_replace("/=\r?\n/", '', $input);
834
835        // Replace encoded characters
836
837        $cb = create_function('$matches',  ' return chr(hexdec($matches[0]));');
838
839        $input = preg_replace_callback( '/=([a-f0-9]{2})/i', $cb, $input);
840
841        return $input;
842    }
843
844    /**
845     * Checks the input for uuencoded files and returns
846     * an array of them. Can be called statically, eg:
847     *
848     * $files =& Mail_mimeDecode::uudecode($some_text);
849     *
850     * It will check for the begin 666 ... end syntax
851     * however and won't just blindly decode whatever you
852     * pass it.
853     *
854     * @param  string Input body to look for attahcments in
855     * @return array  Decoded bodies, filenames and permissions
856     * @access public
857     * @author Unknown
858     */
859    function &uudecode($input)
860    {
861        // Find all uuencoded sections
862        preg_match_all("/begin ([0-7]{3}) (.+)\r?\n(.+)\r?\nend/Us", $input, $matches);
863
864        for ($j = 0; $j < count($matches[3]); $j++) {
865
866            $str      = $matches[3][$j];
867            $filename = $matches[2][$j];
868            $fileperm = $matches[1][$j];
869
870            $file = '';
871            $str = preg_split("/\r?\n/", trim($str));
872            $strlen = count($str);
873
874            for ($i = 0; $i < $strlen; $i++) {
875                $pos = 1;
876                $d = 0;
877                $len=(int)(((ord(substr($str[$i],0,1)) -32) - ' ') & 077);
878
879                while (($d + 3 <= $len) AND ($pos + 4 <= strlen($str[$i]))) {
880                    $c0 = (ord(substr($str[$i],$pos,1)) ^ 0x20);
881                    $c1 = (ord(substr($str[$i],$pos+1,1)) ^ 0x20);
882                    $c2 = (ord(substr($str[$i],$pos+2,1)) ^ 0x20);
883                    $c3 = (ord(substr($str[$i],$pos+3,1)) ^ 0x20);
884                    $file .= chr(((($c0 - ' ') & 077) << 2) | ((($c1 - ' ') & 077) >> 4));
885
886                    $file .= chr(((($c1 - ' ') & 077) << 4) | ((($c2 - ' ') & 077) >> 2));
887
888                    $file .= chr(((($c2 - ' ') & 077) << 6) |  (($c3 - ' ') & 077));
889
890                    $pos += 4;
891                    $d += 3;
892                }
893
894                if (($d + 2 <= $len) && ($pos + 3 <= strlen($str[$i]))) {
895                    $c0 = (ord(substr($str[$i],$pos,1)) ^ 0x20);
896                    $c1 = (ord(substr($str[$i],$pos+1,1)) ^ 0x20);
897                    $c2 = (ord(substr($str[$i],$pos+2,1)) ^ 0x20);
898                    $file .= chr(((($c0 - ' ') & 077) << 2) | ((($c1 - ' ') & 077) >> 4));
899
900                    $file .= chr(((($c1 - ' ') & 077) << 4) | ((($c2 - ' ') & 077) >> 2));
901
902                    $pos += 3;
903                    $d += 2;
904                }
905
906                if (($d + 1 <= $len) && ($pos + 2 <= strlen($str[$i]))) {
907                    $c0 = (ord(substr($str[$i],$pos,1)) ^ 0x20);
908                    $c1 = (ord(substr($str[$i],$pos+1,1)) ^ 0x20);
909                    $file .= chr(((($c0 - ' ') & 077) << 2) | ((($c1 - ' ') & 077) >> 4));
910
911                }
912            }
913            $files[] = array('filename' => $filename, 'fileperm' => $fileperm, 'filedata' => $file);
914        }
915
916        return $files;
917    }
918
919    /**
920     * getSendArray() returns the arguments required for Mail::send()
921     * used to build the arguments for a mail::send() call
922     *
923     * Usage:
924     * $mailtext = Full email (for example generated by a template)
925     * $decoder = new Mail_mimeDecode($mailtext);
926     * $parts =  $decoder->getSendArray();
927     * if (!PEAR::isError($parts) {
928     *     list($recipents,$headers,$body) = $parts;
929     *     $mail = Mail::factory('smtp');
930     *     $mail->send($recipents,$headers,$body);
931     * } else {
932     *     echo $parts->message;
933     * }
934     * @return mixed   array of recipeint, headers,body or Pear_Error
935     * @access public
936     * @author Alan Knowles <alan@akbkhome.com>
937     */
938    function getSendArray()
939    {
940        // prevent warning if this is not set
941        $this->_decode_headers = FALSE;
942        $headerlist =$this->_parseHeaders($this->_header);
943        $to = "";
944        if (!$headerlist) {
945            return $this->raiseError("Message did not contain headers");
946        }
947        foreach($headerlist as $item) {
948            $header[$item['name']] = $item['value'];
949            switch (strtolower($item['name'])) {
950                case "to":
951                case "cc":
952                case "bcc":
953                    $to .= ",".$item['value'];
954                default:
955                   break;
956            }
957        }
958        if ($to == "") {
959            return $this->raiseError("Message did not contain any recipents");
960        }
961        $to = substr($to,1);
962        return array($to,$header,$this->_body);
963    }
964
965    /**
966     * Returns a xml copy of the output of
967     * Mail_mimeDecode::decode. Pass the output in as the
968     * argument. This function can be called statically. Eg:
969     *
970     * $output = $obj->decode();
971     * $xml    = Mail_mimeDecode::getXML($output);
972     *
973     * The DTD used for this should have been in the package. Or
974     * alternatively you can get it from cvs, or here:
975     * http://www.phpguru.org/xmail/xmail.dtd.
976     *
977     * @param  object Input to convert to xml. This should be the
978     *                output of the Mail_mimeDecode::decode function
979     * @return string XML version of input
980     * @access public
981     */
982    function getXML($input)
983    {
984        $crlf    =  "\r\n";
985        $output  = '<?xml version=\'1.0\'?>' . $crlf .
986                   '<!DOCTYPE email SYSTEM "http://www.phpguru.org/xmail/xmail.dtd">' . $crlf .
987                   '<email>' . $crlf .
988                   Mail_mimeDecode::_getXML($input) .
989                   '</email>';
990
991        return $output;
992    }
993
994    /**
995     * Function that does the actual conversion to xml. Does a single
996     * mimepart at a time.
997     *
998     * @param  object  Input to convert to xml. This is a mimepart object.
999     *                 It may or may not contain subparts.
1000     * @param  integer Number of tabs to indent
1001     * @return string  XML version of input
1002     * @access private
1003     */
1004    function _getXML($input, $indent = 1)
1005    {
1006        $htab    =  "\t";
1007        $crlf    =  "\r\n";
1008        $output  =  '';
1009        $headers = @(array)$input->headers;
1010
1011        foreach ($headers as $hdr_name => $hdr_value) {
1012
1013            // Multiple headers with this name
1014            if (is_array($headers[$hdr_name])) {
1015                for ($i = 0; $i < count($hdr_value); $i++) {
1016                    $output .= Mail_mimeDecode::_getXML_helper($hdr_name, $hdr_value[$i], $indent);
1017                }
1018
1019            // Only one header of this sort
1020            } else {
1021                $output .= Mail_mimeDecode::_getXML_helper($hdr_name, $hdr_value, $indent);
1022            }
1023        }
1024
1025        if (!empty($input->parts)) {
1026            for ($i = 0; $i < count($input->parts); $i++) {
1027                $output .= $crlf . str_repeat($htab, $indent) . '<mimepart>' . $crlf .
1028                           Mail_mimeDecode::_getXML($input->parts[$i], $indent+1) .
1029                           str_repeat($htab, $indent) . '</mimepart>' . $crlf;
1030            }
1031        } elseif (isset($input->body)) {
1032            $output .= $crlf . str_repeat($htab, $indent) . '<body><![CDATA[' .
1033                       $input->body . ']]></body>' . $crlf;
1034        }
1035
1036        return $output;
1037    }
1038
1039    /**
1040     * Helper function to _getXML(). Returns xml of a header.
1041     *
1042     * @param  string  Name of header
1043     * @param  string  Value of header
1044     * @param  integer Number of tabs to indent
1045     * @return string  XML version of input
1046     * @access private
1047     */
1048    function _getXML_helper($hdr_name, $hdr_value, $indent)
1049    {
1050        $htab   = "\t";
1051        $crlf   = "\r\n";
1052        $return = '';
1053
1054        $new_hdr_value = ($hdr_name != 'received') ? Mail_mimeDecode::_parseHeaderValue($hdr_value) : array('value' => $hdr_value);
1055        $new_hdr_name  = str_replace(' ', '-', ucwords(str_replace('-', ' ', $hdr_name)));
1056
1057        // Sort out any parameters
1058        if (!empty($new_hdr_value['other'])) {
1059            foreach ($new_hdr_value['other'] as $paramname => $paramvalue) {
1060                $params[] = str_repeat($htab, $indent) . $htab . '<parameter>' . $crlf .
1061                            str_repeat($htab, $indent) . $htab . $htab . '<paramname>' . htmlspecialchars($paramname) . '</paramname>' . $crlf .
1062                            str_repeat($htab, $indent) . $htab . $htab . '<paramvalue>' . htmlspecialchars($paramvalue) . '</paramvalue>' . $crlf .
1063                            str_repeat($htab, $indent) . $htab . '</parameter>' . $crlf;
1064            }
1065
1066            $params = implode('', $params);
1067        } else {
1068            $params = '';
1069        }
1070
1071        $return = str_repeat($htab, $indent) . '<header>' . $crlf .
1072                  str_repeat($htab, $indent) . $htab . '<headername>' . htmlspecialchars($new_hdr_name) . '</headername>' . $crlf .
1073                  str_repeat($htab, $indent) . $htab . '<headervalue>' . htmlspecialchars($new_hdr_value['value']) . '</headervalue>' . $crlf .
1074                  $params .
1075                  str_repeat($htab, $indent) . '</header>' . $crlf;
1076
1077        return $return;
1078    }
1079
1080} // End of class
1081