<?php

/**
 * Message.class.php
 *
 * This file contains functions needed to handle mime messages.
 *
 * @copyright 2003-2026 The SquirrelMail Project Team
 * @license http://opensource.org/licenses/gpl-license.php GNU Public License
 * @version $Id: Message.class.php 15055 2026-02-24 07:33:28Z pdontthink $
 * @package squirrelmail
 * @subpackage mime
 * @since 1.3.2
 */

/**
 * The object that contains a message.
 *
 * message is the object that contains messages. It is a recursive object in
 * that through the $entities variable, it can contain more objects of type
 * message. See documentation in mime.txt for a better description of how this
 * works.
 * @package squirrelmail
 * @subpackage mime
 * @since 1.3.0
 */
class Message {
    var $header;
    /**
     * rfc822header object
     * @var object
     */
    var $rfc822_header = '';
    /**
     * MessageHeader object
     * @var object
     */
    var $mime_header = '';
    /**
     * @var mixed
     */
    var $flags = '';
    /**
     * Media type
     * @var string
     */
    var $type0='';
    /**
     * Media subtype
     * @var string
     */
    var $type1='';
    /**
     * Nested mime parts
     * @var array
     */
    var $entities = array();
    /**
     * Message part id
     * @var string
     */
    var $entity_id = '';
    /**
     * Parent message part id
     * @var string
     */
    var $parent_ent;
    /**
     * @var mixed
     */
    var $entity;
    /**
     * @var mixed
     */
    var $parent = '';
    /**
     * @var string
     */
    var $decoded_body='';
    /**
     * Message \seen status
     * @var boolean
     */
    var $is_seen = 0;
    /**
     * Message \answered status
     * @var boolean
     */
    var $is_answered = 0;
    /**
     * Message forward status
     * @var boolean
     */
    var $is_forwarded = 0;
    /**
     * Message \deleted status
     * @var boolean
     */
    var $is_deleted = 0;
    /**
     * Message \flagged status
     * @var boolean
     */
    var $is_flagged = 0;
    /**
     * Message mdn status
     * @var boolean
     */
    var $is_mdnsent = 0;
    /**
     * Message text body
     * @var string
     */
    var $body_part = '';
    /**
     * Message part offset
     * for fetching body parts out of raw messages
     * @var integer
     */
    var $offset = 0;
    /**
     * Message part length
     * for fetching body parts out of raw messages
     * @var integer
     */
    var $length = 0;
    /**
     * Local attachment filename location where the tempory attachment is
     * stored. For use in delivery class.
     * @var string
     */
    var $att_local_name = '';

    /**
     * @param string $ent entity id
     */
    function setEnt($ent) {
        $this->entity_id= $ent;
    }

    /**
     * Add nested message part
     * @param object $msg
     */
    function addEntity ($msg) {
        $this->entities[] = $msg;
    }

    /**
     * Get file name used for mime part
     * @return string file name
     * @since 1.3.2
     */
    function getFilename() {
         $filename = '';
         $header = $this->header;
         if (is_object($header->disposition)) {
              $filename = $header->disposition->getProperty('filename');
              if (trim($filename) == '') {
                  $name = decodeHeader($header->disposition->getProperty('name'));
                  if (!trim($name)) {
                      $name = $header->getParameter('name');
                      if(!trim($name)) {
                          if (!trim( $header->id )) {
                              $filename = 'untitled-[' . $this->entity_id . ']' . '.' . strtolower($header->type1);
                          } else {
                              $filename = 'cid: ' . $header->id . '.' . strtolower($header->type1);
                          }
                      } else {
                          $filename = $name;
                      }
                  } else {
                      $filename = $name;
                  }
              }
         } else {
              $filename = $header->getParameter('filename');
              if (!trim($filename)) {
                  $filename = $header->getParameter('name');
                  if (!trim($filename)) {
                      if (!trim( $header->id )) {
                          $filename = 'untitled-[' . $this->entity_id . ']' . '.' . strtolower($header->type1);
                      } else {
                          $filename = 'cid: ' . $header->id . '.' . strtolower($header->type1);
                      }
                  }
              }
         }
         return $filename;
    }

    /**
     * Add header object to message object.
     * WARNING: Unfinished code. Don't expect it to work in older sm versions.
     * @param mixed $read array or string with message headers
     * @todo FIXME: rfc822header->parseHeader() does not return rfc822header object
     */
    function addRFC822Header($read) {
        $header = new Rfc822Header();
        $this->rfc822_header = $header->parseHeader($read);
    }

    /**
     * @param string $ent
     * @return mixed (object or string?)
     */
    function getEntity($ent) {
        $cur_ent = $this->entity_id;
        $msg = $this;
        if (($cur_ent == '') || ($cur_ent == '0')) {
            $cur_ent_a = array();
        } else {
            $cur_ent_a = explode('.', $this->entity_id);
        }
        $ent_a = explode('.', $ent);

        for ($i = 0,$entCount = count($ent_a) - 1; $i < $entCount; ++$i) {
            if (isset($cur_ent_a[$i]) && ($cur_ent_a[$i] != $ent_a[$i])) {
                $msg = $msg->parent;
                $cur_ent_a = explode('.', $msg->entity_id);
                --$i;
            } else if (!isset($cur_ent_a[$i])) {
                if (isset($msg->entities[($ent_a[$i]-1)])) {
                    $msg = $msg->entities[($ent_a[$i]-1)];
                } else {
                    $msg = $msg->entities[0];
                }
            }
            if (($msg->type0 == 'message') && ($msg->type1 == 'rfc822')) {
                /*this is a header for a message/rfc822 entity */
                $msg = $msg->entities[0];
            }
        }

        if (($msg->type0 == 'message') && ($msg->type1 == 'rfc822')) {
            /*this is a header for a message/rfc822 entity */
            $msg = $msg->entities[0];
        }

        if (isset($msg->entities[($ent_a[$entCount])-1])) {
            if (is_object($msg->entities[($ent_a[$entCount])-1])) {
                $msg = $msg->entities[($ent_a[$entCount]-1)];
            }
        }

        return $msg;
    }

    /**
     * Set message body
     * @param string $s message body
     */
    function setBody($s) {
        $this->body_part = $s;
    }

    /**
     * Clean message object
     */
    function clean_up() {
        $msg = $this;
        $msg->body_part = '';

        foreach ($msg->entities as $m) {
            $m->clean_up();
        }
    }

    /**
     * @return string
     */
    function getMailbox() {
        $msg = $this;
        while (is_object($msg->parent)) {
            $msg = $msg->parent;
        }
        return $msg->mailbox;
    }

    /*
     * Bodystructure parser, a recursive function for generating the
     * entity-tree with all the mime-parts.
     *
     * It follows RFC2060 and stores all the described fields in the
     * message object.
     *
     * Question/Bugs:
     *
     * Ask for me (Marc Groot Koerkamp, stekkel@users.sourceforge.net)
     * @param string $read
     * @param integer $i
     * @param mixed $sub_msg
     * @return object Message object
     * @todo define argument and return types
     */
    static function parseStructure($read, &$i, $sub_msg = '') {
        $msg = Message::parseBodyStructure($read, $i, $sub_msg);
        if($msg) $msg->setEntIds($msg,false,0);
        return $msg;
    }

    /**
     * @param object $msg
     * @param mixed $init
     * @param integer $i
     * @todo document me
     * @since 1.4.0
     */
    function setEntIds(&$msg,$init=false,$i=0) {
        $iCnt = count($msg->entities);
        if ($init !==false) {
            $iEntSub = $i+1;
            if ($msg->parent->type0 == 'message' &&
                $msg->parent->type1 == 'rfc822' &&
                $msg->type0 == 'multipart') {
                $iEntSub = '0';
            }
            if ($init) {
                $msg->entity_id = "$init.$iEntSub";
            } else {
                $msg->entity_id = $iEntSub;
            }
        } else if ($iCnt) {
            $msg->entity_id='0';
        } else {
            $msg->entity_id='1';
        }
        for ($i=0;$i<$iCnt;++$i) {
            $msg->entities[$i]->parent =& $msg;
            if (strrchr($msg->entity_id, '.') != '.0') {
                $msg->entities[$i]->setEntIds($msg->entities[$i],$msg->entity_id,$i);
            } else {
                $msg->entities[$i]->setEntIds($msg->entities[$i],$msg->parent->entity_id,$i);
            }
        }
    }

    /**
     * @param string $read
     * @param integer $i
     * @param mixed $sub_msg
     * @return object Message object
     * @todo document me
     * @since 1.4.0 (code was part of parseStructure() in 1.3.x)
     */
    static function parseBodyStructure($read, &$i, $sub_msg = '') {
        $arg_no = 0;
        $arg_a  = array();
        if ($sub_msg) {
            $message = $sub_msg;
        } else {
            $message = new Message();
        }

        for ($cnt = strlen($read); $i < $cnt; ++$i) {
            $char = strtoupper($read[$i]);
            switch ($char) {
                case '(':
                    switch($arg_no) {
                        case 0:
                            if (!isset($msg)) {
                                $msg = new Message();
                                $hdr = new MessageHeader();
                                $hdr->type0 = 'text';
                                $hdr->type1 = 'plain';
                                $hdr->encoding = '7bit';
                                $msg->header = $hdr;
                            } else {
                                $msg->header->type0 = 'multipart';
                                $msg->type0 = 'multipart';
                                while ($read[$i] == '(') {
                                    $msg->addEntity($msg->parseBodyStructure($read, $i, $msg));
                                }
                            }
                            break;
                        case 1:
                            /* multipart properties */
                            ++$i;
                            $arg_a[] = $msg->parseProperties($read, $i);
                            ++$arg_no;
                            break;
                        case 2:
                            if (isset($msg->type0) && ($msg->type0 == 'multipart')) {
                                ++$i;
                                $arg_a[] = $msg->parseDisposition($read, $i);
                            } else { /* properties */
                                $arg_a[] = $msg->parseProperties($read, $i);
                            }
                            ++$arg_no;
                            break;
                        case 3:
                            if (isset($msg->type0) && ($msg->type0 == 'multipart')) {
                                ++$i;
                                $arg_a[]= $msg->parseLanguage($read, $i);
                            }
                        case 7:
                            if (($arg_a[0] == 'message') && ($arg_a[1] == 'rfc822')) {
                                $msg->header->type0 = $arg_a[0];
                                $msg->header->type1 = $arg_a[1];
                                $msg->type0 = $arg_a[0];
                                $msg->type1 = $arg_a[1];
                                $rfc822_hdr = new Rfc822Header();
                                $msg->rfc822_header = $msg->parseEnvelope($read, $i, $rfc822_hdr);
                                while (($i < $cnt) && ($read[$i] != '(')) {
                                    ++$i;
                                }
                                $msg->addEntity($msg->parseBodyStructure($read, $i,$msg));
                            }
                            break;
                        case 8:
                            ++$i;
                            $arg_a[] = $msg->parseDisposition($read, $i);
                            ++$arg_no;
                            break;
                        case 9:
                            ++$i;
                            if (($arg_a[0] == 'text') || (($arg_a[0] == 'message') && ($arg_a[1] == 'rfc822'))) {
                                $arg_a[] = $msg->parseDisposition($read, $i);
                            } else {
                                $arg_a[] = $msg->parseLanguage($read, $i);
                            }
                            ++$arg_no;
                            break;
                       case 10:
                           if (($arg_a[0] == 'text') || (($arg_a[0] == 'message') && ($arg_a[1] == 'rfc822'))) {
                               ++$i;
                               $arg_a[] = $msg->parseLanguage($read, $i);
                           } else {
                               $i = $msg->parseParenthesis($read, $i);
                               $arg_a[] = ''; /* not yet described in rfc2060 */
                           }
                           ++$arg_no;
                           break;
                       default:
                           /* unknown argument, skip this part */
                           $i = $msg->parseParenthesis($read, $i);
                           $arg_a[] = '';
                           ++$arg_no;
                           break;
                   } /* switch */
                   break;
                case '"':
                    /* inside an entity -> start processing */
                    $arg_s = $msg->parseQuote($read, $i);
                    ++$arg_no;
                    if ($arg_no < 3) {
                        $arg_s = strtolower($arg_s); /* type0 and type1 */
                    }
                    $arg_a[] = $arg_s;
                    break;
                case 'n':
                case 'N':
                    /* probably NIL argument */
                    $tmpnil = strtoupper(substr($read, $i, 4));
                    if ($tmpnil == 'NIL ' || $tmpnil == 'NIL)') {
                        $arg_a[] = '';
                        ++$arg_no;
                        $i += 2;
                    }
                    break;
                case '{':
                    /* process the literal value */
                    $arg_a[] = $msg->parseLiteral($read, $i);
                    ++$arg_no;
                    break;
                case '0':
                case is_numeric($read[$i]):
                    /* process integers */
                    if ($read[$i] == ' ') { break; }
                    ++$arg_no;
                    if (preg_match('/^([0-9]+).*/',substr($read,$i), $regs)) {
                        $i += strlen($regs[1])-1;
                        $arg_a[] = $regs[1];
                    } else {
                        $arg_a[] = 0;
                    }
                    break;
                case ')':
                    $multipart = (isset($msg->type0) && ($msg->type0 == 'multipart'));
                    if (!$multipart) {
                        $shifted_args = (($arg_a[0] == 'text') || (($arg_a[0] == 'message') && ($arg_a[1] == 'rfc822')));
                        $hdr->type0 = $arg_a[0];
                        $hdr->type1 = $arg_a[1];

                        $msg->type0 = $arg_a[0];
                        $msg->type1 = $arg_a[1];
                        $arr = $arg_a[2];
                        if (is_array($arr)) {
                            $hdr->parameters = $arg_a[2];
                        }
                        $hdr->id = str_replace('<', '', str_replace('>', '', $arg_a[3]));
                        $hdr->description = $arg_a[4];
                        $hdr->encoding = strtolower($arg_a[5]);
                        $hdr->entity_id = $msg->entity_id;
                        $hdr->size = $arg_a[6];
                        if ($shifted_args) {
                            $hdr->lines = $arg_a[7];
                            $s = 1;
                        } else {
                            $s = 0;
                        }
                        $hdr->md5 = (isset($arg_a[7+$s]) ? $arg_a[7+$s] : $hdr->md5);
                        $hdr->disposition = (isset($arg_a[8+$s]) ? $arg_a[8+$s] : $hdr->disposition);
                        $hdr->language = (isset($arg_a[9+$s]) ? $arg_a[9+$s] : $hdr->language);
                        $msg->header = $hdr;
                    } else {
                        $hdr->type0 = 'multipart';
                        $hdr->type1 = $arg_a[0];
                        $msg->type0 = 'multipart';
                        $msg->type1 = $arg_a[0];
                        $hdr->parameters = (isset($arg_a[1]) ? $arg_a[1] : $hdr->parameters);
                        $hdr->disposition = (isset($arg_a[2]) ? $arg_a[2] : $hdr->disposition);
                        $hdr->language = (isset($arg_a[3]) ? $arg_a[3] : $hdr->language);
                        $msg->header = $hdr;
                    }
                    return $msg;
                default: break;
            } /* switch */
        } /* for */
    } /* parsestructure */

    /**
     * @param string $read
     * @param integer $i
     * @return array
     */
    function parseProperties($read, &$i) {
        $properties = array();
        $prop_name = '';

        for (; $read[$i] != ')'; ++$i) {
            $arg_s = '';
            if ($read[$i] == '"') {
                $arg_s = $this->parseQuote($read, $i);
            } else if ($read[$i] == '{') {
                $arg_s = $this->parseLiteral($read, $i);
            }

            if ($arg_s != '') {
                if ($prop_name == '') {
                    $prop_name = strtolower($arg_s);
                    $properties[$prop_name] = '';
                } else if ($prop_name != '') {
                    $properties[$prop_name] = $arg_s;
                    $prop_name = '';
                }
            }
        }
        return $this->handleRfc2231($properties);
    }

    /**
     * Joins RFC-2231 continuations, converts encoding to RFC-2047 style
     * @param array $properties
     * @return array
     */
    function handleRfc2231($properties) {

        /* STAGE 1: look for multi-line parameters, convert to the single line
           form, and normalize values */

        $cont = array();
        foreach($properties as $key=>$value) {
            /* Look for parameters followed by "*", a number, and an optional "*"
               at the end. */
            if (preg_match('/^(.*\*)(\d+)(|\*)$/', $key, $matches)) {
                unset($properties[$key]);
                $prop_name = $matches[1];
                if (!isset($cont[$prop_name])) $cont[$prop_name] = array();

                /* An asterisk at the end of parameter name indicates that there
                   may be an encoding information present, and the parameter
                   value is percent-hex encoded. If parameter is not encoded, we
                   encode it to simplify further processing.
                */
                if ($matches[3] == '') $value = rawurlencode($value);
                /* Use the number from parameter name as segment index */
                $cont[$prop_name][$matches[2]] = $value;
            }
        }
        foreach($cont as $key=>$values) {
            /* Sort segments of multi-line parameters by index number. */
            ksort($values);
            /* Join segments. We can do it safely, because:
               - All segments are encoded.
               - Per RFC-2231, chapter 4.1 notes.
            */
            $value = implode('', $values);
            /* Add language and character set field delimiters if not present,
               as required per RFC-2231, chapter 4.1, note #5. */
            if (strpos($value, "'") === false) $value = "''".$value;
            $properties[$key] = $value;
        }

        /* STAGE 2: Convert single line RFC-2231 encoded parameters, and
           previously converted multi-line parameters, to RFC-2047 encoding */

        foreach($properties as $key=>$value) {
            if ($idx = strpos($key, '*')) {
                unset($properties[$key]);
                /* Extract the charset & language. */
                $charset = substr($value,0,strpos($value,"'"));
                $value = substr($value,strlen($charset)+1);
                $language = substr($value,0,strpos($value,"'"));
                $value = substr($value,strlen($language)+1);
                /* No character set defaults to US-ASCII */
                if (!$charset) $charset = 'US-ASCII';
                if ($language) $language = '*'.$language;
                /* Convert to RFC-2047 base64 encoded string. */
                $properties[substr($key, 0, $idx)] = '=?'.$charset.$language.'?B?'.base64_encode(rawurldecode($value)).'?=';
            }
        }
        return $properties;
    }

    /**
     * @param string $read
     * @param integer $i
     * @param object $hdr MessageHeader object
     * @return object MessageHeader object
     */
    function parseEnvelope($read, &$i, $hdr) {
        $arg_no = 0;
        $arg_a = array();
        ++$i;
        for ($cnt = strlen($read); ($i < $cnt) && ($read[$i] != ')'); ++$i) {
            $char = strtoupper($read[$i]);
            switch ($char) {
                case '"':
                    $arg_a[] = $this->parseQuote($read, $i);
                    ++$arg_no;
                    break;
                case '{':
                    $arg_a[] = $this->parseLiteral($read, $i);
            /* temp bugfix (SM 1.5 will have a working clean version)
               too much work to implement that version right now */
//            --$i;
                    ++$arg_no;
                    break;
                case 'N':
                    /* probably NIL argument */
                    if (strtoupper(substr($read, $i, 3)) == 'NIL') {
                        $arg_a[] = '';
                        ++$arg_no;
                        $i += 2;
                    }
                    break;
                case '(':
                    /* Address structure (with group support)
                     * Note: Group support is useless on SMTP connections
                     *       because the protocol doesn't support it
                     */
                    $addr_a = array();
                    $group = '';
                    $a=0;
                    for (; $i < $cnt && $read[$i] != ')'; ++$i) {
                        if ($read[$i] == '(') {
                            $addr = $this->parseAddress($read, $i);
                            if (($addr->host == '') && ($addr->mailbox != '')) {
                                /* start of group */
                                $group = $addr->mailbox;
                                $group_addr = $addr;
                                $j = $a;
                            } else if ($group && ($addr->host == '') && ($addr->mailbox == '')) {
                               /* end group */
                                if ($a == ($j+1)) { /* no group members */
                                    $group_addr->group = $group;
                                    $group_addr->mailbox = '';
                                    $group_addr->personal = "$group: Undisclosed recipients;";
                                    $addr_a[] = $group_addr;
                                    $group ='';
                                }
                            } else {
                                $addr->group = $group;
                                $addr_a[] = $addr;
                            }
                            ++$a;
                        }
                    }
                    $arg_a[] = $addr_a;
                    break;
                default: break;
            }
        }

        if (count($arg_a) > 9) {
            $d = strtr($arg_a[0], array('  ' => ' '));
            $d_parts = explode(' ', $d);
            if (!$arg_a[1]) $arg_a[1] = _("(no subject)");

            $hdr->date = getTimeStamp($d_parts); /* argument 1: date */
            $hdr->date_unparsed = strtr($d,'<>','  '); /* original date */
            $hdr->subject = $arg_a[1];     /* argument 2: subject */
            $hdr->from = is_array($arg_a[2]) ? $arg_a[2][0] : '';     /* argument 3: from        */
            $hdr->sender = is_array($arg_a[3]) ? $arg_a[3][0] : '';   /* argument 4: sender      */
            $hdr->reply_to = is_array($arg_a[4]) ? $arg_a[4][0] : '';  /* argument 5: reply-to    */
            $hdr->to = $arg_a[5];          /* argument 6: to          */
            $hdr->cc = $arg_a[6];          /* argument 7: cc          */
            $hdr->bcc = $arg_a[7];         /* argument 8: bcc         */
            $hdr->in_reply_to = $arg_a[8];   /* argument 9: in-reply-to */
            $hdr->message_id = $arg_a[9];  /* argument 10: message-id */
        }
        return $hdr;
    }

    /**
     * @param string $read
     * @param integer $i
     * @return string
     * @todo document me
     */
    function parseLiteral($read, &$i) {
        $lit_cnt = '';
        ++$i;
        $iPos = strpos($read,'}',$i);
        if ($iPos) {
            $lit_cnt = substr($read, $i, $iPos - $i);
            $i += strlen($lit_cnt) + 3; /* skip } + \r + \n */
            /* Now read the literal */
            $s = ($lit_cnt ? substr($read,$i,$lit_cnt): '');
            $i += $lit_cnt;
            /* temp bugfix (SM 1.5 will have a working clean version)
               too much work to implement that version right now */
            --$i;
        } else { /* should never happen */
            $i += 3; /* } + \r + \n */
            $s = '';
        }
        return $s;
    }

    /**
     * function parseQuote
     *
     * This extract the string value from a quoted string. After the end-quote
     * character is found it returns the string. The offset $i when calling
     * this function points to the first double quote. At the end it points to
     * The ending quote. This function takes care of escaped double quotes.
     * "some \"string\""
     * ^               ^
     * initial $i      end position $i
     *
     * @param string $read
     * @param integer $i offset in $read
     * @return string string inbetween the double quotes
     * @author Marc Groot Koerkamp
     */
    function parseQuote($read, &$i) {
        $s = '';
        $iPos = ++$i;
        $iPosStart = $iPos;
        while (true) {
            $iPos = strpos($read,'"',$iPos);
            if (!$iPos) break;
            if ($iPos && $read[$iPos -1] != '\\') {
                $s = substr($read,$i,($iPos-$i));
                $i = $iPos;
                break;
            } else if ($iPos > 1 && $read[$iPos -1] == '\\' && $read[$iPos-2] == '\\') {
                // This is an unique situation where the fast detection of the string
                // fails. If the quote string ends with \\ then we need to iterate
                // through the entire string to make sure we detect the unexcaped
                // double quotes correctly.
                $s = '';
                $bEscaped = false;
                $k = 0;
                 for ($j=$iPosStart,$iCnt=strlen($read);$j<$iCnt;++$j) {
                    $cChar = $read[$j];
                    switch ($cChar) {
                        case '\\':
                           $bEscaped = !$bEscaped;
                            $s .= $cChar;
                            break;
                         case '"':
                            if ($bEscaped) {
                                $s .= $cChar;
                                $bEscaped = false;
                            } else {
                                $i = $j;
                                break 3;
                            }
                            break;
                         default:
                            if ($bEscaped) {
                               $bEscaped = false;
                            }
                            $s .= $cChar;
                            break;
                    }
                }
            }
            ++$iPos;
            if ($iPos > strlen($read)) {
                break;
            }
        }
        return $s;
    }

    /**
     * @param string $read
     * @param integer $i
     * @return object AddressStructure object
     */
    function parseAddress($read, &$i) {
        $arg_a = array();
        for (; $read[$i] != ')'; ++$i) {
            $char = strtoupper($read[$i]);
            switch ($char) {
                case '"': $arg_a[] = $this->parseQuote($read, $i); break;
                case '{': $arg_a[] = $this->parseLiteral($read, $i); break;
                case 'n':
                case 'N':
                    if (strtoupper(substr($read, $i, 3)) == 'NIL') {
                        $arg_a[] = '';
                        $i += 2;
                    }
                    break;
                default: break;
            }
        }

        if (count($arg_a) == 4) {
            $adr = new AddressStructure();
            $adr->personal = $arg_a[0];
            $adr->adl = $arg_a[1];
            $adr->mailbox = $arg_a[2];
            $adr->host = $arg_a[3];
        } else {
            $adr = '';
        }
        return $adr;
    }

    /**
     * @param string $read
     * @param integer $i
     * @param object Disposition object or empty string
     */
    function parseDisposition($read, &$i) {
        $arg_a = array();
        for (; $read[$i] != ')'; ++$i) {
            switch ($read[$i]) {
                case '"': $arg_a[] = $this->parseQuote($read, $i); break;
                case '{': $arg_a[] = $this->parseLiteral($read, $i); break;
                case '(': $arg_a[] = $this->parseProperties($read, $i); break;
                default: break;
            }
        }

        if (isset($arg_a[0])) {
            $disp = new Disposition($arg_a[0]);
            if (isset($arg_a[1])) {
                $disp->properties = $arg_a[1];
            }
        }
        return (is_object($disp) ? $disp : '');
    }

    /**
     * @param string $read
     * @param integer $i
     * @return object Language object or empty string
     */
    function parseLanguage($read, &$i) {
        /* no idea how to process this one without examples */
        $arg_a = array();

        for (; $read[$i] != ')'; ++$i) {
            switch ($read[$i]) {
                case '"': $arg_a[] = $this->parseQuote($read, $i); break;
                case '{': $arg_a[] = $this->parseLiteral($read, $i); break;
                case '(': $arg_a[] = $this->parseProperties($read, $i); break;
                default: break;
            }
        }

        if (isset($arg_a[0])) {
            $lang = new Language($arg_a[0]);
            if (isset($arg_a[1])) {
                $lang->properties = $arg_a[1];
            }
        }
        return (is_object($lang) ? $lang : '');
    }

    /**
     * Parse message text enclosed in parenthesis
     * @param string $read
     * @param integer $i
     * @return integer
     */
    function parseParenthesis($read, $i) {
        for ($i++; $read[$i] != ')'; ++$i) {
            switch ($read[$i]) {
                case '"': $this->parseQuote($read, $i); break;
                case '{': $this->parseLiteral($read, $i); break;
                case '(': $this->parseProperties($read, $i); break;
                default: break;
            }
        }
        return $i;
    }

    /**
     * Function to fill the message structure in case the
     * bodystructure is not available
     * NOT FINISHED YET
     * @param string $read
     * @param string $type0 message part type
     * @param string $type1 message part subtype
     * @return string (only when type0 is not message or multipart)
     */
    function parseMessage($read, $type0, $type1) {
        switch ($type0) {
            case 'message':
                $rfc822_header = true;
                $mime_header = false;
                break;
            case 'multipart':
                $rfc822_header = false;
                $mime_header = true;
                break;
            default: return $read;
        }

        for ($i = 1; $i < $count; ++$i) {
            $line = trim($body[$i]);
            if (($mime_header || $rfc822_header) &&
                (preg_match("/^.*boundary=\"?(.+(?=\")|.+).*/i", $line, $reg))) {
                $bnd = $reg[1];
                $bndreg = $bnd;
                $bndreg = str_replace("\\", "\\\\", $bndreg);
                $bndreg = str_replace("?", "\\?", $bndreg);
                $bndreg = str_replace("+", "\\+", $bndreg);
                $bndreg = str_replace(".", "\\.", $bndreg);
                $bndreg = str_replace("/", "\\/", $bndreg);
                $bndreg = str_replace("-", "\\-", $bndreg);
                $bndreg = str_replace("(", "\\(", $bndreg);
                $bndreg = str_replace(")", "\\)", $bndreg);
            } else if ($rfc822_header && $line == '') {
                $rfc822_header = false;
                if ($msg->type0 == 'multipart') {
                    $mime_header = true;
                }
            }

            if ((($line[0] == '-') || $rfc822_header)  && isset($boundaries[0])) {
                $cnt = count($boundaries)-1;
                $bnd = $boundaries[$cnt]['bnd'];
                $bndreg = $boundaries[$cnt]['bndreg'];

                $regstr = '/^--'."($bndreg)".".*".'/';
                if (preg_match($regstr, $line, $reg)) {
                    $bndlen = strlen($reg[1]);
                    $bndend = false;
                    if (strlen($line) > ($bndlen + 3)) {
                        if (($line[$bndlen+2] == '-') && ($line[$bndlen+3] == '-')) {
                            $bndend = true;
                        }
                    }
                    if ($bndend) {
                        /* calc offset and return $msg */
                        //$entStr = CalcEntity("$entStr", -1);
                        array_pop($boundaries);
                        $mime_header = true;
                        $bnd_end = true;
                    } else {
                        $mime_header = true;
                         $bnd_end = false;
                        //$entStr = CalcEntity("$entStr", 0);
                        ++$content_indx;
                    }
                } else {
                    if ($header) { }
                }
            }
        }
    }

    /**
     * Searches a message for entities of the type(s) that
     * are specified in $preferred_entity_types
     *
     * Note that the caller can use this function to extract entities
     * that are not just "display entities"
     *
     * @param array $entity_ids Pre-seeded return value (list of entity
     *                          IDs), useful for the caller to search
     *                          cumulatively for different MIME types
     *                          (OPTIONAL; default is an empty list)
     * @param array $preferred_entity_types List of MIME types the
     *                                      caller will accept in
     *                                      return, in order of
     *                                      preference from low to
     *                                      high (last is most
     *                                      preferred)
     * @param boolean $strict When FALSE, if no appropriate entities
     *                        are found, allows the return of simple
     *                        text entities that are any of the types:
     *                        text/plain, text/html, text/message
     *                        (OPTIONAL; default is FALSE)
     *
     * @return array A list of entity IDs suiting the preferred MIME types
TODO: Is the order of the returned ID list any indication of preference?
     */
    function findDisplayEntity($entity_ids = array(),
                               $preferred_entity_types = array('text/plain', 'text/html'),
                               $strict=FALSE) {
        $found = FALSE;
        if ($this->type0 == 'multipart') {
            if ($this->type1 == 'alternative') {
                $preferred_entities = $this->findAlternativeEntities($preferred_entity_types);
                foreach ($preferred_entities as $entity) {
                    if (count($entity->entities) == 0) {
                        $entity_ids[] = $entity->entity_id;
                        $found = TRUE;
                        break;
                    } else {
                        $new_entity_ids = $entity->findDisplayEntity(array(), $preferred_entity_types, $strict);
                        if (!empty($new_entity_ids)) {
                            $entity_ids = array_merge($entity_ids, $new_entity_ids);
                            $found = TRUE;
                            break;
                        }
                    }
                }
//TODO: I'm not sure the intention of returning a list of entity IDs when the findAlternate above was supposed to only find one yet the findRelated below adds as many to the list as you find (and if there was supposed to be any preference order to the list). I'm leaving it alone for legacy reasons, but those things seem inconsistent
            } else if ($this->type1 == 'related') { /* RFC 2387 */
                $entities = $this->findRelatedEntities($preferred_entity_types);
                foreach ($entities as $entity) {
                    if (count($entity->entities) == 0) {
                        $entity_ids[] = $entity->entity_id;
                        $found = TRUE;
                    } else {
                        $new_entity_ids = $entity->findDisplayEntity(array(), $preferred_entity_types, $strict);
                        if (!empty($new_entity_ids)) {
                            $entity_ids = array_merge($entity_ids, $new_entity_ids);
                            $found = TRUE;
                        }
                    }
                }
            }

            // Treat as multipart/mixed
            //
            // This seems to only look for embedded message
            // entities (message/rfc822 MIME type) and inspect
            // them for a part of the desired type
            else {
                foreach ($this->entities as $ent) {
                    if(!(is_object($ent->header->disposition) && strtolower($ent->header->disposition->name) == 'attachment') &&
                            (!isset($ent->header->parameters['filename'])) &&
                            (!isset($ent->header->parameters['name'])) &&
                            (($ent->type0 != 'message') && ($ent->type1 != 'rfc822'))) {
                        $new_entity_ids = $ent->findDisplayEntity(array(), $preferred_entity_types, $strict);
                        if (!empty($new_entity_ids)) {
                            $entity_ids = array_merge($entity_ids, $new_entity_ids);
                            $found = TRUE;
                        }
                    }
                }
            }
        }

        // If not multipart, then just compare with each entry from $preferred_entity_types
        //
        else {
            $type = $this->type0.'/'.$this->type1;
//        $preferred_entity_types[] = "message/rfc822";
            foreach ($preferred_entity_types as $alt) {
                if( ($alt == $type) && isset($this->entity_id) ) {
                    if ((count($this->entities) == 0) &&
                            (!isset($this->header->parameters['filename'])) &&
                            (!isset($this->header->parameters['name'])) &&
                            (empty($this->header->disposition) || (isset($this->header->disposition) && is_object($this->header->disposition) &&
                             strtolower($this->header->disposition->name) != 'attachment'))) {
                        $entity_ids[] = $this->entity_id;
                        $found = TRUE;
                        break;
                    }
                }
            }
        }
        if(!$found) {
            // When nothing was found, search all message entities
            // *except* attached messages (message/rfc822 MIME type)
            // and inspect each for a part of the desired type
            foreach ($this->entities as $ent) {
                if(!(is_object($ent->header->disposition) && strtolower($ent->header->disposition->name) == 'attachment') &&
                   (($ent->type0 != 'message') && ($ent->type1 != 'rfc822'))) {
                    $new_entity_ids = $ent->findDisplayEntity(array(), $preferred_entity_types, $strict);
                    if (!empty($new_entity_ids)) {
                        $entity_ids = array_merge($entity_ids, $new_entity_ids);
                        $found = TRUE;
                    }
                }
            }
        }
        if(!$strict && !$found) {
            if ($this->type0 == 'text'
             && in_array($this->type1, array('plain', 'html', 'message'))
             && isset($this->entity_id)
             && count($this->entities) == 0
             && (!is_object($this->header->disposition)
              || strtolower($this->header->disposition->name) != 'attachment')) {
                $entity_ids[] = $this->entity_id;
            }
        }
        return $entity_ids;
    }

    /**
     * Iterates through the top-level entities in a message and
     * gathers ones that match one of the MIME types contained
     * in $preferred_entity_types, returning them in order of
     * preference based first on the preference order expressed
     * by $preferred_entity_types and then by order of appearance
     * (where those that appear last in the message are preferred)
     *
     * (Designed with multipart/alternative messages in mind)
     * (RFC 1341 specifies that the last part matching what we
     * accommodate is the one that should be used)
     *
     * So given a $preferred_entity_types of (text/plain, text/html),
     * the last found text/html part will be the first element in
     * the return array, but if the message has no text/html parts,
     * the last found text/plain part will be the top returned entity
     *
     * @param array $preferred_entity_types List of MIME types the
     *                                      caller will accept in
     *                                      return, in order of
     *                                      preference from low to
     *                                      high (last is most
     *                                      preferred)
     *
     * @return array An array of entities in order of preferrence
     *               as described above, where the first item is
     *               the most preferred entity, the last is least
     *               preferred. It is possible that the array could
     *               be returned as empty if no matches were found.
     *
     */
    function findAlternativeEntities($preferred_entity_types) {

        $preferred_entities = array();

        foreach ($preferred_entity_types as $preferred_type) {
            foreach($this->entities as $entity) {
                $entity_type = $entity->findEntityMIMEType($preferred_entity_types);
                if ($preferred_type == $entity_type)
                   $preferred_entities[] = $entity;
            }
        }

        return array_reverse($preferred_entities);
    }

    /**
     * Determine the type of an MIME entity
     *
     * If it is a multipart/related entity that doesn't report
     * the type it represents, we look at its child entities to
     * try and determine the correct type
     *
     * Note that if we have to examine child entities for a
     * multipart/related entity, we take the last entity that
     * matches any of our $preferred_entity_types since there
     * is no other way to know what to expect, and a message
     * with more than one, say, text types is b0rked and should
     * not be seen in the wild
     *
     * @param array $preferred_entity_types List of MIME types the
     *                                      caller is seeking out
     * @param string $default_related_type For multipart/related
     *                                     entities that don't report
     *                                     the type they represent
     *                                     and we can't find one of
     *                                     our preferred types inside
     *                                     of it, this will serve as
     *                                     the default type
     *                                     (OPTIONAL; default is "N/A")
     *
     * @return string The entity's MIME type
     *
     */
    function findEntityMIMEType($preferred_entity_types, $default_related_type='N/A') {
        $entity_type = $this->header->type0 . '/' . $this->header->type1;
        if ($entity_type == 'multipart/related') {
            $entity_type = $this->header->getParameter('type');
            if (!$entity_type) {

                // Default in case not found (earlier versions
                // defaulted to text/html, calling it a Mozilla bug,
                // though it's not clear what Mozilla product)
                $entity_type = $default_related_type;

                // Iterate child entities and take the last
                // one that matches our list of preferred types
                foreach($this->entities as $child_entity) {
                    $child_entity_type = $child_entity->header->type0 . '/' . $child_entity->header->type1;
                    if (in_array($child_entity_type, $preferred_entity_types)) {
                        $entity_type = $child_entity_type;
                        // Nah, don't stop at the first, take the
                        // last more in the spirit of RFC 1341
                        // break;
                    }
                }
            }
        }
        return $entity_type;
    }

    /**
     * Iterates through the top-level entities in a message and
     * gathers ones that match one the MIME type of the message
     * itself (designed with multipart/related messages in mind)
     *
     * @param array $preferred_entity_types List of MIME types the
     *                                      caller is seeking out
     *
     * @return array An array of all entities that match the
     *               message MIME type in order of appearance.
     *               It is possible that the array could be
     *               returned as empty if no matches were found.
     *
     */
    function findRelatedEntities($preferred_entity_types) {
        $entities = array();
        $entity_type = $this->findEntityMIMEType($preferred_entity_types);

        foreach ($this->entities as $entity) {
            if ($entity_type == $entity->header->type0 . '/' . $entity->header->type1)
                $entities[] = $entity;
        }

        return $entities;
    }

    /**
     * @param array $exclude_id
     * @param array $result
     * @return array
     */
    function getAttachments($exclude_id=array(), $result = array()) {
/*
        if (($this->type0 == 'message') &&
        ($this->type1 == 'rfc822') &&
        ($this->entity_id) ) {
            $this = $this->entities[0];
        }
*/
        if (count($this->entities)) {
            foreach ($this->entities as $entity) {
                $exclude = false;
                foreach ($exclude_id as $excl) {
                    if ($entity->entity_id === $excl) {
                        $exclude = true;
                    }
                }

                if (!$exclude) {
                    if ($entity->type0 == 'multipart') {
                        $result = $entity->getAttachments($exclude_id, $result);
                    } else if ($entity->type0 != 'multipart') {
                        $result[] = $entity;
                    }
                }
            }
        } else {
            $exclude = false;
            foreach ($exclude_id as $excl) {
                $exclude = $exclude || ($this->entity_id == $excl);
            }

            if (!$exclude) {
                $result[] = $this;
            }
        }
        return $result;
    }

    /**
     * Add attachment to message object
     * @param string $type attachment type
     * @param string $name attachment name
     * @param string $location path to attachment
     */
    function initAttachment($type, $name, $location) {
        $attachment = new Message();
        $mime_header = new MessageHeader();
        $mime_header->setParameter('name', $name);
        // FIXME: duplicate code. see ContentType class
        $pos = strpos($type, '/');
        if ($pos > 0) {
            $mime_header->type0 = substr($type, 0, $pos);
            $mime_header->type1 = substr($type, $pos+1);
        } else {
            $mime_header->type0 = $type;
        }
        $attachment->att_local_name = $location;
        $disposition = new Disposition('attachment');
        $disposition->properties['filename'] = $name;
        $mime_header->disposition = $disposition;
        $attachment->mime_header = $mime_header;
        $this->entities[]=$attachment;
    }

    /**
     * Delete all attachments from this object from disk.
     * @since 1.5.1
     */
    function purgeAttachments() {
        if ($this->att_local_name) {
            global $username, $attachment_dir;
            $hashed_attachment_dir = getHashedDir($username, $attachment_dir);
            if ( file_exists($hashed_attachment_dir . '/' . $this->att_local_name) ) {
                unlink($hashed_attachment_dir . '/' . $this->att_local_name);
            }
        }
        // recursively delete attachments from entities contained in this object
        for ($i=0, $entCount=count($this->entities);$i< $entCount; ++$i) {
            $this->entities[$i]->purgeAttachments();
        }
    }
}
