X-Git-Url: https://vcs.fsf.org/?p=squirrelmail.git;a=blobdiff_plain;f=class%2Fmime%2FMessage.class.php;h=f53e2a5730869ef42ae20bb6fae550f2f83bbe82;hp=fbfbd3b5dcab22f3557ac70d94e21ca27a341b47;hb=701e7beed3baca980039f978c6d536dd91cae775;hpb=91e0dccca7b2452d8b450791cae3aa4125e8889e diff --git a/class/mime/Message.class.php b/class/mime/Message.class.php index fbfbd3b5..f53e2a57 100644 --- a/class/mime/Message.class.php +++ b/class/mime/Message.class.php @@ -3,68 +3,206 @@ /** * Message.class.php * - * Copyright (c) 2003-2004 The SquirrelMail Project Team - * Licensed under the GNU GPL. For full terms see the file COPYING. - * - * This contains functions needed to handle mime messages. + * This file contains functions needed to handle mime messages. * + * @copyright 2003-2014 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 + * 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 */ class Message { - 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 = $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; @@ -107,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 = ''; @@ -120,6 +265,9 @@ class Message { } } + /** + * @return string + */ function getMailbox() { $msg = $this; while (is_object($msg->parent)) { @@ -138,7 +286,11 @@ 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); @@ -146,6 +298,13 @@ class Message { 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) { @@ -175,6 +334,14 @@ class Message { } } + /** + * @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(); @@ -195,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'; @@ -294,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')); @@ -350,6 +518,11 @@ class Message { } /* for */ } /* parsestructure */ + /** + * @param string $read + * @param integer $i + * @return array + */ function parseProperties($read, &$i) { $properties = array(); $prop_name = ''; @@ -372,9 +545,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(); @@ -441,23 +684,30 @@ 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; } + /** + * @param string $read + * @param integer $i + * @return string + * @todo document me + */ function parseLiteral($read, &$i) { $lit_cnt = ''; ++$i; @@ -478,9 +728,26 @@ class Message { 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; @@ -488,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)) { @@ -497,6 +796,11 @@ class Message { return $s; } + /** + * @param string $read + * @param integer $i + * @return object AddressStructure object + */ function parseAddress($read, &$i) { $arg_a = array(); for (; $read{$i} != ')'; ++$i) { @@ -527,6 +831,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) { @@ -544,10 +853,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(); @@ -567,12 +880,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; @@ -583,8 +901,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': @@ -652,17 +977,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) { @@ -677,10 +1010,10 @@ class Message { } } else { /* Treat as multipart/mixed */ foreach ($this->entities as $ent) { - if((strtolower($ent->header->disposition->name) != 'attachment') && - (!isset($ent->header->parameters['filename'])) && - (!isset($ent->header->parameters['name'])) && - (($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; } @@ -692,9 +1025,10 @@ class Message { foreach ($alt_order as $alt) { if( ($alt == $type) && isset($this->entity_id) ) { if ((count($this->entities) == 0) && - (!isset($ent->header->parameters['filename'])) && - (!isset($ent->header->parameters['name'])) && - (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; } @@ -703,7 +1037,7 @@ 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; @@ -715,7 +1049,7 @@ class Message { 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; } } @@ -724,17 +1058,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'; + // 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) { @@ -744,10 +1082,12 @@ class Message { } } } - return $entity; } + /** + * @return array + */ function findRelatedEntity() { $msgs = array(); $related_type = $this->header->getParameter('type'); @@ -763,6 +1103,11 @@ class Message { return $msgs; } + /** + * @param array $exclude_id + * @param array $result + * @return array + */ function getAttachments($exclude_id=array(), $result = array()) { /* if (($this->type0 == 'message') && @@ -781,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; @@ -802,10 +1146,17 @@ 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); + // FIXME: duplicate code. see ContentType class $pos = strpos($type, '/'); if ($pos > 0) { $mime_header->type0 = substr($type, 0, $pos); @@ -820,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(); + } + } +}