message: fall-back to received for dateless

For the (rare) messages without a Date: header, attempt to guess a date
from the top (most-recent) Received: header. Not perfect, of course, but
better than nothing.

Requires re-indexing to apply to already indexed messages.

Should help for #1083.
This commit is contained in:
Dirk-Jan C. Binnema
2026-07-30 23:38:45 +03:00
committed by Seth Ladygo
parent 4a8b797665
commit 54786e2e97
5 changed files with 86 additions and 15 deletions

View File

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

View File

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

View File

@ -22,7 +22,9 @@
#include "gmime/gmime-message.h"
#include "utils/mu-utils.hh"
#include "utils/mu-utils-file.hh"
#include <algorithm>
#include <mutex>
#include <ranges>
#include <regex>
#include <fcntl.h>
#include <sys/stat.h>
@ -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>
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::Date>
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<GMimeAddressType>
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;
}

View File

@ -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> 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<Date> received() const noexcept;
/**
* Get the references for this message (including in-reply-to), in the

View File

@ -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 <me@localhost> (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 <me@example.com>
To: You <you@example.com>
Subject: no date
Message-ID: <abcdef@example.com>
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);