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:
@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
@ -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;
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user