message: improve attachment heuristics

Do not consider calender-invitations "attachments"; do mark as
"calendar". Do recognize application/ics as calendar messages.

Update mime-object to expose a message part's disposition.

Change the "is-attachment" heuristic to include inline parts if they
have a filename parameter in their content-disposition.

Note that this doesn't change things radically; the delta is +69 and
-202 for ~6500 attachments.
This commit is contained in:
Dirk-Jan C. Binnema
2026-04-10 20:13:13 +03:00
committed by Seth Ladygo
parent 6715ff418a
commit f353e2dacf
5 changed files with 72 additions and 36 deletions

View File

@ -195,17 +195,22 @@ MessagePart::looks_like_attachment() const noexcept
if (!ctype) if (!ctype)
return false; // no content-type: not an attachment. return false; // no content-type: not an attachment.
// we consider some parts _not_ to be attachments regardless of disposition // we consider some parts _not_ to be attachments regardless of
if (matches(*ctype,{{"application", "pgp-keys"}})) // disposition
if (matches(*ctype,{{"application", "pgp-keys"},
{"application", "ics"}}))
return false; return false;
// we consider some parts to be attachments regardless of disposition if (is_attachment()) // i.e., as per content-disposition
if (matches(*ctype,{{"image", "*"},
{"audio", "*"},
{"application", "*"},
{"application", "x-patch"}}))
return true; return true;
// we also consider "inline" parts as attachment, if the
// content-disposition has a filename property.
if (const auto cdisp{mime_object().content_disposition()}; !!cdisp) {
if (const auto& fname{cdisp->parameter("filename")}; !!fname)
return true;
}
// otherwise, rely on the disposition // otherwise, rely on the disposition
return is_attachment(); return is_attachment();
} }

View File

@ -424,9 +424,11 @@ process_part(const MimeObject& parent, const MimePart& part,
if (!ctype) if (!ctype)
return; return;
// flag as calendar, if not already // flag as calendar, if not already.
if (none_of(info.flags & Flags::Calendar) && if (none_of(info.flags & Flags::Calendar) &&
ctype->is_type("text", "calendar")) (ctype->is_type("text", "calendar") ||
ctype->is_type("text", "x-vcalendar") ||
ctype->is_type("application", "ics")))
info.flags |= Flags::Calendar; info.flags |= Flags::Calendar;
// flag as attachment, if not already. // flag as attachment, if not already.

View File

@ -1,5 +1,5 @@
/* /*
** Copyright (C) 2022-2025 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> ** Copyright (C) 2022-2026 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
** **
** This program is free software; you can redistribute it and/or modify it ** This program is free software; you can redistribute it and/or modify it
** under the terms of the GNU General Public License as published by the ** under the terms of the GNU General Public License as published by the
@ -171,7 +171,6 @@ private:
}; };
/** /**
* Thin wrapper around a GMimeContentType * Thin wrapper around a GMimeContentType
* *
@ -197,7 +196,6 @@ struct MimeContentType: public Object {
return g_mime_content_type_is_type(self(), type.c_str(), return g_mime_content_type_is_type(self(), type.c_str(),
subtype.c_str()); subtype.c_str());
} }
Option<std::string> parameter(const std::string& name) const noexcept { Option<std::string> parameter(const std::string& name) const noexcept {
const char *param{g_mime_content_type_get_parameter(self(), name.c_str())}; const char *param{g_mime_content_type_get_parameter(self(), name.c_str())};
if (!param || !param[0]) if (!param || !param[0])
@ -205,17 +203,47 @@ struct MimeContentType: public Object {
else else
return Some(std::string{param}); return Some(std::string{param});
} }
private: private:
GMimeContentType* self() const { GMimeContentType* self() const {
return reinterpret_cast<GMimeContentType*>(object()); return reinterpret_cast<GMimeContentType*>(object());
} }
}; };
/**
* Thin wrapper around a GMimeContentDisposition
*
*/
struct MimeContentDisposition: public Object {
MimeContentDisposition(GMimeContentDisposition *disp) : Object{G_OBJECT(disp)} {
if (!GMIME_IS_CONTENT_DISPOSITION(self()))
throw std::runtime_error("not a content-disposition");
}
std::string disposition() const noexcept {
if (const auto disp{g_mime_content_disposition_get_disposition(self())}; disp)
return disp;
else
return {};
}
bool is_attachment() const noexcept {
return g_mime_content_disposition_is_attachment(self());
}
Option<std::string> parameter(const std::string& name) const noexcept {
const char *param{g_mime_content_disposition_get_parameter(self(), name.c_str())};
if (!param || !param[0])
return Nothing;
else
return Some(std::string{param});
}
private:
GMimeContentDisposition* self() const {
return reinterpret_cast<GMimeContentDisposition*>(object());
}
};
/** /**
* Thin wrapper around a GMimeStream * Thin wrapper around a GMimeStream
* *
@ -272,7 +300,6 @@ constexpr Option<std::string_view> to_string_view_opt(const S& seq, T t) {
return it->second; return it->second;
} }
/** /**
* Thin wrapper around a GMimeDataWrapper * Thin wrapper around a GMimeDataWrapper
* *
@ -298,7 +325,6 @@ private:
}; };
/** /**
* Thin wrapper around a GMimeCertifcate * Thin wrapper around a GMimeCertifcate
* *
@ -494,7 +520,6 @@ constexpr Option<std::string_view> to_string_view_opt(MimeCertificate::Validity
} }
/** /**
* Thin wrapper around a GMimeSignature * Thin wrapper around a GMimeSignature
* *
@ -575,7 +600,6 @@ static inline std::string to_string(MimeSignature::Status status) {
/** /**
* Thin wrapper around a GMimeDecryptResult * Thin wrapper around a GMimeDecryptResult
* *
@ -645,7 +669,6 @@ constexpr Option<std::string_view> to_string_view_opt(MimeDecryptResult::CipherA
return to_string_view_opt(AllCipherAlgos, algo); return to_string_view_opt(AllCipherAlgos, algo);
} }
/** /**
* Thin wrapper around a GMimeCryptoContext * Thin wrapper around a GMimeCryptoContext
* *
@ -748,7 +771,6 @@ private:
} }
}; };
/** /**
* Thin wrapper around a GMimeObject * Thin wrapper around a GMimeObject
* *
@ -819,6 +841,19 @@ public:
g_mime_object_get_content_type_parameter(self(), param.c_str())); g_mime_object_get_content_type_parameter(self(), param.c_str()));
} }
/**
* Get the content disposition
*
* @return the content-disposition or Nothing
*/
Option<MimeContentDisposition> content_disposition() const noexcept {
auto disp{g_mime_object_get_content_disposition(self())};
if (!disp)
return Nothing;
else
return MimeContentDisposition(disp);
}
/** /**
* Write this MimeObject to some stream * Write this MimeObject to some stream
* *
@ -918,7 +953,6 @@ private:
} }
}; };
/** /**
* Thin wrapper around a GMimeMessage * Thin wrapper around a GMimeMessage
* *
@ -1024,7 +1058,6 @@ private:
return reinterpret_cast<GMimeMessage*>(object()); return reinterpret_cast<GMimeMessage*>(object());
} }
}; };
/** /**
* Thin wrapper around a GMimePart. * Thin wrapper around a GMimePart.
* *
@ -1200,7 +1233,6 @@ private:
}; };
/** /**
* Thin wrapper around a GMimeMessagePart. * Thin wrapper around a GMimeMessagePart.
* *
@ -1235,7 +1267,7 @@ private:
} }
}; };
/** /**
* Thin wrapper around a GMimeApplicationPkcs7Mime * Thin wrapper around a GMimeApplicationPkcs7Mime
* *
*/ */
@ -1270,7 +1302,6 @@ private:
} }
}; };
/** /**
* Thin wrapper around a GMimeMultiPart * Thin wrapper around a GMimeMultiPart
* *
@ -1327,7 +1358,6 @@ private:
} }
}; };
/** /**
* Thin wrapper around a GMimeMultiPartEncrypted * Thin wrapper around a GMimeMultiPartEncrypted
* *
@ -1365,7 +1395,6 @@ private:
MU_ENABLE_BITOPS(MimeMultipartEncrypted::DecryptFlags); MU_ENABLE_BITOPS(MimeMultipartEncrypted::DecryptFlags);
/** /**
* Thin wrapper around a GMimeMultiPartSigned * Thin wrapper around a GMimeMultiPartSigned
* *
@ -1388,8 +1417,6 @@ public:
EnableOnlineCertificateChecks = GMIME_VERIFY_ENABLE_ONLINE_CERTIFICATE_CHECKS EnableOnlineCertificateChecks = GMIME_VERIFY_ENABLE_ONLINE_CERTIFICATE_CHECKS
}; };
// Result<std::vector<MimeSignature>> verify(VerifyFlags vflags=VerifyFlags::None) const noexcept;
Result<std::vector<MimeSignature>> verify(const MimeCryptoContext& ctx, Result<std::vector<MimeSignature>> verify(const MimeCryptoContext& ctx,
VerifyFlags vflags=VerifyFlags::None) const noexcept; VerifyFlags vflags=VerifyFlags::None) const noexcept;

View File

@ -814,8 +814,7 @@ RU5EOlZFVkVOVA0KRU5EOlZDQUxFTkRBUg0K
g_assert_true(!!message); g_assert_true(!!message);
assert_equal(message->subject(), assert_equal(message->subject(),
"Invitation: HELLO, @ Thu 9 Jan 2014 08:30 - 09:30 (william@example.com)"); "Invitation: HELLO, @ Thu 9 Jan 2014 08:30 - 09:30 (william@example.com)");
g_assert_true(message->flags() == (Flags::Passed|Flags::Seen| g_assert_true(message->flags() == (Flags::Passed|Flags::Seen|Flags::Calendar));
Flags::HasAttachment|Flags::Calendar));
g_assert_cmpuint(message->body_html().value_or("").find("DETAILS"), ==, 2271); g_assert_cmpuint(message->body_html().value_or("").find("DETAILS"), ==, 2271);
} }

View File

@ -1602,7 +1602,7 @@ mu4e-view-show-mime-parts}. This can be a little slow.
Nowadays, typical e-mail messages can be thought of as a series of Nowadays, typical e-mail messages can be thought of as a series of
``MIME-parts'', which are sections of the message. The most prominent of those ``MIME-parts'', which are sections of the message. The most prominent of those
parts is the 'body', which is the main text of the message your are readings. parts is the 'body', which is the main text of the message your are reading.
Other MIME-parts in the messages include @emph{attachments}. Other MIME-parts in the messages include @emph{attachments}.
@ -1610,9 +1610,12 @@ Other MIME-parts in the messages include @emph{attachments}.
@cindex attachments @cindex attachments
Many e-mail messages contain @emph{attachments}, which are MIME-parts that Many e-mail messages contain @emph{attachments}, which are MIME-parts that
encode files@footnote{Attachments come in two flavors: @t{inline} and encode files which you can extract.
@t{attachment}. @code{mu4e} does not distinguish between the two when operating on
them: everything that specifies a filename is considered an attachment}. @footnote{@code{mu} uses some heuristics to decide if a part should be treated
as an attachment; including @t{inline} parts that specify a filename. The
heuristic tries to balance false-positive and false-negatives, both of which are
possible}
To save attachments as files on your computer, @code{mu4e}'s message-view To save attachments as files on your computer, @code{mu4e}'s message-view
offers the command @code{mu4e-view-save-attachments}; its default keybinding is offers the command @code{mu4e-view-save-attachments}; its default keybinding is