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)
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();
}

View File

@ -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.

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
** 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
*
@ -819,6 +841,19 @@ public:
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
*
@ -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;

View File

@ -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);
}

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
``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