MailJDTourDownloadDocsQualityTestsCoverage

Which source lines and branches the tests execute. Lines 91.5 %, branches 75.0 % (minimum 85 % and 72 %). main @ 399ffe3d · 21 October 2026 · pipeline 7238.

GCC Code Coverage Report


Directory: src/
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 94.2% 553 / 0 / 587
Functions: 100.0% 49 / 0 / 49
Branches: 73.9% 731 / 0 / 989

presentation/QuickRuleEdits.cpp
Line Branch Exec Source
1 // Editing half of QuickRulesBackend: drafts, guards, saves, quick actions
2 // with undo, health fixes, script editor, conflicts and out of office.
3 // Every change becomes script bytes and goes through SieveSession::save.
4 #include "QuickRulesBackend.h"
5
6 #include "RuleEditorFields.h"
7 #include "RuleMatcher.h"
8 #include "RuleSentence.h"
9
10 #include <QDate>
11 #include <QLocale>
12 #include <QLoggingCategory>
13 #include <QTimeZone>
14 #include <QTimer>
15
16 namespace {
17
18 2 bool hasNonAscii(const QString &text) {
19
2/2
✓ Branch 8 → 4 taken 6 times.
✓ Branch 8 → 9 taken 1 time.
7 for (const QChar c : text)
20
2/2
✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 7 taken 5 times.
6 if (c.unicode() > 0x7f)
21 1 return true;
22 1 return false;
23 }
24
25 20 QString issueKind(Sieve::GuardIssue::Kind kind) {
26 using K = Sieve::GuardIssue::Kind;
27
11/12
✓ Branch 2 → 3 taken 4 times.
✓ Branch 2 → 8 taken 1 time.
✓ Branch 2 → 13 taken 1 time.
✓ Branch 2 → 18 taken 1 time.
✓ Branch 2 → 23 taken 3 times.
✓ Branch 2 → 28 taken 2 times.
✓ Branch 2 → 33 taken 1 time.
✓ Branch 2 → 38 taken 4 times.
✓ Branch 2 → 43 taken 1 time.
✓ Branch 2 → 48 taken 1 time.
✓ Branch 2 → 53 taken 1 time.
✗ Branch 2 → 58 not taken.
20 switch (kind) {
28 8 case K::NoActions: return QStringLiteral("noActions");
29 2 case K::EmptyValue: return QStringLiteral("emptyValue");
30 2 case K::InvalidAddress: return QStringLiteral("invalidAddress");
31 2 case K::InvalidRegex: return QStringLiteral("invalidRegex");
32 6 case K::UnsupportedExtension: return QStringLiteral("unsupported");
33 4 case K::ForwardToOwnAddress: return QStringLiteral("forwardToSelf");
34 2 case K::TooManyRedirects: return QStringLiteral("tooManyRedirects");
35 8 case K::ForwardNeedsConfirmation: return QStringLiteral("confirmForward");
36 2 case K::DestructiveNeedsConfirmation: return QStringLiteral("confirmDestructive");
37 2 case K::EveryMessageNeedsConfirmation: return QStringLiteral("confirmEveryMessage");
38 2 case K::RejectWithDelivery: return QStringLiteral("rejectWithDelivery");
39 }
40 ✗ return {};
41 }
42
43 } // namespace
44
45 // --- results ------------------------------------------------------------------------
46
47 96 QVariantMap QuickRulesBackend::outcomeMap(const SieveSession::SaveOutcome &outcome) const {
48 using Kind = SieveSession::SaveOutcome::Kind;
49
2/4
✓ Branch 13 → 14 taken 192 times.
✓ Branch 13 → 15 taken 96 times.
✗ Branch 185 → 186 not taken.
✗ Branch 185 → 187 not taken.
480 QVariantMap map{{QStringLiteral("ok"), outcome.ok()}, {QStringLiteral("race"), outcome.raceDetected}};
50 96 QVariantList errors;
51
2/2
✓ Branch 50 → 21 taken 4 times.
✓ Branch 50 → 51 taken 96 times.
100 for (const ManageSieve::ServerError &error : outcome.errors)
52
1/1
✓ Branch 36 → 37 taken 4 times.
12 errors.append(QVariantMap{{QStringLiteral("line"), error.line},
53 8 {QStringLiteral("column"), error.column},
54
2/4
✓ Branch 40 → 41 taken 12 times.
✓ Branch 40 → 42 taken 4 times.
✗ Branch 212 → 213 not taken.
✗ Branch 212 → 214 not taken.
24 {QStringLiteral("message"), error.message}});
55
1/1
✓ Branch 55 → 56 taken 96 times.
192 map.insert(QStringLiteral("errors"), errors);
56
1/1
✓ Branch 63 → 64 taken 96 times.
192 map.insert(QStringLiteral("details"), outcome.message);
57 96 QString kind;
58 96 QString message;
59
7/9
✓ Branch 67 → 68 taken 74 times.
✓ Branch 67 → 75 taken 2 times.
✓ Branch 67 → 85 taken 8 times.
✓ Branch 67 → 95 taken 4 times.
✓ Branch 67 → 113 taken 2 times.
✓ Branch 67 → 123 taken 1 time.
✗ Branch 67 → 133 not taken.
✓ Branch 67 → 143 taken 5 times.
✗ Branch 67 → 158 not taken.
96 switch (outcome.kind) {
60 148 case Kind::Saved: kind = QStringLiteral("saved"); break;
61 2 case Kind::Merged:
62 2 kind = QStringLiteral("merged");
63
1/1
✓ Branch 81 → 82 taken 2 times.
2 message = tr("Merged with changes made elsewhere.");
64 2 break;
65 8 case Kind::Conflict:
66 8 kind = QStringLiteral("conflict");
67
1/1
✓ Branch 91 → 92 taken 8 times.
8 message = tr("The rules were changed elsewhere.");
68 8 break;
69 4 case Kind::Rejected:
70 4 kind = QStringLiteral("rejected");
71
1/3
✗ Branch 102 → 103 not taken.
✓ Branch 102 → 104 taken 4 times.
✗ Branch 103 → 108 not taken.
12 message = outcome.errors.isEmpty() ? tr("The server did not accept the rules.")
72
3/6
✓ Branch 104 → 105 taken 4 times.
✓ Branch 107 → 108 taken 4 times.
✓ Branch 110 → 111 taken 4 times.
✗ Branch 110 → 112 not taken.
✗ Branch 263 → 264 not taken.
✗ Branch 263 → 265 not taken.
12 : tr("The server did not accept line %1.").arg(outcome.errors.first().line);
73 4 break;
74 2 case Kind::Quota:
75 2 kind = QStringLiteral("quota");
76
1/1
✓ Branch 119 → 120 taken 2 times.
2 message = tr("The rules are too large for the space the server allows.");
77 2 break;
78 1 case Kind::MergeReview:
79 1 kind = QStringLiteral("mergeReview");
80
1/1
✓ Branch 129 → 130 taken 1 time.
1 message = tr("The rules were changed elsewhere. Review the merged version before uploading it.");
81 1 break;
82 ✗ case Kind::NotLoaded:
83 ✗ kind = QStringLiteral("notLoaded");
84 ✗ message = tr("Rules can't be changed right now. Reload them and try again.");
85 ✗ break;
86 5 case Kind::Failed:
87 5 kind = QStringLiteral("failed");
88
3/3
✓ Branch 149 → 150 taken 5 times.
✓ Branch 151 → 152 taken 5 times.
✓ Branch 152 → 153 taken 5 times.
10 message = tr("Not saved to the server: %1").arg(messageFor(outcome.transportError, m_discovery.server.host));
89 5 break;
90 }
91
1/2
✗ Branch 158 → 159 not taken.
✓ Branch 158 → 163 taken 96 times.
96 if (outcome.raceDetected)
92 ✗ message = tr("Changed elsewhere during saving — check the rules.");
93
1/1
✓ Branch 167 → 168 taken 96 times.
192 map.insert(QStringLiteral("kind"), kind);
94
1/1
✓ Branch 175 → 176 taken 96 times.
192 map.insert(QStringLiteral("message"), message);
95 96 return map;
96 400 }
97
98 25 QString QuickRulesBackend::refuseEdit(const QString &message) {
99 25 const QString requestId = nextRequestId();
100
1/3
✓ Branch 5 → 6 taken 25 times.
✗ Branch 11 → 12 not taken.
✗ Branch 11 → 13 not taken.
25 QTimer::singleShot(0, this, [this, requestId, message] {
101
3/5
✓ Branch 18 → 19 taken 24 times.
✓ Branch 21 → 22 taken 72 times.
✓ Branch 21 → 23 taken 24 times.
✗ Branch 35 → 36 not taken.
✗ Branch 35 → 37 not taken.
144 emit editFinished(requestId, {{QStringLiteral("ok"), false},
102 72 {QStringLiteral("kind"), QStringLiteral("readOnly")},
103 48 {QStringLiteral("message"), message}});
104
1/5
✓ Branch 17 → 18 taken 24 times.
✗ Branch 63 → 64 not taken.
✗ Branch 63 → 67 not taken.
✗ Branch 65 → 66 not taken.
✗ Branch 65 → 67 not taken.
144 });
105 25 return requestId;
106 ✗ }
107
108 // Session callbacks can run before the caller holds the request id (no
109 // password, a server without CHECKSCRIPT): results always arrive from the
110 // event loop.
111 99 void QuickRulesBackend::finishLater(const QString &requestId, const QVariantMap &result) {
112
1/3
✓ Branch 4 → 5 taken 99 times.
✗ Branch 9 → 10 not taken.
✗ Branch 9 → 11 not taken.
198 QTimer::singleShot(0, this, [this, requestId, result] { emit editFinished(requestId, result); });
113 99 }
114
115 // Undo keeps the last MaxUndoScripts versions (each up to the 1 MiB script cap).
116 71 void QuickRulesBackend::pushUndoScript(const QByteArray &previous) {
117 71 m_undo.append(previous);
118
2/2
✓ Branch 6 → 4 taken 5 times.
✓ Branch 6 → 7 taken 71 times.
76 while (m_undo.size() > MaxUndoScripts)
119 5 m_undo.removeFirst();
120 71 emit undoChanged();
121 71 }
122
123 57 QString QuickRulesBackend::submit(const Edit &edit) {
124
1/1
✓ Branch 2 → 3 taken 57 times.
57 SieveSession *s = session();
125
4/7
✓ Branch 3 → 4 taken 57 times.
✗ Branch 3 → 6 not taken.
✓ Branch 4 → 5 taken 57 times.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 57 times.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 14 taken 57 times.
57 if (!s || readOnly())
126 ✗ return refuseEdit(tr("Rules can't be changed right now. Reload them and try again."));
127
1/1
✓ Branch 14 → 15 taken 57 times.
57 const QString requestId = nextRequestId();
128 57 const QByteArray previous = s->baseScript();
129 57 SieveSession::SaveOptions options;
130 57 options.reason = edit.reason;
131 57 options.renderedFrom = previous; // S106-319: the edit was rendered from this base
132
1/1
✓ Branch 18 → 19 taken 57 times.
57 QPointer<QuickRulesBackend> guard(this);
133
2/8
✓ Branch 23 → 24 taken 57 times.
✓ Branch 24 → 25 taken 57 times.
✗ Branch 41 → 42 not taken.
✗ Branch 41 → 43 not taken.
✗ Branch 44 → 45 not taken.
✗ Branch 44 → 46 not taken.
✗ Branch 47 → 48 not taken.
✗ Branch 47 → 49 not taken.
57 s->save(edit.bytes, options, [this, guard, edit, previous, requestId](const SieveSession::SaveOutcome &outcome) {
134
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 57 times.
57 if (!guard)
135 ✗ return;
136
1/1
✓ Branch 5 → 6 taken 57 times.
57 const QVariantMap result = outcomeMap(outcome);
137
2/2
✓ Branch 7 → 8 taken 45 times.
✓ Branch 7 → 60 taken 12 times.
57 if (outcome.ok()) {
138
5/6
✓ Branch 8 → 9 taken 43 times.
✓ Branch 8 → 12 taken 2 times.
✓ Branch 10 → 11 taken 43 times.
✗ Branch 10 → 12 not taken.
✓ Branch 13 → 14 taken 43 times.
✓ Branch 13 → 15 taken 2 times.
45 if (edit.undoable && !previous.isNull()) {
139
1/1
✓ Branch 14 → 15 taken 43 times.
43 pushUndoScript(previous);
140 }
141
1/1
✓ Branch 15 → 16 taken 45 times.
45 clearConflict();
142 45 QString text = edit.successText;
143
2/2
✓ Branch 17 → 18 taken 2 times.
✓ Branch 17 → 29 taken 43 times.
45 if (outcome.kind == SieveSession::SaveOutcome::Kind::Merged)
144
3/3
✓ Branch 18 → 19 taken 2 times.
✓ Branch 22 → 23 taken 2 times.
✓ Branch 23 → 24 taken 2 times.
4 text += QStringLiteral(" · ") + tr("Merged with changes made elsewhere.");
145 45 int created = -1;
146
1/1
✓ Branch 29 → 30 taken 45 times.
45 const SieveSession *saved = session();
147
5/6
✓ Branch 31 → 32 taken 4 times.
✓ Branch 31 → 34 taken 41 times.
✓ Branch 32 → 33 taken 4 times.
✗ Branch 32 → 34 not taken.
✓ Branch 35 → 36 taken 4 times.
✓ Branch 35 → 55 taken 41 times.
45 if (!edit.createdRule.isEmpty() && saved) {
148 // Where it was placed, unless a merge with changes made elsewhere
149 // moved it; then the last rule of that name.
150 4 const auto &blocks = saved->document().blocks;
151 4 const auto isCreated = [&](int i) {
152
2/4
✓ Branch 3 → 4 taken 4 times.
✗ Branch 3 → 8 not taken.
✓ Branch 6 → 7 taken 4 times.
✗ Branch 6 → 8 not taken.
4 return blocks.at(i).kind == Sieve::BlockKind::Rule && blocks.at(i).rule.name == edit.createdRule;
153 4 };
154
4/8
✓ Branch 37 → 38 taken 4 times.
✗ Branch 37 → 43 not taken.
✓ Branch 39 → 40 taken 4 times.
✗ Branch 39 → 43 not taken.
✓ Branch 41 → 42 taken 4 times.
✗ Branch 41 → 43 not taken.
✓ Branch 44 → 45 taken 4 times.
✗ Branch 44 → 46 not taken.
4 if (edit.createdBlock >= 0 && edit.createdBlock < blocks.size() && isCreated(edit.createdBlock))
155 4 created = edit.createdBlock;
156
2/4
✓ Branch 52 → 53 taken 4 times.
✗ Branch 52 → 54 not taken.
✗ Branch 53 → 48 not taken.
✓ Branch 53 → 54 taken 4 times.
4 for (int i = int(blocks.size()) - 1; i >= 0 && created < 0; --i)
157 ✗ if (isCreated(i)) created = i;
158 }
159
3/3
✓ Branch 55 → 56 taken 4 times.
✓ Branch 55 → 57 taken 41 times.
✓ Branch 56 → 58 taken 4 times.
45 if (created >= 0) emit ruleCreated(text, created);
160
1/1
✓ Branch 57 → 58 taken 41 times.
41 else emit toast(text, edit.undoable);
161
2/2
✓ Branch 60 → 61 taken 7 times.
✓ Branch 60 → 62 taken 5 times.
57 } else if (outcome.kind == SieveSession::SaveOutcome::Kind::Conflict) {
162
1/1
✓ Branch 61 → 62 taken 7 times.
7 holdConflict(edit.bytes, previous, outcome);
163 }
164
1/1
✓ Branch 62 → 63 taken 57 times.
57 finishLater(requestId, result);
165 57 });
166 57 return requestId;
167 57 }
168
169 // --- drafts ---------------------------------------------------------------------------
170
171 31 QVariantMap QuickRulesBackend::draftFor(int blockIndex) const {
172
1/1
✓ Branch 2 → 3 taken 31 times.
31 const SieveSession *s = session();
173
7/8
✓ Branch 3 → 4 taken 31 times.
✗ Branch 3 → 8 not taken.
✓ Branch 4 → 5 taken 30 times.
✓ Branch 4 → 8 taken 1 time.
✓ Branch 7 → 8 taken 1 time.
✓ Branch 7 → 9 taken 29 times.
✓ Branch 10 → 11 taken 2 times.
✓ Branch 10 → 12 taken 29 times.
31 if (!s || blockIndex < 0 || blockIndex >= s->document().blocks.size())
174 2 return {};
175 29 const Sieve::Block &block = s->document().blocks.at(blockIndex);
176
1/2
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 16 taken 29 times.
29 if (block.kind != Sieve::BlockKind::Rule)
177 ✗ return {};
178
1/1
✓ Branch 16 → 17 taken 29 times.
29 QVariantMap draft = RuleEditorFields::fromRule(block.rule);
179
2/4
✓ Branch 17 → 18 taken 29 times.
✗ Branch 17 → 19 not taken.
✗ Branch 19 → 20 not taken.
✓ Branch 24 → 25 taken 29 times.
58 draft.insert(QStringLiteral("name"), block.named ? block.name : RuleSentence::generatedName(block.rule));
180
1/1
✓ Branch 33 → 34 taken 29 times.
58 draft.insert(QStringLiteral("blockIndex"), blockIndex);
181
2/2
✓ Branch 37 → 38 taken 29 times.
✓ Branch 42 → 43 taken 29 times.
58 draft.insert(QStringLiteral("position"), positionOf(blockIndex));
182 29 return draft;
183 29 }
184
185 2 QVariantMap QuickRulesBackend::newDraft() const {
186
1/1
✓ Branch 2 → 3 taken 2 times.
2 const SieveSession *s = session();
187
2/4
✓ Branch 3 → 4 taken 2 times.
✗ Branch 3 → 7 not taken.
✓ Branch 5 → 6 taken 2 times.
✗ Branch 5 → 7 not taken.
2 const bool stop = s && s->document().style.stopByDefault;
188 4 return {{QStringLiteral("name"), QString()},
189 4 {QStringLiteral("enabled"), true},
190 4 {QStringLiteral("matchAll"), true},
191 4 {QStringLiteral("stop"), stop},
192 4 {QStringLiteral("blockIndex"), -1},
193 4 {QStringLiteral("conditions"),
194 8 QVariantList{QVariantMap{{QStringLiteral("field"), QStringLiteral("from")},
195 6 {QStringLiteral("op"), QStringLiteral("contains")},
196
4/8
✓ Branch 97 → 98 taken 2 times.
✓ Branch 97 → 99 taken 2 times.
✓ Branch 101 → 102 taken 6 times.
✓ Branch 101 → 103 taken 2 times.
✗ Branch 189 → 190 not taken.
✗ Branch 189 → 191 not taken.
✗ Branch 195 → 196 not taken.
✗ Branch 195 → 197 not taken.
16 {QStringLiteral("values"), QStringList{}}}}},
197 4 {QStringLiteral("actions"),
198 10 QVariantList{QVariantMap{{QStringLiteral("kind"), QStringLiteral("move")},
199
6/12
✓ Branch 76 → 77 taken 14 times.
✓ Branch 76 → 78 taken 2 times.
✓ Branch 80 → 81 taken 2 times.
✓ Branch 80 → 82 taken 2 times.
✓ Branch 84 → 85 taken 4 times.
✓ Branch 84 → 86 taken 2 times.
✗ Branch 130 → 131 not taken.
✗ Branch 130 → 132 not taken.
✗ Branch 136 → 137 not taken.
✗ Branch 136 → 138 not taken.
✗ Branch 142 → 143 not taken.
✗ Branch 142 → 144 not taken.
32 {QStringLiteral("target"), QString()}}}}};
200 30 }
201
202 155 Sieve::Rule QuickRulesBackend::prepareRule(const QVariantMap &draft) const {
203 155 Sieve::Rule rule = RuleEditorFields::toRule(draft);
204
2/2
✓ Branch 4 → 5 taken 90 times.
✓ Branch 4 → 9 taken 65 times.
155 if (rule.name.isEmpty())
205
1/1
✓ Branch 5 → 6 taken 90 times.
90 rule.name = RuleSentence::generatedName(rule);
206 // D-13: umlauts match case-insensitively only with i;unicode-casemap.
207
1/1
✓ Branch 9 → 10 taken 155 times.
155 const SieveSession *s = session();
208
5/8
✓ Branch 10 → 11 taken 155 times.
✗ Branch 10 → 18 not taken.
✓ Branch 16 → 17 taken 2 times.
✓ Branch 16 → 18 taken 153 times.
✓ Branch 19 → 20 taken 155 times.
✗ Branch 19 → 21 not taken.
✓ Branch 21 → 22 taken 155 times.
✗ Branch 21 → 23 not taken.
310 const bool unicode = s && s->capabilities().supports(QStringLiteral("comparator-i;unicode-casemap"));
209
4/4
✓ Branch 23 → 24 taken 155 times.
✓ Branch 24 → 25 taken 155 times.
✓ Branch 56 → 26 taken 175 times.
✓ Branch 56 → 57 taken 155 times.
330 for (Sieve::Condition &condition : rule.conditions) {
210
6/8
✓ Branch 27 → 28 taken 2 times.
✓ Branch 27 → 33 taken 173 times.
✓ Branch 29 → 30 taken 2 times.
✗ Branch 29 → 33 not taken.
✗ Branch 32 → 33 not taken.
✓ Branch 32 → 34 taken 2 times.
✓ Branch 35 → 36 taken 173 times.
✓ Branch 35 → 37 taken 2 times.
175 if (!unicode || !condition.comparator.isEmpty() || condition.matchType == QLatin1String("regex"))
211 173 continue;
212
4/4
✓ Branch 37 → 38 taken 2 times.
✓ Branch 38 → 39 taken 2 times.
✓ Branch 52 → 40 taken 2 times.
✓ Branch 52 → 53 taken 1 time.
3 for (const QString &value : condition.values)
213
2/2
✓ Branch 42 → 43 taken 1 time.
✓ Branch 42 → 50 taken 1 time.
2 if (hasNonAscii(value)) {
214 1 condition.comparator = QStringLiteral("i;unicode-casemap");
215 1 break;
216 }
217 }
218 // Junk/Trash use ":specialuse" only where the server offers it (Pigeonhole
219 // 2.4.1 does not); otherwise the folder that carries the flag is named.
220
7/10
✓ Branch 57 → 58 taken 155 times.
✗ Branch 57 → 65 not taken.
✓ Branch 63 → 64 taken 153 times.
✓ Branch 63 → 65 taken 2 times.
✓ Branch 66 → 67 taken 155 times.
✗ Branch 66 → 68 not taken.
✓ Branch 68 → 69 taken 155 times.
✗ Branch 68 → 70 not taken.
✓ Branch 70 → 71 taken 153 times.
✓ Branch 70 → 102 taken 2 times.
310 if (s && !s->capabilities().supports(QStringLiteral("special-use"))) {
221
1/1
✓ Branch 71 → 72 taken 153 times.
153 const SieveMailboxDirectory directory = mailboxDirectory();
222
4/4
✓ Branch 72 → 73 taken 153 times.
✓ Branch 73 → 74 taken 153 times.
✓ Branch 99 → 75 taken 169 times.
✓ Branch 99 → 100 taken 153 times.
322 for (Sieve::Action &action : rule.actions) {
223
6/6
✓ Branch 76 → 77 taken 106 times.
✓ Branch 76 → 79 taken 63 times.
✓ Branch 78 → 79 taken 102 times.
✓ Branch 78 → 80 taken 4 times.
✓ Branch 81 → 82 taken 165 times.
✓ Branch 81 → 83 taken 4 times.
169 if (action.kind != Sieve::Action::Kind::FileInto || action.specialUse.isEmpty())
224 165 continue;
225
1/1
✓ Branch 83 → 84 taken 4 times.
4 const QString folder = directory.folderForSpecialUse(action.specialUse);
226
1/2
✗ Branch 85 → 86 not taken.
✓ Branch 85 → 87 taken 4 times.
4 if (folder.isEmpty())
227 ✗ continue;
228
1/1
✓ Branch 87 → 88 taken 4 times.
4 action.target = directory.sieveName(folder);
229 4 action.specialUse.clear();
230 4 }
231 153 }
232 155 return rule;
233 ✗ }
234
235 23 QString QuickRulesBackend::draftSentence(const QVariantMap &draft) const {
236
2/2
✓ Branch 2 → 3 taken 23 times.
✓ Branch 3 → 4 taken 23 times.
23 return RuleSentence::sentence(RuleEditorFields::toRule(draft), RuleSentence::rich);
237 }
238
239 24 QString QuickRulesBackend::draftSieve(const QVariantMap &draft) const {
240
1/1
✓ Branch 2 → 3 taken 24 times.
24 const SieveSession *s = session();
241
1/12
✓ Branch 3 → 4 taken 24 times.
✗ Branch 3 → 6 not taken.
✗ Branch 6 → 7 not taken.
✗ Branch 7 → 8 not taken.
✗ Branch 18 → 19 not taken.
✗ Branch 18 → 21 not taken.
✗ Branch 19 → 20 not taken.
✗ Branch 19 → 21 not taken.
✗ Branch 22 → 23 not taken.
✗ Branch 22 → 25 not taken.
✗ Branch 23 → 24 not taken.
✗ Branch 23 → 25 not taken.
24 const Sieve::Style style = s ? s->document().style : Sieve::Style{};
242
3/3
✓ Branch 9 → 10 taken 24 times.
✓ Branch 10 → 11 taken 24 times.
✓ Branch 11 → 12 taken 24 times.
48 return QString::fromUtf8(Sieve::renderRule(prepareRule(draft), style));
243 24 }
244
245 54 Sieve::GuardContext QuickRulesBackend::guardContext(int blockIndex, const QVariantMap &confirmations) const {
246 54 Sieve::GuardContext context;
247
2/3
✓ Branch 3 → 4 taken 54 times.
✗ Branch 3 → 5 not taken.
✓ Branch 4 → 6 taken 54 times.
54 const auto account = m_deps.account ? m_deps.account() : std::nullopt;
248
1/2
✓ Branch 7 → 8 taken 54 times.
✗ Branch 7 → 22 not taken.
54 if (account) {
249
2/2
✓ Branch 9 → 10 taken 54 times.
✓ Branch 10 → 11 taken 54 times.
54 context.ownAddresses.append(account->email.toLower());
250
2/3
✓ Branch 15 → 16 taken 54 times.
✗ Branch 16 → 17 not taken.
✓ Branch 16 → 22 taken 54 times.
54 if (account->imap.username.contains(QLatin1Char('@')))
251 ✗ context.ownAddresses.append(account->imap.username.toLower());
252 }
253
1/1
✓ Branch 22 → 23 taken 54 times.
54 const SieveSession *s = session();
254
1/2
✓ Branch 23 → 24 taken 54 times.
✗ Branch 23 → 27 not taken.
54 if (s) {
255 54 context.maxRedirects = s->capabilities().maxRedirects;
256 54 context.serverExtensions = s->capabilities().sieveExtensions;
257 }
258
6/6
✓ Branch 31 → 32 taken 54 times.
✓ Branch 32 → 33 taken 54 times.
✓ Branch 37 → 38 taken 54 times.
✓ Branch 38 → 39 taken 54 times.
✓ Branch 46 → 40 taken 7 times.
✓ Branch 46 → 47 taken 54 times.
115 for (const QString &target : confirmations.value(QStringLiteral("forwardTargets")).toStringList())
259
2/2
✓ Branch 41 → 42 taken 7 times.
✓ Branch 42 → 43 taken 7 times.
61 context.confirmedForwardTargets.append(target.toLower());
260
2/2
✓ Branch 52 → 53 taken 54 times.
✓ Branch 53 → 54 taken 54 times.
108 context.destructiveConfirmed = confirmations.value(QStringLiteral("destructive")).toBool();
261
2/2
✓ Branch 62 → 63 taken 54 times.
✓ Branch 63 → 64 taken 54 times.
108 context.everyMessageConfirmed = confirmations.value(QStringLiteral("everyMessage")).toBool();
262 // What the rule already did before this edit needs no new confirmation.
263
6/8
✓ Branch 68 → 69 taken 54 times.
✗ Branch 68 → 77 not taken.
✓ Branch 69 → 70 taken 9 times.
✓ Branch 69 → 77 taken 45 times.
✓ Branch 72 → 73 taken 9 times.
✗ Branch 72 → 77 not taken.
✓ Branch 78 → 79 taken 9 times.
✓ Branch 78 → 98 taken 45 times.
63 if (s && blockIndex >= 0 && blockIndex < s->document().blocks.size() &&
264
1/2
✓ Branch 75 → 76 taken 9 times.
✗ Branch 75 → 77 not taken.
9 s->document().blocks.at(blockIndex).kind == Sieve::BlockKind::Rule) {
265 9 const Sieve::Rule &previous = s->document().blocks.at(blockIndex).rule;
266
2/2
✓ Branch 94 → 83 taken 11 times.
✓ Branch 94 → 95 taken 9 times.
20 for (const Sieve::Action &action : previous.actions) {
267
2/2
✓ Branch 84 → 85 taken 1 time.
✓ Branch 84 → 89 taken 10 times.
11 if (action.kind == Sieve::Action::Kind::Redirect)
268
2/2
✓ Branch 85 → 86 taken 1 time.
✓ Branch 86 → 87 taken 1 time.
1 context.confirmedForwardTargets.append(action.target.toLower());
269
3/4
✓ Branch 89 → 90 taken 10 times.
✓ Branch 89 → 91 taken 1 time.
✗ Branch 90 → 91 not taken.
✓ Branch 90 → 92 taken 10 times.
11 if (action.kind == Sieve::Action::Kind::Discard || action.kind == Sieve::Action::Kind::Reject)
270 1 context.destructiveConfirmed = true;
271 }
272
1/2
✗ Branch 96 → 97 not taken.
✓ Branch 96 → 98 taken 9 times.
9 if (previous.conditions.isEmpty())
273 ✗ context.everyMessageConfirmed = true;
274 }
275 54 return context;
276 54 }
277
278 54 QVariantList QuickRulesBackend::draftIssues(int blockIndex, const QVariantMap &draft,
279 const QVariantMap &confirmations) const {
280 54 QVariantList result;
281
7/7
✓ Branch 2 → 3 taken 54 times.
✓ Branch 3 → 4 taken 54 times.
✓ Branch 4 → 5 taken 54 times.
✓ Branch 7 → 8 taken 54 times.
✓ Branch 8 → 9 taken 54 times.
✓ Branch 108 → 10 taken 20 times.
✓ Branch 108 → 109 taken 54 times.
74 for (const Sieve::GuardIssue &issue : Sieve::checkRule(prepareRule(draft), guardContext(blockIndex, confirmations))) {
282 20 QString message;
283 using K = Sieve::GuardIssue::Kind;
284
11/12
✓ Branch 11 → 12 taken 4 times.
✓ Branch 11 → 16 taken 1 time.
✓ Branch 11 → 20 taken 1 time.
✓ Branch 11 → 27 taken 1 time.
✓ Branch 11 → 34 taken 3 times.
✓ Branch 11 → 41 taken 2 times.
✓ Branch 11 → 45 taken 1 time.
✓ Branch 11 → 52 taken 4 times.
✓ Branch 11 → 59 taken 1 time.
✓ Branch 11 → 63 taken 1 time.
✓ Branch 11 → 67 taken 1 time.
✗ Branch 11 → 71 not taken.
20 switch (issue.kind) {
285
1/1
✓ Branch 12 → 13 taken 4 times.
4 case K::NoActions: message = tr("Add at least one action."); break;
286
1/1
✓ Branch 16 → 17 taken 1 time.
1 case K::EmptyValue: message = tr("Fill in every value."); break;
287
2/2
✓ Branch 20 → 21 taken 1 time.
✓ Branch 22 → 23 taken 1 time.
2 case K::InvalidAddress: message = tr("„%1“ is not an email address.").arg(issue.detail); break;
288
2/2
✓ Branch 27 → 28 taken 1 time.
✓ Branch 29 → 30 taken 1 time.
2 case K::InvalidRegex: message = tr("„%1“ is not a valid expression.").arg(issue.detail); break;
289
2/2
✓ Branch 34 → 35 taken 3 times.
✓ Branch 36 → 37 taken 3 times.
6 case K::UnsupportedExtension: message = tr("This server does not offer „%1“.").arg(issue.detail); break;
290
1/1
✓ Branch 41 → 42 taken 2 times.
2 case K::ForwardToOwnAddress: message = tr("Forwarding to your own address would loop."); break;
291
2/2
✓ Branch 45 → 46 taken 1 time.
✓ Branch 47 → 48 taken 1 time.
2 case K::TooManyRedirects: message = tr("The server allows at most %1 forwards per rule.").arg(issue.detail); break;
292 4 case K::ForwardNeedsConfirmation:
293
2/2
✓ Branch 52 → 53 taken 4 times.
✓ Branch 54 → 55 taken 4 times.
8 message = tr("Mails matching this rule will be sent to %1. They leave this mailbox.").arg(issue.detail);
294 4 break;
295 1 case K::DestructiveNeedsConfirmation:
296
1/1
✓ Branch 59 → 60 taken 1 time.
1 message = tr("Matching mails are deleted or rejected and cannot be restored. Move them to Trash instead?");
297 1 break;
298
1/1
✓ Branch 63 → 64 taken 1 time.
1 case K::EveryMessageNeedsConfirmation: message = tr("This rule applies to every incoming message."); break;
299 1 case K::RejectWithDelivery:
300
1/1
✓ Branch 67 → 68 taken 1 time.
1 message = tr("Reject can't be combined with moving, keeping or forwarding; the server would ignore the rule.");
301 1 break;
302 }
303
2/2
✓ Branch 74 → 75 taken 20 times.
✓ Branch 90 → 91 taken 20 times.
60 result.append(QVariantMap{{QStringLiteral("kind"), issueKind(issue.kind)},
304 40 {QStringLiteral("detail"), issue.detail},
305 40 {QStringLiteral("blocking"), issue.blocking},
306
2/4
✓ Branch 94 → 95 taken 80 times.
✓ Branch 94 → 96 taken 20 times.
✗ Branch 154 → 155 not taken.
✗ Branch 154 → 156 not taken.
120 {QStringLiteral("message"), message}});
307 74 }
308 54 return result;
309 100 }
310
311 19 QString QuickRulesBackend::saveDraft(int blockIndex, const QVariantMap &draft, const QVariantMap &confirmations) {
312 19 return saveRule(blockIndex, draft, confirmations, false);
313 }
314
315 23 QString QuickRulesBackend::saveRule(int blockIndex, const QVariantMap &draft, const QVariantMap &confirmations,
316 bool announceCreated) {
317
1/1
✓ Branch 2 → 3 taken 23 times.
23 SieveSession *s = session();
318
4/7
✓ Branch 3 → 4 taken 23 times.
✗ Branch 3 → 6 not taken.
✓ Branch 4 → 5 taken 23 times.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 23 times.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 14 taken 23 times.
23 if (!s || readOnly())
319 ✗ return refuseEdit(tr("Rules can't be changed right now. Reload them and try again."));
320
1/1
✓ Branch 14 → 15 taken 23 times.
23 const QVariantList issues = draftIssues(blockIndex, draft, confirmations);
321
2/2
✓ Branch 16 → 17 taken 2 times.
✓ Branch 16 → 25 taken 21 times.
23 if (!issues.isEmpty()) {
322
1/1
✓ Branch 17 → 18 taken 2 times.
2 const QString requestId = nextRequestId();
323
1/3
✓ Branch 20 → 21 taken 2 times.
✗ Branch 87 → 88 not taken.
✗ Branch 87 → 89 not taken.
2 QTimer::singleShot(0, this, [this, requestId, issues] {
324
3/5
✓ Branch 29 → 30 taken 2 times.
✓ Branch 32 → 33 taken 8 times.
✓ Branch 32 → 34 taken 2 times.
✗ Branch 53 → 54 not taken.
✗ Branch 53 → 55 not taken.
14 emit editFinished(requestId, {{QStringLiteral("ok"), false},
325 6 {QStringLiteral("kind"), QStringLiteral("guard")},
326 4 {QStringLiteral("issues"), issues},
327
2/2
✓ Branch 21 → 22 taken 2 times.
✓ Branch 26 → 27 taken 2 times.
6 {QStringLiteral("message"), issues.first().toMap().value(QStringLiteral("message"))}});
328
1/5
✓ Branch 28 → 29 taken 2 times.
✗ Branch 102 → 103 not taken.
✗ Branch 102 → 106 not taken.
✗ Branch 104 → 105 not taken.
✗ Branch 104 → 106 not taken.
14 });
329 2 return requestId;
330 2 }
331
1/1
✓ Branch 25 → 26 taken 21 times.
21 const Sieve::Rule rule = prepareRule(draft);
332 21 Sieve::Document document = s->document();
333 21 int placed = blockIndex;
334
5/6
✓ Branch 28 → 29 taken 8 times.
✓ Branch 28 → 34 taken 13 times.
✓ Branch 30 → 31 taken 8 times.
✗ Branch 30 → 34 not taken.
✓ Branch 35 → 36 taken 8 times.
✓ Branch 35 → 40 taken 13 times.
29 if (blockIndex >= 0 && blockIndex < document.blocks.size() &&
335
1/2
✓ Branch 32 → 33 taken 8 times.
✗ Branch 32 → 34 not taken.
8 document.blocks.at(blockIndex).kind == Sieve::BlockKind::Rule) {
336
1/1
✓ Branch 36 → 37 taken 8 times.
8 Sieve::Block &block = document.blocks[blockIndex];
337 8 block.rule = rule;
338 8 block.name = rule.name;
339 8 block.named = true;
340 8 block.dirty = true;
341 } else {
342
2/2
✓ Branch 44 → 45 taken 13 times.
✓ Branch 45 → 46 taken 13 times.
26 const int before = draft.value(QStringLiteral("insertBefore"), -1).toInt();
343
5/6
✓ Branch 50 → 51 taken 1 time.
✓ Branch 50 → 54 taken 12 times.
✓ Branch 52 → 53 taken 1 time.
✗ Branch 52 → 54 not taken.
✓ Branch 55 → 56 taken 1 time.
✓ Branch 55 → 61 taken 12 times.
13 if (before >= 0 && before < document.blocks.size()) {
344 1 Sieve::Block block;
345 1 block.kind = Sieve::BlockKind::Rule;
346 1 block.rule = rule;
347 1 block.name = rule.name;
348 1 block.named = true;
349 1 block.dirty = true;
350
1/1
✓ Branch 58 → 59 taken 1 time.
1 document.blocks.insert(before, block);
351 1 placed = before;
352 1 } else {
353
1/1
✓ Branch 61 → 62 taken 12 times.
12 placed = Sieve::insertRule(&document, rule);
354 }
355 }
356 21 return submit({Sieve::encode(document), QStringLiteral("rule"), tr("Rule „%1“ saved").arg(rule.name), true,
357 21 announceCreated ? rule.name : QString(), placed});
358 107 }
359
360 162 int QuickRulesBackend::positionOf(int blockIndex) const {
361 162 const SieveSession *s = session();
362
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 162 times.
162 if (!s)
363 ✗ return 0;
364 162 int position = 0;
365
6/6
✓ Branch 14 → 15 taken 831 times.
✓ Branch 14 → 17 taken 12 times.
✓ Branch 15 → 16 taken 681 times.
✓ Branch 15 → 17 taken 150 times.
✓ Branch 18 → 6 taken 681 times.
✓ Branch 18 → 19 taken 162 times.
843 for (int i = 0; i < s->document().blocks.size() && i <= blockIndex; ++i) {
366 681 const Sieve::BlockKind kind = s->document().blocks.at(i).kind;
367
4/4
✓ Branch 8 → 9 taken 275 times.
✓ Branch 8 → 10 taken 406 times.
✓ Branch 9 → 10 taken 19 times.
✓ Branch 9 → 11 taken 256 times.
681 if (kind == Sieve::BlockKind::Rule || kind == Sieve::BlockKind::Advanced)
368 425 ++position;
369 }
370 162 return position;
371 }
372
373 // --- quick actions ----------------------------------------------------------------------
374
375 14 QString QuickRulesBackend::setEnabled(int blockIndex, bool enabled) {
376
1/1
✓ Branch 2 → 3 taken 14 times.
14 SieveSession *s = session();
377
7/8
✓ Branch 3 → 4 taken 13 times.
✓ Branch 3 → 8 taken 1 time.
✓ Branch 4 → 5 taken 13 times.
✗ Branch 4 → 8 not taken.
✓ Branch 7 → 8 taken 1 time.
✓ Branch 7 → 9 taken 12 times.
✓ Branch 10 → 11 taken 2 times.
✓ Branch 10 → 16 taken 12 times.
14 if (!s || blockIndex < 0 || blockIndex >= s->document().blocks.size())
378
2/2
✓ Branch 11 → 12 taken 2 times.
✓ Branch 12 → 13 taken 2 times.
2 return refuseEdit(tr("This rule no longer exists."));
379 12 Sieve::Document document = s->document();
380
1/1
✓ Branch 18 → 19 taken 12 times.
12 Sieve::Block &block = document.blocks[blockIndex];
381 12 QString name = block.name;
382
2/2
✓ Branch 20 → 21 taken 10 times.
✓ Branch 20 → 30 taken 2 times.
12 if (block.kind == Sieve::BlockKind::Rule) {
383 10 block.rule.enabled = enabled;
384
1/2
✗ Branch 22 → 23 not taken.
✓ Branch 22 → 27 taken 10 times.
10 if (name.isEmpty())
385 ✗ name = RuleSentence::generatedName(block.rule);
386
1/2
✗ Branch 27 → 28 not taken.
✓ Branch 27 → 37 taken 10 times.
10 if (!block.named) {
387 ✗ block.name = name;
388 ✗ block.named = true;
389 ✗ block.rule.name = name;
390 }
391
1/2
✓ Branch 30 → 31 taken 2 times.
✗ Branch 30 → 32 not taken.
2 } else if (block.kind == Sieve::BlockKind::Vacation) {
392 2 block.vacation.enabled = enabled;
393 } else {
394 ✗ return refuseEdit(tr("Advanced blocks can only be changed in the script."));
395 }
396 12 block.dirty = true;
397
5/14
✗ Branch 37 → 38 not taken.
✓ Branch 37 → 41 taken 12 times.
✗ Branch 38 → 39 not taken.
✗ Branch 40 → 44 not taken.
✓ Branch 41 → 42 taken 12 times.
✓ Branch 43 → 44 taken 12 times.
✓ Branch 44 → 45 taken 12 times.
✗ Branch 44 → 46 not taken.
✗ Branch 46 → 47 not taken.
✓ Branch 46 → 48 taken 12 times.
✗ Branch 69 → 70 not taken.
✗ Branch 69 → 71 not taken.
✗ Branch 74 → 75 not taken.
✗ Branch 74 → 76 not taken.
12 const QString text = enabled ? tr("Rule „%1“ turned on").arg(name) : tr("Rule „%1“ turned off").arg(name);
398 12 return submit({Sieve::encode(document), QStringLiteral("toggle"), text, true});
399 36 }
400
401 4 QString QuickRulesBackend::moveRule(int blockIndex, int targetBlockIndex) {
402
1/1
✓ Branch 2 → 3 taken 4 times.
4 SieveSession *s = session();
403
5/8
✓ Branch 4 → 5 taken 4 times.
✗ Branch 4 → 13 not taken.
✓ Branch 5 → 6 taken 4 times.
✗ Branch 5 → 13 not taken.
✓ Branch 8 → 9 taken 3 times.
✓ Branch 8 → 13 taken 1 time.
✓ Branch 11 → 12 taken 3 times.
✗ Branch 11 → 13 not taken.
7 if (!s || blockIndex < 0 || targetBlockIndex < 0 || blockIndex >= s->document().blocks.size() ||
404
4/6
✓ Branch 3 → 4 taken 4 times.
✗ Branch 3 → 13 not taken.
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 14 taken 3 times.
✓ Branch 15 → 16 taken 1 time.
✓ Branch 15 → 21 taken 3 times.
11 targetBlockIndex >= s->document().blocks.size() || blockIndex == targetBlockIndex)
405
2/2
✓ Branch 16 → 17 taken 1 time.
✓ Branch 17 → 18 taken 1 time.
1 return refuseEdit(tr("The rule can't be moved there."));
406 3 Sieve::Document document = s->document();
407 3 int firstMovable = 0;
408
5/6
✓ Branch 26 → 27 taken 6 times.
✗ Branch 26 → 30 not taken.
✓ Branch 28 → 29 taken 3 times.
✓ Branch 28 → 30 taken 3 times.
✓ Branch 31 → 24 taken 3 times.
✓ Branch 31 → 32 taken 3 times.
6 while (firstMovable < document.blocks.size() && document.blocks.at(firstMovable).kind == Sieve::BlockKind::Require)
409 3 ++firstMovable;
410 3 const int target = qMax(firstMovable, targetBlockIndex);
411 3 const QString name = document.blocks.at(blockIndex).name;
412
1/1
✓ Branch 35 → 36 taken 3 times.
3 document.blocks.move(blockIndex, target);
413 ✗ return submit({Sieve::encode(document), QStringLiteral("move"),
414
1/1
✓ Branch 42 → 43 taken 3 times.
9 tr("Rule „%1“ moved to position %2").arg(name).arg(positionOf(target)), true});
415 15 }
416
417 5 QString QuickRulesBackend::moveRuleBy(int blockIndex, int delta) {
418
1/1
✓ Branch 2 → 3 taken 5 times.
5 const SieveSession *s = session();
419
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 9 taken 5 times.
5 if (!s)
420 ✗ return refuseEdit(tr("The rule can't be moved there."));
421 4 const auto movable = [s](int i) {
422 4 const Sieve::BlockKind kind = s->document().blocks.at(i).kind;
423
4/6
✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 7 taken 3 times.
✓ Branch 5 → 6 taken 1 time.
✗ Branch 5 → 7 not taken.
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 1 time.
4 return kind == Sieve::BlockKind::Rule || kind == Sieve::BlockKind::Advanced || kind == Sieve::BlockKind::Vacation;
424 5 };
425 5 int target = blockIndex;
426
2/2
✓ Branch 9 → 10 taken 2 times.
✓ Branch 9 → 11 taken 3 times.
5 const int step = delta < 0 ? -1 : 1;
427
2/2
✓ Branch 28 → 13 taken 7 times.
✓ Branch 28 → 29 taken 2 times.
9 for (int remaining = qAbs(delta); remaining > 0;) {
428 7 target += step;
429
6/6
✓ Branch 13 → 14 taken 6 times.
✓ Branch 13 → 17 taken 1 time.
✓ Branch 16 → 17 taken 2 times.
✓ Branch 16 → 18 taken 4 times.
✓ Branch 19 → 20 taken 3 times.
✓ Branch 19 → 25 taken 4 times.
7 if (target < 0 || target >= s->document().blocks.size())
430
2/2
✓ Branch 20 → 21 taken 3 times.
✓ Branch 21 → 22 taken 3 times.
3 return refuseEdit(tr("The rule is already at the edge."));
431
2/2
✓ Branch 26 → 27 taken 3 times.
✓ Branch 26 → 28 taken 1 time.
4 if (movable(target))
432 3 --remaining;
433 }
434
1/1
✓ Branch 29 → 30 taken 2 times.
2 return moveRule(blockIndex, target);
435 }
436
437 5 QString QuickRulesBackend::deleteRule(int blockIndex) {
438
1/1
✓ Branch 2 → 3 taken 5 times.
5 SieveSession *s = session();
439
6/8
✓ Branch 3 → 4 taken 5 times.
✗ Branch 3 → 8 not taken.
✓ Branch 4 → 5 taken 5 times.
✗ Branch 4 → 8 not taken.
✓ Branch 7 → 8 taken 1 time.
✓ Branch 7 → 9 taken 4 times.
✓ Branch 10 → 11 taken 1 time.
✓ Branch 10 → 16 taken 4 times.
5 if (!s || blockIndex < 0 || blockIndex >= s->document().blocks.size())
440
2/2
✓ Branch 11 → 12 taken 1 time.
✓ Branch 12 → 13 taken 1 time.
1 return refuseEdit(tr("This rule no longer exists."));
441 4 Sieve::Document document = s->document();
442
1/1
✓ Branch 18 → 19 taken 4 times.
4 const Sieve::Block block = document.blocks.takeAt(blockIndex);
443
1/3
✓ Branch 19 → 20 taken 4 times.
✗ Branch 19 → 21 not taken.
✗ Branch 21 → 22 not taken.
4 const QString name = block.named ? block.name : RuleSentence::generatedName(block.rule);
444 8 return submit({Sieve::encode(document), QStringLiteral("delete"), tr("Rule „%1“ deleted").arg(name), true});
445 20 }
446
447 8 QString QuickRulesBackend::applyHealthFix(int blockIndex, bool alternative) {
448
1/1
✓ Branch 2 → 3 taken 8 times.
8 SieveSession *s = session();
449
6/8
✓ Branch 3 → 4 taken 8 times.
✗ Branch 3 → 8 not taken.
✓ Branch 4 → 5 taken 8 times.
✗ Branch 4 → 8 not taken.
✓ Branch 7 → 8 taken 1 time.
✓ Branch 7 → 9 taken 7 times.
✓ Branch 10 → 11 taken 1 time.
✓ Branch 10 → 16 taken 7 times.
8 if (!s || blockIndex < 0 || blockIndex >= s->document().blocks.size())
450
2/2
✓ Branch 11 → 12 taken 1 time.
✓ Branch 12 → 13 taken 1 time.
1 return refuseEdit(tr("This rule no longer exists."));
451 7 Sieve::Document document = s->document();
452
1/1
✓ Branch 18 → 19 taken 7 times.
7 Sieve::Block &block = document.blocks[blockIndex];
453
3/3
✓ Branch 19 → 20 taken 7 times.
✓ Branch 20 → 21 taken 2 times.
✓ Branch 20 → 48 taken 5 times.
7 if (Sieve::rejectConflicts(block.rule)) {
454 using Kind = Sieve::Action::Kind;
455
2/2
✓ Branch 21 → 22 taken 1 time.
✓ Branch 21 → 35 taken 1 time.
2 if (alternative) {
456 // "Only reject": the rejected mail is not delivered, so filing,
457 // keeping, forwarding and flags go; reject (and stop) remain.
458
1/1
✓ Branch 22 → 23 taken 1 time.
4 block.rule.actions.removeIf([](const Sieve::Action &action) { return action.kind != Kind::Reject; });
459 1 block.dirty = true;
460 ✗ return submit({Sieve::encode(document), QStringLiteral("health"),
461 2 tr("Rule „%1“ now only rejects").arg(block.name), true});
462 }
463
1/1
✓ Branch 35 → 36 taken 1 time.
3 block.rule.actions.removeIf([](const Sieve::Action &action) { return action.kind == Kind::Reject; });
464 1 block.dirty = true;
465 ✗ return submit({Sieve::encode(document), QStringLiteral("health"),
466 2 tr("Rule „%1“ no longer rejects").arg(block.name), true});
467 }
468
1/1
✓ Branch 48 → 49 taken 5 times.
5 const SieveMailboxDirectory directory = mailboxDirectory();
469
4/4
✓ Branch 49 → 50 taken 5 times.
✓ Branch 50 → 51 taken 5 times.
✓ Branch 81 → 52 taken 5 times.
✓ Branch 81 → 82 taken 3 times.
8 for (Sieve::Action &action : block.rule.actions) {
470
6/7
✓ Branch 53 → 54 taken 5 times.
✗ Branch 53 → 56 not taken.
✓ Branch 54 → 55 taken 5 times.
✓ Branch 55 → 56 taken 3 times.
✓ Branch 55 → 57 taken 2 times.
✓ Branch 58 → 59 taken 3 times.
✓ Branch 58 → 60 taken 2 times.
5 if (action.kind != Sieve::Action::Kind::FileInto || directory.exists(action.target))
471 3 continue;
472
1/1
✓ Branch 60 → 61 taken 2 times.
2 const QString suggestion = directory.suggestionFor(action.target);
473
1/2
✗ Branch 62 → 63 not taken.
✓ Branch 62 → 64 taken 2 times.
2 if (suggestion.isEmpty())
474 ✗ continue;
475 2 action.target = suggestion;
476 2 block.dirty = true;
477 ✗ return submit({Sieve::encode(document), QStringLiteral("health"),
478 2 tr("Rule „%1“ now files into „%2“").arg(block.name, suggestion), true});
479 2 }
480
2/2
✓ Branch 101 → 83 taken 5 times.
✓ Branch 101 → 102 taken 1 time.
6 for (int j = 0; j < blockIndex; ++j) {
481 5 const Sieve::Block &previous = document.blocks.at(j);
482
3/4
✓ Branch 84 → 85 taken 2 times.
✓ Branch 84 → 86 taken 3 times.
✗ Branch 85 → 86 not taken.
✓ Branch 85 → 87 taken 2 times.
5 if (previous.kind != Sieve::BlockKind::Rule || !previous.rule.enabled)
483 3 continue;
484
7/8
✓ Branch 87 → 88 taken 2 times.
✓ Branch 88 → 89 taken 1 time.
✓ Branch 88 → 92 taken 1 time.
✓ Branch 89 → 90 taken 1 time.
✓ Branch 90 → 91 taken 1 time.
✗ Branch 90 → 92 not taken.
✓ Branch 93 → 94 taken 1 time.
✓ Branch 93 → 96 taken 1 time.
2 if (previous.rule.conditions == block.rule.conditions && previous.rule.actions == block.rule.actions)
485
1/1
✓ Branch 94 → 95 taken 1 time.
1 return deleteRule(blockIndex);
486
2/3
✓ Branch 96 → 97 taken 1 time.
✓ Branch 97 → 98 taken 1 time.
✗ Branch 97 → 100 not taken.
1 if (RuleMatcher::shadows(previous.rule, block.rule))
487
1/1
✓ Branch 98 → 99 taken 1 time.
1 return moveRule(blockIndex, j);
488 }
489
2/2
✓ Branch 102 → 103 taken 1 time.
✓ Branch 103 → 104 taken 1 time.
1 return refuseEdit(tr("There is nothing to fix for this rule."));
490 19 }
491
492 3 QString QuickRulesBackend::undo() {
493
2/2
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 9 taken 2 times.
3 if (m_undo.isEmpty())
494
2/2
✓ Branch 4 → 5 taken 1 time.
✓ Branch 5 → 6 taken 1 time.
1 return refuseEdit(tr("Nothing to undo."));
495
1/1
✓ Branch 9 → 10 taken 2 times.
2 const QByteArray bytes = m_undo.takeLast();
496
1/1
✓ Branch 10 → 11 taken 2 times.
2 emit undoChanged();
497 2 return submit({bytes, QStringLiteral("undo"), tr("Change undone"), false});
498 6 }
499
500 // --- script editor and scripts panel ----------------------------------------------------
501
502 13 QString QuickRulesBackend::scriptText() const {
503 13 const SieveSession *s = session();
504
3/7
✓ Branch 3 → 4 taken 13 times.
✗ Branch 3 → 6 not taken.
✓ Branch 5 → 7 taken 13 times.
✓ Branch 8 → 9 taken 13 times.
✗ Branch 8 → 10 not taken.
✗ Branch 12 → 13 not taken.
✗ Branch 12 → 14 not taken.
26 return s ? QString::fromUtf8(s->baseScript()) : QString();
505 }
506
507 6 QString QuickRulesBackend::checkScript(const QString &text) {
508
1/1
✓ Branch 2 → 3 taken 6 times.
6 SieveSession *s = session();
509
2/2
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 9 taken 5 times.
6 if (!s)
510
2/2
✓ Branch 4 → 5 taken 1 time.
✓ Branch 5 → 6 taken 1 time.
1 return refuseEdit(tr("Rules can't be changed right now. Reload them and try again."));
511
1/1
✓ Branch 9 → 10 taken 5 times.
5 const QString requestId = nextRequestId();
512
1/1
✓ Branch 10 → 11 taken 5 times.
5 QPointer<QuickRulesBackend> guard(this);
513
3/5
✓ Branch 13 → 14 taken 5 times.
✓ Branch 14 → 15 taken 5 times.
✓ Branch 15 → 16 taken 5 times.
✗ Branch 34 → 35 not taken.
✗ Branch 34 → 36 not taken.
5 s->check(text.toUtf8(), [this, guard, requestId](const SieveSession::SaveOutcome &outcome) {
514
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 5 times.
5 if (!guard)
515 ✗ return;
516
1/1
✓ Branch 5 → 6 taken 5 times.
5 QVariantMap result = outcomeMap(outcome);
517
2/2
✓ Branch 7 → 8 taken 2 times.
✓ Branch 7 → 23 taken 3 times.
5 if (outcome.ok())
518
1/1
✓ Branch 17 → 18 taken 2 times.
6 result.insert(QStringLiteral("message"), outcome.message == QLatin1String("unchecked")
519
4/4
✓ Branch 10 → 11 taken 1 time.
✓ Branch 10 → 12 taken 1 time.
✓ Branch 11 → 13 taken 1 time.
✓ Branch 12 → 13 taken 1 time.
4 ? tr("This server can only check the script when it is saved.")
520 : tr("The server accepts the script."));
521
14/24
✓ Branch 24 → 25 taken 2 times.
✓ Branch 24 → 29 taken 3 times.
✓ Branch 33 → 34 taken 3 times.
✓ Branch 37 → 38 taken 5 times.
✓ Branch 41 → 42 taken 3 times.
✓ Branch 41 → 43 taken 2 times.
✓ Branch 43 → 44 taken 3 times.
✓ Branch 43 → 45 taken 2 times.
✓ Branch 45 → 46 taken 3 times.
✓ Branch 45 → 47 taken 2 times.
✓ Branch 47 → 48 taken 2 times.
✓ Branch 47 → 49 taken 3 times.
✓ Branch 49 → 50 taken 2 times.
✓ Branch 49 → 51 taken 3 times.
✗ Branch 76 → 77 not taken.
✗ Branch 76 → 78 not taken.
✗ Branch 80 → 81 not taken.
✗ Branch 80 → 82 not taken.
✗ Branch 84 → 85 not taken.
✗ Branch 84 → 86 not taken.
✗ Branch 88 → 89 not taken.
✗ Branch 88 → 90 not taken.
✗ Branch 92 → 93 not taken.
✗ Branch 92 → 94 not taken.
15 result.insert(QStringLiteral("kind"), outcome.ok() ? QStringLiteral("checked") : result.value(QStringLiteral("kind")));
522
1/1
✓ Branch 51 → 52 taken 5 times.
5 finishLater(requestId, result);
523 5 });
524 5 return requestId;
525 5 }
526
527 32 QString QuickRulesBackend::saveScript(const QString &text, const QString &expectedServer) {
528 // The script editor may replace a read-only (unparseable) script on purpose.
529
1/1
✓ Branch 2 → 3 taken 32 times.
32 SieveSession *s = session();
530
6/8
✓ Branch 3 → 4 taken 32 times.
✗ Branch 3 → 8 not taken.
✓ Branch 5 → 6 taken 31 times.
✓ Branch 5 → 8 taken 1 time.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 31 times.
✓ Branch 10 → 11 taken 1 time.
✓ Branch 10 → 16 taken 31 times.
32 if (!s || s->status() == SieveSession::Status::Offline || s->status() == SieveSession::Status::Error)
531
2/2
✓ Branch 11 → 12 taken 1 time.
✓ Branch 12 → 13 taken 1 time.
1 return refuseEdit(tr("Rules can't be changed right now. Reload them and try again."));
532
1/1
✓ Branch 16 → 17 taken 31 times.
31 const QString requestId = nextRequestId();
533
1/1
✓ Branch 17 → 18 taken 31 times.
31 const QByteArray bytes = text.toUtf8();
534 31 const QByteArray previous = s->baseScript();
535 31 SieveSession::SaveOptions options;
536 31 options.reason = QStringLiteral("script");
537 31 options.reviewMerge = true; // a hand-written script is never merged unseen (S95.3.10)
538
2/2
✓ Branch 26 → 27 taken 1 time.
✓ Branch 26 → 31 taken 30 times.
31 if (!expectedServer.isNull())
539
1/1
✓ Branch 27 → 28 taken 1 time.
1 options.expectedServer = expectedServer.toUtf8();
540
1/1
✓ Branch 31 → 32 taken 31 times.
31 QPointer<QuickRulesBackend> guard(this);
541
2/8
✓ Branch 36 → 37 taken 31 times.
✓ Branch 37 → 38 taken 31 times.
✗ Branch 56 → 57 not taken.
✗ Branch 56 → 58 not taken.
✗ Branch 59 → 60 not taken.
✗ Branch 59 → 61 not taken.
✗ Branch 62 → 63 not taken.
✗ Branch 62 → 64 not taken.
31 s->save(bytes, options, [this, guard, requestId, bytes, previous](const SieveSession::SaveOutcome &outcome) {
542
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 31 times.
31 if (!guard)
543 ✗ return;
544
2/2
✓ Branch 6 → 7 taken 28 times.
✓ Branch 6 → 12 taken 3 times.
31 if (outcome.ok()) {
545
1/1
✓ Branch 7 → 8 taken 28 times.
28 pushUndoScript(previous); // S106-320: the same cap as rule edits
546
2/2
✓ Branch 8 → 9 taken 28 times.
✓ Branch 9 → 10 taken 28 times.
28 emit toast(tr("Script saved"), true);
547
1/2
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 14 taken 3 times.
3 } else if (outcome.kind == SieveSession::SaveOutcome::Kind::Conflict) {
548 ✗ holdConflict(bytes, previous, outcome);
549 }
550
1/1
✓ Branch 14 → 15 taken 31 times.
31 QVariantMap result = outcomeMap(outcome);
551
2/2
✓ Branch 15 → 16 taken 1 time.
✓ Branch 15 → 47 taken 30 times.
31 if (outcome.kind == SieveSession::SaveOutcome::Kind::MergeReview) {
552
2/2
✓ Branch 16 → 17 taken 1 time.
✓ Branch 21 → 22 taken 1 time.
2 result.insert(QStringLiteral("merged"), QString::fromUtf8(outcome.merge.merged));
553
2/2
✓ Branch 26 → 27 taken 1 time.
✓ Branch 31 → 32 taken 1 time.
2 result.insert(QStringLiteral("theirs"), QString::fromUtf8(outcome.theirs));
554
2/2
✓ Branch 36 → 37 taken 1 time.
✓ Branch 41 → 42 taken 1 time.
2 result.insert(QStringLiteral("review"), reviewMap(outcome.theirs, outcome.merge.merged));
555 }
556
1/1
✓ Branch 47 → 48 taken 31 times.
31 finishLater(requestId, result);
557 31 });
558 31 return requestId;
559 31 }
560
561 3 QString QuickRulesBackend::activateScript(const QString &name) {
562
1/1
✓ Branch 2 → 3 taken 3 times.
3 SieveSession *s = session();
563
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 9 taken 3 times.
3 if (!s)
564 ✗ return refuseEdit(tr("Rules can't be changed right now. Reload them and try again."));
565
1/1
✓ Branch 9 → 10 taken 3 times.
3 const QString requestId = nextRequestId();
566
2/4
✓ Branch 12 → 13 taken 3 times.
✓ Branch 13 → 14 taken 3 times.
✗ Branch 27 → 28 not taken.
✗ Branch 27 → 29 not taken.
3 s->activateScript(name, [this, requestId, name](bool ok, const QString &message) {
567
3/5
✓ Branch 20 → 21 taken 3 times.
✓ Branch 23 → 24 taken 9 times.
✓ Branch 23 → 25 taken 3 times.
✗ Branch 46 → 47 not taken.
✗ Branch 46 → 48 not taken.
21 finishLater(requestId, {{QStringLiteral("ok"), ok}, {QStringLiteral("details"), message},
568
7/9
✓ Branch 13 → 14 taken 2 times.
✓ Branch 13 → 17 taken 1 time.
✓ Branch 14 → 15 taken 2 times.
✓ Branch 16 → 18 taken 2 times.
✓ Branch 17 → 18 taken 1 time.
✓ Branch 26 → 27 taken 2 times.
✓ Branch 26 → 28 taken 1 time.
✗ Branch 52 → 53 not taken.
✗ Branch 52 → 54 not taken.
6 {QStringLiteral("message"), ok ? tr("Script „%1“ is active").arg(name)
569 : tr("The script could not be activated.")}});
570
2/2
✓ Branch 34 → 35 taken 2 times.
✓ Branch 34 → 42 taken 1 time.
3 if (ok)
571
3/3
✓ Branch 35 → 36 taken 2 times.
✓ Branch 37 → 38 taken 2 times.
✓ Branch 38 → 39 taken 2 times.
4 emit toast(tr("Script „%1“ is active").arg(name), false);
572
1/5
✓ Branch 19 → 20 taken 3 times.
✗ Branch 75 → 76 not taken.
✗ Branch 75 → 79 not taken.
✗ Branch 77 → 78 not taken.
✗ Branch 77 → 79 not taken.
15 });
573 3 return requestId;
574 3 }
575
576 3 QString QuickRulesBackend::renameScript(const QString &from, const QString &to) {
577
1/1
✓ Branch 2 → 3 taken 3 times.
3 SieveSession *s = session();
578
7/11
✓ Branch 3 → 4 taken 3 times.
✗ Branch 3 → 7 not taken.
✓ Branch 4 → 5 taken 3 times.
✓ Branch 6 → 7 taken 1 time.
✓ Branch 6 → 8 taken 2 times.
✓ Branch 9 → 10 taken 3 times.
✗ Branch 9 → 11 not taken.
✓ Branch 11 → 12 taken 1 time.
✓ Branch 11 → 17 taken 2 times.
✗ Branch 29 → 30 not taken.
✗ Branch 29 → 31 not taken.
3 if (!s || to.trimmed().isEmpty())
579
2/2
✓ Branch 12 → 13 taken 1 time.
✓ Branch 13 → 14 taken 1 time.
1 return refuseEdit(tr("Enter a name for the script."));
580
1/1
✓ Branch 17 → 18 taken 2 times.
2 const QString requestId = nextRequestId();
581
3/3
✓ Branch 19 → 20 taken 2 times.
✓ Branch 20 → 21 taken 2 times.
✓ Branch 21 → 22 taken 2 times.
2 s->renameScript(from, to.trimmed(), [this, requestId](bool ok, const QString &message) {
582
3/5
✓ Branch 18 → 19 taken 2 times.
✓ Branch 21 → 22 taken 6 times.
✓ Branch 21 → 23 taken 2 times.
✗ Branch 34 → 35 not taken.
✗ Branch 34 → 36 not taken.
14 finishLater(requestId, {{QStringLiteral("ok"), ok}, {QStringLiteral("details"), message},
583
2/4
✓ Branch 13 → 14 taken 2 times.
✗ Branch 13 → 15 not taken.
✓ Branch 14 → 16 taken 2 times.
✗ Branch 15 → 16 not taken.
4 {QStringLiteral("message"), ok ? tr("Script renamed") : tr("The script could not be renamed.")}});
584
1/5
✓ Branch 17 → 18 taken 2 times.
✗ Branch 58 → 59 not taken.
✗ Branch 58 → 62 not taken.
✗ Branch 60 → 61 not taken.
✗ Branch 60 → 62 not taken.
10 });
585 2 return requestId;
586 2 }
587
588 2 QString QuickRulesBackend::deleteScript(const QString &name) {
589
1/1
✓ Branch 2 → 3 taken 2 times.
2 SieveSession *s = session();
590
6/8
✓ Branch 3 → 4 taken 2 times.
✗ Branch 3 → 7 not taken.
✓ Branch 6 → 7 taken 1 time.
✓ Branch 6 → 8 taken 1 time.
✓ Branch 9 → 10 taken 2 times.
✗ Branch 9 → 11 not taken.
✓ Branch 11 → 12 taken 1 time.
✓ Branch 11 → 17 taken 1 time.
2 if (!s || name == s->activeScript())
591
2/2
✓ Branch 12 → 13 taken 1 time.
✓ Branch 13 → 14 taken 1 time.
1 return refuseEdit(tr("The active script can't be deleted."));
592
1/1
✓ Branch 17 → 18 taken 1 time.
1 const QString requestId = nextRequestId();
593
2/4
✓ Branch 20 → 21 taken 1 time.
✓ Branch 21 → 22 taken 1 time.
✗ Branch 35 → 36 not taken.
✗ Branch 35 → 37 not taken.
1 s->deleteScript(name, [this, requestId, name](bool ok, const QString &message) {
594
3/5
✓ Branch 20 → 21 taken 1 time.
✓ Branch 23 → 24 taken 3 times.
✓ Branch 23 → 25 taken 1 time.
✗ Branch 38 → 39 not taken.
✗ Branch 38 → 40 not taken.
7 finishLater(requestId, {{QStringLiteral("ok"), ok}, {QStringLiteral("details"), message},
595
4/9
✓ Branch 13 → 14 taken 1 time.
✗ Branch 13 → 17 not taken.
✓ Branch 14 → 15 taken 1 time.
✓ Branch 16 → 18 taken 1 time.
✗ Branch 17 → 18 not taken.
✓ Branch 26 → 27 taken 1 time.
✗ Branch 26 → 28 not taken.
✗ Branch 44 → 45 not taken.
✗ Branch 44 → 46 not taken.
2 {QStringLiteral("message"), ok ? tr("Script „%1“ deleted; it stays in the version history").arg(name)
596 : tr("The script could not be deleted.")}});
597
1/5
✓ Branch 19 → 20 taken 1 time.
✗ Branch 67 → 68 not taken.
✗ Branch 67 → 71 not taken.
✗ Branch 69 → 70 not taken.
✗ Branch 69 → 71 not taken.
5 });
598 1 return requestId;
599 1 }
600
601 3 QString QuickRulesBackend::restoreVersion(const QString &id) {
602
1/1
✓ Branch 2 → 3 taken 3 times.
3 SieveSession *s = session();
603
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 9 taken 3 times.
3 if (!s)
604 ✗ return refuseEdit(tr("Rules can't be changed right now. Reload them and try again."));
605
1/1
✓ Branch 10 → 11 taken 3 times.
3 const QByteArray bytes = s->history().read(id);
606
2/2
✓ Branch 12 → 13 taken 1 time.
✓ Branch 12 → 18 taken 2 times.
3 if (bytes.isEmpty())
607
2/2
✓ Branch 13 → 14 taken 1 time.
✓ Branch 14 → 15 taken 1 time.
1 return refuseEdit(tr("This version is no longer available."));
608 2 return submit({bytes, QStringLiteral("restore"), tr("Version restored"), true});
609 7 }
610
611 // --- out of office ------------------------------------------------------------------------
612
613 4344 QVariantMap QuickRulesBackend::vacation() const {
614
2/3
✓ Branch 3 → 4 taken 4344 times.
✗ Branch 3 → 5 not taken.
✓ Branch 4 → 6 taken 4344 times.
4344 const auto account = m_deps.account ? m_deps.account() : std::nullopt;
615
2/2
✓ Branch 7 → 8 taken 4336 times.
✓ Branch 7 → 10 taken 8 times.
4344 const QString email = account ? account->email : QString();
616
1/1
✓ Branch 11 → 12 taken 4344 times.
4344 const SieveSession *s = session();
617
2/2
✓ Branch 12 → 13 taken 245 times.
✓ Branch 12 → 15 taken 4099 times.
4344 const ManageSieve::Capabilities caps = s ? s->capabilities() : ManageSieve::Capabilities{};
618 8688 QVariantMap map{{QStringLiteral("exists"), false},
619 8688 {QStringLiteral("enabled"), true},
620
1/1
✓ Branch 27 → 28 taken 4344 times.
8688 {QStringLiteral("subject"), tr("Out of office")},
621 8688 {QStringLiteral("message"), QString()},
622 8688 {QStringLiteral("days"), 1},
623
6/10
✓ Branch 42 → 43 taken 8 times.
✓ Branch 42 → 44 taken 4336 times.
✓ Branch 124 → 125 taken 4336 times.
✓ Branch 124 → 128 taken 8 times.
✓ Branch 126 → 127 taken 4336 times.
✓ Branch 126 → 128 taken 4336 times.
✗ Branch 399 → 400 not taken.
✗ Branch 399 → 403 not taken.
✗ Branch 401 → 402 not taken.
✗ Branch 401 → 403 not taken.
13024 {QStringLiteral("addresses"), email.isEmpty() ? QStringList{} : QStringList{email}},
624 8688 {QStringLiteral("from"), email},
625 8688 {QStringLiteral("firstDay"), QString()},
626 8688 {QStringLiteral("lastDay"), QString()},
627 8688 {QStringLiteral("active"), false},
628 13032 {QStringLiteral("supported"), caps.supports(QStringLiteral("vacation"))},
629 8688 {QStringLiteral("datesSupported"),
630
7/18
✓ Branch 80 → 81 taken 245 times.
✓ Branch 80 → 87 taken 4099 times.
✓ Branch 85 → 86 taken 245 times.
✗ Branch 85 → 87 not taken.
✓ Branch 101 → 102 taken 245 times.
✓ Branch 101 → 103 taken 4099 times.
✓ Branch 103 → 104 taken 4344 times.
✗ Branch 103 → 105 not taken.
✓ Branch 105 → 106 taken 4344 times.
✗ Branch 105 → 107 not taken.
✗ Branch 330 → 331 not taken.
✗ Branch 330 → 332 not taken.
✗ Branch 334 → 335 not taken.
✗ Branch 334 → 336 not taken.
✗ Branch 338 → 339 not taken.
✗ Branch 338 → 340 not taken.
✗ Branch 342 → 343 not taken.
✗ Branch 342 → 344 not taken.
13277 caps.supports(QStringLiteral("date")) && caps.supports(QStringLiteral("relational"))},
631
4/6
✓ Branch 95 → 96 taken 56472 times.
✓ Branch 95 → 97 taken 4344 times.
✓ Branch 99 → 100 taken 245 times.
✓ Branch 99 → 101 taken 4099 times.
✗ Branch 320 → 321 not taken.
✗ Branch 320 → 322 not taken.
65160 {QStringLiteral("blockIndex"), -1}};
632
2/2
✓ Branch 142 → 143 taken 4099 times.
✓ Branch 142 → 144 taken 245 times.
4344 if (!s)
633 4099 return map;
634
2/2
✓ Branch 312 → 145 taken 578 times.
✓ Branch 312 → 313 taken 32 times.
610 for (int i = 0; i < s->document().blocks.size(); ++i) {
635 578 const Sieve::Block &block = s->document().blocks.at(i);
636
2/2
✓ Branch 147 → 148 taken 365 times.
✓ Branch 147 → 149 taken 213 times.
578 if (block.kind != Sieve::BlockKind::Vacation)
637 365 continue;
638 213 const Sieve::Vacation &v = block.vacation;
639
1/1
✓ Branch 149 → 150 taken 213 times.
213 const QDate today = QDate::currentDate();
640
8/10
✓ Branch 150 → 151 taken 213 times.
✓ Branch 151 → 152 taken 13 times.
✓ Branch 151 → 155 taken 200 times.
✓ Branch 154 → 155 taken 13 times.
✗ Branch 154 → 161 not taken.
✓ Branch 155 → 156 taken 213 times.
✓ Branch 156 → 157 taken 31 times.
✓ Branch 156 → 160 taken 182 times.
✓ Branch 159 → 160 taken 31 times.
✗ Branch 159 → 161 not taken.
213 const bool inRange = (!v.firstDay.isValid() || v.firstDay <= today) && (!v.lastDay.isValid() || v.lastDay >= today);
641
1/1
✓ Branch 166 → 167 taken 213 times.
426 map.insert(QStringLiteral("exists"), true);
642
1/1
✓ Branch 174 → 175 taken 213 times.
426 map.insert(QStringLiteral("enabled"), v.enabled);
643
1/1
✓ Branch 182 → 183 taken 213 times.
426 map.insert(QStringLiteral("subject"), v.subject);
644
1/1
✓ Branch 190 → 191 taken 213 times.
426 map.insert(QStringLiteral("message"), v.message);
645 // S106-094: a script without ":days" shows RFC 5230's recommended 7.
646
2/3
✓ Branch 194 → 195 taken 213 times.
✗ Branch 194 → 196 not taken.
✓ Branch 201 → 202 taken 213 times.
426 map.insert(QStringLiteral("days"), v.days > 0 ? v.days : 7);
647
1/1
✓ Branch 209 → 210 taken 213 times.
426 map.insert(QStringLiteral("addresses"), v.addresses);
648
1/1
✓ Branch 217 → 218 taken 213 times.
426 map.insert(QStringLiteral("from"), v.from);
649
5/5
✓ Branch 221 → 222 taken 213 times.
✓ Branch 222 → 223 taken 13 times.
✓ Branch 222 → 224 taken 200 times.
✓ Branch 223 → 225 taken 13 times.
✓ Branch 229 → 230 taken 213 times.
426 map.insert(QStringLiteral("firstDay"), v.firstDay.isValid() ? v.firstDay.toString(Qt::ISODate) : QString());
650
5/5
✓ Branch 234 → 235 taken 213 times.
✓ Branch 235 → 236 taken 31 times.
✓ Branch 235 → 237 taken 182 times.
✓ Branch 236 → 238 taken 31 times.
✓ Branch 242 → 243 taken 213 times.
426 map.insert(QStringLiteral("lastDay"), v.lastDay.isValid() ? v.lastDay.toString(Qt::ISODate) : QString());
651
4/5
✓ Branch 247 → 248 taken 25 times.
✓ Branch 247 → 250 taken 188 times.
✓ Branch 248 → 249 taken 25 times.
✗ Branch 248 → 250 not taken.
✓ Branch 255 → 256 taken 213 times.
426 map.insert(QStringLiteral("active"), v.enabled && inRange);
652
6/8
✓ Branch 259 → 260 taken 25 times.
✓ Branch 259 → 266 taken 188 times.
✓ Branch 260 → 261 taken 25 times.
✓ Branch 261 → 262 taken 25 times.
✗ Branch 261 → 266 not taken.
✗ Branch 264 → 265 not taken.
✓ Branch 264 → 266 taken 25 times.
✓ Branch 271 → 272 taken 213 times.
426 map.insert(QStringLiteral("expired"), v.enabled && v.lastDay.isValid() && v.lastDay < today);
653
2/2
✓ Branch 275 → 276 taken 213 times.
✓ Branch 280 → 281 taken 213 times.
426 map.insert(QStringLiteral("summary"), RuleSentence::vacationSummary(v));
654
1/1
✓ Branch 294 → 295 taken 213 times.
426 map.insert(QStringLiteral("until"),
655
7/9
✓ Branch 285 → 286 taken 213 times.
✓ Branch 286 → 287 taken 31 times.
✓ Branch 286 → 289 taken 182 times.
✓ Branch 287 → 288 taken 31 times.
✓ Branch 288 → 290 taken 31 times.
✓ Branch 299 → 300 taken 31 times.
✓ Branch 299 → 301 taken 182 times.
✗ Branch 590 → 591 not taken.
✗ Branch 590 → 592 not taken.
426 v.lastDay.isValid() ? QLocale().toString(v.lastDay, QLocale::ShortFormat) : QString());
656
1/1
✓ Branch 305 → 306 taken 213 times.
426 map.insert(QStringLiteral("blockIndex"), i);
657 213 break;
658 }
659 245 return map;
660 78192 }
661
662 5 QString QuickRulesBackend::saveVacation(const QVariantMap &draft) {
663
1/1
✓ Branch 2 → 3 taken 5 times.
5 SieveSession *s = session();
664
4/7
✓ Branch 3 → 4 taken 5 times.
✗ Branch 3 → 6 not taken.
✓ Branch 4 → 5 taken 5 times.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 5 times.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 14 taken 5 times.
5 if (!s || readOnly())
665 ✗ return refuseEdit(tr("Rules can't be changed right now. Reload them and try again."));
666 5 Sieve::Vacation vacation;
667
2/2
✓ Branch 18 → 19 taken 5 times.
✓ Branch 19 → 20 taken 5 times.
10 vacation.enabled = draft.value(QStringLiteral("enabled"), true).toBool();
668
3/3
✓ Branch 28 → 29 taken 5 times.
✓ Branch 29 → 30 taken 5 times.
✓ Branch 30 → 31 taken 5 times.
10 vacation.subject = draft.value(QStringLiteral("subject")).toString().trimmed();
669
2/2
✓ Branch 42 → 43 taken 5 times.
✓ Branch 43 → 44 taken 5 times.
10 vacation.message = draft.value(QStringLiteral("message")).toString();
670
2/2
✓ Branch 54 → 55 taken 5 times.
✓ Branch 55 → 56 taken 5 times.
10 vacation.days = qBound(1, draft.value(QStringLiteral("days"), 1).toInt(), 365);
671
2/2
✓ Branch 65 → 66 taken 5 times.
✓ Branch 66 → 67 taken 5 times.
10 vacation.addresses = draft.value(QStringLiteral("addresses")).toStringList();
672
2/2
✓ Branch 77 → 78 taken 5 times.
✓ Branch 78 → 79 taken 5 times.
10 vacation.from = draft.value(QStringLiteral("from")).toString();
673 // F-46 (SPRINT-104): the window is stored and evaluated in UTC — the
674 // boundaries are fixed at save time instead of following the current
675 // offset, so a window saved in summer that runs past the winter change
676 // starts and ends on the intended days (a fixed +0200 zone was off by
677 // an hour after the change; a named IANA zone in :zone is not portable
678 // across servers). The first boundary uses local midnight (the window
679 // never opens late), the last the local end of day (it never closes
680 // early); both map to stable UTC dates.
681 3 const auto utcStartDay = [](const QDate &day) {
682
5/5
✓ Branch 2 → 3 taken 3 times.
✓ Branch 3 → 4 taken 3 times.
✓ Branch 4 → 5 taken 3 times.
✓ Branch 5 → 6 taken 3 times.
✓ Branch 6 → 7 taken 3 times.
3 return QDateTime(day, QTime(0, 0), QTimeZone::systemTimeZone()).toUTC().date();
683 };
684 4 const auto utcEndDay = [](const QDate &day) {
685
5/5
✓ Branch 2 → 3 taken 4 times.
✓ Branch 3 → 4 taken 4 times.
✓ Branch 4 → 5 taken 4 times.
✓ Branch 5 → 6 taken 4 times.
✓ Branch 6 → 7 taken 4 times.
4 return QDateTime(day, QTime(23, 59, 59), QTimeZone::systemTimeZone()).toUTC().date();
686 };
687
3/3
✓ Branch 89 → 90 taken 5 times.
✓ Branch 90 → 91 taken 5 times.
✓ Branch 91 → 92 taken 5 times.
10 const QDate first = QDate::fromString(draft.value(QStringLiteral("firstDay")).toString(), Qt::ISODate);
688
3/3
✓ Branch 101 → 102 taken 5 times.
✓ Branch 102 → 103 taken 5 times.
✓ Branch 103 → 104 taken 5 times.
10 const QDate last = QDate::fromString(draft.value(QStringLiteral("lastDay")).toString(), Qt::ISODate);
689
8/8
✓ Branch 109 → 110 taken 5 times.
✓ Branch 110 → 111 taken 2 times.
✓ Branch 110 → 113 taken 3 times.
✓ Branch 111 → 112 taken 2 times.
✓ Branch 112 → 113 taken 1 time.
✓ Branch 112 → 114 taken 1 time.
✓ Branch 115 → 116 taken 4 times.
✓ Branch 115 → 129 taken 1 time.
5 if (first.isValid() || last.isValid()) {
690
4/4
✓ Branch 116 → 117 taken 4 times.
✓ Branch 117 → 118 taken 3 times.
✓ Branch 117 → 119 taken 1 time.
✓ Branch 118 → 119 taken 3 times.
4 if (first.isValid()) vacation.firstDay = utcStartDay(first);
691
3/4
✓ Branch 119 → 120 taken 4 times.
✓ Branch 120 → 121 taken 4 times.
✗ Branch 120 → 122 not taken.
✓ Branch 121 → 122 taken 4 times.
4 if (last.isValid()) vacation.lastDay = utcEndDay(last);
692 4 vacation.zone = QStringLiteral("+0000");
693 }
694
3/3
✓ Branch 129 → 130 taken 5 times.
✓ Branch 132 → 133 taken 1 time.
✓ Branch 132 → 138 taken 4 times.
5 if (vacation.message.trimmed().isEmpty())
695
2/2
✓ Branch 133 → 134 taken 1 time.
✓ Branch 134 → 135 taken 1 time.
1 return refuseEdit(tr("Enter the reply text."));
696
9/10
✓ Branch 138 → 139 taken 4 times.
✓ Branch 139 → 140 taken 3 times.
✓ Branch 139 → 146 taken 1 time.
✓ Branch 140 → 141 taken 3 times.
✓ Branch 141 → 142 taken 3 times.
✗ Branch 141 → 146 not taken.
✓ Branch 144 → 145 taken 1 time.
✓ Branch 144 → 146 taken 2 times.
✓ Branch 147 → 148 taken 1 time.
✓ Branch 147 → 153 taken 3 times.
4 if (vacation.firstDay.isValid() && vacation.lastDay.isValid() && vacation.lastDay < vacation.firstDay)
697
2/2
✓ Branch 148 → 149 taken 1 time.
✓ Branch 149 → 150 taken 1 time.
1 return refuseEdit(tr("The last day is before the first day."));
698 3 Sieve::Document document = s->document();
699
1/1
✓ Branch 155 → 156 taken 3 times.
3 Sieve::placeVacation(&document, vacation);
700 3 return submit({Sieve::encode(document), QStringLiteral("vacation"),
701 3 vacation.enabled ? tr("Out of office saved") : tr("Out of office turned off"), true});
702 11 }
703
704 3 QString QuickRulesBackend::setVacationEnabled(bool enabled) {
705
1/1
✓ Branch 2 → 3 taken 3 times.
3 const QVariantMap current = vacation();
706
4/4
✓ Branch 7 → 8 taken 3 times.
✓ Branch 8 → 9 taken 3 times.
✓ Branch 13 → 14 taken 1 time.
✓ Branch 13 → 19 taken 2 times.
6 if (!current.value(QStringLiteral("exists")).toBool())
707
2/2
✓ Branch 14 → 15 taken 1 time.
✓ Branch 15 → 16 taken 1 time.
1 return refuseEdit(tr("There is no out-of-office reply yet."));
708
3/3
✓ Branch 23 → 24 taken 2 times.
✓ Branch 24 → 25 taken 2 times.
✓ Branch 25 → 26 taken 2 times.
6 return setEnabled(current.value(QStringLiteral("blockIndex")).toInt(), enabled);
709 3 }
710
711 2 QString QuickRulesBackend::removeVacation() {
712
1/1
✓ Branch 2 → 3 taken 2 times.
2 const QVariantMap current = vacation();
713
4/4
✓ Branch 7 → 8 taken 2 times.
✓ Branch 8 → 9 taken 2 times.
✓ Branch 13 → 14 taken 1 time.
✓ Branch 13 → 19 taken 1 time.
4 if (!current.value(QStringLiteral("exists")).toBool())
714
2/2
✓ Branch 14 → 15 taken 1 time.
✓ Branch 15 → 16 taken 1 time.
1 return refuseEdit(tr("There is no out-of-office reply yet."));
715
3/3
✓ Branch 23 → 24 taken 1 time.
✓ Branch 24 → 25 taken 1 time.
✓ Branch 25 → 26 taken 1 time.
2 return deleteRule(current.value(QStringLiteral("blockIndex")).toInt());
716 2 }
717