aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/businesssenders.cpp81
-rw-r--r--src/businesssenders.h30
2 files changed, 111 insertions, 0 deletions
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 <QDir>
#include <QFile>
+#include <QFileInfo>
#include <QStandardPaths>
#include <QTextStream>
@@ -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<QString, int> &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<QString> 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
diff --git a/src/businesssenders.h b/src/businesssenders.h
index 0689453..3065ae5 100644
--- a/src/businesssenders.h
+++ b/src/businesssenders.h
@@ -18,6 +18,7 @@
#pragma once
+#include <QHash>
#include <QSet>
#include <QString>
#include <QStringList>
@@ -55,4 +56,33 @@ bool contains(const List &list, const QString &address);
/// QStandardPaths::GenericConfigLocation.
QString defaultPath();
+/// True when a local part looks like bulk mail rather than a person.
+///
+/// A GUESS, and openly one. It misses senders and proposes wrong ones, which
+/// is exactly why nothing it produces takes effect until the user uncomments
+/// it.
+bool looksLikeBulk(const QString &address);
+
+/// Appends anything in `counts` that looks like bulk and is not already in the
+/// file, COMMENTED OUT, with its message count.
+///
+/// Two rules, both load-bearing. It never writes an uncommented entry, so
+/// nothing on screen changes until the user acts. And it skips an address
+/// already present in ANY form, commented or not, so an entry the user
+/// rejected is never re-proposed, and one they deleted only returns if that
+/// sender writes again.
+void appendCandidates(const QString &path, const QHash<QString, int> &counts);
+
+/// The query the candidate scan should run.
+///
+/// A week of mail once the file exists, so the step stays incremental and
+/// cheap. EVERYTHING when the file is missing or holds no entries, because
+/// that is the first run: a week's mail proposes almost nothing, and the file
+/// would then take months to become useful. The whole-database scan is
+/// affordable precisely because it happens once, measured at 76 ms over 5105
+/// messages.
+///
+/// Returns notmuch query syntax, which is wire format and is never translated.
+QString scanQuery(const QString &path);
+
} // namespace BusinessSenders