Web PHP Archives

MIME Message Parser

This class provides a well-abstracted MIME format message parser. I found several MIME parser classes with terrible interfaces. This provides a powerful and more simplified interface that actually uses OOP methodology (not just a pile of associative arrays).

I've read through RFC2822 and this class should decode a modern, compliant message. With the possible variations in message generators, I can't be sure this is a one-size-fits-all solution, and it, honestly, isn't my intention to make something that universal. I will continue to tweak and adjust the implementation as I see more message formats that break the current (reasonably robust) parsing methods.

Known non-conformance with RFC2822:

Example Usage

MimeMessage.example (943 bytes)  
<?php
/**
 NOTE: Examples are being run on an older installation of PHP (pre-5.2).
 A few things had to be adapted to the older object syntax.  The class
 was designed for the proper PHP 5.2+ object syntax.
 **/


echo '<pre style="font-size:7px;line-height:10px;">';

//Read the email message file (this one is from Gmail).
$email = file_get_contents(dirname(__FILE__).'/MimeMessage.eml');

//Instatiate the parser/storage class.
$mm = new MimeMessage($email);

//Output the top-level From: header.
echo '<strong>From:</strong> '
    .htmlspecialchars($mm->getHeader('From')->__toString())."\n\n";

//Example demonstrating proper message-part access.
if($mm->hasParts()) {
    $num_parts = $mm->getPartCount();
    for($i = 0; $i < $num_parts; ++$i) {
        $p = $mm->getPart($i);
        $part_type = $p->getHeader('Content-Type')->getMeta('type');
    }
}

//Diagnostic output tells us everything about the message.
print_r($mm->dumpArray());

echo '</pre>';

?>

Results

From: Zac Hester <zac.hester@gmail.com>

Array
(
    [meta] => Array
        (
            [delimiter] => From zac.hester@gmail.com Wed Jan 21 09:20:13 2009
        )

    [headers] => Array
        (
            [Received] => Array
                (
                    [0] => Array
                        (
                            [name] => Received
                            [data] => from yw-out-1718.google.com (yw-out-1718.google.com [74.125.46.157]) by sv0.rushmoreradio.net (8.14.2/8.14.2) with ESMTP id n0LGKD7h039875 for ; Wed, 21 Jan 2009 09:20:13 -0700 (MST) (envelope-from zac.hester@gmail.com)
                            [meta] => Array
                                (
                                    [type] => from yw-out-1718.google.com  by sv0.rushmoreradio.net  with ESMTP id n0LGKD7h039875 for 
                                    [0] => Wed, 21 Jan 2009 09:20:13 -0700
                                )

                        )

                    [1] => Array
                        (
                            [name] => Received
                            [data] => by yw-out-1718.google.com with SMTP id 5so1653196ywm.72 for ; Wed, 21 Jan 2009 08:33:32 -0800 (PST)
                            [meta] => Array
                                (
                                    [type] => by yw-out-1718.google.com with SMTP id 5so1653196ywm.72 for 
                                    [0] => Wed, 21 Jan 2009 08:33:32 -0800
                                )

                        )

                    [2] => Array
                        (
                            [name] => Received
                            [data] => by 10.142.88.4 with SMTP id l4mr142934wfb.117.1232555612288; Wed, 21 Jan 2009 08:33:32 -0800 (PST)
                            [meta] => Array
                                (
                                    [type] => by 10.142.88.4 with SMTP id l4mr142934wfb.117.1232555612288
                                    [0] => Wed, 21 Jan 2009 08:33:32 -0800
                                )

                        )

                )

            [DKIM-Signature] => Array
                (
                    [name] => DKIM-Signature
                    [data] => v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=gamma; h=domainkey-signature:mime-version:received:date:message-id:subject :from:to:content-type; bh=KTNzrEIa9gyh0qwREfNDTjYxhRk1bcLwgj8imGhSRoY=; b=Al0bN3dYfn7iy1LUZo/W6r4YCKqa6V7buaiLDN0WfbHCJ3ASKDIEHRiLaUEp1nMYlp G3O7S1dQRaPtXsFzO9aVFr9HiQsRwvTXT1E/j0gFjjXUYBwODrHRKaMfHWayh4LhJcOf J7+no9dNw6neKIEe7o4lsQAEEluXkcDJRechQ=
                    [meta] => Array
                        (
                            [v] => 1
                            [a] => rsa-sha256
                            [c] => relaxed/relaxed
                            [d] => gmail.com
                            [s] => gamma
                            [h] => domainkey-signature:mime-version:received:date:message-id:subject :from:to:content-type
                            [bh] => KTNzrEIa9gyh0qwREfNDTjYxhRk1bcLwgj8imGhSRoY=
                            [b] => Al0bN3dYfn7iy1LUZo/W6r4YCKqa6V7buaiLDN0WfbHCJ3ASKDIEHRiLaUEp1nMYlp G3O7S1dQRaPtXsFzO9aVFr9HiQsRwvTXT1E/j0gFjjXUYBwODrHRKaMfHWayh4LhJcOf J7+no9dNw6neKIEe7o4lsQAEEluXkcDJRechQ=
                        )

                )

            [DomainKey-Signature] => Array
                (
                    [name] => DomainKey-Signature
                    [data] => a=rsa-sha1; c=nofws; d=gmail.com; s=gamma; h=mime-version:date:message-id:subject:from:to:content-type; b=m/wokCg74xkgJRkUEnBxgEgmfVZrAV2IoDPIa5ZypviV3+jEvhBlMOx7sIW4VUYSOP hY7OSwEaS2py4TqDz2XYYpUk5T2rRrMBZVz5IDi28xCp7/Mo07sT+1j5dfIer4LdrlQF sAkhKiHFy+glrAYM1ERcNJaPFPBkQcMzsLu/w=
                    [meta] => Array
                        (
                            [a] => rsa-sha1
                            [c] => nofws
                            [d] => gmail.com
                            [s] => gamma
                            [h] => mime-version:date:message-id:subject:from:to:content-type
                            [b] => m/wokCg74xkgJRkUEnBxgEgmfVZrAV2IoDPIa5ZypviV3+jEvhBlMOx7sIW4VUYSOP hY7OSwEaS2py4TqDz2XYYpUk5T2rRrMBZVz5IDi28xCp7/Mo07sT+1j5dfIer4LdrlQF sAkhKiHFy+glrAYM1ERcNJaPFPBkQcMzsLu/w=
                        )

                )

            [MIME-Version] => Array
                (
                    [name] => MIME-Version
                    [data] => 1.0
                )

            [Date] => Array
                (
                    [name] => Date
                    [data] => Wed, 21 Jan 2009 09:33:32 -0700
                    [meta] => Array
                        (
                            [timestamp] => 1232555612
                        )

                )

            [Message-ID] => Array
                (
                    [name] => Message-ID
                    [data] => <8497a30e0901210833p2b6d9706re163f586f6bf55e9@mail.gmail.com>
                )

            [Subject] => Array
                (
                    [name] => Subject
                    [data] => Attachment Test
                )

            [From] => Array
                (
                    [name] => From
                    [data] => Zac Hester 
                )

            [To] => Array
                (
                    [name] => To
                    [data] => dev@rushmoreradio.net
                )

            [Content-Type] => Array
                (
                    [name] => Content-Type
                    [data] => multipart/mixed; boundary=00504502c3c86fb6b1046100b78c
                    [meta] => Array
                        (
                            [type] => multipart/mixed
                            [genus] => multipart
                            [species] => mixed
                            [boundary] => 00504502c3c86fb6b1046100b78c
                        )

                )

        )

    [parts] => Array
        (
            [0] => Array
                (
                    [meta] => Array
                        (
                        )

                    [headers] => Array
                        (
                            [Content-Type] => Array
                                (
                                    [name] => Content-Type
                                    [data] => text/plain; charset=ISO-8859-1
                                    [meta] => Array
                                        (
                                            [type] => text/plain
                                            [genus] => text
                                            [species] => plain
                                            [charset] => ISO-8859-1
                                        )

                                )

                            [Content-Transfer-Encoding] => Array
                                (
                                    [name] => Content-Transfer-Encoding
                                    [data] => 7bit
                                )

                        )

                    [body] => A PNG image is attached to this message.


                )

            [1] => Array
                (
                    [meta] => Array
                        (
                        )

                    [headers] => Array
                        (
                            [Content-Type] => Array
                                (
                                    [name] => Content-Type
                                    [data] => image/png; name="blank_64.png"
                                    [meta] => Array
                                        (
                                            [type] => image/png
                                            [genus] => image
                                            [species] => png
                                            [name] => blank_64.png
                                        )

                                )

                            [Content-Disposition] => Array
                                (
                                    [name] => Content-Disposition
                                    [data] => attachment; filename="blank_64.png"
                                    [meta] => Array
                                        (
                                            [type] => attachment
                                            [filename] => blank_64.png
                                        )

                                )

                            [Content-Transfer-Encoding] => Array
                                (
                                    [name] => Content-Transfer-Encoding
                                    [data] => base64
                                )

                            [X-Attachment-Id] => Array
                                (
                                    [name] => X-Attachment-Id
                                    [data] => f_fq87rnmm0
                                )

                        )

                    [body] => iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAIAAAAlC+aJAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJ
bWFnZVJlYWR5ccllPAAAAhpJREFUeNrsms1qwkAURpM6KBEDAUFREAQhK3e+/1OoG0FQBAVBEBRF
UexB21IqTeIkJjNw72LoItDv3Nzfie5wOLxer46dViqV1OVy6ff7lgKMRiPlui5/KaWsU4/rEf/h
WG4CIAACYLllXD0PhwNt8Xg8/mmOtVqN0/d94wBQvN/vORHNGft8tVqt1+tBEJTL5SIBdrvdZrPh
PJ/PrwJji8UChlarBU8BAOieTCYp//H2bu12GwyLk3i5XE6n0zTTZPFViPcwm83sLqMwrNdru/sA
saQXSBkDsGH43/ZSoUQ9Za2wRhbc7Vk0sggPvJuk2lLcGo1GrgD4m67UbDb/c/bjAdgou7FtDoBc
3wDKOp0OEpNwhmEYu3zr5YAOAKEyGAw0coNwsngaTT81mD5O6xHmB3A6naIf8DzPaIDYIqO3LeQE
QJOKbgVkOdXWUIDHAhBblJNU5AIAUE8Xi67xSGcxMGInfo4cfB/boXq9np773whAyq5WqyTTQbfb
TbPsZwxAptJucXySBR+vo14vd7MHQPF8Pk+i+6dooj793URmAAR6QvWIJmUpO0Veq+hZttJzBUA6
s3fKcC+yE1cqlXeod+R2WgAEQD5w/FoIwzCMmBpMB3jcO0gISQ5oW+w3G8/zmCbMBdC72ZQQEgAB
EADpA1/26icPeQMCIAACIABm9IHb7ebcf4dso3rEK2w8HtvqfqU+BRgA+uXrOvB/+SgAAAAASUVO
RK5CYII=

                )

        )

)

Code Listing

MimeMessage.php (16776 bytes)  
<?php
/****************************************************************************
    MimeMessage
    Zac Hester
    2009-01-22
    Version 1.0.0

    This class provides a well-abstracted MIME format message parser.
    I found several MIME parser classes with terrible interfaces.  This
    provides a powerful and more simplified interface that actually
    uses OOP methodology (not just a pile of associative arrays).

    I've read through RFC2822 (http://www.faqs.org/rfcs/rfc2822.html)
    and this class should decode a modern, compliant message.  With
    the possible variations in message generators, I can't be sure this
    is a one-size-fits-all solution, and it, honestly, isn't my intention
    to make something that universal.  I will continue to tweak and adjust
    the implementation as I see more message formats that break the current
    (reasonably robust) parsing methods.

    Known non-conformance with RFC2822:
        -- Header data fields:
        - Nested '(' or ')' inside comments will break comment stripping
        - Allows nonstandard line endings (for compatibility)
        - Obsolete forms may impact parsing (untested)

    Usage:
    $email = file_get_contents('my_message.eml');
    $mm = new MimeMessage($email);
    $from = $mm->getHeader('From')->data;
    if($mm->hasParts()) {
        $num_parts = $mm->getPartCount();
        for($i = 0; $i < $num_parts; ++$i) {
            $p = $mm->getPart($i);
            $part_type = $p->getHeader('Content-Type')->getMeta('type');
        }
    }
    //Dump parsed info:
    print_r($mm->dumpArray());
****************************************************************************/

/**
 * MimeMessageHeader
 * Deals with the subtleties of various message and part headers.
 */
class MimeMessageHeader {

    //Header metadata (extra descriptors and refined parsing)
    protected $meta = array();

    //Header name field
    protected $name;

    //Header data field (unparsed)
    protected $data;


    /**
     * Constructor.
     * Creates a new message header object.
     *
     * @param name The value of the header name field
     * @param data The value of the header data field
     */
    public function __construct($name, $data) {
        $this->name = $name;
        $this->data = trim($data);
    }


    /**
     * append
     * Appends more data to this header (from whitespace folding).
     *
     * @param data An extra line of data to append to the data field
     * @return true on success
     */
    public function append($data) {
        $this->data .= preg_replace('/^\s+/', ' ', $data);
        return(true);
    }


    /**
     * finalize
     * Called when header data is complete.  This parses header data
     * fields a little more and needs to be done after all additional
     * data lines (from MimeMessageHeader::append()) have been appended.
     *
     */
    public function finalize() {

        //Strip comments.
        $data = preg_replace('/\([^)]*\)/', '', $this->data);

        //Special fields.
        $address_headers = array('To','Cc','Bcc','Reply-To');

        //Complex headers.
        if(strpos($data, ';') !== false) {

            //Begin tokenized processing.
            $nflags = 0;
            $chunk = trim(strtok($data, ';'));
            if(strpos($chunk, '=') === false) {
                $this->meta['type'] = $chunk;
                if(strpos($chunk, '/') !== false) {
                    list($generic, $specific) = explode('/', $chunk, 2);
                    $this->meta['genus'] = $generic;
                    $this->meta['species'] = $specific;
                }
                $chunk = strtok(';');
            }

            //The first one wasn't special, reset tokenizer.
            else {
                $chunk = strtok($data, ';');
            }

            //Scan all later chunks.
            while($chunk !== false) {
                $chunk = trim($chunk);

                //Might be field pair (key=value).
                if(strpos($chunk, '=') !== false) {
                    list($k, $v) = explode('=', $chunk, 2);
                    if(preg_match('/^".*"$/', $v)) {
                        $this->meta[$k] = trim($v, '"');
                        $this->meta[$k] = preg_replace(
                            '/\\"/', '"', $this->meta[$k]
                        );
                    }
                    else {
                        $this->meta[$k] = $v;
                    }
                }

                //Flag-style chunk.
                else if(strlen($chunk)) {
                    $this->meta[$nflags] = $chunk;
                    ++$nflags;
                }

                //Advance to next token.
                $chunk = strtok(';');
            }
        }

        //Date header.
        else if($this->name == 'Date') {
            $this->meta['timestamp'] = strtotime($data);
        }

        //Some headers may contain multiple addresses separated by ","
        else if(in_array($this->name, $address_headers)) {
            if(strpos($data, ',') !== false) {
                $addresses = explode(',', $data);
                array_walk($addresses, 'trim');
                $this->meta['addresses'] = $addresses;
            }
        }
    }


    /**
     * __toString
     * Returns the header's raw string value.
     *
     * @return The complete header data field (without parsing).
     */
    public function __toString() {
        return($this->data);
    }


    /**
     * __get
     * Accesses several common properties for the header.
     *
     * @param key The key given to us from the magic method invocation
     * @return The value of the specified property
     */
    public function __get($key) {
        switch($key) {
            case 'name': case 'key':
                return($this->name);
            break;
            case 'data': case 'value':
                return($this->data);
            break;
            case 'meta':
                return($this->meta);
            break;
        }
        return(false);
    }


    /**
     * getMeta
     * Returns a metadata field value or the list of metadata.  If no field
     * is specified with the key parameter, the entire list of metadata is
     * returned.
     *
     * @param key An optional metadata field specifier
     * @return A metadata value or the list of metadata
     */
    public function getMeta($key = false) {
        if($key && isset($this->meta[$key])) {
            return($this->meta[$key]);
        }
        else if($key === false) {
            return($this->meta);
        }
        return(false);
    }


    /**
     * dumpArray
     * Dumps an array of info for diagnostics.
     *
     * @return A tree structure representing this header's parsed info
     */
    public function dumpArray() {
        $array = array(
            'name' => $this->name,
            'data' => $this->data
        );
        if(count($this->meta)) {
            $array['meta'] = $this->meta;
        }
        return($array);
    }
}


/**
 * MimeMessagePart
 * Provides storage and specific methods for a MIME message part.
 *
 */
class MimeMessagePart {

    //Raw message data.
    protected $data;

    //Non-MIME metadata.
    protected $meta = array();

    //Part headers.
    protected $headers = array();

    //Raw content body (normal message for terminal parts).
    protected $body;

    //Component parts (if this isn't a terminal part).
    protected $parts = array();

    //Detected line-ending (for compatibility).
    private $nl = false;

    //Helps continued headers.
    private $parse_last_header = '';


    /**
     * Constructor.
     * Sets up and begins parsing the message data.
     *
     * @param data The entire contents of the message or part.
     */
    public function __construct($data) {
        $this->data = $data;
        $this->parse();
    }


    /**
     * getMeta
     * Tells the user derived information about the message part.
     * Returns a metadata field value or the list of metadata.  If no field
     * is specified with the key parameter, the entire list of metadata is
     * returned.
     *
     * @param key An optional metadata field specifier
     * @return A metadata value or the list of metadata
     */
    public function getMeta($key = false) {
        if($key && isset($this->meta[$key])) {
            return($this->meta[$key]);
        }
        else if($key === false) {
            return($this->meta);
        }
        return(false);
    }


    /**
     * getHeader
     * Returns a header object (or list of header objects) to the user.
     *
     * @param label The header name field/label
     * @return A MimeMessageHeader object representing this header
     */
    public function getHeader($label = false) {
        if($label && isset($this->headers[$label])) {
            return($this->headers[$label]);
        }
        else if($label === false) {
            return($this->headers);
        }
        return(false);
    }


    /**
     * isTerminal
     * Indicates if this message part contains no other message parts.
     *
     * @return true if this part contains no other parts
     */
    public function isTerminal() {
        return(!$this->hasParts());
    }


    /**
     * getContent
     * Returns the message content.
     *
     * @return The unparsed contents of this message or part
     */
    public function getContent() {
        $type = $this->getHeader('Content-Type');
        if($type && $type->getMeta('type') == 'text/html') {
            return(preg_replace(
                    '/=3D/',
                    '=',
                    preg_replace(
                        '/=\r?\n/',
                        '',
                        $this->body
                    )
                )
            );
        }
        return($this->body);
    }


    /**
     * hasParts
     * Indicates if this message or part contains other parts.
     *
     * @return true if this consists of multiple parts, false otherwise
     */
    public function hasParts() {
        if(count($this->parts) > 0) {
            return(true);
        }
        return(false);
    }


    /**
     * getPartCount
     * Returns the number of message parts within this message or part.
     *
     * @return The number of message parts, 0 if none
     */
    public function getPartCount() {
        return(count($this->parts));
    }


    /**
     * getPart
     * Retrieves a message part by index within the current message.
     *
     * @param index The index of a part within the current part
     * @return A MimeMessagePart object representing this message part
     */
    public function getPart($index) {
        if(isset($this->parts[$index])) {
            return($this->parts[$index]);
        }
        return(false);
    }


    /**
     * getEncodedFiles
     * As this class is extremely useful for extracting file attachments,
     * and otherwise encoded chunks of a message, this function builds a
     * list of all the parts encoded in the given format (base64 by default).
     *
     * @param encoding The encoding method of the desired message parts
     * @return A list of MimeMessagePart objects representing all message
     *    parts that are encoded as specified
     */
    public function getEncodedFiles($encoding = 'base64') {
        return(
            $this->getPartsByHeader(
                'Content-Transfer-Encoding',
                $encoding
            )
        );
    }


    /**
     * getPartsByHeader
     * Allows users to query for message parts based on the value of
     * any of their headers.
     *
     * @param name The header name
     * @param value The header value
     * @return A list of MimeMessagePart objects representing the match
     *       matched parts, or false if none are found to match
     */
    public function getPartsByHeader($name, $value) {

        //If not a terminal part, scan component parts.
        if($this->hasParts()) {
            $test = false;
            $files = array();
            $nparts = $this->getPartCount();
            for($i = 0; $i < $nparts; ++$i) {
                $test = $this->getPart($i)->getPartsByHeader($name, $value);
                if(is_array($test)) {
                    $files = array_merge($files, $test);
                }
            }
            return($files);
        }

        //This is a terminal part, check for the header.
        else if($this->getHeader($name) == $value) {
            return(array($this));
        }

        //No parts match this encoding.
        return(false);
    }


    /**
     * dumpArray
     * Diagnostic tree output.
     *
     * @return A tree structure representing all the parsed information
     */
    public function dumpArray() {
        $array = array(
            'meta' => $this->meta,
            'headers' => array()
        );
        foreach($this->headers as $k => $v) {
            if(is_array($v)) {
                foreach($v as $v1) {
                    $array['headers'][$k][] = $v1->dumpArray();
                }
            }
            else {
                $array['headers'][$k] = $v->dumpArray();
            }
        }
        if($this->hasParts()) {
            $array['parts'] = array();
            $n = $this->getPartCount();
            for($i = 0; $i < $n; ++$i) {
                $p = $this->getPart($i);
                $array['parts'][] = $p->dumpArray();
            }
        }
        else {
            $array['body'] = $this->getContent();
        }
        return($array);
    }

    /*--------------------------------------------------------------------*/

    /**
     * parse
     * Parses the message part headers and content.
     *
     */
    protected function parse() {

        //Separate header and body (account for crazy people).
        if(strpos($this->data, "\r\n\r\n") !== false) {
            list($head, $body) = explode("\r\n\r\n", $this->data, 2);
        }
        else if(strpos($this->data, "\n\n") !== false) {
            list($head, $body) = explode("\n\n", $this->data, 2);
        }
        else if(strpos($this->data, "\r\r") !== false) {
            list($head, $body) = explode("\r\r", $this->data, 2);
        }
        else {
            $head = '';
            $body = $this->data;
        }

        //Parse and store all current headers.
        $this->parseHeaders($head);

        //Check for multipart message.
        $type = $this->getHeader('Content-Type');
        if($type && $type->getMeta('genus') == 'multipart') {

            //Set the label string.
            $label = '--'.$type->getMeta('boundary');

            //Remove the first label from this part.
            $part = substr($body, strlen($label));

            //Run through each section.
            while(($offset = strpos($part, $label)) !== false) {

                //Get everything up to the next label.
                $current = substr($part, 0, $offset);

                //Create a new message part with this information.
                $this->parts[] = new MimeMessagePart($current);

                //Remove this message part.
                $part = substr($part, $offset+strlen($label));

                //Check for closing label.
                if(substr($part,0,2) == '--') { break; }
            }
        }

        //This is a terminal message part.
        else {
            $this->body = $body;
        }
    }


    /**
     * parseHeaders
     * Parses the header section of a message or part and stores the info
     * in the headers list.
     *
     * @param head The message before the body delimiter
     */
    protected function parseHeaders($head) {

        //Detect line ending for compatibility (non-standard).
        if($this->nl == false) {
            if(substr($head, strpos($head,"\n")-1, 1) != "\r") {
                $this->nl = "\n";
            }
            else {
                $this->nl = "\r\n";
            }
        }

        //Line-based parsing.
        $hlines = explode($this->nl, $head);
        foreach($hlines as $line) {

            //Message delimiter.
            if(preg_match('/^From /', $line)) {
                $this->meta['delimiter'] = $line;
            }

            //Continued header.
            else if(preg_match('/^\s/', $line)) {
                $this->appendHeader($line);
            }

            //Normal header or beginning of header.
            else if(strpos($line, ':') !== false) {
                list($name, $data) = explode(':', $line, 2);
                $this->setHeader($name, $data);
            }
        }

        //Send a signal to indicate we are finished extracting headers.
        $this->finishHeaders();
    }


    /**
     * setHeader
     * Attempts to intelligently add new headers to the headers list.
     *
     * @param name The header name field value
     * @param data The header data field value
     */
    protected function setHeader($name, $data) {

        //List of current headers.
        $hkeys = array_keys($this->headers);

        //Minor amount of header field name normalization.
        $hkey = trim($name);

        //Repeated header.
        if(in_array($hkey, $hkeys)) {

            //Already a list of repeated headers.
            if(is_array($this->headers[$hkey])) {
                $this->headers[$hkey][] = new MimeMessageHeader($hkey,$data);
            }

            //Convert to list of repeated headers.
            else {
                $buffer = $this->headers[$hkey];
                $this->headers[$hkey] = array(
                    $buffer,
                    new MimeMessageHeader($hkey,$data)
                );
            }
        }

        //Normal header.
        else {
            $this->headers[$hkey] = new MimeMessageHeader($hkey,$data);
        }

        //Remember the last header in case of continuation.
        $this->parse_last_header = $hkey;
    }


    /**
     * appendHeader
     * Appends data to the "last" header.  Useful when single header data
     * fields span multiple lines.
     *
     * @param data The additional header data field information
     */
    protected function appendHeader($data) {

        //Shortcut, shhh.
        $k = $this->parse_last_header;

        //If it's a repeated header, append to the last one.
        if(is_array($this->headers[$k])) {
            $this->headers[$k][count($this->headers[$k])-1]->append($data);
        }

        //Regular header.
        else {
            $this->headers[$k]->append($data);
        }
    }


    /**
     * finishHeaders
     * Informs each header that it is complete.
     *
     */
    protected function finishHeaders() {
        foreach($this->headers as $header) {
            if(is_array($header)) {
                foreach($header as $subheader) {
                    $subheader->finalize();
                }
            }
            else {
                $header->finalize();
            }
        }
    }
}


/**
 * MimeMessage
 * The root user interface for MIME formatted messages.
 */
class MimeMessage extends MimeMessagePart {


    /**
     * Constructor.
     *
     * @param message The entire RFC2822 formatted message.
     */
    public function __construct($message) {
        parent::__construct($message);
    }


    /**
     * __toString
     * Makes some assumptions about what the user would want when asking
     * for just a string representation of the message.  It pretty much
     * always returns the plain text message body (which can depend on
     * the original construction of the message).
     *
     * @return A string representation of the message
     */
    public function __toString() {

        //Look for multipart message.
        if($this->hasParts()) {

            //Grab all plain text parts.
            $parts = $this->getPartsByHeader('Content-Type', 'text/plain');

            //Make sure the message has a plain text part.
            if(is_array($parts) && count($parts)) {

                //Send back the first plain text part.
                return($parts[0]->getContent());
            }

            //No plain text parts, send back the first part.
            return($this->getPart(0)->getContent());
        }

        //Simple message, send back the body.
        return($this->getContent());
    }
}

?>

Page last updated 2010-06-08