From c3cfd28844c34aa7f7b390215f73d0b56534bdd0 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Wed, 26 Aug 2026 15:42:12 +0200 Subject: feat: propose business-sender candidates, always commented out --- src/businesssenders.cpp | 81 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) (limited to 'src/businesssenders.cpp') diff --git a/src/businesssenders.cpp b/src/businesssenders.cpp index 13a9374..06f4d43 100644 --- a/src/businesssenders.cpp +++ b/src/businesssenders.cpp @@ -20,6 +20,7 @@ #include #include +#include #include #include @@ -75,4 +76,84 @@ QString defaultPath() QStringLiteral("qtmaildir/business-senders")); } +bool looksLikeBulk(const QString &address) +{ + static const QStringList kBulkLocalParts { + QStringLiteral("noreply"), QStringLiteral("no-reply"), + QStringLiteral("donotreply"), QStringLiteral("do-not-reply"), + QStringLiteral("info"), QStringLiteral("support"), + QStringLiteral("billing"), QStringLiteral("newsletter"), + QStringLiteral("notifications"), QStringLiteral("mailer-daemon"), + }; + const int at = address.indexOf(QLatin1Char('@')); + if (at <= 0) + return false; + const QString local = address.left(at).toLower(); + for (const QString &candidate : kBulkLocalParts) { + if (local == candidate || local.startsWith(candidate)) + return true; + } + return false; +} + +void appendCandidates(const QString &path, const QHash &counts) +{ + // Every address the file MENTIONS, active or rejected. Parsed separately + // from parse() above, which deliberately drops comments: here a comment is + // exactly what must be remembered. + QSet mentioned; + QFile existing(path); + if (existing.open(QIODevice::ReadOnly | QIODevice::Text)) { + const QStringList lines = + QString::fromUtf8(existing.readAll()).split(QLatin1Char('\n')); + for (const QString &raw : lines) { + QString line = raw.trimmed(); + if (line.startsWith(QLatin1Char('#'))) + line = line.mid(1).trimmed(); + if (line.isEmpty()) + continue; + // "noreply@cofidis.it (47 messages)" mentions the address before + // its count. + mentioned.insert(line.section(QLatin1Char(' '), 0, 0).toLower()); + } + existing.close(); + } + + QStringList additions; + for (auto it = counts.constBegin(); it != counts.constEnd(); ++it) { + const QString address = it.key().trimmed().toLower(); + if (address.isEmpty() || mentioned.contains(address)) + continue; + if (!looksLikeBulk(address)) + continue; + additions.append(QStringLiteral("# %1 (%2 messages)") + .arg(address) + .arg(it.value())); + } + if (additions.isEmpty()) + return; + + additions.sort(); + + QDir().mkpath(QFileInfo(path).absolutePath()); + QFile file(path); + if (!file.open(QIODevice::Append | QIODevice::Text)) + return; + QTextStream out(&file); + for (const QString &line : additions) + out << line << '\n'; +} + +QString scanQuery(const QString &path) +{ + // "*" is notmuch's match-everything. An EMPTY string would also match + // everything, which is why Config::matchNothingQuery() exists elsewhere in + // this codebase; being explicit here means a reader never has to wonder + // which of the two an empty return meant. + const List existing = load(path); + if (existing.addresses.isEmpty() && existing.domains.isEmpty()) + return QStringLiteral("*"); + return QStringLiteral("date:1week.."); +} + } // namespace BusinessSenders -- cgit v1.2.3