From 3afbce6e2f7d711ab58985d64c3e236484cc7345 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Wed, 26 Aug 2026 15:27:15 +0200 Subject: feat: carry the first message's sender address on a thread summary --- src/notmuchworker.cpp | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/types.h | 19 +++++++++++++++++ 2 files changed, 78 insertions(+) (limited to 'src') diff --git a/src/notmuchworker.cpp b/src/notmuchworker.cpp index 8ab3ab5..43f4758 100644 --- a/src/notmuchworker.cpp +++ b/src/notmuchworker.cpp @@ -16,6 +16,13 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ +// gmime.h pulls in glib's gio headers, which declare a struct field named +// "signals". Qt's #defines "signals" to "Q_SIGNALS" +// (unless QT_NO_KEYWORDS is set), so gmime.h must be included before any Qt +// header in this translation unit to avoid a macro collision. That means +// before notmuchworker.h too, which includes Qt headers. +#include + #include "notmuchworker.h" #include @@ -113,6 +120,52 @@ QString recipientsOf(notmuch_thread_t *thread) return QString(); } +/// The bare address of a message's From, with any display name discarded. +/// +/// Index-served, unlike recipientsOf() above, which is why this is not behind +/// the withRecipients flag: `From` is in notmuch's index and `To` is not. +/// +/// The header is untrusted, so it is parsed rather than split: a display name +/// may legally contain an `@`, and "Ian " split on `@` yields nonsense. +QString senderAddressOf(notmuch_message_t *message) +{ + // GMime must be initialised once per process before any parse, or the + // first internet_address_list_parse() call dereferences an uninitialised + // type registry and SEGVs. Function-local static, exactly as the other + // gmime-using units do, so this file does not lean on libnotmuch having + // initialised it as a side effect (measured 2026-08-26: it currently + // does, but that is not documented anywhere). + static const bool initialised = [] { + g_mime_init(); + return true; + }(); + Q_UNUSED(initialised); + + const char *from = notmuch_message_get_header(message, "From"); + if (!from || !*from) + return QString(); + + InternetAddressList *list = internet_address_list_parse(nullptr, from); + if (!list) + return QString(); + + QString address; + const int count = internet_address_list_length(list); + for (int i = 0; i < count; ++i) { + InternetAddress *entry = internet_address_list_get_address(list, i); + if (!entry || !INTERNET_ADDRESS_IS_MAILBOX(entry)) + continue; + const char *addr = + internet_address_mailbox_get_addr(INTERNET_ADDRESS_MAILBOX(entry)); + if (addr && *addr) { + address = QString::fromUtf8(addr); + break; + } + } + g_object_unref(list); + return address; +} + /// Collects the message ids a query matches. Returns false if the query could /// not be run at all, which is different from a query that matched nothing. bool collectMessageIds(notmuch_database_t *db, const QString &query, @@ -434,6 +487,9 @@ void NotmuchWorker::runQuery(const QString &query, quint64 generation, // The card's own tags, beside the thread's union above. // Same walk, same index read, no extra query. summary.firstMessageTags = tagsOf(message); + // The card's sender, for the avatar hash (item 169). Same + // walk, and From is in the index like the tags. + summary.firstMessageSender = senderAddressOf(message); // Which account this belongs to, for Delete's destination. summary.firstMessagePath = QDir(dbRoot).relativeFilePath( QString::fromUtf8( @@ -450,6 +506,9 @@ void NotmuchWorker::runQuery(const QString &query, quint64 generation, // The card's own tags, beside the thread's union above. // Same walk, same index read, no extra query. summary.firstMessageTags = tagsOf(first); + // The card's sender, for the avatar hash (item 169). Same + // walk, and From is in the index like the tags. + summary.firstMessageSender = senderAddressOf(first); // Which account this belongs to, for Delete's destination. summary.firstMessagePath = QDir(dbRoot).relativeFilePath( QString::fromUtf8( diff --git a/src/types.h b/src/types.h index cf2411d..47ddb78 100644 --- a/src/types.h +++ b/src/types.h @@ -62,6 +62,25 @@ struct ThreadSummary /// file. Do not move it behind a flag by analogy with `recipients`. QStringList firstMessageTags; + /// That message's sender, as a BARE ADDRESS with no display name. + /// + /// `authors` above is notmuch's own summarised string and carries display + /// names ONLY: measured against the real index, 'Ryanair' and 'The Hacker + /// News tramite LinkedIn', with no `@` anywhere. A card therefore has no + /// address to hash for its avatar and nothing for the business-sender list + /// to match, which is why this exists (item 169). + /// + /// Hashing the display name instead was rejected: notmuch BUILDS those + /// strings, so one sender's identity varies as the string does. + /// + /// Free, for the same reason `firstMessageId` and `firstMessageTags` are: + /// the walk that finds that message is already happening and From is + /// served from the INDEX, not the message file. Measured 2026-08-26 on the + /// developer's database: 1322 distinct senders in 12 ms, 5105 messages + /// enumerated in 76 ms. Do not move it behind a flag by analogy with + /// `recipients`. + QString firstMessageSender; + /// That message's file, RELATIVE to the database path, which is what says /// which ACCOUNT it belongs to. /// -- cgit v1.2.3