aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-08-17 19:51:27 +0200
committerDanilo M. <danix@danix.xyz>2026-08-17 19:51:27 +0200
commit9899b9af523ad2aef55300c9132d21a687a0a19d (patch)
tree267cb755844190c1bd6fd0714930950bd8ccf93d
parentacec616bee1c7a336a9dcee8338849b970c3eab1 (diff)
downloadqtmaildir-9899b9af523ad2aef55300c9132d21a687a0a19d.tar.gz
qtmaildir-9899b9af523ad2aef55300c9132d21a687a0a19d.zip
feat(config): warn when an account configures no trash folder
The trash key is mandatory: Delete moves a file into it, so an account without one cannot delete at all. Report it as a config problem naming the account and the key, rather than degrading Delete silently, per the existing "a warning the user cannot act on teaches them to ignore warnings" rule (item 83). Several existing test fixtures loaded accounts with no trash key and asserted zero problems/warnings; added trash=Trash to those where it was incidental to what the test actually covers.
-rw-r--r--src/config.cpp15
-rw-r--r--tests/test_config.cpp56
-rw-r--r--translations/qtmaildir_it_IT.ts10
3 files changed, 67 insertions, 14 deletions
diff --git a/src/config.cpp b/src/config.cpp
index d56099a..8294e4b 100644
--- a/src/config.cpp
+++ b/src/config.cpp
@@ -473,6 +473,21 @@ void Config::load(const QString &path)
.arg(account.key));
continue;
}
+
+ // Mandatory, unlike sent: Delete moves a file into this folder, so an
+ // account without one cannot delete at all. Reported rather than
+ // silently disabled, so the user finds out from a warning rather than
+ // from a Delete that quietly does nothing. The account still loads;
+ // only Delete is unusable, which does not warrant losing the rest of
+ // the account's mail.
+ if (account.trash.isEmpty()) {
+ addProblem(
+ tr("Account '%1' has no trash folder configured; add a "
+ "'trash' key to its section. Delete will not work for "
+ "this account until it does.")
+ .arg(account.key));
+ }
+
m_accounts.append(account);
}
diff --git a/tests/test_config.cpp b/tests/test_config.cpp
index 98743f5..10dcf68 100644
--- a/tests/test_config.cpp
+++ b/tests/test_config.cpp
@@ -118,6 +118,7 @@ private slots:
void allDraftsQueryIsIndependentOfSent();
void anAccountCarriesItsTrashFolder();
void aBracketedTrashFolderIsQuoted();
+ void anAccountWithoutATrashFolderWarns();
};
static QString writeIni(const QTemporaryDir &dir, const QString &body)
@@ -430,7 +431,8 @@ void TestConfig::absentSyncCommandIsNoticeNotProblem()
QTemporaryDir dir;
const QString path = writeIni(dir, QStringLiteral(
"[account.work]\n"
- "maildir=work-mail\n"));
+ "maildir=work-mail\n"
+ "trash=Trash\n"));
Config config;
config.load(path);
@@ -450,7 +452,8 @@ void TestConfig::brokenSyncCommandIsAProblem()
"command=/nonexistent/qtmaildir-test/mailsync.sh\n"
"\n"
"[account.work]\n"
- "maildir=work-mail\n"));
+ "maildir=work-mail\n"
+ "trash=Trash\n"));
Config config;
config.load(path);
@@ -485,7 +488,8 @@ void TestConfig::validConfigHasNoProblems()
"\n"
"[account.work]\n"
"maildir=work-mail\n"
- "address=user@example.org\n"));
+ "address=user@example.org\n"
+ "trash=Trash\n"));
Config config;
config.load(path);
@@ -905,7 +909,8 @@ void TestConfig::sentQueryIsEmptyWithoutTheKey()
Config config;
config.load(writeIni(dir, QStringLiteral(
"[account.provider-c]\n"
- "maildir = provider-c\n")));
+ "maildir = provider-c\n"
+ "trash = Trash\n")));
QCOMPARE(config.accounts().size(), 1);
QVERIFY(config.accounts().at(0).sentQuery().isEmpty());
@@ -983,6 +988,27 @@ void TestConfig::aBracketedTrashFolderIsQuoted()
QStringLiteral("path:\"provider-a/[Provider]/Cestino/**\""));
}
+void TestConfig::anAccountWithoutATrashFolderWarns()
+{
+ QTemporaryDir dir;
+ Config config;
+ config.load(writeIni(dir, QStringLiteral(
+ "[account.work]\n"
+ "maildir=work\n")));
+
+ // The account still loads. A missing trash folder disables Delete, it does
+ // not invalidate the account: the user can still read mail.
+ QVERIFY(config.account(QStringLiteral("work")).isValid());
+
+ // Names the account and the key, so the warning is actionable. A warning
+ // the user cannot act on teaches them to ignore warnings, which item 83
+ // recorded the hard way.
+ QVERIFY(!config.problems().isEmpty());
+ const QString joined = config.warnings().join(QLatin1Char('\n'));
+ QVERIFY(joined.contains(QStringLiteral("work")));
+ QVERIFY(joined.contains(QStringLiteral("trash")));
+}
+
void TestConfig::sentQueryComposesWithScopedQuery()
{
// A Sent view under one account must not show another account's sent mail.
@@ -1103,9 +1129,11 @@ void TestConfig::theStartupAccountIsReadAndValidated()
"\n"
"[account.work]\n"
"maildir=work\n"
+ "trash=Trash\n"
"\n"
"[account.personal]\n"
- "maildir=personal\n")));
+ "maildir=personal\n"
+ "trash=Trash\n")));
QCOMPARE(config.startupAccount(), QStringLiteral("work"));
QVERIFY(config.problems().isEmpty());
@@ -1130,7 +1158,8 @@ void TestConfig::theStartupAccountIsReadAndValidated()
"startup_account=nosuchaccount\n"
"\n"
"[account.work]\n"
- "maildir=work\n")));
+ "maildir=work\n"
+ "trash=Trash\n")));
QVERIFY2(wrong.startupAccount().isEmpty(),
"an unknown startup account was passed through rather than "
"falling back to All accounts");
@@ -1155,7 +1184,8 @@ void TestConfig::theStartupAccountTakesTheKeyNotTheSyncChannel()
"\n"
"[account.provider-work.mailbox]\n"
"maildir=provider-work.mailbox\n"
- "channel=provider-workmailbox\n")));
+ "channel=provider-workmailbox\n"
+ "trash=Trash\n")));
QCOMPARE(config.accounts().size(), 1);
QCOMPARE(config.accounts().constFirst().key,
@@ -1177,7 +1207,8 @@ void TestConfig::theStartupAccountTakesTheKeyNotTheSyncChannel()
"\n"
"[account.provider-work.mailbox]\n"
"maildir=provider-work.mailbox\n"
- "channel=provider-workmailbox\n")));
+ "channel=provider-workmailbox\n"
+ "trash=Trash\n")));
QVERIFY2(byChannel.startupAccount().isEmpty(),
"the sync channel was accepted as an account key");
@@ -1261,7 +1292,8 @@ void TestConfig::theStartupQuerySurvivesATranslatedFilterName()
"\n"
"[account.work]\n"
"maildir=work\n"
- "sent=Sent\n")));
+ "sent=Sent\n"
+ "trash=Trash\n")));
QVERIFY2(!config.savedQueries().isEmpty(),
"queries.json did not load, so the warning path is unreachable");
@@ -1286,7 +1318,8 @@ void TestConfig::theStartupQuerySurvivesATranslatedFilterName()
"\n"
"[account.work]\n"
"maildir=work\n"
- "sent=Sent\n")));
+ "sent=Sent\n"
+ "trash=Trash\n")));
QVERIFY(!byLabel.savedQueries().isEmpty());
QCOMPARE(byLabel.startupSavedQuery().generated, QStringLiteral("inbox"));
QVERIFY(byLabel.problems().isEmpty());
@@ -1513,7 +1546,8 @@ void TestConfig::draftsQueryIsEmptyWithoutTheKey()
Config config;
config.load(writeIni(dir, QStringLiteral(
"[account.provider-c]\n"
- "maildir = provider-c\n")));
+ "maildir = provider-c\n"
+ "trash = Trash\n")));
QCOMPARE(config.accounts().size(), 1);
QVERIFY(config.accounts().at(0).draftsQuery().isEmpty());
diff --git a/translations/qtmaildir_it_IT.ts b/translations/qtmaildir_it_IT.ts
index 036ce4b..fd51547 100644
--- a/translations/qtmaildir_it_IT.ts
+++ b/translations/qtmaildir_it_IT.ts
@@ -44,6 +44,10 @@
<translation>[completion] extra_mimetypes: la voce &apos;%1&apos; non ha un mimetype; verrà ignorata.</translation>
</message>
<message>
+ <source>Account &apos;%1&apos; has no trash folder configured; add a &apos;trash&apos; key to its section. Delete will not work for this account until it does.</source>
+ <translation>L&apos;account &apos;%1&apos; non ha un cestino configurato; aggiungere una chiave &apos;trash&apos; alla sua sezione. L&apos;eliminazione non funzionerà per questo account finché non verrà fatto.</translation>
+ </message>
+ <message>
<source>Startup account &apos;%1&apos; is not a configured account; starting on all accounts.</source>
<translation>L&apos;account iniziale &apos;%1&apos; non è un account configurato; si parte da tutti gli account.</translation>
</message>
@@ -280,7 +284,7 @@
</message>
<message>
<source>Add or remove the important tag</source>
- <translation>Aggiunge o rimuove l'etichetta importante</translation>
+ <translation>Aggiunge o rimuove l&apos;etichetta importante</translation>
</message>
<message>
<source>Unmark important</source>
@@ -340,7 +344,7 @@
</message>
<message>
<source>Add or remove the deleted tag on whole threads</source>
- <translation>Aggiunge o rimuove l'etichetta eliminato su intere conversazioni</translation>
+ <translation>Aggiunge o rimuove l&apos;etichetta eliminato su intere conversazioni</translation>
</message>
<message>
<source>Undelete thread</source>
@@ -364,7 +368,7 @@
</message>
<message>
<source>Toggle the unread tag on whole threads</source>
- <translation>Inverte l'etichetta non letto su intere conversazioni</translation>
+ <translation>Inverte l&apos;etichetta non letto su intere conversazioni</translation>
</message>
<message>
<source>Mark thread read</source>