diff --git a/NEWS.org b/NEWS.org index 2f808e7a..87abf56e 100644 --- a/NEWS.org +++ b/NEWS.org @@ -10,7 +10,12 @@ - significant speed-ups in the cleanup process after ~mu index~ (1.14.0) - - record the timezone-offset for message sent dates (1.14.3) + - Record the timezone-offset for message sent dates as part of the index + (1.14.3) + + - for (rare) messages without a =Date:= header, fall-back to the date of the + most recent =Received:= header. Not perfect, but better than nothing. + Requires re-indexing to apply to older messages (1.14.3) *** mu4e diff --git a/lib/message/mu-message.cc b/lib/message/mu-message.cc index 3a2fa106..e64910c3 100644 --- a/lib/message/mu-message.cc +++ b/lib/message/mu-message.cc @@ -741,7 +741,10 @@ fill_document(Message::Private& priv) doc.add(field.id, priv.ctime); break; case Field::Id::Date: - if (const auto& date{mime_msg.date()}; date) { + /* for the rare message without a Date: header, fall back + * to the most recent Received: header */ + if (const auto date{mime_msg.date().or_else([&]{ + return mime_msg.received(); })}; date) { doc.add(field.id, date->first); doc.add(Field::Id::UtcOffset, date->second); } diff --git a/lib/message/mu-mime-object.cc b/lib/message/mu-mime-object.cc index 27d974fa..a94b2b9f 100644 --- a/lib/message/mu-mime-object.cc +++ b/lib/message/mu-mime-object.cc @@ -22,7 +22,9 @@ #include "gmime/gmime-message.h" #include "utils/mu-utils.hh" #include "utils/mu-utils-file.hh" +#include #include +#include #include #include #include @@ -334,19 +336,47 @@ MimeMessage::make_from_text(const std::string& text) return make_from_stream(std::move(stream)); } +static MimeMessage::Date date_from_date_time(GDateTime* dt) +{ + constexpr auto usecs_per_sec = 1'000'000; + return MimeMessage::Date {g_date_time_to_unix(dt), + g_date_time_get_utc_offset(dt) / usecs_per_sec }; +} + + Option MimeMessage::date() const noexcept { if (/*const*/GDateTime *dt{g_mime_message_get_date(self())}; !dt) return Nothing; else { - constexpr auto usecs_per_sec = 1'000'000; - return Date{ g_date_time_to_unix(dt), - g_date_time_get_utc_offset(dt) / usecs_per_sec }; - + return date_from_date_time(dt); } } +Option +MimeMessage::received() const noexcept +{ + const auto recv{header("Received")}; + if (!recv) + return Nothing; + + /* per RFC 5322, "Received:" is *received-token ";" date-time, so the + * date-time follows the _last_ semicolon. */ + const auto pos{recv->rfind(';')}; + if (pos == std::string::npos) + return Nothing; + + GDateTime *dt{g_mime_utils_header_decode_date(recv->c_str() + pos + 1)}; + if (!dt) + return Nothing; + + const auto date{date_from_date_time(dt)}; + g_date_time_unref(dt); + + return date; +} + constexpr Option address_type(Contact::Type ctype) { @@ -372,20 +402,20 @@ address_type(Contact::Type ctype) static Mu::Contacts all_contacts(const MimeMessage& msg) { - Contacts contacts; - - for (auto&& cctype: { + constexpr auto ctypes = std::to_array({ Contact::Type::Sender, Contact::Type::From, Contact::Type::ReplyTo, Contact::Type::To, Contact::Type::Cc, Contact::Type::Bcc - }) { - auto addrs{msg.contacts(cctype)}; - std::move(addrs.begin(), addrs.end(), + }); + + Contacts contacts; + std::ranges::move(ctypes | std::views::transform([&](auto ctype) { + return msg.contacts(ctype); }) + | std::views::join, std::back_inserter(contacts)); - } return contacts; } diff --git a/lib/message/mu-mime-object.hh b/lib/message/mu-mime-object.hh index dae4a53c..544cae30 100644 --- a/lib/message/mu-mime-object.hh +++ b/lib/message/mu-mime-object.hh @@ -1051,16 +1051,23 @@ public: /**< Date type */ /** - * Gets a date, utf-offset pair if it exists, or nullopt otherwise. + * Gets a { date, utf-offset } pair if it exists, or nullopt otherwise. * * The date is the number of seconds since epoch (i.e., unix time), * while utf-offset is the number of seconds offset from UTC * (i.e., negative numbers west of GMT, positive numbers for east) * - * @return a time_t value (expressed as a 64-bit number) or nullopt + * @return a Date pair or nullopt */ Option date() const noexcept; + /** + * Gets a { date, utc-offset } pair from the first (more recent) + * Received: header, or nullopt if there is none. + * + * @return a Date pair or nullopt + */ + Option received() const noexcept; /** * Get the references for this message (including in-reply-to), in the diff --git a/lib/tests/test-mu-msg.cc b/lib/tests/test-mu-msg.cc index e46019a0..626c9200 100644 --- a/lib/tests/test-mu-msg.cc +++ b/lib/tests/test-mu-msg.cc @@ -368,6 +368,31 @@ k+ZGGoQ0v8b7RwmyskMAAAAAAAAAAAAA } +static void +test_mu_msg_received_date() +{ + /* without a Date: header, fall back to the date in the topmost (most + * recent) Received: header */ + const auto txt = +R"(Return-Path: me@example.com +Received: from imap.example.com [61.223.64.18] + by evergrey with IMAP (fetchmail-6.6.6) + for (single-drop); Thu, 30 Jul 2026 13:47:56 +0300 (EEST) +Received: from mail.example.com ([10.11.12.13]); + Wed, 29 Jul 2026 01:02:03 +0000 +From: Me +To: You +Subject: no date +Message-ID: + +Test. +)"; + const auto msg{Message::make_from_text(txt, "boo/cur/msg3:2,S")}; + assert_valid_result(msg); + + g_assert_cmpuint(msg->date(), ==, 1785408476); /* 2026-07-30 10:47:56 UTC */ +} + [[maybe_unused]] static gboolean ignore_error(const char* log_domain, GLogLevelFlags log_level, const gchar* msg, gpointer user_data) { @@ -402,6 +427,7 @@ main(int argc, char* argv[]) g_test_add_func("/msg/mu-msg-comp-unix-programmer", test_mu_msg_comp_unix_programmer); g_test_add_func("/msg/mu-smime", test_mu_smime); + g_test_add_func("/msg/mu-msg-received-date", test_mu_msg_received_date); g_test_add_func("/str/mu-str-prio-01", test_mu_str_prio_01);