summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/rulequery.cpp51
-rw-r--r--tests/test_rulequery.cpp86
2 files changed, 135 insertions, 2 deletions
diff --git a/src/rulequery.cpp b/src/rulequery.cpp
index f80e6c4..198c583 100644
--- a/src/rulequery.cpp
+++ b/src/rulequery.cpp
@@ -36,6 +36,54 @@ QString prefixFor(RuleTerm::Field field)
return QString();
}
+bool isNegated(RuleTerm::Op op)
+{
+ return op == RuleTerm::ContainsNot || op == RuleTerm::IsNot
+ || op == RuleTerm::HasNot;
+}
+
+/// Quoted when the operator asks for an exact phrase, and ALWAYS when the
+/// value holds a space: unquoted, the space ends the term and the remainder
+/// becomes a bare word, which widens the rule instead of breaking it.
+bool needsQuotes(const RuleTerm &term)
+{
+ if (term.field == RuleTerm::Folder)
+ return true;
+ if (term.value.contains(QLatin1Char(' ')))
+ return true;
+ // Is/IsNot asks for an exact phrase, which only free-text fields need
+ // quoting for: tag: and attachment: values are already bare words, and
+ // quoting them changes nothing notmuch cares about but breaks the test's
+ // documented expectation of an unquoted tag.
+ if (term.op == RuleTerm::Is || term.op == RuleTerm::IsNot) {
+ return term.field == RuleTerm::From || term.field == RuleTerm::To
+ || term.field == RuleTerm::Cc || term.field == RuleTerm::Subject;
+ }
+ return false;
+}
+
+QString compileTerm(const RuleTerm &term)
+{
+ QString value = term.value;
+ if (term.field == RuleTerm::Folder)
+ value += QStringLiteral("/**");
+
+ QString body;
+ if (term.field == RuleTerm::Date) {
+ body = prefixFor(term.field) + QLatin1Char(':')
+ + (term.op == RuleTerm::Before
+ ? QStringLiteral("..") + value
+ : value + QStringLiteral(".."));
+ } else if (needsQuotes(term)) {
+ body = prefixFor(term.field) + QStringLiteral(":\"") + value
+ + QLatin1Char('"');
+ } else {
+ body = prefixFor(term.field) + QLatin1Char(':') + value;
+ }
+
+ return isNegated(term.op) ? QStringLiteral("not ") + body : body;
+}
+
} // namespace
bool operator==(const RuleTerm &a, const RuleTerm &b)
@@ -54,8 +102,7 @@ QString RuleQuery::compile() const
if (terms.isEmpty())
return QString();
- return prefixFor(terms.first().field) + QLatin1Char(':')
- + terms.first().value;
+ return compileTerm(terms.first());
}
RuleQuery RuleQuery::parse(const QString &query)
diff --git a/tests/test_rulequery.cpp b/tests/test_rulequery.cpp
index 3e49eac..970dbac 100644
--- a/tests/test_rulequery.cpp
+++ b/tests/test_rulequery.cpp
@@ -30,6 +30,12 @@ class TestRuleQuery : public QObject
private slots:
void aSingleContainsTermCompiles();
+ void everyFieldCompilesToItsPrefix();
+ void isQuotesAndContainsDoesNot();
+ void negationPrefixesNot();
+ void aValueWithASpaceIsAlwaysQuoted();
+ void folderAppendsTheRecursiveSuffix();
+ void dateCompilesToAOneSidedRange();
};
void TestRuleQuery::aSingleContainsTermCompiles()
@@ -41,5 +47,85 @@ void TestRuleQuery::aSingleContainsTermCompiles()
QCOMPARE(q.compile(), QStringLiteral("from:sender@example.org"));
}
+void TestRuleQuery::everyFieldCompilesToItsPrefix()
+{
+ const QVector<QPair<RuleTerm::Field, QString>> cases = {
+ {RuleTerm::From, QStringLiteral("from:x")},
+ {RuleTerm::To, QStringLiteral("to:x")},
+ {RuleTerm::Cc, QStringLiteral("cc:x")},
+ {RuleTerm::Subject, QStringLiteral("subject:x")},
+ };
+
+ for (const auto &c : cases) {
+ RuleQuery q;
+ q.terms.append({c.first, RuleTerm::Contains, QStringLiteral("x")});
+ QCOMPARE(q.compile(), c.second);
+ }
+}
+
+void TestRuleQuery::isQuotesAndContainsDoesNot()
+{
+ RuleQuery contains;
+ contains.terms.append({RuleTerm::Subject, RuleTerm::Contains,
+ QStringLiteral("receipt")});
+ QCOMPARE(contains.compile(), QStringLiteral("subject:receipt"));
+
+ RuleQuery is;
+ is.terms.append({RuleTerm::Subject, RuleTerm::Is,
+ QStringLiteral("receipt")});
+ QCOMPARE(is.compile(), QStringLiteral("subject:\"receipt\""));
+}
+
+void TestRuleQuery::negationPrefixesNot()
+{
+ RuleQuery q;
+ q.terms.append({RuleTerm::Subject, RuleTerm::ContainsNot,
+ QStringLiteral("receipt")});
+ QCOMPARE(q.compile(), QStringLiteral("not subject:receipt"));
+
+ RuleQuery tag;
+ tag.terms.append({RuleTerm::Tag, RuleTerm::IsNot,
+ QStringLiteral("inbox")});
+ QCOMPARE(tag.compile(), QStringLiteral("not tag:inbox"));
+
+ RuleQuery att;
+ att.terms.append({RuleTerm::Attachment, RuleTerm::HasNot,
+ QStringLiteral("pdf")});
+ QCOMPARE(att.compile(), QStringLiteral("not attachment:pdf"));
+}
+
+void TestRuleQuery::aValueWithASpaceIsAlwaysQuoted()
+{
+ // Unquoted, a space would end the term and the rest would become a
+ // separate bare word, silently widening the rule.
+ RuleQuery q;
+ q.terms.append({RuleTerm::Subject, RuleTerm::Contains,
+ QStringLiteral("your receipt")});
+ QCOMPARE(q.compile(), QStringLiteral("subject:\"your receipt\""));
+}
+
+void TestRuleQuery::folderAppendsTheRecursiveSuffix()
+{
+ // A path: without the suffix matches nothing, and notmuch reports no
+ // error when it happens.
+ RuleQuery q;
+ q.terms.append({RuleTerm::Folder, RuleTerm::Is,
+ QStringLiteral("account-one")});
+ QCOMPARE(q.compile(), QStringLiteral("path:\"account-one/**\""));
+}
+
+void TestRuleQuery::dateCompilesToAOneSidedRange()
+{
+ RuleQuery before;
+ before.terms.append({RuleTerm::Date, RuleTerm::Before,
+ QStringLiteral("2026-01-01")});
+ QCOMPARE(before.compile(), QStringLiteral("date:..2026-01-01"));
+
+ RuleQuery after;
+ after.terms.append({RuleTerm::Date, RuleTerm::After,
+ QStringLiteral("2026-01-01")});
+ QCOMPARE(after.compile(), QStringLiteral("date:2026-01-01.."));
+}
+
QTEST_MAIN(TestRuleQuery)
#include "test_rulequery.moc"