1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
|
/*
* 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 "searchterm.h"
#include "config.h"
#include <algorithm>
namespace SearchTerm {
QString quote(const QString &value)
{
// simplified() collapses every run of whitespace, newlines and tabs
// included, and trims the ends. A selection spanning paragraphs arrives
// full of newlines, which would otherwise reach the query bar verbatim.
QString cleaned = value.simplified();
if (cleaned.isEmpty())
return {};
if (cleaned.size() > kMaxValueLength)
cleaned.truncate(kMaxValueLength);
// notmuch escapes a quote inside a quoted term by DOUBLING it and has no
// backslash escape at all. A backslash-escaped quote ENDS the term, so
// `subject:"x \" or tag:inbox or id:"` let a selection walk out of the
// query it was meant to narrow: measured matching every message under a
// `tag:nomatch and` scope, where the doubled form matched none. A
// backslash is literal to notmuch and is left as it is.
cleaned.replace(QLatin1Char('"'), QStringLiteral("\"\""));
return QLatin1Char('"') + cleaned + QLatin1Char('"');
}
QString field(const QString &name, const QString &value)
{
const QString quoted = quote(value);
if (quoted.isEmpty())
return {};
return name + QLatin1Char(':') + quoted;
}
QString onDate(const QDate &day)
{
if (!day.isValid())
return {};
const QString text = day.toString(QStringLiteral("yyyy-MM-dd"));
return QStringLiteral("date:%1..%1").arg(text);
}
QString tag(const QString &name)
{
const QString trimmed = name.simplified();
if (trimmed.isEmpty())
return {};
// A tag is a token from a vocabulary the user chose, so it reads better
// unquoted in the bar they are about to edit. Quoted only when it holds
// something that would not survive.
const bool needsQuoting =
std::any_of(trimmed.cbegin(), trimmed.cend(), [](QChar ch) {
return !(ch.isLetterOrNumber() || ch == QLatin1Char('-')
|| ch == QLatin1Char('_') || ch == QLatin1Char('.')
|| ch == QLatin1Char('/'));
});
return QStringLiteral("tag:") + (needsQuoting ? quote(trimmed) : trimmed);
}
QString extend(const QString &existing, const QString &addition)
{
const QString left = existing.trimmed();
const QString right = addition.trimmed();
if (right.isEmpty())
return left;
if (left.isEmpty())
return right;
return QStringLiteral("(%1) AND (%2)").arg(left, right);
}
QString exclude(const QString &existing, const QString &addition)
{
const QString left = existing.trimmed();
const QString right = addition.trimmed();
if (right.isEmpty())
return left;
// NOT a replace, unlike extend(): see the header. An empty left would make
// this "everything except", which no right-click asked for.
if (left.isEmpty())
return QString();
return QStringLiteral("(%1) AND NOT (%2)").arg(left, right);
}
QString changes(const QString &query, const QStringList &add,
const QStringList &remove)
{
QStringList terms;
for (const QString &name : add) {
const QString term = tag(name);
if (!term.isEmpty())
terms.append(QStringLiteral("not ") + term);
}
for (const QString &name : remove) {
const QString term = tag(name);
if (!term.isEmpty())
terms.append(term);
}
if (query.trimmed().isEmpty() || terms.isEmpty())
return Config::matchNothingQuery();
return extend(query, terms.join(QStringLiteral(" or ")));
}
} // namespace SearchTerm
|