Skip to content

Commit

Permalink
Filter "real" attachments by being referenced
Browse files Browse the repository at this point in the history
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".
  • Loading branch information
pabzm committed Jun 20, 2024
1 parent 7a3e91a commit dd29cf7
Show file tree
Hide file tree
Showing 2 changed files with 58 additions and 28 deletions.
30 changes: 2 additions & 28 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 @@ -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;
}

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

Please sign in to comment.