diff --git a/program/actions/mail/show.php b/program/actions/mail/show.php index efe6a481aa0..a2105e5bc42 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; } @@ -745,7 +745,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 +889,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.