aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-08-26 15:27:15 +0200
committerDanilo M. <danix@danix.xyz>2026-08-26 15:27:15 +0200
commit3afbce6e2f7d711ab58985d64c3e236484cc7345 (patch)
tree6a510063e362acd7d8d4cd3091277332c5d22a8c /src
parent804454e43cf08ddf39dbf0a765f047f0ac96f529 (diff)
downloadqtmaildir-3afbce6e2f7d711ab58985d64c3e236484cc7345.tar.gz
qtmaildir-3afbce6e2f7d711ab58985d64c3e236484cc7345.zip
feat: carry the first message's sender address on a thread summary
Diffstat (limited to 'src')
-rw-r--r--src/notmuchworker.cpp59
-rw-r--r--src/types.h19
2 files changed, 78 insertions, 0 deletions
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 <QtCore/qnamespace.h> #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 <gmime/gmime.h>
+
#include "notmuchworker.h"
#include <notmuch.h>
@@ -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 <a@b>" 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.
///