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