diff options
| author | Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> | 2026-04-10 20:13:13 +0300 |
|---|---|---|
| committer | Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> | 2026-04-10 20:14:16 +0300 |
| commit | 581b3e4efec80e468c6d27be87004c5cb0378a21 (patch) | |
| tree | b66a25b3f08194ca3cf70de99cc598216c3c02ab | |
| parent | b85ce9b37791aa11efa68afec824bce3ab1f91c7 (diff) | |
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.
| -rw-r--r-- | lib/message/mu-message-part.cc | 19 | ||||
| -rw-r--r-- | lib/message/mu-message.cc | 6 | ||||
| -rw-r--r-- | lib/message/mu-mime-object.hh | 67 | ||||
| -rw-r--r-- | lib/message/test-mu-message.cc | 3 | ||||
| -rw-r--r-- | mu4e/mu4e.texi | 11 |
5 files changed, 71 insertions, 35 deletions
diff --git a/lib/message/mu-message-part.cc b/lib/message/mu-message-part.cc index d1c7ac5..3ed0b9c 100644 --- a/lib/message/mu-message-part.cc +++ b/lib/message/mu-message-part.cc @@ -195,17 +195,22 @@ MessagePart::looks_like_attachment() const noexcept if (!ctype) return false; // no content-type: not an attachment. - // we consider some parts _not_ to be attachments regardless of disposition - if (matches(*ctype,{{"application", "pgp-keys"}})) + // we consider some parts _not_ to be attachments regardless of + // disposition + if (matches(*ctype,{{"application", "pgp-keys"}, + {"application", "ics"}})) return false; - // we consider some parts to be attachments regardless of disposition - if (matches(*ctype,{{"image", "*"}, - {"audio", "*"}, - {"application", "*"}, - {"application", "x-patch"}})) + if (is_attachment()) // i.e., as per content-disposition 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 return is_attachment(); } diff --git a/lib/message/mu-message.cc b/lib/message/mu-message.cc index 126e8df..ebe3f81 100644 --- a/lib/message/mu-message.cc +++ b/lib/message/mu-message.cc @@ -424,9 +424,11 @@ process_part(const MimeObject& parent, const MimePart& part, if (!ctype) return; - // flag as calendar, if not already + // flag as calendar, if not already. 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; // flag as attachment, if not already. diff --git a/lib/message/mu-mime-object.hh b/lib/message/mu-mime-object.hh index 428c399..6c846a3 100644 --- a/lib/message/mu-mime-object.hh +++ b/lib/message/mu-mime-object.hh @@ -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 ** under the terms of the GNU General Public License as published by the @@ -171,7 +171,6 @@ private: }; - /** * Thin wrapper around a GMimeContentType * @@ -197,7 +196,6 @@ struct MimeContentType: public Object { return g_mime_content_type_is_type(self(), type.c_str(), subtype.c_str()); } - Option<std::string> parameter(const std::string& name) const noexcept { const char *param{g_mime_content_type_get_parameter(self(), name.c_str())}; if (!param || !param[0]) @@ -205,17 +203,47 @@ struct MimeContentType: public Object { else return Some(std::string{param}); } - private: GMimeContentType* self() const { 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 * @@ -272,7 +300,6 @@ constexpr Option<std::string_view> to_string_view_opt(const S& seq, T t) { return it->second; } - /** * Thin wrapper around a GMimeDataWrapper * @@ -298,7 +325,6 @@ private: }; - /** * 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 * @@ -575,7 +600,6 @@ static inline std::string to_string(MimeSignature::Status status) { - /** * 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); } - /** * Thin wrapper around a GMimeCryptoContext * @@ -748,7 +771,6 @@ private: } }; - /** * Thin wrapper around a GMimeObject * @@ -820,6 +842,19 @@ public: } /** + * 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 * * @param f_opts formatting options @@ -918,7 +953,6 @@ private: } }; - /** * Thin wrapper around a GMimeMessage * @@ -1024,7 +1058,6 @@ private: return reinterpret_cast<GMimeMessage*>(object()); } }; - /** * Thin wrapper around a GMimePart. * @@ -1200,7 +1233,6 @@ private: }; - /** * Thin wrapper around a GMimeMessagePart. * @@ -1235,7 +1267,7 @@ private: } }; -/** +/** * Thin wrapper around a GMimeApplicationPkcs7Mime * */ @@ -1270,7 +1302,6 @@ private: } }; - /** * Thin wrapper around a GMimeMultiPart * @@ -1327,7 +1358,6 @@ private: } }; - /** * Thin wrapper around a GMimeMultiPartEncrypted * @@ -1365,7 +1395,6 @@ private: MU_ENABLE_BITOPS(MimeMultipartEncrypted::DecryptFlags); - /** * Thin wrapper around a GMimeMultiPartSigned * @@ -1388,8 +1417,6 @@ public: 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, VerifyFlags vflags=VerifyFlags::None) const noexcept; diff --git a/lib/message/test-mu-message.cc b/lib/message/test-mu-message.cc index 1e365f7..c562ba2 100644 --- a/lib/message/test-mu-message.cc +++ b/lib/message/test-mu-message.cc @@ -814,8 +814,7 @@ RU5EOlZFVkVOVA0KRU5EOlZDQUxFTkRBUg0K g_assert_true(!!message); assert_equal(message->subject(), "Invitation: HELLO, @ Thu 9 Jan 2014 08:30 - 09:30 (william@example.com)"); - g_assert_true(message->flags() == (Flags::Passed|Flags::Seen| - Flags::HasAttachment|Flags::Calendar)); + g_assert_true(message->flags() == (Flags::Passed|Flags::Seen|Flags::Calendar)); g_assert_cmpuint(message->body_html().value_or("").find("DETAILS"), ==, 2271); } diff --git a/mu4e/mu4e.texi b/mu4e/mu4e.texi index e1ab6ed..383d975 100644 --- a/mu4e/mu4e.texi +++ b/mu4e/mu4e.texi @@ -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 ``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}. @@ -1610,9 +1610,12 @@ Other MIME-parts in the messages include @emph{attachments}. @cindex attachments Many e-mail messages contain @emph{attachments}, which are MIME-parts that -encode files@footnote{Attachments come in two flavors: @t{inline} and -@t{attachment}. @code{mu4e} does not distinguish between the two when operating on -them: everything that specifies a filename is considered an attachment}. +encode files which you can extract. + +@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 offers the command @code{mu4e-view-save-attachments}; its default keybinding is |
