From 8f9f1f12cd58860841b48b366995a463d51fbc0f Mon Sep 17 00:00:00 2001 From: Pablo Zmdl <57864086+pabzm@users.noreply.github.com> Date: Sun, 21 Jul 2024 13:12:57 +0200 Subject: [PATCH] Filter "real" attachments by being referenced (#9472) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Filter "real" attachments by being referenced This changes the way in which attachments are determined to be shown as such ("standalone"), or not ("inline"). In theory this should be determined by their Content-Disposition, but in reality this often doesn't work. Now we check if the Content-ID or Content-Location of the attachment is actually being used in other parts of the message. If not, the attachment is considered to be "standalone". * Consider all mime-parts to check if message is empty Previously only `parts` and `body` were checked, so mime-parts that were classified into `attachments` and `inline_parts` didn't count – thus messages that contained only those parts were shown blank. --- program/actions/mail/show.php | 35 +++------------- program/lib/Roundcube/rcube_message.php | 56 +++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 29 deletions(-) diff --git a/program/actions/mail/show.php b/program/actions/mail/show.php index efe6a481aa0..b5e1de64afc 100644 --- a/program/actions/mail/show.php +++ b/program/actions/mail/show.php @@ -204,7 +204,7 @@ public static function message_attachments($attrib) } // Skip inline images - if (strpos($mimetype, 'image/') === 0 && !self::is_attachment(self::$MESSAGE, $attach_prop)) { + if (strpos($mimetype, 'image/') === 0 && self::$MESSAGE->is_referred_attachment($attach_prop)) { continue; } @@ -621,7 +621,10 @@ public static function message_full_headers($attrib) */ public static function message_body($attrib) { - if (empty(self::$MESSAGE) || (empty(self::$MESSAGE->parts) && empty(self::$MESSAGE->body))) { + // Exit early if there's no content to be shown anyway. + // `mime_parts` also includes a message's body, even if it originally + // was the only part of the message. + if (empty(self::$MESSAGE) || empty(self::$MESSAGE->mime_parts)) { return ''; } @@ -745,7 +748,7 @@ public static function message_body($attrib) // Content-Type: image/*... if ($mimetype = self::part_image_type($attach_prop)) { // Skip inline images - if (!self::is_attachment(self::$MESSAGE, $attach_prop)) { + if (self::$MESSAGE->is_referred_attachment($attach_prop)) { continue; } @@ -889,30 +892,4 @@ public static function mdn_request_handler($message) } } } - - /** - * Check whether the message part is a normal attachment - * - * @param rcube_message $message Message object - * @param rcube_message_part $part Message part - * - * @return bool - */ - protected static function is_attachment($message, $part) - { - // Inline attachment with Content-Id specified - if (!empty($part->content_id) && $part->disposition == 'inline') { - return false; - } - - // Any image attached to multipart/related message (#7184) - $parent_id = preg_replace('/\.[0-9]+$/', '', $part->mime_id); - $parent = $message->mime_parts[$parent_id] ?? null; - - if ($parent && $parent->mimetype == 'multipart/related') { - return false; - } - - return true; - } } diff --git a/program/lib/Roundcube/rcube_message.php b/program/lib/Roundcube/rcube_message.php index eb6be0419c1..15a432f33f0 100644 --- a/program/lib/Roundcube/rcube_message.php +++ b/program/lib/Roundcube/rcube_message.php @@ -50,6 +50,14 @@ class rcube_message protected $got_html_part = false; protected $tnef_decode = false; + /** + * This holds a list of Content-IDs and Content-Locations by which parts of + * this message are referenced (e.g. in HTML parts). + * + * @var array + */ + protected $replacement_references; + public $uid; public $folder; public $headers; @@ -569,6 +577,54 @@ public function is_attachment($part) return false; } + /** + * Get a cached list of replacement references, which are collected during + * parsing from Content-Id and Content-Location headers of mime-parts. + */ + protected function get_replacement_references(): array + { + if ($this->replacement_references === null) { + $this->replacement_references = []; + foreach ($this->mime_parts as $mime_part) { + foreach ($mime_part->replaces as $key => $value) { + $this->replacement_references[] = preg_replace('/^cid:/', '', $key); + } + } + } + return $this->replacement_references; + } + + /** + * Checks if a given message part is referred to from another message part. + * Usually this happens if an HTML-part includes images to show inline, but + * technically there can be other cases, too. + * In any case, an attachment that is *not* referred to, shall be shown to + * the users (either in/after the message body or as downloadable file). + * + * @param rcube_message_part $part Message part + * + * @return bool True if the part is an attachment part + */ + public function is_referred_attachment(rcube_message_part $part): bool + { + $references = $this->get_replacement_references(); + + // This code is intentionally verbose to keep it comprehensible. + // Filter out attachments that are reference by their Content-ID in + // another mime-part. + if (!empty($part->content_id) && in_array($part->content_id, $references)) { + return true; + } + + // Filter out attachments that are reference by their + // Content-Location in another mime-part. + if (!empty($part->content_location) && in_array($part->content_location, $references)) { + return true; + } + + return false; + } + /** * In a multipart/encrypted encrypted message, * find the encrypted message payload part.