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,v 1.48 2006/12/03 13:43:33 cipri Exp $
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     *
145     * @var    boolean
146     * @access private
147     */
148    var $_decode_headers;
149
150    /**
151     * Constructor.
152     *
153     * Sets up the object, initialise the variables, and splits and
154     * stores the header and body of the input.
155     *
156     * @param string The input to decode
157     * @access public
158     */
159    function Mail_mimeDecode($input)
160    {
161        list($header, $body)   = $this->_splitBodyHeader($input);
162
163        $this->_input          = $input;
164        $this->_header         = $header;
165        $this->_body           = $body;
166        $this->_decode_bodies  = false;
167        $this->_include_bodies = true;
168    }
169
170    /**
171     * Begins the decoding process. If called statically
172     * it will create an object and call the decode() method
173     * of it.
174     *
175     * @param array An array of various parameters that determine
176     *              various things:
177     *              include_bodies - Whether to include the body in the returned
178     *                               object.
179     *              decode_bodies  - Whether to decode the bodies
180     *                               of the parts. (Transfer encoding)
181     *              decode_headers - Whether to decode headers
182     *              input          - If called statically, this will be treated
183     *                               as the input
184     * @return object Decoded results
185     * @access public
186     */
187    function decode($params = null)
188    {
189        // determine if this method has been called statically
190        $isStatic = !(isset($this) && get_class($this) == __CLASS__);
191
192        // Have we been called statically?
193	// If so, create an object and pass details to that.
194        if ($isStatic AND isset($params['input'])) {
195
196            $obj = new Mail_mimeDecode($params['input']);
197            $structure = $obj->decode($params);
198
199        // Called statically but no input
200        } elseif ($isStatic) {
201            return PEAR::raiseError('Called statically and no input given');
202
203        // Called via an object
204        } else {
205            $this->_include_bodies = isset($params['include_bodies']) ?
206	                             $params['include_bodies'] : false;
207            $this->_decode_bodies  = isset($params['decode_bodies']) ?
208	                             $params['decode_bodies']  : false;
209            $this->_decode_headers = isset($params['decode_headers']) ?
210	                             $params['decode_headers'] : false;
211
212            $structure = $this->_decode($this->_header, $this->_body);
213            if ($structure === false) {
214                $structure = $this->raiseError($this->_error);
215            }
216        }
217
218        return $structure;
219    }
220
221    /**
222     * Performs the decoding. Decodes the body string passed to it
223     * If it finds certain content-types it will call itself in a
224     * recursive fashion
225     *
226     * @param string Header section
227     * @param string Body section
228     * @return object Results of decoding process
229     * @access private
230     */
231    function _decode($headers, $body, $default_ctype = 'text/plain')
232    {
233        $return = new stdClass;
234        $return->headers = array();
235        $headers = $this->_parseHeaders($headers);
236
237        foreach ($headers as $value) {
238            if (isset($return->headers[strtolower($value['name'])]) AND !is_array($return->headers[strtolower($value['name'])])) {
239                $return->headers[strtolower($value['name'])]   = array($return->headers[strtolower($value['name'])]);
240                $return->headers[strtolower($value['name'])][] = $value['value'];
241
242            } elseif (isset($return->headers[strtolower($value['name'])])) {
243                $return->headers[strtolower($value['name'])][] = $value['value'];
244
245            } else {
246                $return->headers[strtolower($value['name'])] = $value['value'];
247            }
248        }
249
250        reset($headers);
251        while (list($key, $value) = each($headers)) {
252            $headers[$key]['name'] = strtolower($headers[$key]['name']);
253            switch ($headers[$key]['name']) {
254
255                case 'content-type':
256                    $content_type = $this->_parseHeaderValue($headers[$key]['value']);
257
258                    if (preg_match('/([0-9a-z+.-]+)\/([0-9a-z+.-]+)/i', $content_type['value'], $regs)) {
259                        $return->ctype_primary   = $regs[1];
260                        $return->ctype_secondary = $regs[2];
261                    }
262
263                    if (isset($content_type['other'])) {
264                        while (list($p_name, $p_value) = each($content_type['other'])) {
265                            $return->ctype_parameters[$p_name] = $p_value;
266                        }
267                    }
268                    break;
269
270                case 'content-disposition':
271                    $content_disposition = $this->_parseHeaderValue($headers[$key]['value']);
272                    $return->disposition   = $content_disposition['value'];
273                    if (isset($content_disposition['other'])) {
274                        while (list($p_name, $p_value) = each($content_disposition['other'])) {
275                            $return->d_parameters[$p_name] = $p_value;
276                        }
277                    }
278                    break;
279
280                case 'content-transfer-encoding':
281                    $content_transfer_encoding = $this->_parseHeaderValue($headers[$key]['value']);
282                    break;
283            }
284        }
285
286        if (isset($content_type)) {
287            switch (strtolower($content_type['value'])) {
288                case 'text/plain':
289                    $encoding = isset($content_transfer_encoding) ? $content_transfer_encoding['value'] : '7bit';
290                    $this->_include_bodies ? $return->body = ($this->_decode_bodies ? $this->_decodeBody($body, $encoding) : $body) : null;
291                    break;
292
293                case 'text/html':
294                    $encoding = isset($content_transfer_encoding) ? $content_transfer_encoding['value'] : '7bit';
295                    $this->_include_bodies ? $return->body = ($this->_decode_bodies ? $this->_decodeBody($body, $encoding) : $body) : null;
296                    break;
297
298                case 'multipart/parallel':
299                case 'multipart/appledouble': // Appledouble mail
300                case 'multipart/report': // RFC1892
301                case 'multipart/signed': // PGP
302                case 'multipart/digest':
303                case 'multipart/alternative':
304                case 'multipart/related':
305                case 'multipart/mixed':
306                    if(!isset($content_type['other']['boundary'])){
307                        $this->_error = 'No boundary found for ' . $content_type['value'] . ' part';
308                        return false;
309                    }
310
311                    $default_ctype = (strtolower($content_type['value']) === 'multipart/digest') ? 'message/rfc822' : 'text/plain';
312
313                    $parts = $this->_boundarySplit($body, $content_type['other']['boundary']);
314                    for ($i = 0; $i < count($parts); $i++) {
315                        list($part_header, $part_body) = $this->_splitBodyHeader($parts[$i]);
316                        $part = $this->_decode($part_header, $part_body, $default_ctype);
317                        if($part === false)
318                            $part = $this->raiseError($this->_error);
319                        $return->parts[] = $part;
320                    }
321                    break;
322
323                case 'message/rfc822':
324                    $obj = &new Mail_mimeDecode($body);
325                    $return->parts[] = $obj->decode(array('include_bodies' => $this->_include_bodies,
326					                                      'decode_bodies'  => $this->_decode_bodies,
327														  'decode_headers' => $this->_decode_headers));
328                    unset($obj);
329                    break;
330
331                default:
332                    if(!isset($content_transfer_encoding['value']))
333                        $content_transfer_encoding['value'] = '7bit';
334                    $this->_include_bodies ? $return->body = ($this->_decode_bodies ? $this->_decodeBody($body, $content_transfer_encoding['value']) : $body) : null;
335                    break;
336            }
337
338        } else {
339            $ctype = explode('/', $default_ctype);
340            $return->ctype_primary   = $ctype[0];
341            $return->ctype_secondary = $ctype[1];
342            $this->_include_bodies ? $return->body = ($this->_decode_bodies ? $this->_decodeBody($body) : $body) : null;
343        }
344
345        return $return;
346    }
347
348    /**
349     * Given the output of the above function, this will return an
350     * array of references to the parts, indexed by mime number.
351     *
352     * @param  object $structure   The structure to go through
353     * @param  string $mime_number Internal use only.
354     * @return array               Mime numbers
355     */
356    function &getMimeNumbers(&$structure, $no_refs = false, $mime_number = '', $prepend = '')
357    {
358        $return = array();
359        if (!empty($structure->parts)) {
360            if ($mime_number != '') {
361                $structure->mime_id = $prepend . $mime_number;
362                $return[$prepend . $mime_number] = &$structure;
363            }
364            for ($i = 0; $i < count($structure->parts); $i++) {
365
366
367                if (!empty($structure->headers['content-type']) AND substr(strtolower($structure->headers['content-type']), 0, 8) == 'message/') {
368                    $prepend      = $prepend . $mime_number . '.';
369                    $_mime_number = '';
370                } else {
371                    $_mime_number = ($mime_number == '' ? $i + 1 : sprintf('%s.%s', $mime_number, $i + 1));
372                }
373
374                $arr = &Mail_mimeDecode::getMimeNumbers($structure->parts[$i], $no_refs, $_mime_number, $prepend);
375                foreach ($arr as $key => $val) {
376                    $no_refs ? $return[$key] = '' : $return[$key] = &$arr[$key];
377                }
378            }
379        } else {
380            if ($mime_number == '') {
381                $mime_number = '1';
382            }
383            $structure->mime_id = $prepend . $mime_number;
384            $no_refs ? $return[$prepend . $mime_number] = '' : $return[$prepend . $mime_number] = &$structure;
385        }
386
387        return $return;
388    }
389
390    /**
391     * Given a string containing a header and body
392     * section, this function will split them (at the first
393     * blank line) and return them.
394     *
395     * @param string Input to split apart
396     * @return array Contains header and body section
397     * @access private
398     */
399    function _splitBodyHeader($input)
400    {
401        if (preg_match("/^(.*?)\r?\n\r?\n(.*)/s", $input, $match)) {
402            return array($match[1], $match[2]);
403        }
404        $this->_error = 'Could not split header and body';
405        return false;
406    }
407
408    /**
409     * Parse headers given in $input and return
410     * as assoc array.
411     *
412     * @param string Headers to parse
413     * @return array Contains parsed headers
414     * @access private
415     */
416    function _parseHeaders($input)
417    {
418
419        if ($input !== '') {
420            // Unfold the input
421            $input   = preg_replace("/\r?\n/", "\r\n", $input);
422            $input   = preg_replace("/\r\n(\t| )+/", ' ', $input);
423            $headers = explode("\r\n", trim($input));
424
425            foreach ($headers as $value) {
426                $hdr_name = substr($value, 0, $pos = strpos($value, ':'));
427                $hdr_value = substr($value, $pos+1);
428                if($hdr_value[0] == ' ')
429                    $hdr_value = substr($hdr_value, 1);
430
431                $return[] = array(
432                                  'name'  => $hdr_name,
433                                  'value' => $this->_decode_headers ? $this->_decodeHeader($hdr_value) : $hdr_value
434                                 );
435            }
436        } else {
437            $return = array();
438        }
439
440        return $return;
441    }
442
443    /**
444     * Function to parse a header value,
445     * extract first part, and any secondary
446     * parts (after ;) This function is not as
447     * robust as it could be. Eg. header comments
448     * in the wrong place will probably break it.
449     *
450     * @param string Header value to parse
451     * @return array Contains parsed result
452     * @access private
453     */
454    function _parseHeaderValue($input)
455    {
456
457        if (($pos = strpos($input, ';')) !== false) {
458
459            $return['value'] = trim(substr($input, 0, $pos));
460            $input = trim(substr($input, $pos+1));
461
462            if (strlen($input) > 0) {
463
464                // This splits on a semi-colon, if there's no preceeding backslash
465                // Now works with quoted values; had to glue the \; breaks in PHP
466                // the regex is already bordering on incomprehensible
467                $splitRegex = '/([^;\'"]*[\'"]([^\'"]*([^\'"]*)*)[\'"][^;\'"]*|([^;]+))(;|$)/';
468                preg_match_all($splitRegex, $input, $matches);
469                $parameters = array();
470                for ($i=0; $i<count($matches[0]); $i++) {
471                    $param = $matches[0][$i];
472                    while (substr($param, -2) == '\;') {
473                        $param .= $matches[0][++$i];
474                    }
475                    $parameters[] = $param;
476                }
477
478                for ($i = 0; $i < count($parameters); $i++) {
479                    $param_name  = trim(substr($parameters[$i], 0, $pos = strpos($parameters[$i], '=')), "'\";\t\\ ");
480                    $param_value = trim(str_replace('\;', ';', substr($parameters[$i], $pos + 1)), "'\";\t\\ ");
481                    if ($param_value[0] == '"') {
482                        $param_value = substr($param_value, 1, -1);
483                    }
484                    $return['other'][$param_name] = $param_value;
485                    $return['other'][strtolower($param_name)] = $param_value;
486                }
487            }
488        } else {
489            $return['value'] = trim($input);
490        }
491
492        return $return;
493    }
494
495    /**
496     * This function splits the input based
497     * on the given boundary
498     *
499     * @param string Input to parse
500     * @return array Contains array of resulting mime parts
501     * @access private
502     */
503    function _boundarySplit($input, $boundary)
504    {
505        $parts = array();
506
507        $bs_possible = substr($boundary, 2, -2);
508        $bs_check = '\"' . $bs_possible . '\"';
509
510        if ($boundary == $bs_check) {
511            $boundary = $bs_possible;
512        }
513
514        $tmp = explode('--' . $boundary, $input);
515
516        for ($i = 1; $i < count($tmp) - 1; $i++) {
517            $parts[] = $tmp[$i];
518        }
519
520        return $parts;
521    }
522
523    /**
524     * Given a header, this function will decode it
525     * according to RFC2047. Probably not *exactly*
526     * conformant, but it does pass all the given
527     * examples (in RFC2047).
528     *
529     * @param string Input header value to decode
530     * @return string Decoded header value
531     * @access private
532     */
533    function _decodeHeader($input)
534    {
535        // Remove white space between encoded-words
536        $input = preg_replace('/(=\?[^?]+\?(q|b)\?[^?]*\?=)(\s)+=\?/i', '\1=?', $input);
537
538        // For each encoded-word...
539        while (preg_match('/(=\?([^?]+)\?(q|b)\?([^?]*)\?=)/i', $input, $matches)) {
540
541            $encoded  = $matches[1];
542            $charset  = $matches[2];
543            $encoding = $matches[3];
544            $text     = $matches[4];
545
546            switch (strtolower($encoding)) {
547                case 'b':
548                    $text = base64_decode($text);
549                    break;
550
551                case 'q':
552                    $text = str_replace('_', ' ', $text);
553                    preg_match_all('/=([a-f0-9]{2})/i', $text, $matches);
554                    foreach($matches[1] as $value)
555                        $text = str_replace('='.$value, chr(hexdec($value)), $text);
556                    break;
557            }
558
559            $input = str_replace($encoded, $text, $input);
560        }
561
562        return $input;
563    }
564
565    /**
566     * Given a body string and an encoding type,
567     * this function will decode and return it.
568     *
569     * @param  string Input body to decode
570     * @param  string Encoding type to use.
571     * @return string Decoded body
572     * @access private
573     */
574    function _decodeBody($input, $encoding = '7bit')
575    {
576        switch (strtolower($encoding)) {
577            case '7bit':
578                return $input;
579                break;
580
581            case 'quoted-printable':
582                return $this->_quotedPrintableDecode($input);
583                break;
584
585            case 'base64':
586                return base64_decode($input);
587                break;
588
589            default:
590                return $input;
591        }
592    }
593
594    /**
595     * Given a quoted-printable string, this
596     * function will decode and return it.
597     *
598     * @param  string Input body to decode
599     * @return string Decoded body
600     * @access private
601     */
602    function _quotedPrintableDecode($input)
603    {
604        // Remove soft line breaks
605        $input = preg_replace("/=\r?\n/", '', $input);
606
607        // Replace encoded characters
608		$input = preg_replace('/=([a-f0-9]{2})/ie', "chr(hexdec('\\1'))", $input);
609
610        return $input;
611    }
612
613    /**
614     * Checks the input for uuencoded files and returns
615     * an array of them. Can be called statically, eg:
616     *
617     * $files =& Mail_mimeDecode::uudecode($some_text);
618     *
619     * It will check for the begin 666 ... end syntax
620     * however and won't just blindly decode whatever you
621     * pass it.
622     *
623     * @param  string Input body to look for attahcments in
624     * @return array  Decoded bodies, filenames and permissions
625     * @access public
626     * @author Unknown
627     */
628    function &uudecode($input)
629    {
630        // Find all uuencoded sections
631        preg_match_all("/begin ([0-7]{3}) (.+)\r?\n(.+)\r?\nend/Us", $input, $matches);
632
633        for ($j = 0; $j < count($matches[3]); $j++) {
634
635            $str      = $matches[3][$j];
636            $filename = $matches[2][$j];
637            $fileperm = $matches[1][$j];
638
639            $file = '';
640            $str = preg_split("/\r?\n/", trim($str));
641            $strlen = count($str);
642
643            for ($i = 0; $i < $strlen; $i++) {
644                $pos = 1;
645                $d = 0;
646                $len=(int)(((ord(substr($str[$i],0,1)) -32) - ' ') & 077);
647
648                while (($d + 3 <= $len) AND ($pos + 4 <= strlen($str[$i]))) {
649                    $c0 = (ord(substr($str[$i],$pos,1)) ^ 0x20);
650                    $c1 = (ord(substr($str[$i],$pos+1,1)) ^ 0x20);
651                    $c2 = (ord(substr($str[$i],$pos+2,1)) ^ 0x20);
652                    $c3 = (ord(substr($str[$i],$pos+3,1)) ^ 0x20);
653                    $file .= chr(((($c0 - ' ') & 077) << 2) | ((($c1 - ' ') & 077) >> 4));
654
655                    $file .= chr(((($c1 - ' ') & 077) << 4) | ((($c2 - ' ') & 077) >> 2));
656
657                    $file .= chr(((($c2 - ' ') & 077) << 6) |  (($c3 - ' ') & 077));
658
659                    $pos += 4;
660                    $d += 3;
661                }
662
663                if (($d + 2 <= $len) && ($pos + 3 <= strlen($str[$i]))) {
664                    $c0 = (ord(substr($str[$i],$pos,1)) ^ 0x20);
665                    $c1 = (ord(substr($str[$i],$pos+1,1)) ^ 0x20);
666                    $c2 = (ord(substr($str[$i],$pos+2,1)) ^ 0x20);
667                    $file .= chr(((($c0 - ' ') & 077) << 2) | ((($c1 - ' ') & 077) >> 4));
668
669                    $file .= chr(((($c1 - ' ') & 077) << 4) | ((($c2 - ' ') & 077) >> 2));
670
671                    $pos += 3;
672                    $d += 2;
673                }
674
675                if (($d + 1 <= $len) && ($pos + 2 <= strlen($str[$i]))) {
676                    $c0 = (ord(substr($str[$i],$pos,1)) ^ 0x20);
677                    $c1 = (ord(substr($str[$i],$pos+1,1)) ^ 0x20);
678                    $file .= chr(((($c0 - ' ') & 077) << 2) | ((($c1 - ' ') & 077) >> 4));
679
680                }
681            }
682            $files[] = array('filename' => $filename, 'fileperm' => $fileperm, 'filedata' => $file);
683        }
684
685        return $files;
686    }
687
688    /**
689     * getSendArray() returns the arguments required for Mail::send()
690     * used to build the arguments for a mail::send() call
691     *
692     * Usage:
693     * $mailtext = Full email (for example generated by a template)
694     * $decoder = new Mail_mimeDecode($mailtext);
695     * $parts =  $decoder->getSendArray();
696     * if (!PEAR::isError($parts) {
697     *     list($recipents,$headers,$body) = $parts;
698     *     $mail = Mail::factory('smtp');
699     *     $mail->send($recipents,$headers,$body);
700     * } else {
701     *     echo $parts->message;
702     * }
703     * @return mixed   array of recipeint, headers,body or Pear_Error
704     * @access public
705     * @author Alan Knowles <alan@akbkhome.com>
706     */
707    function getSendArray()
708    {
709        // prevent warning if this is not set
710        $this->_decode_headers = FALSE;
711        $headerlist =$this->_parseHeaders($this->_header);
712        $to = "";
713        if (!$headerlist) {
714            return $this->raiseError("Message did not contain headers");
715        }
716        foreach($headerlist as $item) {
717            $header[$item['name']] = $item['value'];
718            switch (strtolower($item['name'])) {
719                case "to":
720                case "cc":
721                case "bcc":
722                    $to = ",".$item['value'];
723                default:
724                   break;
725            }
726        }
727        if ($to == "") {
728            return $this->raiseError("Message did not contain any recipents");
729        }
730        $to = substr($to,1);
731        return array($to,$header,$this->_body);
732    }
733
734    /**
735     * Returns a xml copy of the output of
736     * Mail_mimeDecode::decode. Pass the output in as the
737     * argument. This function can be called statically. Eg:
738     *
739     * $output = $obj->decode();
740     * $xml    = Mail_mimeDecode::getXML($output);
741     *
742     * The DTD used for this should have been in the package. Or
743     * alternatively you can get it from cvs, or here:
744     * http://www.phpguru.org/xmail/xmail.dtd.
745     *
746     * @param  object Input to convert to xml. This should be the
747     *                output of the Mail_mimeDecode::decode function
748     * @return string XML version of input
749     * @access public
750     */
751    function getXML($input)
752    {
753        $crlf    =  "\r\n";
754        $output  = '<?xml version=\'1.0\'?>' . $crlf .
755                   '<!DOCTYPE email SYSTEM "http://www.phpguru.org/xmail/xmail.dtd">' . $crlf .
756                   '<email>' . $crlf .
757                   Mail_mimeDecode::_getXML($input) .
758                   '</email>';
759
760        return $output;
761    }
762
763    /**
764     * Function that does the actual conversion to xml. Does a single
765     * mimepart at a time.
766     *
767     * @param  object  Input to convert to xml. This is a mimepart object.
768     *                 It may or may not contain subparts.
769     * @param  integer Number of tabs to indent
770     * @return string  XML version of input
771     * @access private
772     */
773    function _getXML($input, $indent = 1)
774    {
775        $htab    =  "\t";
776        $crlf    =  "\r\n";
777        $output  =  '';
778        $headers = @(array)$input->headers;
779
780        foreach ($headers as $hdr_name => $hdr_value) {
781
782            // Multiple headers with this name
783            if (is_array($headers[$hdr_name])) {
784                for ($i = 0; $i < count($hdr_value); $i++) {
785                    $output .= Mail_mimeDecode::_getXML_helper($hdr_name, $hdr_value[$i], $indent);
786                }
787
788            // Only one header of this sort
789            } else {
790                $output .= Mail_mimeDecode::_getXML_helper($hdr_name, $hdr_value, $indent);
791            }
792        }
793
794        if (!empty($input->parts)) {
795            for ($i = 0; $i < count($input->parts); $i++) {
796                $output .= $crlf . str_repeat($htab, $indent) . '<mimepart>' . $crlf .
797                           Mail_mimeDecode::_getXML($input->parts[$i], $indent+1) .
798                           str_repeat($htab, $indent) . '</mimepart>' . $crlf;
799            }
800        } elseif (isset($input->body)) {
801            $output .= $crlf . str_repeat($htab, $indent) . '<body><![CDATA[' .
802                       $input->body . ']]></body>' . $crlf;
803        }
804
805        return $output;
806    }
807
808    /**
809     * Helper function to _getXML(). Returns xml of a header.
810     *
811     * @param  string  Name of header
812     * @param  string  Value of header
813     * @param  integer Number of tabs to indent
814     * @return string  XML version of input
815     * @access private
816     */
817    function _getXML_helper($hdr_name, $hdr_value, $indent)
818    {
819        $htab   = "\t";
820        $crlf   = "\r\n";
821        $return = '';
822
823        $new_hdr_value = ($hdr_name != 'received') ? Mail_mimeDecode::_parseHeaderValue($hdr_value) : array('value' => $hdr_value);
824        $new_hdr_name  = str_replace(' ', '-', ucwords(str_replace('-', ' ', $hdr_name)));
825
826        // Sort out any parameters
827        if (!empty($new_hdr_value['other'])) {
828            foreach ($new_hdr_value['other'] as $paramname => $paramvalue) {
829                $params[] = str_repeat($htab, $indent) . $htab . '<parameter>' . $crlf .
830                            str_repeat($htab, $indent) . $htab . $htab . '<paramname>' . htmlspecialchars($paramname) . '</paramname>' . $crlf .
831                            str_repeat($htab, $indent) . $htab . $htab . '<paramvalue>' . htmlspecialchars($paramvalue) . '</paramvalue>' . $crlf .
832                            str_repeat($htab, $indent) . $htab . '</parameter>' . $crlf;
833            }
834
835            $params = implode('', $params);
836        } else {
837            $params = '';
838        }
839
840        $return = str_repeat($htab, $indent) . '<header>' . $crlf .
841                  str_repeat($htab, $indent) . $htab . '<headername>' . htmlspecialchars($new_hdr_name) . '</headername>' . $crlf .
842                  str_repeat($htab, $indent) . $htab . '<headervalue>' . htmlspecialchars($new_hdr_value['value']) . '</headervalue>' . $crlf .
843                  $params .
844                  str_repeat($htab, $indent) . '</header>' . $crlf;
845
846        return $return;
847    }
848
849} // End of class
850