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: 91.2% 229 / 0 / 251
Functions: 94.1% 16 / 0 / 17
Branches: 62.7% 251 / 0 / 400

presentation/ComposeReplyPrefill.cpp
Line Branch Exec Source
1 #include "ComposeReplyPrefill.h"
2
3 #include "ComposeLog.h"
4 #include "ComposeQuoteSource.h"
5 #include "ComposeRecipients.h"
6 #include "QuickComposeForwardAttachments.h"
7 #include "WorkspaceState.h"
8 #include "app/core/MailSessionCore.h"
9 #include "app/core/AccountSession.h"
10 #include "controller/MailController.h"
11 #include "data/MailCache.h"
12 #include "service/MimeParser.h"
13 #include <QCoreApplication>
14 #include <QFutureWatcher>
15 #include <QLocale>
16 #include <QTemporaryDir>
17 #include <QTimer>
18 #include <QtConcurrent>
19
20 795 ComposeReplyPrefill::ComposeReplyPrefill(MailSessionCore &session, WorkspaceState &workspace,
21 const QString &profileDirectory,
22 const QHash<qint64, QString> &folderPathById,
23 const QHash<qint64, quint32> &folderEpochById,
24 795 Deps deps, QObject *parent)
25 795 : QObject(parent), m_session(session), m_workspace(workspace),
26 795 m_profileDirectory(profileDirectory), m_folderPathById(folderPathById),
27
1/1
✓ Branch 6 → 7 taken 795 times.
795 m_folderEpochById(folderEpochById), m_deps(std::move(deps)) {}
28
29 795 void ComposeReplyPrefill::connectSource() {
30 795 MailSessionCore &session = m_session;
31 795 m_composeSourcePool.setMaxThreadCount(1);
32 795 m_composeSourcePool.setExpiryTimeout(-1);
33 // Another mail selected: the reply being prepared is no longer wanted. A
34 // selection signal for the same mail (the list re-selects it when its
35 // page loads) keeps it — Ctrl+R right after the start was dropped and said
36 // "could not be loaded" (KJ2, pipeline #7218).
37
1/1
✓ Branch 4 → 5 taken 795 times.
795 connect(&m_workspace, &WorkspaceState::selectionChanged, this, [this] {
38
5/6
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 7 taken 1568 times.
✓ Branch 5 → 6 taken 1 time.
✗ Branch 5 → 7 not taken.
✓ Branch 8 → 9 taken 1 time.
✓ Branch 8 → 10 taken 1568 times.
1569 if (m_composeSourceToken > 0 && m_workspace.mailId() == m_composeSourceMail) return;
39 1568 cancelReplyDraft();
40 });
41 // The backend drops the folder's cached epoch in the receiver connected
42 // right before this one (formerly the first statement of this handler).
43
1/1
✓ Branch 7 → 8 taken 795 times.
795 connect(session.cache(), &MailCache::folderIdentityInvalidated, this,
44 795 [this](qint64 folderId) {
45
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 16 times.
16 if (m_composeSourceMail.folderId == folderId) cancelReplyDraft();
46 16 });
47
1/1
✓ Branch 10 → 11 taken 795 times.
795 connect(session.controller(), &MailController::folderIdentityInvalidated,
48 795 this, [this](qint64 folderId) {
49
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 11 times.
11 if (m_composeSourceMail.folderId == folderId) cancelReplyDraft();
50 11 });
51 795 }
52
53 795 void ComposeReplyPrefill::shutdown() {
54 795 m_composeSourceToken = 0;
55 795 cancelReplyDraft();
56
1/1
✓ Branch 4 → 5 taken 795 times.
795 m_composeSourcePool.waitForDone();
57 795 }
58
59 2402 void ComposeReplyPrefill::cancelReplyDraft() {
60 2402 const int oldToken = m_composeSourceToken;
61 2402 m_composeSourceToken = 0;
62 2402 ++m_composeSourceGeneration;
63
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 6 taken 2402 times.
2402 if (m_composeSourceCancelled) m_composeSourceCancelled->store(true);
64 2402 m_composeSourceCancelled.reset();
65 2402 m_composeSourceMail = {};
66
1/3
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 14 taken 2402 times.
✗ Branch 11 → 12 not taken.
2402 if (oldToken > 0) emit replyDraftReady(oldToken, {});
67 2402 }
68
69 // F-20 (SPRINT-104): every common reply prefix counts as one — an
70 // already-prefixed subject keeps a single canonical "Re:" instead of
71 // growing "Re: AW: …" on every round.
72 42 QString ComposeReplyPrefill::normalizeReplySubject(const QString &subject) {
73 static const QStringList prefixes = {
74 7 QStringLiteral("re"), QStringLiteral("aw"), QStringLiteral("fw"),
75 7 QStringLiteral("fwd"), QStringLiteral("wg"), QStringLiteral("sv"),
76 7 QStringLiteral("vs"), QStringLiteral("vb"), QStringLiteral("ref"),
77
5/10
✓ Branch 2 → 3 taken 7 times.
✓ Branch 2 → 52 taken 35 times.
✓ Branch 4 → 5 taken 7 times.
✗ Branch 4 → 52 not taken.
✓ Branch 39 → 40 taken 70 times.
✓ Branch 39 → 41 taken 7 times.
✗ Branch 94 → 95 not taken.
✗ Branch 94 → 96 not taken.
✗ Branch 133 → 134 not taken.
✗ Branch 133 → 135 not taken.
126 QStringLiteral("antwort")};
78 42 QString rest = subject;
79 forever {
80 53 bool matched = false;
81
2/2
✓ Branch 79 → 55 taken 443 times.
✓ Branch 79 → 80 taken 42 times.
485 for (const QString &prefix : prefixes) {
82 443 if (rest.size() > prefix.size() &&
83
6/7
✓ Branch 58 → 59 taken 427 times.
✓ Branch 58 → 68 taken 16 times.
✓ Branch 59 → 60 taken 427 times.
✓ Branch 60 → 61 taken 11 times.
✓ Branch 60 → 68 taken 416 times.
✓ Branch 66 → 67 taken 11 times.
✗ Branch 66 → 68 not taken.
454 rest.startsWith(prefix, Qt::CaseInsensitive) &&
84
2/2
✓ Branch 69 → 70 taken 11 times.
✓ Branch 69 → 77 taken 432 times.
454 rest.at(prefix.size()) == QLatin1Char(':')) {
85
2/2
✓ Branch 71 → 72 taken 11 times.
✓ Branch 72 → 73 taken 11 times.
11 rest = rest.mid(prefix.size() + 1).trimmed();
86 11 matched = true;
87 11 break;
88 }
89 }
90
2/2
✓ Branch 80 → 81 taken 42 times.
✓ Branch 80 → 82 taken 11 times.
53 if (!matched) break;
91 11 }
92 // A subject without a prefix gets one, as every reply did before F-20.
93
1/1
✓ Branch 86 → 87 taken 42 times.
84 return QStringLiteral("Re: ") + rest;
94 119 }
95
96 32 bool ComposeReplyPrefill::startComposeSource(int requestToken, const std::function<SourceBuilder()> &prepare) {
97
1/1
✓ Branch 2 → 3 taken 32 times.
32 cancelReplyDraft();
98 32 const MailId id = m_workspace.mailId();
99
6/13
✓ Branch 5 → 6 taken 32 times.
✗ Branch 5 → 13 not taken.
✓ Branch 7 → 8 taken 32 times.
✗ Branch 7 → 13 not taken.
✓ Branch 8 → 9 taken 32 times.
✓ Branch 10 → 11 taken 32 times.
✗ Branch 10 → 13 not taken.
✓ Branch 15 → 16 taken 32 times.
✗ Branch 15 → 17 not taken.
✗ Branch 17 → 18 not taken.
✓ Branch 17 → 19 taken 32 times.
✗ Branch 58 → 59 not taken.
✗ Branch 58 → 60 not taken.
64 if (requestToken <= 0 || !id.isValid() || id.accountId != m_deps.accountName() ||
100
1/2
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 14 taken 32 times.
32 !m_folderPathById.contains(id.folderId)) return false;
101 32 const quint32 expectedEpoch = m_folderEpochById.value(id.folderId, 0);
102
1/2
✗ Branch 20 → 21 not taken.
✓ Branch 20 → 22 taken 32 times.
32 if (expectedEpoch == 0) return false;
103 32 const QString databasePath = m_session.cache()->databasePath();
104
1/2
✗ Branch 25 → 26 not taken.
✓ Branch 25 → 27 taken 32 times.
32 if (databasePath.isEmpty()) return false;
105 32 const quint64 generation = m_composeSourceGeneration;
106 32 const quint64 accountGeneration = m_session.accounts()->generation();
107
1/1
✓ Branch 29 → 30 taken 32 times.
32 auto cancelled = std::make_shared<std::atomic_bool>(false);
108 32 m_composeSourceCancelled = cancelled;
109 32 m_composeSourceMail = id;
110 32 m_composeSourceToken = requestToken;
111
3/6
✓ Branch 32 → 33 taken 32 times.
✓ Branch 33 → 34 taken 32 times.
✗ Branch 34 → 35 not taken.
✓ Branch 34 → 36 taken 32 times.
✗ Branch 63 → 64 not taken.
✗ Branch 63 → 65 not taken.
32 auto *watcher = new QFutureWatcher<QVariantMap>(this);
112 32 connect(watcher, &QFutureWatcher<QVariantMap>::finished, this,
113
2/8
✓ Branch 39 → 40 taken 32 times.
✓ Branch 40 → 41 taken 32 times.
✗ Branch 68 → 69 not taken.
✗ Branch 68 → 70 not taken.
✗ Branch 71 → 72 not taken.
✗ Branch 71 → 73 not taken.
✗ Branch 74 → 75 not taken.
✗ Branch 74 → 76 not taken.
64 [this, watcher, cancelled, id, databasePath, expectedEpoch, generation,
114 accountGeneration, requestToken, prepare] {
115
1/1
✓ Branch 2 → 3 taken 32 times.
32 const auto draft = watcher->result();
116
1/1
✓ Branch 3 → 4 taken 32 times.
32 watcher->deleteLater();
117
1/2
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 32 times.
64 const char *dropped = cancelled->load() ? "cancelled"
118
1/2
✓ Branch 8 → 9 taken 32 times.
✗ Branch 8 → 30 not taken.
64 : generation != m_composeSourceGeneration ? "superseded"
119
1/2
✓ Branch 11 → 12 taken 32 times.
✗ Branch 11 → 28 not taken.
64 : accountGeneration != m_session.accounts()->generation() ? "account changed"
120
1/2
✓ Branch 14 → 15 taken 32 times.
✗ Branch 14 → 26 not taken.
64 : m_workspace.mailId() != id ? "another mail selected"
121
1/2
✓ Branch 16 → 17 taken 32 times.
✗ Branch 16 → 24 not taken.
64 : m_folderEpochById.value(id.folderId, 0) != expectedEpoch ? "folder epoch changed"
122
1/2
✗ Branch 20 → 21 not taken.
✓ Branch 20 → 22 taken 32 times.
64 : m_session.cache()->databasePath() != databasePath ? "database changed"
123
1/2
✓ Branch 32 → 33 taken 32 times.
✗ Branch 32 → 34 not taken.
32 : nullptr;
124 // The folder was re-read (a new epoch, another database) or the cache
125 // had no ready snapshot while the same mail is still shown: once more,
126 // instead of swallowing the request (Ctrl+R right after the start,
127 // KJ2 in pipelines #7218/#7220). Cancelled and superseded requests
128 // were already answered by cancelReplyDraft.
129
1/5
✗ Branch 34 → 35 not taken.
✓ Branch 34 → 40 taken 32 times.
✗ Branch 35 → 36 not taken.
✗ Branch 36 → 37 not taken.
✗ Branch 36 → 39 not taken.
32 const bool reread = dropped && (qstrcmp(dropped, "folder epoch changed") == 0 ||
130 ✗ qstrcmp(dropped, "database changed") == 0);
131
5/11
✓ Branch 41 → 42 taken 32 times.
✗ Branch 41 → 45 not taken.
✓ Branch 42 → 43 taken 32 times.
✗ Branch 42 → 50 not taken.
✓ Branch 43 → 44 taken 32 times.
✗ Branch 44 → 45 not taken.
✓ Branch 44 → 50 taken 32 times.
✗ Branch 47 → 48 not taken.
✗ Branch 47 → 50 not taken.
✗ Branch 51 → 52 not taken.
✓ Branch 51 → 72 taken 32 times.
32 if ((reread || (!dropped && draft.isEmpty())) && m_workspace.mailId() == id &&
132 ✗ m_composeSourceRetriedToken != requestToken) {
133 ✗ qCInfo(lcCompose) << "reply source read again:" << (dropped ? dropped : "no ready snapshot") << "token"
134 ✗ << requestToken;
135 ✗ m_composeSourceRetriedToken = requestToken;
136 ✗ m_composeSourceToken = 0; // the same request: no "could not be loaded" in between
137 ✗ QTimer::singleShot(0, this, [this, requestToken, prepare] {
138 ✗ if (!startComposeSource(requestToken, prepare)) emit replyDraftReady(requestToken, {});
139 ✗ });
140 ✗ return;
141 }
142
1/2
✗ Branch 72 → 73 not taken.
✓ Branch 72 → 92 taken 32 times.
32 if (dropped) {
143 ✗ qCInfo(lcCompose) << "reply source dropped:" << dropped << "token" << requestToken;
144 ✗ if (reread) emit replyDraftReady(requestToken, {}); // nobody else tells the composer
145 ✗ return;
146 }
147 32 m_composeSourceCancelled.reset();
148 32 m_composeSourceMail = {};
149 32 m_composeSourceToken = 0;
150
1/1
✓ Branch 95 → 96 taken 32 times.
32 emit replyDraftReady(requestToken, draft);
151
1/2
✓ Branch 98 → 99 taken 32 times.
✗ Branch 98 → 101 not taken.
32 });
152
1/1
✓ Branch 43 → 44 taken 32 times.
32 const SourceBuilder build = prepare();
153
1/1
✓ Branch 49 → 50 taken 32 times.
32 watcher->setFuture(QtConcurrent::run(&m_composeSourcePool,
154
2/8
✓ Branch 47 → 48 taken 32 times.
✓ Branch 48 → 49 taken 32 times.
✗ Branch 83 → 84 not taken.
✗ Branch 83 → 85 not taken.
✗ Branch 86 → 87 not taken.
✗ Branch 86 → 88 not taken.
✗ Branch 89 → 90 not taken.
✗ Branch 89 → 91 not taken.
64 [id, databasePath, expectedEpoch, cancelled, build]() -> QVariantMap {
155
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 32 times.
32 if (cancelled->load()) return {};
156
2/2
✓ Branch 6 → 7 taken 22 times.
✓ Branch 6 → 8 taken 10 times.
32 static thread_local std::unique_ptr<MailCache> reader;
157
2/2
✓ Branch 8 → 9 taken 22 times.
✓ Branch 8 → 10 taken 10 times.
32 static thread_local QString readerPath;
158
5/6
✓ Branch 11 → 12 taken 10 times.
✓ Branch 11 → 14 taken 22 times.
✗ Branch 13 → 14 not taken.
✓ Branch 13 → 15 taken 10 times.
✓ Branch 16 → 17 taken 22 times.
✓ Branch 16 → 28 taken 10 times.
32 if (!reader || readerPath != databasePath) {
159 22 reader.reset();
160
1/1
✓ Branch 18 → 19 taken 22 times.
22 reader = std::make_unique<MailCache>();
161
2/3
✓ Branch 22 → 23 taken 22 times.
✗ Branch 23 → 24 not taken.
✓ Branch 23 → 27 taken 22 times.
22 if (!reader->openReadOnly(databasePath)) {
162 ✗ reader.reset();
163 ✗ readerPath.clear();
164 ✗ return {};
165 }
166 22 readerPath = databasePath;
167 }
168 32 auto source = reader->snapshots().composeSourceSnapshot(id.accountId, id.folderId,
169
1/1
✓ Branch 31 → 32 taken 32 times.
32 id.uid, expectedEpoch, *cancelled);
170
2/4
✓ Branch 34 → 35 taken 32 times.
✗ Branch 34 → 36 not taken.
✗ Branch 38 → 39 not taken.
✓ Branch 38 → 40 taken 32 times.
64 if (cancelled->load() ||
171
1/2
✗ Branch 35 → 36 not taken.
✓ Branch 35 → 37 taken 32 times.
32 source.outcome != MailCache::ComposeSourceSnapshot::Outcome::Ready)
172 ✗ return {};
173 // An HTML-only original has no text projection: the quote's text (the
174 // reply's plain part, a plain-text reply, a forward) comes from its HTML.
175
11/18
✓ Branch 40 → 41 taken 32 times.
✗ Branch 40 → 48 not taken.
✓ Branch 41 → 42 taken 32 times.
✓ Branch 43 → 44 taken 1 time.
✓ Branch 43 → 48 taken 31 times.
✓ Branch 44 → 45 taken 1 time.
✓ Branch 46 → 47 taken 1 time.
✗ Branch 46 → 48 not taken.
✓ Branch 49 → 50 taken 1 time.
✓ Branch 49 → 51 taken 31 times.
✓ Branch 51 → 52 taken 32 times.
✗ Branch 51 → 53 not taken.
✓ Branch 53 → 54 taken 1 time.
✓ Branch 53 → 58 taken 31 times.
✗ Branch 71 → 72 not taken.
✗ Branch 71 → 73 not taken.
✗ Branch 75 → 76 not taken.
✗ Branch 75 → 77 not taken.
32 if (source.bodyPresent && source.quoteText.trimmed().isEmpty() && !source.quoteHtml.trimmed().isEmpty())
176
1/1
✓ Branch 54 → 55 taken 1 time.
1 source.quoteText = ComposeQuoteSource::textOf(source.quoteHtml);
177
1/1
✓ Branch 59 → 60 taken 32 times.
32 const QVariantMap draft = build(*reader, source, id);
178
1/2
✗ Branch 62 → 63 not taken.
✓ Branch 62 → 64 taken 32 times.
32 return cancelled->load() ? QVariantMap{} : draft;
179 32 }));
180 32 return true;
181 32 }
182
183 30 bool ComposeReplyPrefill::requestReplyDraft(int requestToken, bool all,
184 bool forward) {
185
1/1
✓ Branch 3 → 4 taken 30 times.
60 return startComposeSource(requestToken, [this, all, forward]() -> SourceBuilder {
186
1/1
✓ Branch 2 → 3 taken 30 times.
30 const QString ownEmail = m_deps.accountEmail();
187
1/1
✓ Branch 3 → 4 taken 30 times.
30 const QString replyAttribution = QCoreApplication::translate("QuickComposeBackend", "On %1, %2 wrote:");
188 const QString forwardAttribution = QCoreApplication::translate("QuickComposeBackend", "---------- Forwarded message ----------\n"
189
1/1
✓ Branch 4 → 5 taken 30 times.
30 "From: %1\nDate: %2\nSubject: %3\n");
190
1/1
✓ Branch 5 → 6 taken 30 times.
30 const QString trimmedText = QCoreApplication::translate("QuickComposeBackend", "… [trimmed]");
191
1/1
✓ Branch 6 → 7 taken 30 times.
30 const QLocale locale;
192
1/1
✓ Branch 7 → 8 taken 30 times.
30 const QString messageLanguage = locale.name();
193 // F-06 (SPRINT-104): the forward's attachment scratch directory is
194 // created and owned on the GUI thread; the worker fills it, and the
195 // files live until the next forward or teardown (the send reads them).
196 30 std::shared_ptr<QTemporaryDir> forwardScratch;
197
2/2
✓ Branch 8 → 9 taken 3 times.
✓ Branch 8 → 14 taken 27 times.
30 if (forward)
198
1/1
✓ Branch 9 → 10 taken 3 times.
3 m_forwardScratch = forwardScratch = QuickComposeForwardAttachments::makeScratch(m_profileDirectory);
199
0/12
✗ Branch 36 → 37 not taken.
✗ Branch 36 → 38 not taken.
✗ Branch 39 → 40 not taken.
✗ Branch 39 → 41 not taken.
✗ Branch 42 → 43 not taken.
✗ Branch 42 → 44 not taken.
✗ Branch 45 → 46 not taken.
✗ Branch 45 → 47 not taken.
✗ Branch 48 → 49 not taken.
✗ Branch 48 → 50 not taken.
✗ Branch 51 → 52 not taken.
✗ Branch 51 → 53 not taken.
60 return [ownEmail, all, forward, locale, messageLanguage, replyAttribution, forwardAttribution, trimmedText,
200 forwardScratch](MailCache &reader, const MailCache::ComposeSourceSnapshot &source,
201 const MailId &id) -> QVariantMap {
202 // F-06 (SPRINT-104, D-3 Must): a forward keeps the original's
203 // attachments. The cached blobs are exported to files of a forwarding
204 // scratch directory (the composer and the send path work with file
205 // paths); only the compose budget's worth travels, the rest is named.
206 // F-06 (SPRINT-104, D-3 Must): the forward's attachments travel as
207 // exported files (the composer and send path work with paths).
208 const QStringList forwardFiles =
209
1/2
✓ Branch 4 → 5 taken 3 times.
✗ Branch 4 → 9 not taken.
3 forward && forwardScratch
210 33 ? QuickComposeForwardAttachments::exportTo(reader, id.folderId, id.uid,
211
3/5
✓ Branch 6 → 7 taken 3 times.
✓ Branch 10 → 11 taken 3 times.
✓ Branch 10 → 12 taken 27 times.
✗ Branch 231 → 232 not taken.
✗ Branch 231 → 233 not taken.
33 forwardScratch->path())
212
3/3
✓ Branch 2 → 3 taken 3 times.
✓ Branch 2 → 9 taken 27 times.
✓ Branch 7 → 8 taken 3 times.
33 : QStringList{};
213 30 const auto &header = source.header;
214 30 const QString date = locale.toString(header.date.toLocalTime(),
215
2/2
✓ Branch 12 → 13 taken 30 times.
✓ Branch 13 → 14 taken 30 times.
30 QLocale::ShortFormat);
216 30 QString subject = header.subject;
217 30 QVariantMap draft;
218
2/2
✓ Branch 16 → 17 taken 3 times.
✓ Branch 16 → 92 taken 27 times.
30 if (forward) {
219
2/3
✓ Branch 18 → 19 taken 3 times.
✓ Branch 19 → 20 taken 3 times.
✗ Branch 19 → 29 not taken.
3 if (!subject.startsWith(QLatin1String("Fwd:"), Qt::CaseInsensitive))
220
1/1
✓ Branch 23 → 24 taken 3 times.
3 subject = QStringLiteral("Fwd: ") + subject;
221
1/1
✓ Branch 33 → 34 taken 3 times.
6 draft.insert(QStringLiteral("subject"), subject);
222
1/2
✓ Branch 37 → 38 taken 3 times.
✗ Branch 37 → 81 not taken.
3 if (source.bodyPresent) {
223
1/1
✓ Branch 41 → 42 taken 3 times.
3 QStringList lines = source.quoteText.split(QLatin1Char('\n'));
224
2/4
✓ Branch 42 → 43 taken 3 times.
✗ Branch 42 → 45 not taken.
✗ Branch 44 → 45 not taken.
✓ Branch 44 → 46 taken 3 times.
3 const bool trimmed = source.quoteTruncated || lines.size() > 200;
225
1/3
✗ Branch 48 → 49 not taken.
✓ Branch 48 → 53 taken 3 times.
✗ Branch 49 → 50 not taken.
3 if (lines.size() > 200) lines = lines.mid(0, 200);
226
1/3
✗ Branch 53 → 54 not taken.
✓ Branch 53 → 55 taken 3 times.
✗ Branch 54 → 55 not taken.
3 if (trimmed) lines.append(trimmedText);
227
1/1
✓ Branch 70 → 71 taken 3 times.
6 draft.insert(QStringLiteral("body"),
228
1/1
✓ Branch 62 → 63 taken 3 times.
6 forwardAttribution.arg(header.from, date, header.subject) +
229
4/4
✓ Branch 59 → 60 taken 3 times.
✓ Branch 63 → 64 taken 3 times.
✓ Branch 64 → 65 taken 3 times.
✓ Branch 65 → 66 taken 3 times.
12 QLatin1Char('\n') + lines.join(QLatin1Char('\n')) + QLatin1Char('\n'));
230 3 }
231
1/2
✗ Branch 82 → 83 not taken.
✓ Branch 82 → 225 taken 3 times.
3 if (!forwardFiles.isEmpty())
232 ✗ draft.insert(QStringLiteral("attachments"), forwardFiles);
233 } else {
234
1/1
✓ Branch 92 → 93 taken 27 times.
27 subject = normalizeReplySubject(subject);
235
1/1
✓ Branch 95 → 96 taken 27 times.
27 const auto envelope = MimeParser::messageHeaders(source.envelopePrefix);
236 const auto recipients = ComposeRecipients::replyTargets(
237
2/2
✓ Branch 104 → 105 taken 27 times.
✓ Branch 105 → 106 taken 27 times.
54 envelope.value("from", header.from),
238
4/4
✓ Branch 100 → 101 taken 27 times.
✓ Branch 101 → 102 taken 27 times.
✓ Branch 102 → 103 taken 27 times.
✓ Branch 103 → 104 taken 27 times.
54 envelope.value("to", header.to), envelope.value("cc"),
239
3/3
✓ Branch 97 → 98 taken 27 times.
✓ Branch 98 → 99 taken 27 times.
✓ Branch 106 → 107 taken 27 times.
81 envelope.value("reply-to"), ownEmail, all);
240
1/1
✓ Branch 121 → 122 taken 27 times.
54 draft.insert(QStringLiteral("to"), recipients.to);
241
1/1
✓ Branch 129 → 130 taken 27 times.
54 draft.insert(QStringLiteral("cc"), recipients.cc);
242
1/1
✓ Branch 137 → 138 taken 27 times.
54 draft.insert(QStringLiteral("subject"), subject);
243
1/2
✓ Branch 141 → 142 taken 27 times.
✗ Branch 141 → 201 not taken.
27 if (source.bodyPresent) {
244
1/1
✓ Branch 145 → 146 taken 27 times.
27 QStringList lines = source.quoteText.split(QLatin1Char('\n'));
245
2/4
✓ Branch 146 → 147 taken 27 times.
✗ Branch 146 → 149 not taken.
✗ Branch 148 → 149 not taken.
✓ Branch 148 → 150 taken 27 times.
27 const bool trimmed = source.quoteTruncated || lines.size() > 200;
246
1/3
✗ Branch 152 → 153 not taken.
✓ Branch 152 → 157 taken 27 times.
✗ Branch 153 → 154 not taken.
27 if (lines.size() > 200) lines = lines.mid(0, 200);
247
1/3
✗ Branch 157 → 158 not taken.
✓ Branch 157 → 159 taken 27 times.
✗ Branch 158 → 159 not taken.
27 if (trimmed) lines.append(trimmedText);
248 27 QStringList quotedLines;
249
1/1
✓ Branch 160 → 161 taken 27 times.
27 quotedLines.reserve(lines.size());
250
4/4
✓ Branch 161 → 162 taken 27 times.
✓ Branch 162 → 163 taken 27 times.
✓ Branch 173 → 164 taken 77 times.
✓ Branch 173 → 174 taken 27 times.
104 for (const QString &line : lines)
251
3/3
✓ Branch 166 → 167 taken 77 times.
✓ Branch 167 → 168 taken 77 times.
✓ Branch 168 → 169 taken 77 times.
77 quotedLines.append(QLatin1String("> ") + line);
252
1/1
✓ Branch 189 → 190 taken 27 times.
54 draft.insert(QStringLiteral("body"),
253
2/2
✓ Branch 181 → 182 taken 27 times.
✓ Branch 182 → 183 taken 27 times.
54 replyAttribution.arg(date, header.from) + QLatin1Char('\n') +
254
3/3
✓ Branch 178 → 179 taken 27 times.
✓ Branch 183 → 184 taken 27 times.
✓ Branch 184 → 185 taken 27 times.
108 quotedLines.join(QLatin1Char('\n')) + QLatin1Char('\n'));
255 27 }
256 27 QStringList references = header.references;
257
3/3
✓ Branch 203 → 204 taken 24 times.
✓ Branch 203 → 205 taken 3 times.
✓ Branch 204 → 205 taken 24 times.
27 if (!header.messageId.isEmpty()) references.append(header.messageId);
258
1/1
✓ Branch 209 → 210 taken 27 times.
54 draft.insert(QStringLiteral("inReplyTo"), header.messageId);
259
1/1
✓ Branch 217 → 218 taken 27 times.
54 draft.insert(QStringLiteral("references"), references);
260 27 }
261 // Sprint 108: the new composer's quote, format and identity inputs.
262
1/1
✓ Branch 225 → 226 taken 30 times.
30 ComposeQuoteSource::add(draft, source, forward, messageLanguage);
263 30 return draft;
264
1/1
✓ Branch 21 → 22 taken 30 times.
90 };
265 90 });
266 }
267
268 10 QVariantMap ComposeReplyPrefill::replyDraft(bool all) const {
269 10 const auto id = m_workspace.mailId();
270
2/3
✓ Branch 4 → 5 taken 10 times.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 10 times.
10 if (!m_deps.acceptsIdentity(id)) return {};
271
1/1
✓ Branch 8 → 9 taken 10 times.
10 const auto header = m_session.cache()->header(id.folderId, id.uid);
272
1/2
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 10 times.
10 if (!header) return {};
273
1/1
✓ Branch 13 → 14 taken 10 times.
10 const QString subject = normalizeReplySubject(header->subject); // F-20
274 // Quote the stored plain text: the standard attribution header plus
275 // "> "-prefixed lines, bounded so huge mails cannot flood the editor.
276 // P90.14: the envelope prefix + plain text projection replaces the full
277 // raw-source load; header-row fallbacks cover pathological envelopes.
278 10 QString quoted;
279 10 QMap<QString, QString> envelopeHeaders;
280
3/3
✓ Branch 15 → 16 taken 10 times.
✓ Branch 17 → 18 taken 6 times.
✓ Branch 17 → 81 taken 4 times.
10 if (const auto original = m_session.cache()->replySnapshot(id.folderId, id.uid)) {
281
1/1
✓ Branch 19 → 20 taken 6 times.
6 envelopeHeaders = MimeParser::messageHeaders(original->rawSource);
282
1/1
✓ Branch 22 → 23 taken 6 times.
6 const QString attribution = QCoreApplication::translate("QuickComposeBackend", "On %1, %2 wrote:")
283
2/2
✓ Branch 26 → 27 taken 6 times.
✓ Branch 27 → 28 taken 6 times.
12 .arg(QLocale().toString(header->date.toLocalTime(), QLocale::ShortFormat),
284
2/2
✓ Branch 24 → 25 taken 6 times.
✓ Branch 28 → 29 taken 6 times.
12 header->from);
285
1/1
✓ Branch 37 → 38 taken 6 times.
6 QStringList lines = original->textPlain.split(QLatin1Char('\n'));
286
1/2
✗ Branch 39 → 40 not taken.
✓ Branch 39 → 47 taken 6 times.
6 if (lines.size() > 200) {
287 ✗ lines = lines.mid(0, 200);
288 ✗ lines.append(QCoreApplication::translate("QuickComposeBackend", "… [trimmed]"));
289 }
290 6 QStringList quotedLines;
291
1/1
✓ Branch 48 → 49 taken 6 times.
6 quotedLines.reserve(lines.size());
292
7/7
✓ Branch 49 → 50 taken 6 times.
✓ Branch 50 → 51 taken 6 times.
✓ Branch 54 → 55 taken 6 times.
✓ Branch 55 → 56 taken 6 times.
✓ Branch 56 → 57 taken 6 times.
✓ Branch 61 → 52 taken 6 times.
✓ Branch 61 → 62 taken 6 times.
12 for (const QString &line : lines) quotedLines.append(QLatin1String("> ") + line);
293
4/4
✓ Branch 66 → 67 taken 6 times.
✓ Branch 69 → 70 taken 6 times.
✓ Branch 70 → 71 taken 6 times.
✓ Branch 71 → 72 taken 6 times.
6 quoted = attribution + QLatin1Char('\n') + quotedLines.join(QLatin1Char('\n')) + QLatin1Char('\n');
294 16 }
295 10 QStringList references = header->references;
296
3/3
✓ Branch 86 → 87 taken 4 times.
✓ Branch 86 → 89 taken 6 times.
✓ Branch 88 → 89 taken 4 times.
10 if (!header->messageId.isEmpty()) references.append(header->messageId);
297 const auto recipients = ComposeRecipients::replyTargets(
298
4/4
✓ Branch 97 → 98 taken 10 times.
✓ Branch 98 → 99 taken 10 times.
✓ Branch 100 → 101 taken 10 times.
✓ Branch 101 → 102 taken 10 times.
20 envelopeHeaders.value("from", header->from), envelopeHeaders.value("to", header->to),
299
6/6
✓ Branch 89 → 90 taken 10 times.
✓ Branch 91 → 92 taken 10 times.
✓ Branch 92 → 93 taken 10 times.
✓ Branch 94 → 95 taken 10 times.
✓ Branch 95 → 96 taken 10 times.
✓ Branch 102 → 103 taken 10 times.
30 envelopeHeaders.value("cc"), envelopeHeaders.value("reply-to"), m_deps.accountEmail(), all);
300 10 QVariantMap draft;
301
1/1
✓ Branch 118 → 119 taken 10 times.
20 draft.insert(QStringLiteral("to"), recipients.to);
302
1/1
✓ Branch 126 → 127 taken 10 times.
20 draft.insert(QStringLiteral("cc"), recipients.cc);
303
1/1
✓ Branch 134 → 135 taken 10 times.
20 draft.insert(QStringLiteral("subject"), subject);
304
1/1
✓ Branch 142 → 143 taken 10 times.
20 draft.insert(QStringLiteral("body"), quoted);
305
1/1
✓ Branch 151 → 152 taken 10 times.
20 draft.insert(QStringLiteral("inReplyTo"), header->messageId);
306
1/1
✓ Branch 159 → 160 taken 10 times.
20 draft.insert(QStringLiteral("references"), references);
307 10 return draft;
308 10 }
309
310 1 QVariantMap ComposeReplyPrefill::forwardDraft() const {
311 1 const auto id = m_workspace.mailId();
312
2/3
✓ Branch 4 → 5 taken 1 time.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 1 time.
1 if (!m_deps.acceptsIdentity(id)) return {};
313
1/1
✓ Branch 8 → 9 taken 1 time.
1 const auto header = m_session.cache()->header(id.folderId, id.uid);
314
1/2
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 1 time.
1 if (!header) return {};
315 1 QString subject = header->subject;
316
2/3
✓ Branch 15 → 16 taken 1 time.
✓ Branch 16 → 17 taken 1 time.
✗ Branch 16 → 26 not taken.
1 if (!subject.startsWith(QLatin1String("Fwd:"), Qt::CaseInsensitive))
317
1/1
✓ Branch 20 → 21 taken 1 time.
1 subject = QStringLiteral("Fwd: ") + subject;
318 1 QString body;
319 // P90.14: forwarding quotes the plain text only; no raw-source load.
320
2/3
✓ Branch 27 → 28 taken 1 time.
✓ Branch 29 → 30 taken 1 time.
✗ Branch 29 → 74 not taken.
1 if (const auto original = m_session.cache()->displayBody(id.folderId, id.uid)) {
321
1/1
✓ Branch 30 → 31 taken 1 time.
1 const QString attribution = QCoreApplication::translate("QuickComposeBackend", "---------- Forwarded message ----------\n"
322 "From: %1\nDate: %2\nSubject: %3\n")
323 1 .arg(header->from,
324
2/2
✓ Branch 34 → 35 taken 1 time.
✓ Branch 35 → 36 taken 1 time.
2 QLocale().toString(header->date.toLocalTime(), QLocale::ShortFormat),
325
2/2
✓ Branch 32 → 33 taken 1 time.
✓ Branch 37 → 38 taken 1 time.
2 header->subject);
326
1/1
✓ Branch 46 → 47 taken 1 time.
1 QStringList lines = original->textPlain.split(QLatin1Char('\n'));
327
1/2
✗ Branch 48 → 49 not taken.
✓ Branch 48 → 56 taken 1 time.
1 if (lines.size() > 200) {
328 ✗ lines = lines.mid(0, 200);
329 ✗ lines.append(QCoreApplication::translate("QuickComposeBackend", "… [trimmed]"));
330 }
331
4/4
✓ Branch 60 → 61 taken 1 time.
✓ Branch 63 → 64 taken 1 time.
✓ Branch 64 → 65 taken 1 time.
✓ Branch 65 → 66 taken 1 time.
1 body = attribution + QLatin1Char('\n') + lines.join(QLatin1Char('\n')) + QLatin1Char('\n');
332 2 }
333 2 return {{QStringLiteral("subject"), subject},
334
2/4
✓ Branch 85 → 86 taken 2 times.
✓ Branch 85 → 87 taken 1 time.
✗ Branch 149 → 150 not taken.
✗ Branch 149 → 151 not taken.
4 {QStringLiteral("body"), body}};
335 4 }
336