summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDirk-Jan C. Binnema <djcb@djcbsoftware.nl>2026-04-10 20:13:13 +0300
committerDirk-Jan C. Binnema <djcb@djcbsoftware.nl>2026-04-10 20:14:16 +0300
commit581b3e4efec80e468c6d27be87004c5cb0378a21 (patch)
treeb66a25b3f08194ca3cf70de99cc598216c3c02ab
parentb85ce9b37791aa11efa68afec824bce3ab1f91c7 (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.cc19
-rw-r--r--lib/message/mu-message.cc6
-rw-r--r--lib/message/mu-mime-object.hh67
-rw-r--r--lib/message/test-mu-message.cc3
-rw-r--r--mu4e/mu4e.texi11
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