Happy 2014
[squirrelmail.git] / class / mime / Message.class.php
index 2d54b359de051da99f1b6f9d8c9bf18b6ee4d68f..f53e2a5730869ef42ae20bb6fae550f2f83bbe82 100644 (file)
@@ -5,7 +5,7 @@
  *
  * This file contains functions needed to handle mime messages.
  *
- * @copyright © 2003-2005 The SquirrelMail Project Team
+ * @copyright 2003-2014 The SquirrelMail Project Team
  * @license http://opensource.org/licenses/gpl-license.php GNU Public License
  * @version $Id$
  * @package squirrelmail
  */
 
 /**
- * The object that contains a message
+ * 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.
+ * 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
@@ -86,6 +86,11 @@ class Message {
      * @var boolean
      */
     var $is_answered = 0;
+    /**
+     * Message forward status
+     * @var boolean
+     */
+    var $is_forwarded = 0;
     /**
      * Message \deleted status
      * @var boolean
@@ -119,9 +124,8 @@ class Message {
      */
     var $length = 0;
     /**
-     * Local attachment filename
-     * location where the tempory attachment
-     * is stored. For use in delivery class.
+     * Local attachment filename location where the tempory attachment is
+     * stored. For use in delivery class.
      * @var string
      */
     var $att_local_name = '';
@@ -157,9 +161,9 @@ class Message {
                       $name = $header->getParameter('name');
                       if(!trim($name)) {
                           if (!trim( $header->id )) {
-                              $filename = 'untitled-[' . $this->entity_id . ']' ;
+                              $filename = 'untitled-[' . $this->entity_id . ']' . '.' . strtolower($header->type1);
                           } else {
-                              $filename = 'cid: ' . $header->id;
+                              $filename = 'cid: ' . $header->id . '.' . strtolower($header->type1);
                           }
                       } else {
                           $filename = $name;
@@ -174,9 +178,9 @@ class Message {
                   $filename = $header->getParameter('name');
                   if (!trim($filename)) {
                       if (!trim( $header->id )) {
-                          $filename = 'untitled-[' . $this->entity_id . ']' ;
+                          $filename = 'untitled-[' . $this->entity_id . ']' . '.' . strtolower($header->type1);
                       } else {
-                          $filename = 'cid: ' . $header->id;
+                          $filename = 'cid: ' . $header->id . '.' . strtolower($header->type1);
                       }
                   }
               }
@@ -358,7 +362,8 @@ class Message {
                                 $hdr = new MessageHeader();
                                 $hdr->type0 = 'text';
                                 $hdr->type1 = 'plain';
-                                $hdr->encoding = 'us-ascii';
+                                $hdr->encoding = '7bit';
+                                $msg->header = $hdr;
                             } else {
                                 $msg->header->type0 = 'multipart';
                                 $msg->type0 = 'multipart';
@@ -457,17 +462,17 @@ class Message {
                     $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'));
@@ -540,6 +545,70 @@ 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;
     }
 
@@ -615,18 +684,19 @@ class Message {
 
         if (count($arg_a) > 9) {
             $d = strtr($arg_a[0], array('  ' => ' '));
-            $d = explode(' ', $d);
-        if (!$arg_a[1]) $arg_a[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 = 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->replyto = is_array($arg_a[4]) ? $arg_a[4][0] : '';  /* argument 5: reply-to    */
+            $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;
@@ -659,14 +729,25 @@ class Message {
     }
 
     /**
+     * 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
-     * @return string
-     * @todo document me
+     * @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;
@@ -674,6 +755,38 @@ class Message {
                 $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)) {
@@ -777,7 +890,7 @@ class Message {
      * @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;
@@ -875,12 +988,14 @@ class Message {
         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) {
@@ -896,9 +1011,9 @@ class Message {
             } else { /* Treat as multipart/mixed */
                 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'))) {
+                            (!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;
                     }
@@ -910,10 +1025,10 @@ class Message {
             foreach ($alt_order as $alt) {
                 if( ($alt == $type) && isset($this->entity_id) ) {
                     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')) {
+                            (!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;
                     }
@@ -945,19 +1060,19 @@ class Message {
 
     /**
      * @param array $alt_order
-     * @return array
+     * @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';
+                // 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) {
@@ -1011,8 +1126,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;
@@ -1057,6 +1171,22 @@ class Message {
         $attachment->mime_header = $mime_header;
         $this->entities[]=$attachment;
     }
-}
 
-?>
\ No newline at end of file
+    /**
+     * 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();
+        }
+    }
+}