Force the addition of a file suffix to attachments that lack a filename (helps forwar...
[squirrelmail.git] / class / mime / Message.class.php
index 2db2a0f96610bb4c4cf6039e31df6ef935227b78..b6d0f221ecd593da2edfea57ed45c901540a4230 100644 (file)
 /**
  * Message.class.php
  *
- * Copyright (c) 2002 The SquirrelMail Project Team
- * Licensed under the GNU GPL. For full terms see the file COPYING.
+ * This file contains functions needed to handle mime messages.
  *
- * This contains functions needed to handle mime messages.
- *
- * $Id$
+ * @copyright 2003-2010 The SquirrelMail Project Team
+ * @license http://opensource.org/licenses/gpl-license.php GNU Public License
+ * @version $Id$
+ * @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 {
-    /** 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.
-    **/
-    var $rfc822_header = '',
-        $mime_header = '',
-        $flags = '',
-        $type0='',
-        $type1='',
-        $entities = array(),
-       $entity_id = '',
-        $parent_ent, $entity,
-        $parent = '', $decoded_body='',
-        $is_seen = 0, $is_answered = 0, $is_deleted = 0, $is_flagged = 0,
-        $is_mdnsent = 0,
-        $body_part = '',
-        $offset = 0,  /* for fetching body parts out of raw messages */
-        $length = 0,  /* for fetching body parts out of raw messages */
-       $att_local_name = ''; /* location where the tempory attachment
-                                is stored. For future usage in smtp.php */
+    /**
+     * 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 = '';
-        $filename = $this->header->getParameter('filename');
-        if (!$filename) {
-            $filename = $this->header->getParameter('name');
-        }
-
-        if (!$filename) {
-            $filename = 'untitled-'.$this->entity_id;
-        }
-        return $filename;
+         $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;
@@ -70,7 +212,7 @@ class Message {
             $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;
@@ -103,10 +245,17 @@ class Message {
         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 = '';
@@ -116,6 +265,9 @@ class Message {
         }
     }
 
+    /**
+     * @return string
+     */
     function getMailbox() {
         $msg = $this;
         while (is_object($msg->parent)) {
@@ -134,53 +286,71 @@ class Message {
      * 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
      */
     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';
-       }
+        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);
-           }
-           
-       }
+            $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)
+     */
     function parseBodyStructure($read, &$i, $sub_msg = '') {
         $arg_no = 0;
         $arg_a  = array();
-       if ($sub_msg) {
-           $message = $sub_msg;
-       } else {
-           $message = new Message();
-       }
-       $this = $message;    
+        if ($sub_msg) {
+            $message = $sub_msg;
+        } else {
+            $message = new Message();
+        }
+
         for ($cnt = strlen($read); $i < $cnt; ++$i) {
             $char = strtoupper($read{$i});
             switch ($char) {
@@ -192,34 +362,34 @@ class Message {
                                 $hdr = new MessageHeader();
                                 $hdr->type0 = 'text';
                                 $hdr->type1 = 'plain';
-                                $hdr->encoding = 'us-ascii';
+                                $hdr->encoding = '7bit';
                             } else {
                                 $msg->header->type0 = 'multipart';
                                 $msg->type0 = 'multipart';
                                 while ($read{$i} == '(') {
-                                    $msg->addEntity($this->parseBodyStructure($read, $i, $msg));
+                                    $msg->addEntity($msg->parseBodyStructure($read, $i, $msg));
                                 }
                             }
                             break;
                         case 1:
                             /* multipart properties */
                             ++$i;
-                            $arg_a[] = $this->parseProperties($read, $i);
+                            $arg_a[] = $msg->parseProperties($read, $i);
                             ++$arg_no;
                             break;
                         case 2:
                             if (isset($msg->type0) && ($msg->type0 == 'multipart')) {
                                 ++$i;
-                                $arg_a[] = $this->parseDisposition($read, $i);
+                                $arg_a[] = $msg->parseDisposition($read, $i);
                             } else { /* properties */
-                                $arg_a[] = $this->parseProperties($read, $i);
+                                $arg_a[] = $msg->parseProperties($read, $i);
                             }
                             ++$arg_no;
                             break;
                         case 3:
                             if (isset($msg->type0) && ($msg->type0 == 'multipart')) {
                                 ++$i;
-                                $arg_a[]= $this->parseLanguage($read, $i);
+                                $arg_a[]= $msg->parseLanguage($read, $i);
                             }
                         case 7:
                             if (($arg_a[0] == 'message') && ($arg_a[1] == 'rfc822')) {
@@ -228,40 +398,40 @@ class Message {
                                 $msg->type0 = $arg_a[0];
                                 $msg->type1 = $arg_a[1];
                                 $rfc822_hdr = new Rfc822Header();
-                                $msg->rfc822_header = $this->parseEnvelope($read, $i, $rfc822_hdr);
+                                $msg->rfc822_header = $msg->parseEnvelope($read, $i, $rfc822_hdr);
                                 while (($i < $cnt) && ($read{$i} != '(')) {
                                     ++$i;
                                 }
-                                $msg->addEntity($this->parseBodyStructure($read, $i,$msg));
+                                $msg->addEntity($msg->parseBodyStructure($read, $i,$msg));
                             }
                             break;
                         case 8:
                             ++$i;
-                            $arg_a[] = $this->parseDisposition($read, $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[] = $this->parseDisposition($read, $i);
+                                $arg_a[] = $msg->parseDisposition($read, $i);
                             } else {
-                                $arg_a[] = $this->parseLanguage($read, $i);
+                                $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[] = $this->parseLanguage($read, $i);
+                               $arg_a[] = $msg->parseLanguage($read, $i);
                            } else {
-                               $i = $this->parseParenthesis($read, $i);
+                               $i = $msg->parseParenthesis($read, $i);
                                $arg_a[] = ''; /* not yet described in rfc2060 */
                            }
                            ++$arg_no;
                            break;
                        default:
                            /* unknown argument, skip this part */
-                           $i = $this->parseParenthesis($read, $i);
+                           $i = $msg->parseParenthesis($read, $i);
                            $arg_a[] = '';
                            ++$arg_no;
                            break;
@@ -269,7 +439,7 @@ class Message {
                    break;
                 case '"':
                     /* inside an entity -> start processing */
-                    $arg_s = $this->parseQuote($read, $i);
+                    $arg_s = $msg->parseQuote($read, $i);
                     ++$arg_no;
                     if ($arg_no < 3) {
                         $arg_s = strtolower($arg_s); /* type0 and type1 */
@@ -288,20 +458,20 @@ class Message {
                     break;
                 case '{':
                     /* process the literal value */
-                    $arg_s = $this->parseLiteral($read, $i);
+                    $arg_a[] = $msg->parseLiteral($read, $i);
                     ++$arg_no;
                     break;
-               case '0':    
+        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;
-                   }
+            ++$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'));
@@ -347,6 +517,11 @@ class Message {
         } /* for */
     } /* parsestructure */
 
+    /**
+     * @param string $read
+     * @param integer $i
+     * @return array
+     */
     function parseProperties($read, &$i) {
         $properties = array();
         $prop_name = '';
@@ -369,9 +544,79 @@ class Message {
                 }
             }
         }
+        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();
@@ -385,9 +630,9 @@ class Message {
                     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;
+            /* 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':
@@ -438,55 +683,123 @@ class Message {
 
         if (count($arg_a) > 9) {
             $d = strtr($arg_a[0], array('  ' => ' '));
-            $d = explode(' ', $d);
-           if (!$arg_a[1]) $arg_1[1] = _("(no subject)");          
+            $d_parts = explode(' ', $d);
+            if (!$arg_a[1]) $arg_a[1] = _("(no subject)");
 
-            $hdr->date = getTimeStamp($d); /* argument 1: date */
+            $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 = $arg_a[2][0];     /* argument 3: from        */
-            $hdr->sender = $arg_a[3][0];   /* argument 4: sender      */
-            $hdr->replyto = $arg_a[4][0];  /* argument 5: reply-to    */
+            $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->inreplyto = $arg_a[8];   /* argument 9: in-reply-to */
+            $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;
-       } else { /* should never happen */
-          $i += 3; /* } + \r + \n */
-          $s = '';
-       }
+        ++$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;
-       while (true) {
-          $iPos = strpos($read,'"',$iPos);
-          if ($iPos === false) break;
-          if ($iPos && $read{$iPos -1} != '\\') {
-             $s = substr($read,$i,($iPos-$i));
-             $i = $iPos;
-             break;
-          }
-       }
+        $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) {
@@ -517,6 +830,11 @@ class Message {
         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) {
@@ -534,10 +852,14 @@ class Message {
                 $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();
@@ -557,12 +879,17 @@ class Message {
                 $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 (; $read{$i} != ')'; ++$i) {
+        for ($i++; $read{$i} != ')'; ++$i) {
             switch ($read{$i}) {
                 case '"': $this->parseQuote($read, $i); break;
                 case '{': $this->parseLiteral($read, $i); break;
@@ -573,8 +900,15 @@ class Message {
         return $i;
     }
 
-    /* Function to fill the message structure in case the */
-    /* bodystructure is not available NOT FINISHED YET    */
+    /**
+     * 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':
@@ -642,17 +976,25 @@ class Message {
         }
     }
 
+    /**
+     * @param array $entity
+     * @param array $alt_order
+     * @param boolean $strict
+     * @return array
+     */
     function findDisplayEntity($entity = array(), $alt_order = array('text/plain', 'text/html'), $strict=false) {
         $found = false;
         if ($this->type0 == 'multipart') {
             if($this->type1 == 'alternative') {
                 $msg = $this->findAlternativeEntity($alt_order);
-                if (count($msg->entities) == 0) {
-                    $entity[] = $msg->entity_id;
-                } else {
-                    $entity = $msg->findDisplayEntity($entity, $alt_order, $strict);
+                if ( ! is_null($msg) ) {
+                    if (count($msg->entities) == 0) {
+                        $entity[] = $msg->entity_id;
+                    } else {
+                        $entity = $msg->findDisplayEntity($entity, $alt_order, $strict);
+                    }
+                    $found = true;
                 }
-                $found = true;
             } else if ($this->type1 == 'related') { /* RFC 2387 */
                 $msgs = $this->findRelatedEntity();
                 foreach ($msgs as $msg) {
@@ -667,8 +1009,10 @@ class Message {
                 }
             } else { /* Treat as multipart/mixed */
                 foreach ($this->entities as $ent) {
-                    if((strtolower($ent->header->disposition->name) != 'attachment') &&
-                       (($ent->type0 != 'message') && ($ent->type1 != 'rfc822'))) {
+                    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'))) {
                         $entity = $ent->findDisplayEntity($entity, $alt_order, $strict);
                         $found = true;
                     }
@@ -676,11 +1020,14 @@ class Message {
             }
         } else { /* If not multipart, then just compare with each entry from $alt_order */
             $type = $this->type0.'/'.$this->type1;
-//         $alt_order[] = "message/rfc822";
+//        $alt_order[] = "message/rfc822";
             foreach ($alt_order as $alt) {
                 if( ($alt == $type) && isset($this->entity_id) ) {
-                    if ((count($this->entities) == 0) && 
-                        (strtolower($this->header->disposition->name) != 'attachment')) {
+                    if ((count($this->entities) == 0) &&
+                            (!isset($this->header->parameters['filename'])) &&
+                            (!isset($this->header->parameters['name'])) &&
+                            isset($this->header->disposition) && is_object($this->header->disposition) &&
+                            !(is_object($this->header->disposition) && strtolower($this->header->disposition->name) == 'attachment')) {
                         $entity[] = $this->entity_id;
                         $found = true;
                     }
@@ -689,20 +1036,19 @@ class Message {
         }
         if(!$found) {
             foreach ($this->entities as $ent) {
-                if((strtolower($ent->header->disposition->name) != 'attachment') &&
+                if(!(is_object($ent->header->disposition) && strtolower($ent->header->disposition->name) == 'attachment') &&
                    (($ent->type0 != 'message') && ($ent->type1 != 'rfc822'))) {
                     $entity = $ent->findDisplayEntity($entity, $alt_order, $strict);
                     $found = true;
                 }
             }
-           
         }
         if(!$strict && !$found) {
             if (($this->type0 == 'text') &&
                 in_array($this->type1, array('plain', 'html', 'message')) &&
                 isset($this->entity_id)) {
                 if (count($this->entities) == 0) {
-                    if (strtolower($this->header->disposition->name) != 'attachment') {
+                    if (!is_object($this->header->disposition) || strtolower($this->header->disposition->name) != 'attachment') {
                         $entity[] = $this->entity_id;
                     }
                 }
@@ -711,15 +1057,21 @@ class Message {
         return $entity;
     }
 
+    /**
+     * @param array $alt_order
+     * @return entity
+     */
     function findAlternativeEntity($alt_order) {
         /* If we are dealing with alternative parts then we  */
         /* choose the best viewable message supported by SM. */
         $best_view = 0;
-        $entity = array();
+        $entity = null;
         foreach($this->entities as $ent) {
             $type = $ent->header->type0 . '/' . $ent->header->type1;
             if ($type == 'multipart/related') {
                 $type = $ent->header->getParameter('type');
+                // Mozilla bug. Mozilla does not provide the parameter type.
+                if (!$type) $type = 'text/html';
             }
             $altCount = count($alt_order);
             for ($j = $best_view; $j < $altCount; ++$j) {
@@ -729,29 +1081,37 @@ class Message {
                 }
             }
         }
-
         return $entity;
     }
 
+    /**
+     * @return array
+     */
     function findRelatedEntity() {
         $msgs = array();
-
+        $related_type = $this->header->getParameter('type');
+        // Mozilla bug. Mozilla does not provide the parameter type.
+        if (!$related_type) $related_type = 'text/html';
         $entCount = count($this->entities);
         for ($i = 0; $i < $entCount; ++$i) {
             $type = $this->entities[$i]->header->type0.'/'.$this->entities[$i]->header->type1;
-            if ($this->header->getParameter('type') == $type) {
+            if ($related_type == $type) {
                 $msgs[] = $this->entities[$i];
             }
         }
-
         return $msgs;
     }
 
+    /**
+     * @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) ) {
+        if (($this->type0 == 'message') &&
+        ($this->type1 == 'rfc822') &&
+        ($this->entity_id) ) {
             $this = $this->entities[0];
         }
 */
@@ -765,8 +1125,7 @@ class Message {
                 }
 
                 if (!$exclude) {
-                    if (($entity->type0 == 'multipart') &&
-                        ($entity->type1 != 'related')) {
+                    if ($entity->type0 == 'multipart') {
                         $result = $entity->getAttachments($exclude_id, $result);
                     } else if ($entity->type0 != 'multipart') {
                         $result[] = $entity;
@@ -785,25 +1144,48 @@ class Message {
         }
         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);
-       $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;
+        // 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();
+        }
+    }
+}