/** * Build message part object * * @param array $part * @param int $count * @param string $parent */ protected function structure_part($part, $count = 0, $parent = '', $mime_headers = null) { $struct = new rcube_message_part(); $struct->mime_id = empty($parent) ? (string) $count : "{$parent}.{$count}"; // multipart if (is_array($part[0])) { $struct->ctype_primary = 'multipart'; /* RFC3501: BODYSTRUCTURE fields of multipart part part1 array part2 array part3 array .... 1. subtype 2. parameters (optional) 3. description (optional) 4. language (optional) 5. location (optional) */ // find first non-array entry for ($i = 1; $i < count($part); $i++) { if (!is_array($part[$i])) { $struct->ctype_secondary = strtolower($part[$i]); // read content type parameters if (is_array($part[$i + 1])) { $struct->ctype_parameters = array(); for ($j = 0; $j < count($part[$i + 1]); $j += 2) { $param = strtolower($part[$i + 1][$j]); $struct->ctype_parameters[$param] = $part[$i + 1][$j + 1]; } } break; } } $struct->mimetype = 'multipart/' . $struct->ctype_secondary; // build parts list for headers pre-fetching for ($i = 0; $i < count($part); $i++) { if (!is_array($part[$i])) { break; } // fetch message headers if message/rfc822 // or named part (could contain Content-Location header) if (!is_array($part[$i][0])) { $tmp_part_id = $struct->mime_id ? $struct->mime_id . '.' . ($i + 1) : $i + 1; if (strtolower($part[$i][0]) == 'message' && strtolower($part[$i][1]) == 'rfc822') { $mime_part_headers[] = $tmp_part_id; } else { if (in_array('name', (array) $part[$i][2]) && empty($part[$i][3])) { $mime_part_headers[] = $tmp_part_id; } } } } // pre-fetch headers of all parts (in one command for better performance) // @TODO: we could do this before _structure_part() call, to fetch // headers for parts on all levels if ($mime_part_headers) { $mime_part_headers = $this->conn->fetchMIMEHeaders($this->folder, $this->msg_uid, $mime_part_headers); } $struct->parts = array(); for ($i = 0, $count = 0; $i < count($part); $i++) { if (!is_array($part[$i])) { break; } $tmp_part_id = $struct->mime_id ? $struct->mime_id . '.' . ($i + 1) : $i + 1; $struct->parts[] = $this->structure_part($part[$i], ++$count, $struct->mime_id, $mime_part_headers[$tmp_part_id]); } return $struct; } /* RFC3501: BODYSTRUCTURE fields of non-multipart part 0. type 1. subtype 2. parameters 3. id 4. description 5. encoding 6. size -- text 7. lines -- message/rfc822 7. envelope structure 8. body structure 9. lines -- x. md5 (optional) x. disposition (optional) x. language (optional) x. location (optional) */ // regular part $struct->ctype_primary = strtolower($part[0]); $struct->ctype_secondary = strtolower($part[1]); $struct->mimetype = $struct->ctype_primary . '/' . $struct->ctype_secondary; // read content type parameters if (is_array($part[2])) { $struct->ctype_parameters = array(); for ($i = 0; $i < count($part[2]); $i += 2) { $struct->ctype_parameters[strtolower($part[2][$i])] = $part[2][$i + 1]; } if (isset($struct->ctype_parameters['charset'])) { $struct->charset = $struct->ctype_parameters['charset']; } } // #1487700: workaround for lack of charset in malformed structure if (empty($struct->charset) && !empty($mime_headers) && $mime_headers->charset) { $struct->charset = $mime_headers->charset; } // read content encoding if (!empty($part[5])) { $struct->encoding = strtolower($part[5]); $struct->headers['content-transfer-encoding'] = $struct->encoding; } // get part size if (!empty($part[6])) { $struct->size = intval($part[6]); } // read part disposition $di = 8; if ($struct->ctype_primary == 'text') { $di += 1; } else { if ($struct->mimetype == 'message/rfc822') { $di += 3; } } if (is_array($part[$di]) && count($part[$di]) == 2) { $struct->disposition = strtolower($part[$di][0]); if (is_array($part[$di][1])) { for ($n = 0; $n < count($part[$di][1]); $n += 2) { $struct->d_parameters[strtolower($part[$di][1][$n])] = $part[$di][1][$n + 1]; } } } // get message/rfc822's child-parts if (is_array($part[8]) && $di != 8) { $struct->parts = array(); for ($i = 0, $count = 0; $i < count($part[8]); $i++) { if (!is_array($part[8][$i])) { break; } $struct->parts[] = $this->structure_part($part[8][$i], ++$count, $struct->mime_id); } } // get part ID if (!empty($part[3])) { $struct->content_id = $part[3]; $struct->headers['content-id'] = $part[3]; if (empty($struct->disposition)) { $struct->disposition = 'inline'; } } // fetch message headers if message/rfc822 or named part (could contain Content-Location header) if ($struct->ctype_primary == 'message' || $struct->ctype_parameters['name'] && !$struct->content_id) { if (empty($mime_headers)) { $mime_headers = $this->conn->fetchPartHeader($this->folder, $this->msg_uid, true, $struct->mime_id); } if (is_string($mime_headers)) { $struct->headers = rcube_mime::parse_headers($mime_headers) + $struct->headers; } else { if (is_object($mime_headers)) { $struct->headers = get_object_vars($mime_headers) + $struct->headers; } } // get real content-type of message/rfc822 if ($struct->mimetype == 'message/rfc822') { // single-part if (!is_array($part[8][0])) { $struct->real_mimetype = strtolower($part[8][0] . '/' . $part[8][1]); } else { for ($n = 0; $n < count($part[8]); $n++) { if (!is_array($part[8][$n])) { break; } } $struct->real_mimetype = 'multipart/' . strtolower($part[8][$n]); } } if ($struct->ctype_primary == 'message' && empty($struct->parts)) { if (is_array($part[8]) && $di != 8) { $struct->parts[] = $this->structure_part($part[8], ++$count, $struct->mime_id); } } } // normalize filename property $this->set_part_filename($struct, $mime_headers); return $struct; }
/** * Read the message structure returend by the IMAP server * and build flat lists of content parts and attachments * * @param rcube_message_part $structure Message structure node * @param bool $recursive True when called recursively */ private function parse_structure($structure, $recursive = false) { // real content-type of message/rfc822 part if ($structure->mimetype == 'message/rfc822' && $structure->real_mimetype) { $mimetype = $structure->real_mimetype; // parse headers from message/rfc822 part if (!isset($structure->headers['subject']) && !isset($structure->headers['from'])) { list($headers, ) = explode("\r\n\r\n", $this->get_part_body($structure->mime_id, false, 32768)); $structure->headers = rcube_mime::parse_headers($headers); } } else { $mimetype = $structure->mimetype; } // show message headers if ($recursive && is_array($structure->headers) && (isset($structure->headers['subject']) || $structure->headers['from'] || $structure->headers['to'])) { $c = new stdClass(); $c->type = 'headers'; $c->headers = $structure->headers; $this->parts[] = $c; } // Allow plugins to handle message parts $plugin = $this->app->plugins->exec_hook('message_part_structure', array('object' => $this, 'structure' => $structure, 'mimetype' => $mimetype, 'recursive' => $recursive)); if ($plugin['abort']) { return; } $structure = $plugin['structure']; list($message_ctype_primary, $message_ctype_secondary) = explode('/', $plugin['mimetype']); // print body if message doesn't have multiple parts if ($message_ctype_primary == 'text' && !$recursive) { // parts with unsupported type add to attachments list if (!in_array($message_ctype_secondary, array('plain', 'html', 'enriched'))) { $this->attachments[] = $structure; return; } $structure->type = 'content'; $this->parts[] = $structure; // Parse simple (plain text) message body if ($message_ctype_secondary == 'plain') { foreach ((array) $this->uu_decode($structure) as $uupart) { $this->mime_parts[$uupart->mime_id] = $uupart; $this->attachments[] = $uupart; } } } else { if ($mimetype == 'application/pgp' && !$recursive) { $structure->type = 'content'; $this->parts[] = $structure; } else { if ($mimetype == 'multipart/alternative' && is_array($structure->parts) && count($structure->parts) > 1) { // get html/plaintext parts, other add to attachments list foreach ($structure->parts as $p => $sub_part) { $sub_mimetype = $sub_part->mimetype; $is_multipart = preg_match('/^multipart\\/(related|relative|mixed|alternative)/', $sub_mimetype); // skip empty text parts if (!$sub_part->size && !$is_multipart) { continue; } // We've encountered (malformed) messages with more than // one text/plain or text/html part here. There's no way to choose // which one is better, so we'll display first of them and add // others as attachments (#1489358) // check if sub part is if ($is_multipart) { $related_part = $p; } else { if ($sub_mimetype == 'text/plain' && !$plain_part) { $plain_part = $p; } else { if ($sub_mimetype == 'text/html' && !$html_part) { $html_part = $p; $this->got_html_part = true; } else { if ($sub_mimetype == 'text/enriched' && !$enriched_part) { $enriched_part = $p; } else { // add unsupported/unrecognized parts to attachments list $this->attachments[] = $sub_part; } } } } } // parse related part (alternative part could be in here) if ($related_part !== null && !$this->parse_alternative) { $this->parse_alternative = true; $this->parse_structure($structure->parts[$related_part], true); $this->parse_alternative = false; // if plain part was found, we should unset it if html is preferred if ($this->opt['prefer_html'] && count($this->parts)) { $plain_part = null; } } // choose html/plain part to print if ($html_part !== null && $this->opt['prefer_html']) { $print_part = $structure->parts[$html_part]; } else { if ($enriched_part !== null) { $print_part = $structure->parts[$enriched_part]; } else { if ($plain_part !== null) { $print_part = $structure->parts[$plain_part]; } } } // add the right message body if (is_object($print_part)) { $print_part->type = 'content'; $this->parts[] = $print_part; } else { if ($html_part !== null && empty($this->parts)) { $c = new stdClass(); $c->type = 'content'; $c->ctype_primary = 'text'; $c->ctype_secondary = 'plain'; $c->mimetype = 'text/plain'; $c->realtype = 'text/html'; $this->parts[] = $c; } } } else { if ($mimetype == 'multipart/encrypted') { $p = new stdClass(); $p->type = 'content'; $p->ctype_primary = 'text'; $p->ctype_secondary = 'plain'; $p->mimetype = 'text/plain'; $p->realtype = 'multipart/encrypted'; $p->mime_id = $structure->mime_id; $this->parts[] = $p; // add encrypted payload part as attachment if (is_array($structure->parts)) { for ($i = 0; $i < count($structure->parts); $i++) { $subpart = $structure->parts[$i]; if ($subpart->mimetype == 'application/octet-stream' || !empty($subpart->filename)) { $this->attachments[] = $subpart; } } } } else { if ($mimetype == 'application/pkcs7-mime') { $p = new stdClass(); $p->type = 'content'; $p->ctype_primary = 'text'; $p->ctype_secondary = 'plain'; $p->mimetype = 'text/plain'; $p->realtype = 'application/pkcs7-mime'; $p->mime_id = $structure->mime_id; $this->parts[] = $p; if (!empty($structure->filename)) { $this->attachments[] = $structure; } } else { if (is_array($structure->parts) && !empty($structure->parts)) { // iterate over parts for ($i = 0; $i < count($structure->parts); $i++) { $mail_part =& $structure->parts[$i]; $primary_type = $mail_part->ctype_primary; $secondary_type = $mail_part->ctype_secondary; // real content-type of message/rfc822 if ($mail_part->real_mimetype) { $part_orig_mimetype = $mail_part->mimetype; $part_mimetype = $mail_part->real_mimetype; list($primary_type, $secondary_type) = explode('/', $part_mimetype); } else { $part_mimetype = $part_orig_mimetype = $mail_part->mimetype; } // multipart/alternative if ($primary_type == 'multipart') { $this->parse_structure($mail_part, true); // list message/rfc822 as attachment as well (mostly .eml) if ($part_orig_mimetype == 'message/rfc822' && !empty($mail_part->filename)) { $this->attachments[] = $mail_part; } } else { if (($part_mimetype == 'text/plain' || $part_mimetype == 'text/html') && $mail_part->disposition != 'attachment' || in_array($part_mimetype, array('message/delivery-status', 'text/rfc822-headers', 'message/disposition-notification'))) { // Allow plugins to handle also this part $plugin = $this->app->plugins->exec_hook('message_part_structure', array('object' => $this, 'structure' => $mail_part, 'mimetype' => $part_mimetype, 'recursive' => true)); if ($plugin['abort']) { continue; } if ($part_mimetype == 'text/html' && $mail_part->size) { $this->got_html_part = true; } $mail_part = $plugin['structure']; list($primary_type, $secondary_type) = explode('/', $plugin['mimetype']); // add text part if it matches the prefs if (!$this->parse_alternative || $secondary_type == 'html' && $this->opt['prefer_html'] || $secondary_type == 'plain' && !$this->opt['prefer_html']) { $mail_part->type = 'content'; $this->parts[] = $mail_part; } // list as attachment as well if (!empty($mail_part->filename)) { $this->attachments[] = $mail_part; } } else { if ($primary_type == 'message') { $this->parse_structure($mail_part, true); // list as attachment as well (mostly .eml) if (!empty($mail_part->filename)) { $this->attachments[] = $mail_part; } } else { if ($primary_type == 'protocol') { continue; } else { if ($part_mimetype == 'application/ms-tnef') { foreach ((array) $this->tnef_decode($mail_part) as $tpart) { $this->mime_parts[$tpart->mime_id] = $tpart; $this->attachments[] = $tpart; } } else { if (preg_match('/^(inline|attach)/', $mail_part->disposition) || $mail_part->headers['content-id'] || $mail_part->filename && (empty($mail_part->disposition) || preg_match('/^[a-z0-9!#$&.+^_-]+$/i', $mail_part->disposition))) { // skip apple resource forks if ($message_ctype_secondary == 'appledouble' && $secondary_type == 'applefile') { continue; } // part belongs to a related message and is linked if (preg_match('/^multipart\\/(related|relative)/', $mimetype) && ($mail_part->headers['content-id'] || $mail_part->headers['content-location'])) { if ($mail_part->headers['content-id']) { $mail_part->content_id = preg_replace(array('/^</', '/>$/'), '', $mail_part->headers['content-id']); } if ($mail_part->headers['content-location']) { $mail_part->content_location = $mail_part->headers['content-base'] . $mail_part->headers['content-location']; } $this->inline_parts[] = $mail_part; } else { if ($part_orig_mimetype == 'message/rfc822') { $this->parse_structure($mail_part, true); // list as attachment as well (mostly .eml) if (!empty($mail_part->filename)) { $this->attachments[] = $mail_part; } } else { if (preg_match('/^[a-z0-9!#$&.+^_-]+\\/[a-z0-9!#$&.+^_-]+$/i', $part_mimetype)) { $this->attachments[] = $mail_part; } else { if ($mail_part->filename) { $mail_part->ctype_primary = 'application'; $mail_part->ctype_secondary = 'octet-stream'; $mail_part->mimetype = 'application/octet-stream'; $this->attachments[] = $mail_part; } } } } } else { if ($mail_part->mimetype == 'message/rfc822') { $this->parse_structure($mail_part); } else { if ($part_mimetype == 'text/calendar') { if (!$mail_part->filename) { $mail_part->filename = 'calendar.ics'; } $this->attachments[] = $mail_part; } } } } } } } } } // if this was a related part try to resolve references if (preg_match('/^multipart\\/(related|relative)/', $mimetype) && sizeof($this->inline_parts)) { $a_replaces = array(); $img_regexp = '/^image\\/(gif|jpe?g|png|tiff|bmp|svg)/'; foreach ($this->inline_parts as $inline_object) { $part_url = $this->get_part_url($inline_object->mime_id, $inline_object->ctype_primary); if (isset($inline_object->content_id)) { $a_replaces['cid:' . $inline_object->content_id] = $part_url; } if ($inline_object->content_location) { $a_replaces[$inline_object->content_location] = $part_url; } if (!empty($inline_object->filename)) { // MS Outlook sends sometimes non-related attachments as related // In this case multipart/related message has only one text part // We'll add all such attachments to the attachments list if (!isset($this->got_html_part)) { $this->attachments[] = $inline_object; } else { if (!preg_match($img_regexp, $inline_object->mimetype)) { $this->attachments[] = $inline_object; } } // @TODO: we should fetch HTML body and find attachment's content-id // to handle also image attachments without reference in the body // @TODO: should we list all image attachments in text mode? } } // add replace array to each content part // (will be applied later when part body is available) foreach ($this->parts as $i => $part) { if ($part->type == 'content') { $this->parts[$i]->replaces = $a_replaces; } } } } else { if ($structure->filename) { $this->attachments[] = $structure; } else { if (preg_match('/application\\//i', $mimetype)) { $this->attachments[] = $structure; } } } } } } } } }