Skip to content

Commit

Permalink
Filter "real" attachments by being referenced (#9472)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
pabzm authored Jul 21, 2024
1 parent b14a278 commit 8f9f1f1
Show file tree
Hide file tree
Showing 2 changed files with 62 additions and 29 deletions.
35 changes: 6 additions & 29 deletions program/actions/mail/show.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -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 '';
}

Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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;
}
}
56 changes: 56 additions & 0 deletions program/lib/Roundcube/rcube_message.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
Expand Down

0 comments on commit 8f9f1f1

Please sign in to comment.