aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-08-26 15:40:11 +0200
committerDanilo M. <danix@danix.xyz>2026-08-26 15:40:11 +0200
commit1b9188fff9b767192d5d1c62d64df81d2af2fe4d (patch)
treeb70a415cb2749b1879ac84035d50a6742648fad3
parent67cc7bf2357980c51bc345855d9a8d69ff9a7a9f (diff)
downloadqtmaildir-1b9188fff9b767192d5d1c62d64df81d2af2fe4d.tar.gz
qtmaildir-1b9188fff9b767192d5d1c62d64df81d2af2fe4d.zip
feat: read the business-senders list
-rw-r--r--src/CMakeLists.txt1
-rw-r--r--src/businesssenders.cpp78
-rw-r--r--src/businesssenders.h58
-rw-r--r--tests/CMakeLists.txt1
-rw-r--r--tests/test_businesssenders.cpp101
5 files changed, 239 insertions, 0 deletions
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index d4f1cb7..7cec9b3 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -10,6 +10,7 @@ add_library(qtmaildir_lib STATIC
cardlayout.cpp
avatar.cpp
busyindicator.cpp
+ businesssenders.cpp
marks.cpp
carddelegate.cpp
notmuchworker.cpp
diff --git a/src/businesssenders.cpp b/src/businesssenders.cpp
new file mode 100644
index 0000000..13a9374
--- /dev/null
+++ b/src/businesssenders.cpp
@@ -0,0 +1,78 @@
+/*
+ * qtmaildir - a Qt6 mail client for notmuch-indexed Maildirs
+ * Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "businesssenders.h"
+
+#include <QDir>
+#include <QFile>
+#include <QStandardPaths>
+#include <QTextStream>
+
+namespace BusinessSenders {
+
+List parse(const QString &contents)
+{
+ List list;
+ const QStringList lines = contents.split(QLatin1Char('\n'));
+ for (const QString &raw : lines) {
+ const QString line = raw.trimmed();
+ // A commented entry is the reject gesture: it stays in the file so it
+ // is never proposed again, and it is not applied.
+ if (line.isEmpty() || line.startsWith(QLatin1Char('#')))
+ continue;
+
+ const QString entry = line.toLower();
+ if (entry.startsWith(QLatin1Char('@')))
+ list.domains.insert(entry.mid(1));
+ else
+ list.addresses.insert(entry);
+ }
+ return list;
+}
+
+List load(const QString &path)
+{
+ QFile file(path);
+ if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
+ return List();
+ return parse(QString::fromUtf8(file.readAll()));
+}
+
+bool contains(const List &list, const QString &address)
+{
+ const QString lowered = address.trimmed().toLower();
+ if (lowered.isEmpty())
+ return false;
+ if (list.addresses.contains(lowered))
+ return true;
+
+ const int at = lowered.indexOf(QLatin1Char('@'));
+ if (at < 0)
+ return false;
+ return list.domains.contains(lowered.mid(at + 1));
+}
+
+QString defaultPath()
+{
+ const QString base = QStandardPaths::writableLocation(
+ QStandardPaths::GenericConfigLocation);
+ return QDir(base).filePath(
+ QStringLiteral("qtmaildir/business-senders"));
+}
+
+} // namespace BusinessSenders
diff --git a/src/businesssenders.h b/src/businesssenders.h
new file mode 100644
index 0000000..0689453
--- /dev/null
+++ b/src/businesssenders.h
@@ -0,0 +1,58 @@
+/*
+ * qtmaildir - a Qt6 mail client for notmuch-indexed Maildirs
+ * Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#pragma once
+
+#include <QSet>
+#include <QString>
+#include <QStringList>
+
+/// The list of senders that read as businesses rather than people.
+///
+/// `~/.config/qtmaildir/business-senders`, plain text, one entry per line,
+/// `#` comments, blank lines ignored. Deliberately NOT in qtmaildir.conf and
+/// deliberately not INI: the user's stated workflow is grep-and-edit, QSettings
+/// would fight a bare list, and the main config is already large.
+///
+/// An entry is an exact address (`noreply@cofidis.it`) or a whole domain
+/// (`@cofidis.it`). No globs: a pattern language is a rule the user cannot grep
+/// for literally, which defeats the file's purpose.
+namespace BusinessSenders
+{
+
+/// Parsed entries, lower-cased. Two sets rather than one list so a lookup is a
+/// hash probe per repaint rather than a walk.
+struct List
+{
+ QSet<QString> addresses;
+ QSet<QString> domains; ///< Stored WITHOUT the leading '@'.
+};
+
+List parse(const QString &contents);
+
+/// Reads `path`. A missing or unreadable file yields an empty list rather than
+/// an error: the feature is cosmetic and must never block startup.
+List load(const QString &path);
+
+bool contains(const List &list, const QString &address);
+
+/// `~/.config/qtmaildir/business-senders`, built from
+/// QStandardPaths::GenericConfigLocation.
+QString defaultPath();
+
+} // namespace BusinessSenders
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 2b6f846..69c57ee 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -53,6 +53,7 @@ add_qtmaildir_test(notmuchworker)
add_qtmaildir_test(tagcolors)
add_qtmaildir_test(cardlayout)
add_qtmaildir_test(avatar)
+add_qtmaildir_test(businesssenders)
add_qtmaildir_test(marks)
add_qtmaildir_test(carddelegate)
add_qtmaildir_test(threadlistmodel)
diff --git a/tests/test_businesssenders.cpp b/tests/test_businesssenders.cpp
new file mode 100644
index 0000000..64a96fa
--- /dev/null
+++ b/tests/test_businesssenders.cpp
@@ -0,0 +1,101 @@
+/*
+ * qtmaildir - a Qt6 mail client for notmuch-indexed Maildirs
+ * Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include <QTemporaryDir>
+#include <QTest>
+
+#include "businesssenders.h"
+
+class TestBusinessSenders : public QObject
+{
+ Q_OBJECT
+
+private slots:
+ void anExactAddressMatches();
+ void aDomainEntryMatchesEveryAddressUnderIt();
+ void commentsAndBlankLinesAreIgnored();
+ void whitespaceAroundAnEntryIsIgnored();
+ void matchingIsCaseInsensitive();
+ void anAbsentFileMatchesNothing();
+};
+
+void TestBusinessSenders::anExactAddressMatches()
+{
+ const BusinessSenders::List list = BusinessSenders::parse(
+ QStringLiteral("noreply@cofidis.it\n"));
+ QVERIFY(BusinessSenders::contains(list,
+ QStringLiteral("noreply@cofidis.it")));
+ QVERIFY(!BusinessSenders::contains(list,
+ QStringLiteral("someone@cofidis.it")));
+}
+
+void TestBusinessSenders::aDomainEntryMatchesEveryAddressUnderIt()
+{
+ const BusinessSenders::List list =
+ BusinessSenders::parse(QStringLiteral("@cofidis.it\n"));
+ QVERIFY(BusinessSenders::contains(list,
+ QStringLiteral("noreply@cofidis.it")));
+ QVERIFY(BusinessSenders::contains(list,
+ QStringLiteral("billing@cofidis.it")));
+ QVERIFY(!BusinessSenders::contains(list,
+ QStringLiteral("a@example.org")));
+}
+
+void TestBusinessSenders::commentsAndBlankLinesAreIgnored()
+{
+ // A commented entry is the REJECT gesture: present in the file, not
+ // applied. This is the property the whole file format rests on.
+ const BusinessSenders::List list = BusinessSenders::parse(
+ QStringLiteral("# noreply@cofidis.it (47 messages)\n"
+ "\n"
+ " \n"
+ "billing@example.org\n"));
+ QVERIFY(!BusinessSenders::contains(list,
+ QStringLiteral("noreply@cofidis.it")));
+ QVERIFY(BusinessSenders::contains(list,
+ QStringLiteral("billing@example.org")));
+}
+
+void TestBusinessSenders::whitespaceAroundAnEntryIsIgnored()
+{
+ const BusinessSenders::List list =
+ BusinessSenders::parse(QStringLiteral(" billing@example.org \n"));
+ QVERIFY(BusinessSenders::contains(list,
+ QStringLiteral("billing@example.org")));
+}
+
+void TestBusinessSenders::matchingIsCaseInsensitive()
+{
+ // Addresses arrive from headers in whatever case the sender used, so a
+ // list entry that matched only one casing would look broken at random.
+ const BusinessSenders::List list =
+ BusinessSenders::parse(QStringLiteral("NoReply@Cofidis.IT\n"));
+ QVERIFY(BusinessSenders::contains(list,
+ QStringLiteral("noreply@cofidis.it")));
+}
+
+void TestBusinessSenders::anAbsentFileMatchesNothing()
+{
+ QTemporaryDir dir;
+ const BusinessSenders::List list =
+ BusinessSenders::load(dir.filePath(QStringLiteral("does-not-exist")));
+ QVERIFY(!BusinessSenders::contains(list, QStringLiteral("a@example.org")));
+}
+
+QTEST_MAIN(TestBusinessSenders)
+#include "test_businesssenders.moc"