From 1b9188fff9b767192d5d1c62d64df81d2af2fe4d Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Wed, 26 Aug 2026 15:40:11 +0200 Subject: feat: read the business-senders list --- src/CMakeLists.txt | 1 + src/businesssenders.cpp | 78 +++++++++++++++++++++++++++++++ src/businesssenders.h | 58 +++++++++++++++++++++++ tests/CMakeLists.txt | 1 + tests/test_businesssenders.cpp | 101 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 239 insertions(+) create mode 100644 src/businesssenders.cpp create mode 100644 src/businesssenders.h create mode 100644 tests/test_businesssenders.cpp 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. + * + * 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 +#include +#include +#include + +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. + * + * 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 +#include +#include + +/// 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 addresses; + QSet 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. + * + * 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 +#include + +#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" -- cgit v1.2.3