diff options
Diffstat (limited to 'src/notmuchworker.cpp')
| -rw-r--r-- | src/notmuchworker.cpp | 136 |
1 files changed, 133 insertions, 3 deletions
diff --git a/src/notmuchworker.cpp b/src/notmuchworker.cpp index 8ab3ab5..e78d8c8 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> @@ -74,6 +81,51 @@ QStringList tagsOf(notmuch_thread_t *thread) return result; } +/// The first mailbox address in a raw address header, display names dropped. +/// +/// Parsed with GMime rather than split: the header is untrusted and a display +/// name may legally contain an `@`, so "Ian <a@b>" split on `@` yields +/// nonsense. +QString firstMailboxOf(const QString &rawHeader) +{ + // 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 QByteArray utf8 = rawHeader.trimmed().toUtf8(); + if (utf8.isEmpty()) + return QString(); + + InternetAddressList *list = + internet_address_list_parse(nullptr, utf8.constData()); + 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; +} + /// Who a thread's messages were addressed to, summarised for one line. /// /// EXPENSIVE, and only called when a query asks. "To" is not served from @@ -87,7 +139,7 @@ QStringList tagsOf(notmuch_thread_t *thread) /// frees again, and the walk finishes before the caller drops the thread. This /// is the same rule walkReplies follows, and getting it wrong is a double-free /// rather than a leak. -QString recipientsOf(notmuch_thread_t *thread) +QString recipientsOf(notmuch_thread_t *thread, QString *firstAddress) { // The first message with a usable To wins. A thread is one conversation, // and the alternative, folding every message's recipients together, is the @@ -107,12 +159,33 @@ QString recipientsOf(notmuch_thread_t *thread) continue; const QString summary = recipientSummary(QString::fromUtf8(to)); - if (!summary.isEmpty()) + if (!summary.isEmpty()) { + // The same header, for the avatar's hash (item 169). Parsed rather + // than split for the reason senderAddressOf() documents: a display + // name may legally contain an `@`. + if (firstAddress) + *firstAddress = firstMailboxOf(QString::fromUtf8(to)); return summary; + } } 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) +{ + const char *from = notmuch_message_get_header(message, "From"); + if (!from || !*from) + return QString(); + return firstMailboxOf(QString::fromUtf8(from)); +} + /// 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, @@ -169,6 +242,7 @@ void walkReplies(notmuch_messages_t *messages, int depth, QString::fromUtf8(notmuch_message_get_filename(message)); node.from = QString::fromUtf8(notmuch_message_get_header(message, "from")); + node.senderAddress = senderAddressOf(message); node.subject = QString::fromUtf8(notmuch_message_get_header(message, "subject")); node.date = @@ -267,10 +341,19 @@ QString folderOfMessageFile(const QString &root, const QString &filePath) static const int kSortOrderMetaType = qRegisterMetaType<NotmuchWorker::SortOrder>("NotmuchWorker::SortOrder"); +/// The same registration for the sender-count map. The QHash crosses the +/// queued senderCountsReady connection from the worker thread to the UI, and +/// an unregistered type is dropped there with a warning, exactly like +/// SortOrder above. Registered with the name invokeMethod/moc resolve, so a +/// caller that never builds a worker still gets the type. +static const int kSenderCountsMetaType = + qRegisterMetaType<QHash<QString, int>>("QHash<QString,int>"); + NotmuchWorker::NotmuchWorker(const QString ¬muchConfigPath, QObject *parent) : QObject(parent), m_configPath(notmuchConfigPath) { Q_UNUSED(kSortOrderMetaType); + Q_UNUSED(kSenderCountsMetaType); } NotmuchWorker::~NotmuchWorker() @@ -392,7 +475,8 @@ void NotmuchWorker::runQuery(const QString &query, quint64 generation, summary.matchedCount = notmuch_thread_get_matched_messages(thread.get()); summary.tags = tagsOf(thread.get()); if (withRecipients) - summary.recipients = recipientsOf(thread.get()); + summary.recipients = + recipientsOf(thread.get(), &summary.firstMessageRecipient); // The message the row's card stands for. Raw pointers on purpose: // messages reached through a thread are owned by the THREAD and freed @@ -434,6 +518,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 +537,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( @@ -1321,6 +1411,46 @@ void NotmuchWorker::requestMessageCounts(const QStringList &queries, emit messageCountsReady(counts, generation); } +void NotmuchWorker::countSenders(const QString &query) +{ + if (!openReadOnly()) { + // Answered anyway, with an empty map, so the sync path that asked is + // not left waiting on a signal it can never receive. + emit senderCountsReady({}); + return; + } + + QHash<QString, int> counts; + NmQuery nmQuery(notmuch_query_create(m_db, query.toUtf8().constData())); + if (!nmQuery) { + emit senderCountsReady(counts); + return; + } + + notmuch_messages_t *raw = nullptr; + if (notmuch_query_search_messages(nmQuery.get(), &raw) + != NOTMUCH_STATUS_SUCCESS) { + emit senderCountsReady(counts); + return; + } + + NmMessages messages(raw); + for (; notmuch_messages_valid(messages.get()); + notmuch_messages_move_to_next(messages.get())) { + notmuch_message_t *message = notmuch_messages_get(messages.get()); + if (!message) + continue; + // The BARE address, lower-cased, because that is the key + // BusinessSenders matches on: a display name would defeat the + // bulk-sender guess and a mixed-case key would duplicate one sender. + const QString sender = senderAddressOf(message); + if (!sender.isEmpty()) + counts[sender.toLower()] += 1; + } + + emit senderCountsReady(counts); +} + void NotmuchWorker::requestMailRoot() { if (!openReadOnly()) { |
