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: 88.9% 563 / 0 / 633
Functions: 81.9% 59 / 0 / 72
Branches: 56.1% 676 / 0 / 1205

presentation/QuickComposeBackendSessions.cpp
Line Branch Exec Source
1 // Sprint 108: the composer sessions, identities and the outbox of
2 // QuickComposeBackend — the new compose path (S108.7.3, S108.9.1). The
3 // older single-composer API in QuickComposeBackend.cpp / *Drafts.cpp /
4 // *Send.cpp stays until the new composer replaces it (§19).
5 #include "QuickComposeBackend.h"
6 #include "ComposeLog.h"
7 #include "service/MimeParser.h"
8
9 #include <QRegularExpression>
10 #include <QFileInfo>
11 #include "ComposeRecipients.h"
12 #include <QUuid>
13 #include <QSettings>
14 #include <QSet>
15
16 #include "ComposeDraftFiles.h"
17 #include "ComposeDraftStore.h"
18 #include "app/core/AccountSession.h"
19 #include "app/core/ContactSyncCoordinator.h"
20 #include "app/core/MailSessionCore.h"
21 #include "app/core/SettingsSyncSession.h"
22 #include "controller/MailController.h"
23 #include "controller/MailMutationPipeline.h"
24 #include "data/AccountConfig.h"
25 #include "data/CredentialStore.h"
26 #include "data/IdentityChoice.h"
27 #include "data/SendChecks.h"
28 #include "service/Rfc2822Builder.h"
29 #include "service/SmtpService.h"
30 #include <QCryptographicHash>
31 #include <QJsonDocument>
32 #include <QJsonObject>
33 #include <QLocale>
34 #include <QLoggingCategory>
35 #include <QPointer>
36 #include <QtConcurrent>
37
38
39 namespace {
40 27 QString summaryOf(const ComposeMessage &message) {
41 27 QStringList names;
42
5/5
✓ Branch 2 → 3 taken 27 times.
✓ Branch 3 → 4 taken 27 times.
✓ Branch 4 → 5 taken 27 times.
✓ Branch 32 → 6 taken 39 times.
✓ Branch 32 → 33 taken 14 times.
53 for (const QString &mailbox : message.to + message.cc) {
43 39 QString name = mailbox;
44 39 const qsizetype angle = name.lastIndexOf(QLatin1Char('<'));
45
5/5
✓ Branch 11 → 12 taken 14 times.
✓ Branch 11 → 21 taken 25 times.
✓ Branch 12 → 13 taken 14 times.
✓ Branch 13 → 14 taken 14 times.
✓ Branch 16 → 17 taken 14 times.
39 if (angle > 0) name = name.left(angle).trimmed().remove(QLatin1Char('"'));
46
1/1
✓ Branch 21 → 22 taken 39 times.
39 names.append(name);
47
2/2
✓ Branch 23 → 24 taken 13 times.
✓ Branch 23 → 25 taken 26 times.
39 if (names.size() == 2) break;
48 66 }
49 27 const int more = int(message.to.size() + message.cc.size() + message.bcc.size()) - int(names.size());
50
1/1
✓ Branch 41 → 42 taken 27 times.
27 QString summary = names.join(QStringLiteral(", "));
51
4/4
✓ Branch 44 → 45 taken 3 times.
✓ Branch 44 → 55 taken 24 times.
✓ Branch 49 → 50 taken 3 times.
✓ Branch 50 → 51 taken 3 times.
33 if (more > 0) summary += QStringLiteral(" +%1").arg(more);
52 27 return summary;
53 27 }
54
55 // Every recipient once (the domain compares case-insensitively, the local
56 // part as written — RFC 5321), in To, Cc, Bcc order.
57 27 QStringList envelopeOf(const ComposeMessage &message) {
58 27 QStringList envelope;
59 27 QSet<QString> seen;
60
2/2
✓ Branch 41 → 5 taken 81 times.
✓ Branch 41 → 42 taken 27 times.
108 for (const QStringList *field : {&message.to, &message.cc, &message.bcc})
61
2/2
✓ Branch 39 → 7 taken 43 times.
✓ Branch 39 → 40 taken 81 times.
124 for (const QString &mailbox : *field) {
62
1/1
✓ Branch 8 → 9 taken 43 times.
43 const auto parsed = ComposeRecipients::parse(mailbox);
63
1/2
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 43 times.
43 if (!parsed) continue;
64
2/2
✓ Branch 30 → 15 taken 43 times.
✓ Branch 30 → 31 taken 43 times.
86 for (const QString &address : parsed->envelope) {
65
1/1
✓ Branch 16 → 17 taken 43 times.
43 const QString key = ComposeRecipients::addressKey(address);
66
2/2
✓ Branch 18 → 19 taken 1 time.
✓ Branch 18 → 20 taken 42 times.
43 if (seen.contains(key)) continue;
67
1/1
✓ Branch 20 → 21 taken 42 times.
42 seen.insert(key);
68
1/1
✓ Branch 21 → 22 taken 42 times.
42 envelope.append(address);
69 43 }
70 43 }
71 27 return envelope;
72 27 }
73 } // namespace
74
75 795 ComposeSession::Deps QuickComposeBackend::sessionDeps() {
76 795 ComposeSession::Deps deps;
77 795 deps.profileDirectory = m_profileDirectory;
78 3155 deps.identities = [this] { return m_identityStore.identities(); };
79 952 deps.knownAddress = [](const QString &) { return false; };
80 796 deps.serverSizeLimit = [this](const QString &accountId) { return m_sizeLimits.value(accountId, 0); };
81 1633 deps.ownDomains = [this] {
82 43 QStringList domains;
83
2/2
✓ Branch 16 → 5 taken 51 times.
✓ Branch 16 → 17 taken 43 times.
94 for (const Identity &identity : m_identityStore.identities())
84
3/3
✓ Branch 9 → 10 taken 51 times.
✓ Branch 10 → 11 taken 51 times.
✓ Branch 11 → 12 taken 51 times.
51 domains.append(identity.email.section(QLatin1Char('@'), 1).toLower());
85
1/1
✓ Branch 17 → 18 taken 43 times.
43 domains.removeDuplicates();
86 43 return domains;
87 795 };
88 838 deps.attachmentWords = [] { return SendChecks::defaultAttachmentWords(); };
89 830 deps.undoDelaySeconds = [this] { return m_undoDelaySeconds; };
90 948 deps.markdownTyping = [this] { return m_markdownTyping; };
91 795 deps.spell = &m_spell;
92 1715 deps.rememberedLanguage = [this](const QString &address) { return m_spellMemory.languageFor(address); };
93 1617 deps.rememberLanguage = [this](const QString &address, const QString &language) {
94 27 m_spellMemory.remember(address, language);
95 795 };
96
2/2
✓ Branch 2 → 3 taken 1003 times.
✓ Branch 3 → 4 taken 1003 times.
1798 deps.uiLanguage = [] { return QLocale().name(); };
97 1617 deps.enqueue = [this](ComposeSession *session, const ComposeMessage &message, const QDateTime &due, bool scheduled) {
98
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 27 times.
27 if (!m_outbox) return false;
99
1/1
✓ Branch 6 → 7 taken 27 times.
27 const auto identity = m_identityStore.find(session->identityId());
100
1/2
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 27 times.
27 if (!identity) return false;
101 // The MIME bytes are built here (attachments are local copies of bounded
102 // size); the Date header is replaced when the message leaves.
103 27 ComposeMessage fixed = message;
104 27 fixed.messageId.clear();
105
1/1
✓ Branch 13 → 14 taken 27 times.
27 const auto built = fixed.build(ComposeMessage::Form::Smtp);
106
1/2
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 17 taken 27 times.
27 if (built.bytes.isEmpty()) return false;
107 27 OutboxEntry entry;
108 27 entry.accountId = identity->accountId;
109 27 entry.identityId = identity->id;
110
6/6
✓ Branch 22 → 23 taken 8 times.
✓ Branch 22 → 26 taken 19 times.
✓ Branch 31 → 32 taken 19 times.
✓ Branch 31 → 33 taken 8 times.
✓ Branch 33 → 34 taken 8 times.
✓ Branch 33 → 35 taken 19 times.
54 entry.state = scheduled ? QStringLiteral("scheduled") : QStringLiteral("undo");
111 27 entry.due = due;
112 27 entry.messageId = built.messageId;
113 27 entry.envelopeFrom = identity->email;
114
1/1
✓ Branch 39 → 40 taken 27 times.
27 entry.recipients = envelopeOf(message);
115 27 entry.bcc = message.bcc;
116 27 entry.mime = built.bytes;
117 27 entry.draftSession = session->sessionId();
118
1/1
✓ Branch 47 → 48 taken 27 times.
27 entry.summaryTo = summaryOf(message);
119 27 entry.subject = message.subject;
120 // D-23: an invitation is never scheduled; its event is written when it leaves.
121 27 entry.invitation = !message.calendar.isEmpty();
122
2/2
✓ Branch 52 → 53 taken 3 times.
✓ Branch 52 → 111 taken 24 times.
27 if (entry.invitation) {
123 3 const QVariantMap meeting = session->meeting();
124
2/2
✓ Branch 58 → 59 taken 3 times.
✓ Branch 59 → 60 taken 3 times.
6 const QString calendarAccount = meeting.value(QStringLiteral("calendarAccount")).toString();
125 // D-24: who delivers it is decided now — the server (the event write
126 // at the end of the undo window) or MailJD (this mail).
127
1/1
✓ Branch 87 → 88 taken 3 times.
9 entry.replyReference = QString::fromUtf8(QJsonDocument(QJsonObject{
128 6 {QStringLiteral("calendarAccount"), calendarAccount},
129
2/2
✓ Branch 75 → 76 taken 3 times.
✓ Branch 76 → 77 taken 3 times.
9 {QStringLiteral("calendarPath"), meeting.value(QStringLiteral("calendarPath")).toString()},
130
4/6
✓ Branch 82 → 83 taken 3 times.
✓ Branch 85 → 86 taken 3 times.
✓ Branch 94 → 95 taken 9 times.
✓ Branch 94 → 96 taken 3 times.
✗ Branch 255 → 256 not taken.
✗ Branch 255 → 257 not taken.
18 {QStringLiteral("delivery"), invitationDeliveryMode(calendarAccount, identity->email)}})
131
1/1
✓ Branch 86 → 87 taken 3 times.
9 .toJson(QJsonDocument::Compact));
132 3 }
133
9/11
✓ Branch 111 → 112 taken 24 times.
✓ Branch 111 → 116 taken 3 times.
✓ Branch 113 → 114 taken 24 times.
✓ Branch 114 → 115 taken 2 times.
✓ Branch 114 → 116 taken 22 times.
✓ Branch 117 → 118 taken 24 times.
✓ Branch 117 → 119 taken 3 times.
✓ Branch 119 → 120 taken 2 times.
✓ Branch 119 → 156 taken 25 times.
✗ Branch 309 → 310 not taken.
✗ Branch 309 → 311 not taken.
27 if (!entry.invitation && !session->mailReference().isEmpty()) {
134 2 QVariantMap reference = session->mailReference();
135
2/5
✗ Branch 124 → 125 not taken.
✓ Branch 124 → 128 taken 2 times.
✓ Branch 135 → 136 taken 2 times.
✗ Branch 327 → 328 not taken.
✗ Branch 327 → 329 not taken.
8 reference.insert(QStringLiteral("flag"), session->kind() == QLatin1String("forward") ? QStringLiteral("$Forwarded")
136
2/6
✓ Branch 140 → 141 taken 2 times.
✗ Branch 140 → 142 not taken.
✗ Branch 142 → 143 not taken.
✓ Branch 142 → 144 taken 2 times.
✗ Branch 323 → 324 not taken.
✗ Branch 323 → 325 not taken.
2 : QStringLiteral("\\Answered"));
137
4/4
✓ Branch 145 → 146 taken 2 times.
✓ Branch 146 → 147 taken 2 times.
✓ Branch 147 → 148 taken 2 times.
✓ Branch 148 → 149 taken 2 times.
2 entry.replyReference = QString::fromUtf8(QJsonDocument(QJsonObject::fromVariantMap(reference)).toJson(QJsonDocument::Compact));
138 2 }
139
1/1
✓ Branch 157 → 158 taken 27 times.
27 const qint64 id = m_outbox->enqueue(entry);
140
1/2
✗ Branch 158 → 159 not taken.
✓ Branch 158 → 160 taken 27 times.
27 if (id <= 0) return false;
141
1/1
✓ Branch 160 → 161 taken 27 times.
27 m_sessionStore.parkForOutbox(session, id);
142
2/2
✓ Branch 161 → 162 taken 19 times.
✓ Branch 161 → 211 taken 8 times.
27 if (!scheduled) {
143 38 m_pendingSend = {{QStringLiteral("id"), id},
144 38 {QStringLiteral("summary"), entry.summaryTo},
145
1/1
✓ Branch 173 → 174 taken 19 times.
38 {QStringLiteral("deadline"), due.toMSecsSinceEpoch()},
146
4/6
✓ Branch 178 → 179 taken 19 times.
✓ Branch 179 → 180 taken 19 times.
✓ Branch 185 → 186 taken 76 times.
✓ Branch 185 → 187 taken 19 times.
✗ Branch 351 → 352 not taken.
✗ Branch 351 → 353 not taken.
114 {QStringLiteral("seconds"), int(QDateTime::currentDateTimeUtc().secsTo(due))}};
147
1/1
✓ Branch 196 → 197 taken 19 times.
19 emit pendingSendChanged();
148
6/6
✓ Branch 197 → 198 taken 19 times.
✓ Branch 199 → 200 taken 19 times.
✓ Branch 201 → 202 taken 19 times.
✓ Branch 202 → 203 taken 19 times.
✓ Branch 204 → 205 taken 19 times.
✓ Branch 205 → 206 taken 19 times.
57 m_deps.setStatus(tr("Sending to %1 in %2 s").arg(entry.summaryTo).arg(qMax(0LL, QDateTime::currentDateTimeUtc().secsTo(due))));
149 } else {
150
2/2
✓ Branch 211 → 212 taken 8 times.
✓ Branch 217 → 218 taken 8 times.
16 m_deps.setStatus(tr("Scheduled for %1 — MailJD sends it from this computer")
151
4/4
✓ Branch 213 → 214 taken 8 times.
✓ Branch 214 → 215 taken 8 times.
✓ Branch 215 → 216 taken 8 times.
✓ Branch 216 → 217 taken 8 times.
16 .arg(QLocale().toString(due.toLocalTime(), QLocale::ShortFormat)));
152 }
153 27 return true;
154
5/13
✓ Branch 67 → 68 taken 3 times.
✓ Branch 77 → 78 taken 3 times.
✓ Branch 83 → 84 taken 3 times.
✓ Branch 84 → 85 taken 3 times.
✓ Branch 181 → 182 taken 19 times.
✗ Branch 294 → 295 not taken.
✗ Branch 294 → 298 not taken.
✗ Branch 296 → 297 not taken.
✗ Branch 296 → 298 not taken.
✗ Branch 383 → 384 not taken.
✗ Branch 383 → 387 not taken.
✗ Branch 385 → 386 not taken.
✗ Branch 385 → 387 not taken.
929 };
155 1666 deps.saveServerDraft = [this](ComposeSession *session, const ComposeMessage &message) {
156
2/2
✓ Branch 3 → 4 taken 67 times.
✓ Branch 3 → 5 taken 9 times.
76 if (m_draftsFolder.isEmpty()) return;
157
1/1
✓ Branch 6 → 7 taken 9 times.
9 const auto identity = m_identityStore.find(session->identityId());
158 // Server drafts live in the selected account's Drafts folder.
159 9 const auto &account = m_session.accounts()->primary();
160
4/8
✓ Branch 11 → 12 taken 9 times.
✗ Branch 11 → 18 not taken.
✓ Branch 13 → 14 taken 9 times.
✗ Branch 13 → 18 not taken.
✗ Branch 17 → 18 not taken.
✓ Branch 17 → 19 taken 9 times.
✗ Branch 20 → 21 not taken.
✓ Branch 20 → 22 taken 9 times.
9 if (!identity || !account || identity->accountId != account->id) return;
161
1/1
✓ Branch 22 → 23 taken 9 times.
9 QPointer<QuickComposeBackend> self(this);
162 9 const QString folder = m_draftsFolder;
163 9 const QString sessionId = session->sessionId();
164
1/7
✓ Branch 29 → 30 taken 9 times.
✗ Branch 47 → 48 not taken.
✗ Branch 47 → 49 not taken.
✗ Branch 50 → 51 not taken.
✗ Branch 50 → 52 not taken.
✗ Branch 53 → 54 not taken.
✗ Branch 53 → 55 not taken.
9 (void)QtConcurrent::run(&m_threadPool, [self, message, folder, sessionId] {
165
1/1
✓ Branch 2 → 3 taken 9 times.
9 const auto built = message.build(ComposeMessage::Form::Draft);
166
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 9 times.
9 if (built.bytes.isEmpty()) return;
167
1/1
✓ Branch 7 → 8 taken 9 times.
9 const QByteArray sha = QCryptographicHash::hash(built.bytes, QCryptographicHash::Sha256);
168
1/9
✓ Branch 14 → 15 taken 9 times.
✗ Branch 27 → 28 not taken.
✗ Branch 27 → 29 not taken.
✗ Branch 30 → 31 not taken.
✗ Branch 30 → 32 not taken.
✗ Branch 33 → 34 not taken.
✗ Branch 33 → 35 not taken.
✗ Branch 36 → 37 not taken.
✗ Branch 36 → 38 not taken.
9 QMetaObject::invokeMethod(self, [self, built, sha, folder, sessionId] {
169
3/6
✓ Branch 3 → 4 taken 5 times.
✗ Branch 3 → 7 not taken.
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 5 times.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 5 times.
5 if (!self || self->m_draftsFolder != folder) return;
170 5 const qint64 token = self->m_session.controller()->mutations()->queueAppend(
171
1/1
✓ Branch 17 → 18 taken 5 times.
10 folder, built.bytes, QStringLiteral("\\Seen \\Draft"), built.messageId, sha);
172 // One server copy per draft: the completed append replaces the
173 // previous revision (connectDraftAppendResults); a revision still in
174 // flight when a newer one is queued is removed when it lands.
175
2/3
✓ Branch 20 → 21 taken 5 times.
✗ Branch 20 → 26 not taken.
✓ Branch 24 → 25 taken 5 times.
5 if (token > 0) self->retireDraftToken(self->m_sessionDraftTokens.value(sessionId, 0));
176
2/3
✓ Branch 26 → 27 taken 5 times.
✗ Branch 26 → 29 not taken.
✓ Branch 28 → 29 taken 5 times.
5 if (token > 0) self->m_sessionDraftTokens.insert(sessionId, token);
177 }, Qt::QueuedConnection);
178
1/2
✓ Branch 19 → 20 taken 9 times.
✗ Branch 19 → 22 not taken.
9 });
179
1/2
✓ Branch 37 → 38 taken 9 times.
✗ Branch 37 → 40 not taken.
804 };
180 795 return deps;
181 }
182
183 794 MailOutboxSender::Deps QuickComposeBackend::outboxDeps() {
184 794 MailOutboxSender::Deps deps;
185 1597 deps.resolveAccount = [this](const QString &accountId, std::function<void(std::optional<AccountConfig>)> done) {
186 9 const auto &primary = m_session.accounts()->primary();
187
3/6
✓ Branch 5 → 6 taken 9 times.
✗ Branch 5 → 10 not taken.
✓ Branch 8 → 9 taken 9 times.
✗ Branch 8 → 10 not taken.
✓ Branch 11 → 12 taken 9 times.
✗ Branch 11 → 16 not taken.
9 if (primary && primary->id == accountId) {
188
2/2
✓ Branch 12 → 13 taken 9 times.
✓ Branch 13 → 14 taken 9 times.
9 done(primary);
189 9 return;
190 }
191 ✗ auto configuration = m_session.accounts()->configuration(accountId);
192 ✗ if (!configuration) {
193 ✗ done(std::nullopt);
194 ✗ return;
195 }
196 ✗ auto accounts = std::make_shared<std::vector<AccountConfig>>(std::vector<AccountConfig>{*configuration});
197 ✗ auto *store = new CredentialStore;
198 ✗ AccountConfigLoader::resolvePasswords(*accounts, store, [store, accounts, done] {
199 ✗ store->deleteLater();
200 ✗ done(accounts->front());
201 ✗ });
202
0/6
✗ Branch 49 → 50 not taken.
✗ Branch 49 → 52 not taken.
✗ Branch 69 → 70 not taken.
✗ Branch 69 → 73 not taken.
✗ Branch 71 → 72 not taken.
✗ Branch 71 → 73 not taken.
794 };
203 1597 deps.transmit = [this](const AccountConfig &account, const QString &from, const QStringList &recipients,
204 const QByteArray &message, MailOutboxSender::TransmitDone done) {
205
3/6
✓ Branch 2 → 3 taken 9 times.
✓ Branch 3 → 4 taken 9 times.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 9 times.
✗ Branch 23 → 24 not taken.
✗ Branch 23 → 25 not taken.
9 auto *smtp = new SmtpService(this);
206 9 smtp->setPartialRecipients(true);
207 9 const QString accountId = account.id;
208
1/1
✓ Branch 9 → 10 taken 9 times.
9 connect(smtp, &SmtpService::sizeLimitAnnounced, this,
209 18 [this, accountId](const QString &, qint64 bytes) { m_sizeLimits.insert(accountId, bytes); });
210
2/2
✓ Branch 12 → 13 taken 9 times.
✓ Branch 13 → 14 taken 9 times.
9 connect(smtp, &SmtpService::sendSuccess, this, [smtp, done] {
211 ✗ MailOutboxSender::TransmitResult result;
212 ✗ result.ok = true;
213 ✗ result.refused = smtp->refusedRecipients();
214 ✗ result.refusalReply = smtp->refusalReply();
215 ✗ smtp->deleteLater();
216 ✗ done(result);
217 ✗ });
218
2/2
✓ Branch 16 → 17 taken 9 times.
✓ Branch 17 → 18 taken 9 times.
9 connect(smtp, &SmtpService::sendFailed, this, [smtp, done](const QString &error) {
219 9 MailOutboxSender::TransmitResult result;
220 9 result.code = smtp->lastReplyCode();
221 9 result.error = error;
222
1/1
✓ Branch 4 → 5 taken 9 times.
9 smtp->deleteLater();
223
1/1
✓ Branch 5 → 6 taken 9 times.
9 done(result);
224 9 });
225
1/1
✓ Branch 20 → 21 taken 9 times.
9 smtp->sendMail(account.smtp, from, recipients, message);
226 803 };
227 1588 deps.storeSentCopy = [this](const OutboxEntry &entry, const QByteArray &copy) {
228 ✗ const auto &primary = m_session.accounts()->primary();
229 ✗ if (!primary || primary->id != entry.sentFolderAccount) return false;
230 ✗ if (m_sentFolder.isEmpty()) return true; // nothing to store into
231 ✗ m_session.controller()->mutations()->queueAppendAsync(
232 ✗ m_sentFolder, copy, QStringLiteral("\\Seen"), entry.messageId,
233 ✗ QCryptographicHash::hash(copy, QCryptographicHash::Sha256), [](qint64) {});
234 ✗ return true;
235 794 };
236 1588 deps.flagOriginal = [this](const OutboxEntry &entry) {
237 ✗ const QJsonObject reference = QJsonDocument::fromJson(entry.replyReference.toUtf8()).object();
238 ✗ const auto &primary = m_session.accounts()->primary();
239 ✗ const qint64 folderId = reference.value(QStringLiteral("folderId")).toVariant().toLongLong();
240 ✗ const qint64 uid = reference.value(QStringLiteral("uid")).toVariant().toLongLong();
241 ✗ if (!primary || reference.value(QStringLiteral("accountId")).toString() != m_deps.accountName() || uid <= 0 ||
242 ✗ !m_folderPathById.contains(folderId))
243 ✗ return;
244 ✗ m_session.controller()->crossFolderStoreFlag(m_folderPathById.value(folderId), uid,
245 ✗ reference.value(QStringLiteral("flag")).toString(), true);
246
0/2
✗ Branch 71 → 72 not taken.
✗ Branch 71 → 74 not taken.
794 };
247 1588 deps.afterTransmit = [this](const OutboxEntry &entry, const QByteArray &sentBytes) {
248 // S97.1.6: each recipient of a sent message counts once (collected
249 // addresses, the completion order) — To and Cc from the sent header,
250 // Bcc from the entry (it never travels in the message).
251 ✗ if (auto *contacts = m_session.contactSync())
252 ✗ contacts->recordSentHeaders(ComposeRecipients::sentHeaders(sentBytes, entry.bcc));
253 ✗ if (entry.invitation) writeOrganizedEvent(entry, sentBytes);
254 794 };
255 803 deps.serverDelivers = [](const OutboxEntry &entry) {
256
2/12
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 15 taken 9 times.
✗ Branch 4 → 5 not taken.
✗ Branch 5 → 6 not taken.
✗ Branch 13 → 14 not taken.
✗ Branch 13 → 15 not taken.
✗ Branch 28 → 29 not taken.
✓ Branch 28 → 30 taken 9 times.
✗ Branch 52 → 53 not taken.
✗ Branch 52 → 54 not taken.
✗ Branch 56 → 57 not taken.
✗ Branch 56 → 58 not taken.
18 return entry.invitation && QJsonDocument::fromJson(entry.replyReference.toUtf8())
257
1/5
✗ Branch 6 → 7 not taken.
✗ Branch 26 → 27 not taken.
✓ Branch 26 → 28 taken 9 times.
✗ Branch 48 → 49 not taken.
✗ Branch 48 → 50 not taken.
9 .object()
258
3/13
✗ Branch 10 → 11 not taken.
✗ Branch 20 → 21 not taken.
✓ Branch 20 → 22 taken 9 times.
✗ Branch 22 → 23 not taken.
✓ Branch 22 → 24 taken 9 times.
✗ Branch 24 → 25 not taken.
✓ Branch 24 → 26 taken 9 times.
✗ Branch 36 → 37 not taken.
✗ Branch 36 → 38 not taken.
✗ Branch 40 → 41 not taken.
✗ Branch 40 → 42 not taken.
✗ Branch 44 → 45 not taken.
✗ Branch 44 → 46 not taken.
9 .value(QStringLiteral("delivery"))
259
2/7
✗ Branch 11 → 12 not taken.
✗ Branch 16 → 17 not taken.
✓ Branch 16 → 18 taken 9 times.
✗ Branch 18 → 19 not taken.
✓ Branch 18 → 20 taken 9 times.
✗ Branch 32 → 33 not taken.
✗ Branch 32 → 34 not taken.
18 .toString() == QLatin1String("server");
260 794 };
261 794 deps.deliverByServer = [this](const OutboxEntry &entry) { return writeOrganizedEvent(entry, entry.mime, true); };
262 810 deps.online = [this] { return m_session.accounts()->networkEnabled(); };
263
3/3
✓ Branch 5 → 6 taken 7 times.
✓ Branch 6 → 7 taken 7 times.
✓ Branch 7 → 8 taken 7 times.
808 deps.notify = [this](const QString &title, const QString &body) { m_deps.reportError(title + QStringLiteral(": ") + body); };
264 794 return deps;
265 }
266
267 795 void QuickComposeBackend::initComposeSessions() {
268 795 m_sessionStore.setDeps(sessionDeps());
269 {
270
1/1
✓ Branch 5 → 6 taken 795 times.
795 const QSettings settings;
271
2/2
✓ Branch 11 → 12 taken 795 times.
✓ Branch 12 → 13 taken 795 times.
1590 m_undoDelaySeconds = qBound(0, settings.value(QStringLiteral("compose/undoDelaySeconds"), 10).toInt(), 30);
272
2/2
✓ Branch 23 → 24 taken 795 times.
✓ Branch 24 → 25 taken 795 times.
1590 m_markdownTyping = settings.value(QStringLiteral("compose/markdownTyping"), true).toBool();
273
2/2
✓ Branch 34 → 35 taken 795 times.
✓ Branch 35 → 36 taken 795 times.
1590 m_quickReply = settings.value(QStringLiteral("compose/quickReply"), true).toBool();
274 795 }
275 // Never create a profile: a host that refuses a foreign or unopened
276 // profile leaves no outbox, identities or drafts behind in it.
277
8/12
✓ Branch 42 → 43 taken 795 times.
✗ Branch 42 → 46 not taken.
✓ Branch 43 → 44 taken 795 times.
✓ Branch 44 → 45 taken 795 times.
✓ Branch 45 → 46 taken 1 time.
✓ Branch 45 → 47 taken 794 times.
✓ Branch 48 → 49 taken 795 times.
✗ Branch 48 → 50 not taken.
✓ Branch 50 → 51 taken 1 time.
✓ Branch 50 → 52 taken 794 times.
✗ Branch 244 → 245 not taken.
✗ Branch 244 → 246 not taken.
795 if (m_profileDirectory.isEmpty() || !QFileInfo(m_profileDirectory).isDir()) return;
278
1/1
✓ Branch 52 → 53 taken 794 times.
794 m_identityStore = IdentityStore(IdentityStore::pathFor(m_profileDirectory));
279 794 QString error;
280
2/8
✓ Branch 57 → 58 taken 794 times.
✗ Branch 58 → 59 not taken.
✓ Branch 58 → 70 taken 794 times.
✗ Branch 59 → 60 not taken.
✗ Branch 63 → 64 not taken.
✗ Branch 64 → 65 not taken.
✗ Branch 68 → 61 not taken.
✗ Branch 68 → 69 not taken.
794 if (!m_identityStore.load(&error)) qCWarning(lcCompose) << "identities not loaded";
281
1/1
✓ Branch 70 → 71 taken 794 times.
794 m_spellMemory = SpellLanguageMemory(SpellLanguageMemory::pathFor(m_profileDirectory));
282
1/1
✓ Branch 75 → 76 taken 794 times.
794 m_spellMemory.load();
283
1/1
✓ Branch 76 → 77 taken 794 times.
794 m_templateStore = TemplateStore(TemplateStore::pathFor(m_profileDirectory));
284
2/9
✓ Branch 81 → 82 taken 794 times.
✗ Branch 82 → 83 not taken.
✓ Branch 82 → 95 taken 794 times.
✗ Branch 83 → 84 not taken.
✗ Branch 87 → 88 not taken.
✗ Branch 88 → 89 not taken.
✗ Branch 89 → 90 not taken.
✗ Branch 93 → 85 not taken.
✗ Branch 93 → 94 not taken.
794 if (!m_templateStore.load(&error)) qCWarning(lcCompose) << "templates not loaded" << error;
285 // S108.5.4: settings sync carries addresses' signatures and the templates —
286 // a sync that changed them reloads the files; a local edit uploads.
287
2/2
✓ Branch 96 → 97 taken 793 times.
✓ Branch 96 → 104 taken 1 time.
794 if (auto *sync = m_session.settingsSync()) {
288
1/1
✓ Branch 97 → 98 taken 793 times.
793 connect(sync, &SettingsSyncSession::settingsApplied, this, [this](const QStringList &categories) {
289 ✗ if (categories.contains(QStringLiteral("identities")) && m_identityStore.load()) identitiesEdited();
290 ✗ if (categories.contains(QStringLiteral("templates")) && m_templateStore.load()) emit templatesChanged();
291 ✗ });
292
1/1
✓ Branch 99 → 100 taken 793 times.
793 connect(this, &QuickComposeBackend::identitiesChanged, sync, &SettingsSyncSession::localSettingsChanged);
293
1/1
✓ Branch 101 → 102 taken 793 times.
793 connect(this, &QuickComposeBackend::templatesChanged, sync, &SettingsSyncSession::localSettingsChanged);
294 }
295
1/1
✓ Branch 104 → 105 taken 794 times.
794 m_outboxStore = std::make_unique<MailOutboxStore>(m_profileDirectory);
296
2/3
✓ Branch 108 → 109 taken 794 times.
✗ Branch 109 → 110 not taken.
✓ Branch 109 → 121 taken 794 times.
794 if (!m_outboxStore->open(&error)) {
297 ✗ qCWarning(lcCompose) << "outbox not opened";
298 ✗ m_outboxStore.reset();
299 } else {
300
1/1
✓ Branch 123 → 124 taken 794 times.
794 m_outbox = std::make_unique<MailOutboxSender>(*m_outboxStore, outboxDeps());
301
1/1
✓ Branch 128 → 129 taken 794 times.
794 connect(m_outbox.get(), &MailOutboxSender::changed, this, &QuickComposeBackend::outboxChanged);
302
1/1
✓ Branch 131 → 132 taken 794 times.
794 connect(m_outbox.get(), &MailOutboxSender::transmitted, this, [this](qint64 id, const QString &summary, bool late) {
303 ✗ m_sessionStore.outboxFinished(id);
304 ✗ if (m_pendingSend.value(QStringLiteral("id")).toLongLong() == id) {
305 ✗ m_pendingSend.clear();
306 ✗ emit pendingSendChanged();
307 }
308 ✗ const QString prepared = m_preparedDoneTexts.take(id);
309 ✗ m_deps.setStatus(!prepared.isEmpty() ? prepared
310 ✗ : late ? tr("Sent to %1 (late — MailJD was not running at the scheduled time)").arg(summary)
311 ✗ : tr("Sent to %1").arg(summary));
312 ✗ });
313
1/1
✓ Branch 134 → 135 taken 794 times.
794 connect(m_outbox.get(), &MailOutboxSender::failedEntry, this, [this](qint64 id, const QString &, const QString &error) {
314
4/4
✓ Branch 6 → 7 taken 9 times.
✓ Branch 7 → 8 taken 9 times.
✓ Branch 12 → 13 taken 2 times.
✓ Branch 12 → 15 taken 7 times.
18 if (m_pendingSend.value(QStringLiteral("id")).toLongLong() == id) {
315 2 m_pendingSend.clear();
316 2 emit pendingSendChanged();
317 }
318
3/3
✓ Branch 15 → 16 taken 9 times.
✓ Branch 17 → 18 taken 9 times.
✓ Branch 18 → 19 taken 9 times.
18 m_deps.reportError(tr("Could not send: %1").arg(error));
319 9 });
320 }
321
1/1
✓ Branch 138 → 139 taken 794 times.
794 connect(m_session.accounts(), &AccountSession::accountsLoaded, this, [this] {
322 793 reconcileIdentities();
323 793 migrateOldDrafts(); // the accounts may arrive after start-up; retired files never migrate twice
324 793 });
325
1/1
✓ Branch 141 → 142 taken 794 times.
794 connect(m_session.accounts(), &AccountSession::configurationChanged, this, [this](const QString &id) {
326 9 reconcileIdentities();
327
1/2
✓ Branch 4 → 5 taken 9 times.
✗ Branch 4 → 7 not taken.
9 if (m_outbox) m_outbox->accountChanged(id);
328 9 });
329
1/1
✓ Branch 144 → 145 taken 794 times.
794 connect(m_session.accounts(), &AccountSession::credentialsResolved, this, [this] {
330
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 1 time.
1 if (!m_outbox) return;
331 1 m_outbox->start();
332 1 m_outbox->networkAvailable();
333 });
334
1/1
✓ Branch 146 → 147 taken 794 times.
794 reconcileIdentities();
335
3/3
✓ Branch 147 → 148 taken 794 times.
✓ Branch 148 → 149 taken 794 times.
✓ Branch 150 → 151 taken 794 times.
794 ComposeDraftFiles::sweep(m_profileDirectory, {}, QDateTime::currentDateTime().addDays(-30));
336
1/1
✓ Branch 154 → 155 taken 794 times.
794 m_sessionStore.restoreOpenSessions();
337
1/1
✓ Branch 155 → 156 taken 794 times.
794 migrateOldDrafts();
338 // Drafts that waited in the Outbox when MailJD stopped wait there again
339 // (the entry still names its draft), so sending still closes them.
340
1/2
✓ Branch 157 → 158 taken 794 times.
✗ Branch 157 → 173 not taken.
794 if (m_outboxStore)
341
4/5
✓ Branch 159 → 160 taken 794 times.
✓ Branch 160 → 161 taken 794 times.
✓ Branch 161 → 162 taken 794 times.
✗ Branch 170 → 163 not taken.
✓ Branch 170 → 171 taken 794 times.
794 for (const OutboxEntry &entry : m_outboxStore->entries())
342 ✗ if (auto *parked = qobject_cast<ComposeSession *>(m_sessionStore.session(entry.draftSession)))
343
0/1
✗ Branch 167 → 168 not taken.
794 m_sessionStore.parkForOutbox(parked, entry.id);
344 // A restored draft's server copy is replaced by its next revision.
345
5/5
✓ Branch 173 → 174 taken 794 times.
✓ Branch 174 → 175 taken 794 times.
✓ Branch 175 → 176 taken 794 times.
✓ Branch 192 → 177 taken 5 times.
✓ Branch 192 → 193 taken 794 times.
799 for (ComposeSession *session : m_sessionStore.sessions())
346
2/2
✓ Branch 179 → 180 taken 1 time.
✓ Branch 179 → 190 taken 4 times.
5 if (session->serverDraftUid() > 0) {
347
1/1
✓ Branch 182 → 183 taken 1 time.
1 m_sessionDraftUids.insert(session->sessionId(), session->serverDraftUid());
348
2/3
✓ Branch 185 → 186 taken 1 time.
✗ Branch 185 → 190 not taken.
✓ Branch 188 → 189 taken 1 time.
1 if (session->serverDraftUidValidity() > 0) m_draftUidValidity.insert(session->serverDraftUid(), session->serverDraftUidValidity());
349 794 }
350 // A sent or discarded draft leaves no copy in the Drafts folder.
351
1/1
✓ Branch 194 → 195 taken 794 times.
794 connect(&m_sessionStore, &ComposeSessionStore::sessionClosed, this, [this](const QString &id, const QString &reason) {
352
5/6
✓ Branch 4 → 5 taken 40 times.
✗ Branch 4 → 9 not taken.
✓ Branch 7 → 8 taken 19 times.
✓ Branch 7 → 9 taken 21 times.
✓ Branch 10 → 11 taken 19 times.
✓ Branch 10 → 12 taken 21 times.
40 if (reason != QLatin1String("sent") && reason != QLatin1String("discarded")) return;
353
2/2
✓ Branch 12 → 13 taken 21 times.
✓ Branch 13 → 14 taken 21 times.
21 retireDraftToken(m_sessionDraftTokens.take(id));
354
1/1
✓ Branch 14 → 15 taken 21 times.
21 const qint64 uid = m_sessionDraftUids.take(id);
355
2/2
✓ Branch 15 → 16 taken 21 times.
✓ Branch 16 → 17 taken 21 times.
21 const auto *closed = qobject_cast<ComposeSession *>(m_sessionStore.session(id));
356
2/3
✗ Branch 17 → 18 not taken.
✓ Branch 17 → 19 taken 21 times.
✓ Branch 20 → 21 taken 21 times.
21 retireSessionServerCopy(uid, closed ? closed->serverDraftUidValidity() : m_draftUidValidity.value(uid, 0));
357 });
358 794 }
359
360 // S108.7.1: drafts of the old composer (one per account, plus its extra
361 // composer windows) become drafts of the new one — an open tab each, with
362 // the account's own address — and the old files are kept as
363 // "*.v1-migrated" for a rollback.
364 1587 void QuickComposeBackend::migrateOldDrafts() {
365
3/6
✓ Branch 3 → 4 taken 1587 times.
✗ Branch 3 → 6 not taken.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 1587 times.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 1587 times.
1587 if (m_profileDirectory.isEmpty() || !m_session.accounts()) return;
366
1/1
✓ Branch 11 → 12 taken 1587 times.
1587 const ComposeDraftStore store(m_profileDirectory);
367 1587 const auto &primary = m_session.accounts()->primary();
368 1587 int migrated = 0;
369
2/2
✓ Branch 260 → 19 taken 807 times.
✓ Branch 260 → 261 taken 1587 times.
2394 for (const AccountConfig &account : m_session.accounts()->configurations()) {
370
3/4
✓ Branch 21 → 22 taken 807 times.
✗ Branch 21 → 26 not taken.
✓ Branch 24 → 25 taken 794 times.
✓ Branch 24 → 26 taken 13 times.
807 const bool ownsLegacy = primary && primary->id == account.id;
371
5/5
✓ Branch 27 → 28 taken 807 times.
✓ Branch 28 → 29 taken 807 times.
✓ Branch 29 → 30 taken 807 times.
✓ Branch 256 → 31 taken 2 times.
✓ Branch 256 → 257 taken 807 times.
809 for (const QString &source : store.migrationSources(account.id, ownsLegacy)) {
372
1/1
✓ Branch 32 → 33 taken 2 times.
2 const QJsonObject json = ComposeDraftStore::readFile(source);
373
2/2
✓ Branch 36 → 37 taken 2 times.
✓ Branch 37 → 38 taken 2 times.
2 const QString to = json.value(QStringLiteral("to")).toString();
374
2/2
✓ Branch 44 → 45 taken 2 times.
✓ Branch 45 → 46 taken 2 times.
2 const QString subject = json.value(QStringLiteral("subject")).toString();
375
2/2
✓ Branch 52 → 53 taken 2 times.
✓ Branch 53 → 54 taken 2 times.
2 const QString body = json.value(QStringLiteral("body")).toString();
376
6/23
✓ Branch 57 → 58 taken 2 times.
✗ Branch 59 → 60 not taken.
✓ Branch 59 → 66 taken 2 times.
✗ Branch 60 → 61 not taken.
✗ Branch 62 → 63 not taken.
✗ Branch 62 → 66 not taken.
✗ Branch 63 → 64 not taken.
✗ Branch 65 → 66 not taken.
✗ Branch 65 → 67 not taken.
✗ Branch 68 → 69 not taken.
✓ Branch 68 → 70 taken 2 times.
✗ Branch 70 → 71 not taken.
✓ Branch 70 → 72 taken 2 times.
✓ Branch 72 → 73 taken 2 times.
✗ Branch 72 → 74 not taken.
✓ Branch 74 → 75 taken 2 times.
✗ Branch 74 → 235 not taken.
✗ Branch 308 → 309 not taken.
✗ Branch 308 → 310 not taken.
✗ Branch 312 → 313 not taken.
✗ Branch 312 → 314 not taken.
✗ Branch 316 → 317 not taken.
✗ Branch 316 → 318 not taken.
2 if (!to.trimmed().isEmpty() || !subject.trimmed().isEmpty() || !body.trimmed().isEmpty()) {
377 4 QVariantMap prefill{{QStringLiteral("to"), to},
378
2/2
✓ Branch 85 → 86 taken 2 times.
✓ Branch 86 → 87 taken 2 times.
6 {QStringLiteral("cc"), json.value(QStringLiteral("cc")).toString()},
379
2/2
✓ Branch 94 → 95 taken 2 times.
✓ Branch 95 → 96 taken 2 times.
6 {QStringLiteral("bcc"), json.value(QStringLiteral("bcc")).toString()},
380 4 {QStringLiteral("subject"), subject},
381 4 {QStringLiteral("body"), body},
382
2/2
✓ Branch 111 → 112 taken 2 times.
✓ Branch 112 → 113 taken 2 times.
6 {QStringLiteral("inReplyTo"), json.value(QStringLiteral("inReplyTo")).toString()},
383
3/3
✓ Branch 120 → 121 taken 2 times.
✓ Branch 121 → 122 taken 2 times.
✓ Branch 122 → 123 taken 2 times.
6 {QStringLiteral("references"), json.value(QStringLiteral("references")).toVariant().toStringList()},
384
2/4
✓ Branch 130 → 131 taken 16 times.
✓ Branch 130 → 132 taken 2 times.
✗ Branch 321 → 322 not taken.
✗ Branch 321 → 323 not taken.
22 {QStringLiteral("formatted"), false}};
385
2/3
✓ Branch 165 → 166 taken 2 times.
✓ Branch 167 → 168 taken 2 times.
✗ Branch 167 → 178 not taken.
2 if (const auto identity = m_identityStore.defaultFor(account.id))
386
1/1
✓ Branch 173 → 174 taken 2 times.
4 prefill.insert(QStringLiteral("identityId"), identity->id);
387
1/1
✓ Branch 186 → 187 taken 2 times.
6 auto *session = m_sessionStore.create(QStringLiteral("new"), prefill, QStringLiteral("tab"));
388
1/2
✗ Branch 193 → 194 not taken.
✓ Branch 193 → 195 taken 2 times.
2 if (!session) continue;
389 2 QStringList files;
390
5/7
✓ Branch 198 → 199 taken 2 times.
✓ Branch 199 → 200 taken 2 times.
✓ Branch 203 → 204 taken 2 times.
✓ Branch 204 → 205 taken 2 times.
✗ Branch 207 → 208 not taken.
✗ Branch 221 → 206 not taken.
✓ Branch 221 → 222 taken 2 times.
2 for (const QJsonValue &value : json.value(QStringLiteral("attachments")).toArray())
391
0/7
✗ Branch 208 → 209 not taken.
✗ Branch 209 → 210 not taken.
✗ Branch 210 → 211 not taken.
✗ Branch 213 → 214 not taken.
✗ Branch 213 → 218 not taken.
✗ Branch 214 → 215 not taken.
✗ Branch 215 → 216 not taken.
2 if (QFileInfo(value.toString()).isFile()) files.append(value.toString());
392
1/3
✗ Branch 224 → 225 not taken.
✓ Branch 224 → 227 taken 2 times.
✗ Branch 226 → 227 not taken.
2 if (!files.isEmpty()) session->attachments().addPaths(files);
393
1/1
✓ Branch 227 → 228 taken 2 times.
2 session->save();
394 2 ++migrated;
395 2 }
396
1/1
✓ Branch 235 → 236 taken 2 times.
2 ComposeDraftStore::retire(source);
397 809 }
398 }
399
4/8
✓ Branch 261 → 262 taken 1 time.
✓ Branch 261 → 274 taken 1586 times.
✓ Branch 262 → 263 taken 1 time.
✗ Branch 266 → 267 not taken.
✗ Branch 267 → 268 not taken.
✗ Branch 268 → 269 not taken.
✗ Branch 272 → 264 not taken.
✓ Branch 272 → 273 taken 1 time.
1587 if (migrated > 0) qCInfo(lcCompose) << "old drafts migrated" << migrated;
400 1607 }
401
402 1596 void QuickComposeBackend::reconcileIdentities() {
403
2/2
✓ Branch 5 → 6 taken 785 times.
✓ Branch 5 → 7 taken 811 times.
1596 if (m_identityStore.reconcile(m_session.accounts()->configurations())) m_identityStore.save();
404 1596 }
405
406 46 void QuickComposeBackend::setUndoDelaySeconds(int seconds) {
407 46 seconds = qBound(0, seconds, 30);
408
2/2
✓ Branch 3 → 4 taken 41 times.
✓ Branch 3 → 5 taken 5 times.
46 if (m_undoDelaySeconds == seconds) return;
409 5 m_undoDelaySeconds = seconds;
410
2/2
✓ Branch 5 → 6 taken 5 times.
✓ Branch 11 → 12 taken 5 times.
10 QSettings().setValue(QStringLiteral("compose/undoDelaySeconds"), seconds);
411 5 emit undoDelayChanged();
412 }
413
414 2 void QuickComposeBackend::setMarkdownTyping(bool on) {
415
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 2 times.
2 if (m_markdownTyping == on) return;
416 2 m_markdownTyping = on;
417
2/2
✓ Branch 4 → 5 taken 2 times.
✓ Branch 10 → 11 taken 2 times.
4 QSettings().setValue(QStringLiteral("compose/markdownTyping"), on);
418
4/6
✓ Branch 15 → 16 taken 2 times.
✓ Branch 16 → 17 taken 2 times.
✓ Branch 17 → 18 taken 2 times.
✗ Branch 21 → 22 not taken.
✗ Branch 24 → 19 not taken.
✓ Branch 24 → 25 taken 2 times.
2 for (ComposeSession *session : m_sessionStore.sessions()) session->editor().setMarkdownTyping(on);
419 2 emit markdownTypingChanged();
420 }
421
422 4 void QuickComposeBackend::setQuickReply(bool on) {
423
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 4 times.
4 if (m_quickReply == on) return;
424 4 m_quickReply = on;
425
2/2
✓ Branch 4 → 5 taken 4 times.
✓ Branch 10 → 11 taken 4 times.
8 QSettings().setValue(QStringLiteral("compose/quickReply"), on);
426 4 emit quickReplyChanged();
427 }
428
429 6680 bool QuickComposeBackend::isOwnAddress(const QString &mailbox) const {
430
1/1
✓ Branch 2 → 3 taken 6680 times.
6680 const auto parsed = ComposeRecipients::parse(mailbox);
431
5/6
✓ Branch 4 → 5 taken 6680 times.
✗ Branch 4 → 8 not taken.
✓ Branch 7 → 8 taken 3567 times.
✓ Branch 7 → 9 taken 3113 times.
✓ Branch 10 → 11 taken 3567 times.
✓ Branch 10 → 12 taken 3113 times.
6680 if (!parsed || parsed->envelope.isEmpty()) return false;
432 3113 const QString address = parsed->envelope.first();
433
2/2
✓ Branch 24 → 18 taken 3134 times.
✓ Branch 24 → 25 taken 2111 times.
5245 for (const Identity &identity : m_identityStore.identities())
434
2/2
✓ Branch 20 → 21 taken 1002 times.
✓ Branch 20 → 22 taken 2132 times.
3134 if (identity.email.compare(address, Qt::CaseInsensitive) == 0) return true;
435 2111 return false;
436 6680 }
437
438 namespace {
439 constexpr int MaxIdentityNameChars = 256;
440 constexpr int MaxSignatureNameChars = 64;
441
442 10 QString signatureHtml(const QString &text) {
443 10 QStringList paragraphs;
444
5/5
✓ Branch 5 → 6 taken 10 times.
✓ Branch 6 → 7 taken 10 times.
✓ Branch 7 → 8 taken 10 times.
✓ Branch 36 → 9 taken 15 times.
✓ Branch 36 → 37 taken 10 times.
25 for (const QString &line : text.split(QLatin1Char('\n')))
445
1/1
✓ Branch 24 → 25 taken 15 times.
45 paragraphs.append(QStringLiteral("<p style=\"margin:0\">") +
446
5/9
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 18 taken 15 times.
✓ Branch 18 → 19 taken 15 times.
✓ Branch 22 → 23 taken 15 times.
✓ Branch 23 → 24 taken 15 times.
✗ Branch 30 → 31 not taken.
✓ Branch 30 → 32 taken 15 times.
✗ Branch 60 → 61 not taken.
✗ Branch 60 → 62 not taken.
70 (line.isEmpty() ? QStringLiteral("<br>") : line.toHtmlEscaped()) + QStringLiteral("</p>"));
447
1/1
✓ Branch 39 → 40 taken 10 times.
20 return paragraphs.join(QString());
448 10 }
449 } // namespace
450
451 42 QVariantList QuickComposeBackend::identitiesFor(const QString &accountId) const {
452 42 QVariantList out;
453
5/5
✓ Branch 2 → 3 taken 42 times.
✓ Branch 3 → 4 taken 42 times.
✓ Branch 4 → 5 taken 42 times.
✓ Branch 116 → 6 taken 39 times.
✓ Branch 116 → 117 taken 42 times.
81 for (const Identity &identity : m_identityStore.identitiesFor(accountId)) {
454 39 QVariantList signatures;
455
2/2
✓ Branch 38 → 9 taken 10 times.
✓ Branch 38 → 39 taken 39 times.
49 for (const Signature &signature : identity.signatures)
456
1/1
✓ Branch 24 → 25 taken 10 times.
30 signatures.append(QVariantMap{{QStringLiteral("id"), signature.id},
457 20 {QStringLiteral("name"), signature.name},
458
2/4
✓ Branch 28 → 29 taken 30 times.
✓ Branch 28 → 30 taken 10 times.
✗ Branch 125 → 126 not taken.
✗ Branch 125 → 127 not taken.
60 {QStringLiteral("text"), signature.text}});
459
1/1
✓ Branch 85 → 86 taken 39 times.
117 out.append(QVariantMap{{QStringLiteral("id"), identity.id},
460 78 {QStringLiteral("name"), identity.name},
461 78 {QStringLiteral("email"), identity.email},
462 78 {QStringLiteral("replyTo"), identity.replyTo},
463 78 {QStringLiteral("formattedByDefault"), identity.formattedByDefault},
464 78 {QStringLiteral("wrapPlainText"), identity.wrapPlainText},
465 78 {QStringLiteral("bccSelf"), identity.bccSelf},
466 78 {QStringLiteral("isDefault"), identity.isDefault},
467 78 {QStringLiteral("newMailSignatureId"), identity.newMailSignatureId},
468 78 {QStringLiteral("replySignatureId"), identity.replySignatureId},
469
2/4
✓ Branch 89 → 90 taken 429 times.
✓ Branch 89 → 91 taken 39 times.
✗ Branch 160 → 161 not taken.
✗ Branch 160 → 162 not taken.
507 {QStringLiteral("signatures"), signatures}});
470 81 }
471 42 return out;
472 508 }
473
474 12 QVariantMap QuickComposeBackend::saveIdentity(const QVariantMap &map) {
475 2 const auto refuse = [](const QString &error) {
476
2/4
✓ Branch 12 → 13 taken 4 times.
✓ Branch 12 → 14 taken 2 times.
✗ Branch 21 → 22 not taken.
✗ Branch 21 → 23 not taken.
12 return QVariantMap{{QStringLiteral("ok"), false}, {QStringLiteral("error"), error}};
477
1/5
✓ Branch 10 → 11 taken 2 times.
✗ Branch 37 → 38 not taken.
✗ Branch 37 → 41 not taken.
✗ Branch 39 → 40 not taken.
✗ Branch 39 → 41 not taken.
8 };
478
2/2
✓ Branch 6 → 7 taken 12 times.
✓ Branch 7 → 8 taken 12 times.
24 const QString id = map.value(QStringLiteral("id")).toString();
479
3/3
✓ Branch 13 → 14 taken 8 times.
✓ Branch 13 → 15 taken 4 times.
✓ Branch 15 → 16 taken 4 times.
12 std::optional<Identity> existing = id.isEmpty() ? std::nullopt : m_identityStore.find(id);
480
4/8
✓ Branch 17 → 18 taken 4 times.
✓ Branch 17 → 21 taken 8 times.
✗ Branch 19 → 20 not taken.
✓ Branch 19 → 21 taken 4 times.
✗ Branch 22 → 23 not taken.
✓ Branch 22 → 28 taken 12 times.
✗ Branch 23 → 24 not taken.
✗ Branch 24 → 25 not taken.
12 if (!id.isEmpty() && !existing) return refuse(tr("This address no longer exists."));
481 12 Identity identity = existing.value_or(Identity{});
482
2/2
✓ Branch 31 → 32 taken 8 times.
✓ Branch 31 → 69 taken 4 times.
12 if (!existing) {
483
2/2
✓ Branch 36 → 37 taken 8 times.
✓ Branch 37 → 38 taken 8 times.
16 const QString accountId = map.value(QStringLiteral("accountId")).toString();
484
1/1
✓ Branch 42 → 43 taken 8 times.
8 const auto account = m_identityStore.defaultFor(accountId);
485
1/4
✗ Branch 44 → 45 not taken.
✓ Branch 44 → 50 taken 8 times.
✗ Branch 45 → 46 not taken.
✗ Branch 46 → 47 not taken.
8 if (!account) return refuse(tr("The account is not set up for writing."));
486
2/2
✓ Branch 50 → 51 taken 8 times.
✓ Branch 51 → 52 taken 8 times.
8 identity.id = QUuid::createUuid().toString(QUuid::WithoutBraces);
487 8 identity.accountId = accountId;
488 8 identity.accountAddress = account->accountAddress;
489 8 identity.accountServer = account->accountServer;
490 8 }
491
4/4
✓ Branch 73 → 74 taken 12 times.
✓ Branch 74 → 75 taken 12 times.
✓ Branch 75 → 76 taken 12 times.
✓ Branch 76 → 77 taken 12 times.
24 identity.name = map.value(QStringLiteral("name")).toString().trimmed().left(MaxIdentityNameChars);
492
3/3
✓ Branch 89 → 90 taken 12 times.
✓ Branch 90 → 91 taken 12 times.
✓ Branch 91 → 92 taken 12 times.
24 const QString email = map.value(QStringLiteral("email")).toString().trimmed();
493
1/1
✓ Branch 97 → 98 taken 12 times.
12 const auto parsed = ComposeRecipients::parse(email);
494
7/9
✓ Branch 99 → 100 taken 11 times.
✓ Branch 99 → 107 taken 1 time.
✓ Branch 102 → 103 taken 11 times.
✗ Branch 102 → 107 not taken.
✓ Branch 105 → 106 taken 11 times.
✗ Branch 106 → 107 not taken.
✓ Branch 106 → 108 taken 11 times.
✓ Branch 109 → 110 taken 1 time.
✓ Branch 109 → 115 taken 11 times.
12 if (!parsed || parsed->envelope.size() != 1 || email.contains(QLatin1Char('<')))
495
2/2
✓ Branch 110 → 111 taken 1 time.
✓ Branch 111 → 112 taken 1 time.
1 return refuse(tr("Enter one e-mail address."));
496 11 identity.email = email;
497
3/3
✓ Branch 120 → 121 taken 11 times.
✓ Branch 121 → 122 taken 11 times.
✓ Branch 122 → 123 taken 11 times.
22 const QString replyTo = map.value(QStringLiteral("replyTo")).toString().trimmed();
498
3/13
✗ Branch 129 → 130 not taken.
✓ Branch 129 → 134 taken 11 times.
✗ Branch 130 → 131 not taken.
✗ Branch 132 → 133 not taken.
✗ Branch 132 → 134 not taken.
✗ Branch 135 → 136 not taken.
✓ Branch 135 → 137 taken 11 times.
✗ Branch 137 → 138 not taken.
✓ Branch 137 → 143 taken 11 times.
✗ Branch 138 → 139 not taken.
✗ Branch 139 → 140 not taken.
✗ Branch 465 → 466 not taken.
✗ Branch 465 → 467 not taken.
11 if (!replyTo.isEmpty() && !ComposeRecipients::parse(replyTo)) return refuse(tr("The reply address is not valid."));
499 11 identity.replyTo = replyTo;
500
2/2
✓ Branch 148 → 149 taken 11 times.
✓ Branch 149 → 150 taken 11 times.
22 identity.formattedByDefault = map.value(QStringLiteral("formattedByDefault")).toBool();
501
2/2
✓ Branch 158 → 159 taken 11 times.
✓ Branch 159 → 160 taken 11 times.
22 identity.wrapPlainText = map.value(QStringLiteral("wrapPlainText")).toBool();
502
2/2
✓ Branch 168 → 169 taken 11 times.
✓ Branch 169 → 170 taken 11 times.
22 identity.bccSelf = map.value(QStringLiteral("bccSelf")).toBool();
503
2/2
✓ Branch 178 → 179 taken 11 times.
✓ Branch 179 → 180 taken 11 times.
22 const QVariantList signatures = map.value(QStringLiteral("signatures")).toList();
504
1/4
✗ Branch 185 → 186 not taken.
✓ Branch 185 → 191 taken 11 times.
✗ Branch 186 → 187 not taken.
✗ Branch 187 → 188 not taken.
11 if (signatures.size() > IdentityStore::MaxSignaturesPerIdentity) return refuse(tr("At most 20 signatures per address."));
505 11 QList<Signature> kept;
506
2/2
✓ Branch 284 → 193 taken 11 times.
✓ Branch 284 → 285 taken 10 times.
21 for (const QVariant &value : signatures) {
507
1/1
✓ Branch 194 → 195 taken 11 times.
11 const QVariantMap entry = value.toMap();
508 11 Signature signature;
509
2/2
✓ Branch 199 → 200 taken 11 times.
✓ Branch 200 → 201 taken 11 times.
22 signature.id = entry.value(QStringLiteral("id")).toString();
510
3/4
✓ Branch 208 → 209 taken 11 times.
✗ Branch 208 → 214 not taken.
✓ Branch 209 → 210 taken 11 times.
✓ Branch 210 → 211 taken 11 times.
11 if (signature.id.isEmpty()) signature.id = QUuid::createUuid().toString(QUuid::WithoutBraces);
511
4/4
✓ Branch 218 → 219 taken 11 times.
✓ Branch 219 → 220 taken 11 times.
✓ Branch 220 → 221 taken 11 times.
✓ Branch 221 → 222 taken 11 times.
22 signature.name = entry.value(QStringLiteral("name")).toString().trimmed().left(MaxSignatureNameChars);
512
4/4
✓ Branch 231 → 232 taken 1 time.
✓ Branch 231 → 237 taken 10 times.
✓ Branch 232 → 233 taken 1 time.
✓ Branch 233 → 234 taken 1 time.
11 if (signature.name.isEmpty()) return refuse(tr("Every signature needs a name."));
513
2/2
✓ Branch 241 → 242 taken 10 times.
✓ Branch 242 → 243 taken 10 times.
20 signature.text = entry.value(QStringLiteral("text")).toString();
514
1/1
✓ Branch 255 → 256 taken 10 times.
20 signature.text.replace(QStringLiteral("\r\n"), QStringLiteral("\n"));
515
2/5
✓ Branch 260 → 261 taken 10 times.
✗ Branch 263 → 264 not taken.
✓ Branch 263 → 269 taken 10 times.
✗ Branch 264 → 265 not taken.
✗ Branch 265 → 266 not taken.
10 if (signature.text.toUtf8().size() > IdentityStore::MaxSignatureBytes) return refuse(tr("A signature is too long."));
516
1/1
✓ Branch 269 → 270 taken 10 times.
10 signature.html = signatureHtml(signature.text);
517
1/1
✓ Branch 272 → 273 taken 10 times.
10 kept.append(signature);
518 12 }
519 10 identity.signatures = kept;
520 10 const auto known = [&kept](const QString &signatureId) {
521 10 return std::any_of(kept.cbegin(), kept.cend(), [&](const Signature &s) { return s.id == signatureId; });
522 10 };
523 // The defaults name signatures by id; a new signature's id is its index ("#2").
524 20 const auto resolve = [&](const QString &value) -> QString {
525
3/3
✓ Branch 4 → 5 taken 20 times.
✓ Branch 5 → 6 taken 10 times.
✓ Branch 5 → 16 taken 10 times.
20 if (value.startsWith(QLatin1Char('#'))) {
526
2/2
✓ Branch 6 → 7 taken 10 times.
✓ Branch 7 → 8 taken 10 times.
10 const int index = value.mid(1).toInt();
527
2/4
✓ Branch 9 → 10 taken 10 times.
✗ Branch 9 → 14 not taken.
✓ Branch 11 → 12 taken 10 times.
✗ Branch 11 → 14 not taken.
10 return index >= 0 && index < kept.size() ? kept.at(index).id : QString();
528 }
529
1/2
✗ Branch 17 → 18 not taken.
✓ Branch 17 → 19 taken 10 times.
10 return known(value) ? value : QString();
530 10 };
531
3/3
✓ Branch 290 → 291 taken 10 times.
✓ Branch 291 → 292 taken 10 times.
✓ Branch 292 → 293 taken 10 times.
20 identity.newMailSignatureId = resolve(map.value(QStringLiteral("newMailSignatureId")).toString());
532
3/3
✓ Branch 304 → 305 taken 10 times.
✓ Branch 305 → 306 taken 10 times.
✓ Branch 306 → 307 taken 10 times.
20 identity.replySignatureId = resolve(map.value(QStringLiteral("replySignatureId")).toString());
533
2/5
✓ Branch 314 → 315 taken 10 times.
✗ Branch 315 → 316 not taken.
✓ Branch 315 → 321 taken 10 times.
✗ Branch 316 → 317 not taken.
✗ Branch 317 → 318 not taken.
10 if (!m_identityStore.upsert(identity)) return refuse(tr("The address could not be stored."));
534 10 QString error;
535
2/6
✓ Branch 321 → 322 taken 10 times.
✗ Branch 322 → 323 not taken.
✓ Branch 322 → 331 taken 10 times.
✗ Branch 323 → 324 not taken.
✗ Branch 325 → 326 not taken.
✗ Branch 326 → 327 not taken.
10 if (!m_identityStore.save(&error)) return refuse(tr("Could not save: %1").arg(error));
536
2/8
✓ Branch 331 → 332 taken 10 times.
✗ Branch 335 → 336 not taken.
✗ Branch 336 → 337 not taken.
✗ Branch 337 → 338 not taken.
✗ Branch 338 → 339 not taken.
✗ Branch 340 → 341 not taken.
✗ Branch 344 → 333 not taken.
✓ Branch 344 → 345 taken 10 times.
10 qCInfo(lcCompose) << "identity saved" << identity.id << "signatures" << kept.size();
537
1/1
✓ Branch 345 → 346 taken 10 times.
10 identitiesEdited();
538
2/4
✓ Branch 356 → 357 taken 20 times.
✓ Branch 356 → 358 taken 10 times.
✗ Branch 644 → 645 not taken.
✗ Branch 644 → 646 not taken.
60 return {{QStringLiteral("ok"), true}, {QStringLiteral("id"), identity.id}};
539 52 }
540
541 2 bool QuickComposeBackend::removeIdentity(const QString &identityId) {
542
1/1
✓ Branch 2 → 3 taken 2 times.
2 const auto identity = m_identityStore.find(identityId);
543
5/6
✓ Branch 4 → 5 taken 2 times.
✗ Branch 4 → 7 not taken.
✓ Branch 6 → 7 taken 1 time.
✓ Branch 6 → 8 taken 1 time.
✓ Branch 9 → 10 taken 1 time.
✓ Branch 9 → 11 taken 1 time.
2 if (!identity || identity->isDefault) return false;
544
5/8
✓ Branch 11 → 12 taken 1 time.
✓ Branch 12 → 13 taken 1 time.
✗ Branch 12 → 15 not taken.
✓ Branch 13 → 14 taken 1 time.
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 16 taken 1 time.
✗ Branch 17 → 18 not taken.
✓ Branch 17 → 19 taken 1 time.
1 if (!m_identityStore.remove(identityId) || !m_identityStore.save()) return false;
545
1/1
✓ Branch 19 → 20 taken 1 time.
1 identitiesEdited();
546 1 return true;
547 2 }
548
549 11 void QuickComposeBackend::identitiesEdited() {
550 11 ++m_identitiesRevision;
551
6/6
✓ Branch 2 → 3 taken 11 times.
✓ Branch 3 → 4 taken 11 times.
✓ Branch 4 → 5 taken 11 times.
✓ Branch 7 → 8 taken 9 times.
✓ Branch 10 → 6 taken 9 times.
✓ Branch 10 → 11 taken 11 times.
20 for (ComposeSession *session : m_sessionStore.sessions()) session->identitiesChanged();
552 11 emit identitiesChanged();
553 11 }
554
555
3/7
✓ Branch 3 → 4 taken 1736 times.
✗ Branch 3 → 8 not taken.
✓ Branch 6 → 7 taken 1736 times.
✓ Branch 10 → 11 taken 1736 times.
✗ Branch 10 → 12 not taken.
✗ Branch 14 → 15 not taken.
✗ Branch 14 → 16 not taken.
1736 int QuickComposeBackend::outboxCount() const { return m_outboxStore ? m_outboxStore->count() : 0; }
556
557 1705 bool QuickComposeBackend::outboxHasFailures() const {
558
10/25
✓ Branch 3 → 4 taken 1705 times.
✗ Branch 3 → 15 not taken.
✓ Branch 12 → 13 taken 1705 times.
✓ Branch 13 → 14 taken 28 times.
✓ Branch 13 → 15 taken 1677 times.
✓ Branch 17 → 18 taken 1705 times.
✗ Branch 17 → 19 not taken.
✓ Branch 19 → 20 taken 1705 times.
✗ Branch 19 → 23 not taken.
✓ Branch 21 → 22 taken 3410 times.
✓ Branch 21 → 23 taken 1705 times.
✓ Branch 23 → 24 taken 1705 times.
✗ Branch 23 → 25 not taken.
✓ Branch 25 → 26 taken 1705 times.
✗ Branch 25 → 27 not taken.
✗ Branch 29 → 30 not taken.
✗ Branch 29 → 31 not taken.
✗ Branch 33 → 34 not taken.
✗ Branch 33 → 37 not taken.
✗ Branch 35 → 36 not taken.
✗ Branch 35 → 37 not taken.
✗ Branch 39 → 40 not taken.
✗ Branch 39 → 41 not taken.
✗ Branch 43 → 44 not taken.
✗ Branch 43 → 45 not taken.
6820 return m_outboxStore && m_outboxStore->count({QStringLiteral("failed"), QStringLiteral("waiting")}) > 0;
559 5115 }
560
561 115 QObject *QuickComposeBackend::openNewMessage(const QVariantMap &prefill, const QString &place) {
562
1/1
✓ Branch 6 → 7 taken 115 times.
230 auto *session = m_sessionStore.create(QStringLiteral("new"), prefill, place);
563 // vCards, calendar files and mailto attachments come in as paths.
564
2/2
✓ Branch 15 → 16 taken 115 times.
✓ Branch 16 → 17 taken 115 times.
230 const QStringList files = prefill.value(QStringLiteral("attachments")).toStringList();
565
6/7
✓ Branch 21 → 22 taken 115 times.
✗ Branch 21 → 25 not taken.
✓ Branch 23 → 24 taken 2 times.
✓ Branch 23 → 25 taken 113 times.
✓ Branch 26 → 27 taken 2 times.
✓ Branch 26 → 29 taken 113 times.
✓ Branch 28 → 29 taken 2 times.
115 if (session && !files.isEmpty()) session->attachments().addPaths(files);
566 115 return session;
567 115 }
568
569 2 QObject *QuickComposeBackend::openResumed(const QVariantMap &draft, const QString &place) {
570 2 QVariantMap prefill = draft;
571 // An identity of another installation (or a foreign draft) is not ours:
572 // the account's default address writes.
573
6/6
✓ Branch 7 → 8 taken 2 times.
✓ Branch 8 → 9 taken 2 times.
✓ Branch 9 → 10 taken 2 times.
✓ Branch 17 → 18 taken 1 time.
✓ Branch 17 → 25 taken 1 time.
✓ Branch 21 → 22 taken 1 time.
5 if (!m_identityStore.find(draft.value(QStringLiteral("identityId")).toString())) prefill.remove(QStringLiteral("identityId"));
574
1/1
✓ Branch 29 → 30 taken 2 times.
4 auto *session = m_sessionStore.create(QStringLiteral("resumed"), prefill, place);
575
1/2
✗ Branch 34 → 35 not taken.
✓ Branch 34 → 36 taken 2 times.
2 if (!session) return nullptr;
576
2/2
✓ Branch 40 → 41 taken 2 times.
✓ Branch 41 → 42 taken 2 times.
4 const QStringList files = draft.value(QStringLiteral("attachments")).toStringList();
577
1/3
✗ Branch 47 → 48 not taken.
✓ Branch 47 → 50 taken 2 times.
✗ Branch 49 → 50 not taken.
2 if (!files.isEmpty()) session->attachments().addPaths(files);
578
2/2
✓ Branch 54 → 55 taken 2 times.
✓ Branch 55 → 56 taken 2 times.
4 const qint64 uid = draft.value(QStringLiteral("resumedUid")).toLongLong();
579
2/2
✓ Branch 64 → 65 taken 2 times.
✓ Branch 65 → 66 taken 2 times.
4 const quint32 validity = draft.value(QStringLiteral("resumedUidValidity")).toUInt();
580
1/2
✓ Branch 70 → 71 taken 2 times.
✗ Branch 70 → 77 not taken.
2 if (uid > 0) {
581
1/1
✓ Branch 71 → 72 taken 2 times.
2 session->setServerDraftUid(uid, validity);
582
1/1
✓ Branch 73 → 74 taken 2 times.
2 m_sessionDraftUids.insert(session->sessionId(), uid);
583
2/3
✓ Branch 75 → 76 taken 2 times.
✗ Branch 75 → 77 not taken.
✓ Branch 76 → 77 taken 2 times.
2 if (validity > 0) m_draftUidValidity.insert(uid, validity);
584 }
585
2/10
✓ Branch 77 → 78 taken 2 times.
✗ Branch 81 → 82 not taken.
✗ Branch 82 → 83 not taken.
✗ Branch 84 → 85 not taken.
✗ Branch 85 → 86 not taken.
✗ Branch 86 → 87 not taken.
✗ Branch 87 → 88 not taken.
✗ Branch 89 → 90 not taken.
✗ Branch 94 → 79 not taken.
✓ Branch 94 → 95 taken 2 times.
2 qCInfo(lcCompose) << "resumed draft opened" << session->sessionId() << "place" << place << "files" << files.size();
586 2 return session;
587 2 }
588
589 9 bool QuickComposeBackend::enqueuePrepared(const QStringList &recipients, const QByteArray &message,
590 const QString &doneText, const QString &identityId) {
591
3/3
✓ Branch 3 → 4 taken 4 times.
✓ Branch 3 → 5 taken 5 times.
✓ Branch 5 → 6 taken 5 times.
9 const auto chosen = identityId.isEmpty() ? std::nullopt : m_identityStore.find(identityId);
592
4/4
✓ Branch 7 → 8 taken 5 times.
✓ Branch 7 → 11 taken 4 times.
✓ Branch 10 → 14 taken 5 times.
✓ Branch 13 → 14 taken 4 times.
9 const auto account = chosen ? m_session.accounts()->configuration(chosen->accountId) : m_session.accounts()->primary();
593
10/14
✓ Branch 15 → 16 taken 9 times.
✗ Branch 15 → 27 not taken.
✓ Branch 17 → 18 taken 9 times.
✗ Branch 17 → 27 not taken.
✓ Branch 20 → 21 taken 9 times.
✗ Branch 20 → 27 not taken.
✓ Branch 22 → 23 taken 8 times.
✓ Branch 22 → 27 taken 1 time.
✓ Branch 24 → 25 taken 7 times.
✓ Branch 24 → 27 taken 1 time.
✗ Branch 26 → 27 not taken.
✓ Branch 26 → 28 taken 7 times.
✓ Branch 29 → 30 taken 2 times.
✓ Branch 29 → 34 taken 7 times.
16 if (!m_outbox || !account || account->smtp.host.isEmpty() || recipients.isEmpty() || message.isEmpty() ||
594 7 message.size() > MailOutboxStore::MaxMimeBytes) {
595
2/2
✓ Branch 30 → 31 taken 2 times.
✓ Branch 31 → 32 taken 2 times.
2 m_deps.setStatus(tr("SMTP is not configured for this account."));
596 2 return false;
597 }
598
4/4
✓ Branch 35 → 36 taken 5 times.
✓ Branch 35 → 37 taken 2 times.
✓ Branch 36 → 39 taken 5 times.
✓ Branch 38 → 39 taken 2 times.
7 const auto identity = chosen ? chosen : m_identityStore.defaultFor(account->id);
599 7 OutboxEntry entry;
600 7 entry.accountId = account->id;
601
1/2
✓ Branch 43 → 44 taken 7 times.
✗ Branch 43 → 46 not taken.
7 entry.identityId = identity ? identity->id : QString();
602 7 entry.state = QStringLiteral("queued");
603
1/1
✓ Branch 55 → 56 taken 7 times.
7 entry.due = QDateTime::currentDateTimeUtc();
604
1/1
✓ Branch 58 → 59 taken 7 times.
7 entry.messageId = Rfc2822Builder::messageIdOf(message);
605
1/2
✓ Branch 62 → 63 taken 7 times.
✗ Branch 62 → 65 not taken.
7 entry.envelopeFrom = identity ? identity->email : account->email;
606 7 entry.recipients = recipients;
607 7 entry.mime = message;
608
1/1
✓ Branch 73 → 74 taken 7 times.
7 entry.summaryTo = recipients.join(QStringLiteral(", "));
609
3/3
✓ Branch 78 → 79 taken 7 times.
✓ Branch 83 → 84 taken 7 times.
✓ Branch 84 → 85 taken 7 times.
14 entry.subject = MimeParser::messageHeaders(message).value(QStringLiteral("subject")).left(200);
610 7 entry.sentFolderAccount = account->id;
611
1/1
✓ Branch 95 → 96 taken 7 times.
7 const qint64 id = m_outbox->enqueue(entry);
612
1/2
✗ Branch 96 → 97 not taken.
✓ Branch 96 → 98 taken 7 times.
7 if (id <= 0) return false;
613
2/3
✓ Branch 99 → 100 taken 7 times.
✗ Branch 99 → 101 not taken.
✓ Branch 100 → 101 taken 7 times.
7 if (!doneText.isEmpty()) m_preparedDoneTexts.insert(id, doneText);
614
1/1
✓ Branch 102 → 103 taken 7 times.
7 m_outbox->sendNow(id);
615
2/10
✓ Branch 103 → 104 taken 7 times.
✗ Branch 107 → 108 not taken.
✗ Branch 108 → 109 not taken.
✗ Branch 109 → 110 not taken.
✗ Branch 110 → 111 not taken.
✗ Branch 112 → 113 not taken.
✗ Branch 113 → 114 not taken.
✗ Branch 115 → 116 not taken.
✗ Branch 119 → 105 not taken.
✓ Branch 119 → 120 taken 7 times.
7 qCInfo(lcCompose) << "prepared message queued" << id << "recipients" << recipients.size() << "bytes" << message.size();
616 7 return true;
617 9 }
618
619 3 QObject *QuickComposeBackend::sessionForServerDraft(qint64 uid) const {
620
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 3 times.
3 if (uid <= 0) return nullptr;
621
5/5
✓ Branch 4 → 5 taken 3 times.
✓ Branch 5 → 6 taken 3 times.
✓ Branch 6 → 7 taken 3 times.
✓ Branch 29 → 8 taken 1 time.
✓ Branch 29 → 30 taken 2 times.
3 for (ComposeSession *session : m_sessionStore.sessions())
622
3/6
✓ Branch 10 → 11 taken 1 time.
✗ Branch 10 → 20 not taken.
✓ Branch 14 → 15 taken 1 time.
✗ Branch 14 → 20 not taken.
✓ Branch 25 → 26 taken 1 time.
✗ Branch 25 → 27 not taken.
3 if (session->serverDraftUid() == uid && session->place() != QLatin1String("outbox") &&
623
3/6
✓ Branch 18 → 19 taken 1 time.
✗ Branch 18 → 20 not taken.
✓ Branch 21 → 22 taken 1 time.
✗ Branch 21 → 23 not taken.
✓ Branch 23 → 24 taken 1 time.
✗ Branch 23 → 25 not taken.
2 session->place() != QLatin1String("discarded"))
624
2/2
✓ Branch 32 → 33 taken 2 times.
✓ Branch 32 → 35 taken 1 time.
3 return session;
625 2 return nullptr;
626 }
627
628 29 QObject *QuickComposeBackend::openReply(const QVariantMap &draft, const QString &kind, const QString &place,
629 const QVariantMap &mailReference) {
630 29 QVariantMap prefill = draft;
631 29 const bool forward = kind == QLatin1String("forward");
632 // D-4: a reply or forward takes the original's format.
633
3/3
✓ Branch 9 → 10 taken 29 times.
✓ Branch 10 → 11 taken 29 times.
✓ Branch 15 → 16 taken 29 times.
87 prefill.insert(QStringLiteral("formatted"), draft.value(QStringLiteral("originalFormatted")).toBool());
634 // D-15: the identity the original was sent to.
635 29 IdentityChoice::Original original;
636
1/2
✓ Branch 26 → 27 taken 29 times.
✗ Branch 26 → 31 not taken.
29 original.accountId = m_session.accounts()->primary() ? m_session.accounts()->primary()->id : QString();
637
2/2
✓ Branch 38 → 39 taken 29 times.
✓ Branch 39 → 40 taken 29 times.
58 original.to = draft.value(QStringLiteral("originalTo")).toString();
638
2/2
✓ Branch 50 → 51 taken 29 times.
✓ Branch 51 → 52 taken 29 times.
58 original.cc = draft.value(QStringLiteral("originalCc")).toString();
639
2/2
✓ Branch 62 → 63 taken 29 times.
✓ Branch 63 → 64 taken 29 times.
58 original.deliveredTo = draft.value(QStringLiteral("deliveredTo")).toString();
640
2/2
✓ Branch 74 → 75 taken 29 times.
✓ Branch 75 → 76 taken 29 times.
58 original.xOriginalTo = draft.value(QStringLiteral("xOriginalTo")).toString();
641
1/1
✓ Branch 83 → 84 taken 29 times.
29 const auto choice = IdentityChoice::forReply(m_identityStore.identities(), original);
642
1/1
✓ Branch 88 → 89 taken 29 times.
58 prefill.insert(QStringLiteral("identityId"), choice.identityId);
643
1/1
✓ Branch 96 → 97 taken 29 times.
58 prefill.insert(QStringLiteral("originalSentTo"), choice.matchedAddress);
644 29 ComposeSession::Quote quote;
645
2/2
✓ Branch 100 → 101 taken 3 times.
✓ Branch 100 → 132 taken 26 times.
29 if (forward) {
646
2/2
✓ Branch 105 → 106 taken 3 times.
✓ Branch 109 → 110 taken 3 times.
9 prefill.insert(QStringLiteral("body"), draft.value(QStringLiteral("forwardText")));
647
2/2
✓ Branch 120 → 121 taken 3 times.
✓ Branch 124 → 125 taken 3 times.
9 prefill.insert(QStringLiteral("html"), draft.value(QStringLiteral("forwardHtml")));
648 } else {
649
1/1
✓ Branch 135 → 136 taken 26 times.
26 prefill.remove(QStringLiteral("body"));
650
2/2
✓ Branch 142 → 143 taken 26 times.
✓ Branch 148 → 149 taken 26 times.
104 quote = {draft.value(QStringLiteral("quoteAttribution")).toString(), draft.value(QStringLiteral("quoteText")).toString(),
651
1/1
✓ Branch 154 → 155 taken 26 times.
78 draft.value(QStringLiteral("quoteHtml")).toString()};
652 }
653
1/1
✓ Branch 171 → 172 taken 29 times.
29 auto *session = m_sessionStore.create(kind, prefill, place, mailReference, quote);
654 // A forward keeps the original's files (F-06).
655
2/2
✓ Branch 176 → 177 taken 29 times.
✓ Branch 177 → 178 taken 29 times.
58 const QStringList files = draft.value(QStringLiteral("attachments")).toStringList();
656
1/3
✗ Branch 183 → 184 not taken.
✓ Branch 183 → 186 taken 29 times.
✗ Branch 185 → 186 not taken.
29 if (!files.isEmpty()) session->attachments().addPaths(files);
657
1/2
✓ Branch 187 → 188 taken 29 times.
✗ Branch 187 → 195 not taken.
29 if (!choice.matchedAddress.isEmpty())
658
3/3
✓ Branch 188 → 189 taken 29 times.
✓ Branch 190 → 191 taken 29 times.
✓ Branch 191 → 192 taken 29 times.
58 m_deps.setStatus(tr("Identity chosen: %1 (the mail was sent to it)").arg(choice.matchedAddress));
659 29 return session;
660 107 }
661
662 2 bool QuickComposeBackend::undoSend(qint64 outboxId) {
663
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 2 times.
2 if (!m_outbox) return false;
664
4/4
✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 17 taken 1 time.
✓ Branch 10 → 11 taken 1 time.
✓ Branch 11 → 12 taken 1 time.
3 if (outboxId <= 0) outboxId = m_pendingSend.value(QStringLiteral("id")).toLongLong();
665
1/1
✓ Branch 18 → 19 taken 2 times.
2 const auto entry = m_outbox->takeBack(outboxId);
666
1/2
✗ Branch 20 → 21 not taken.
✓ Branch 20 → 22 taken 2 times.
2 if (!entry) return false;
667
3/4
✓ Branch 26 → 27 taken 2 times.
✓ Branch 27 → 28 taken 2 times.
✓ Branch 32 → 33 taken 2 times.
✗ Branch 32 → 35 not taken.
4 if (m_pendingSend.value(QStringLiteral("id")).toLongLong() == outboxId) {
668
1/1
✓ Branch 33 → 34 taken 2 times.
2 m_pendingSend.clear();
669
1/1
✓ Branch 34 → 35 taken 2 times.
2 emit pendingSendChanged();
670 }
671
1/1
✓ Branch 35 → 36 taken 2 times.
2 ComposeSession *session = m_sessionStore.reopenFromOutbox(outboxId);
672
2/2
✓ Branch 36 → 37 taken 2 times.
✓ Branch 37 → 38 taken 2 times.
2 m_deps.setStatus(tr("Sending undone — the draft is back"));
673
2/3
✓ Branch 39 → 40 taken 2 times.
✗ Branch 39 → 41 not taken.
✓ Branch 40 → 41 taken 2 times.
2 if (session) emit sessionReturned(session);
674 2 return true;
675 2 }
676
677 namespace {
678 // §4.16: what an Outbox row says about its message, in words.
679 56 QString outboxStateText(const OutboxEntry &entry, const QDateTime &now) {
680
3/3
✓ Branch 2 → 3 taken 56 times.
✓ Branch 3 → 4 taken 56 times.
✓ Branch 4 → 5 taken 56 times.
56 const QString at = QLocale().toString(entry.due.toLocalTime(), QLocale::ShortFormat);
681
2/2
✓ Branch 9 → 10 taken 30 times.
✓ Branch 9 → 18 taken 26 times.
56 if (entry.state == QLatin1String("undo"))
682
3/3
✓ Branch 10 → 11 taken 30 times.
✓ Branch 12 → 13 taken 30 times.
✓ Branch 14 → 15 taken 30 times.
60 return QuickComposeBackend::tr("Sending in %1 s").arg(qMax<qint64>(0, now.secsTo(entry.due)));
683
4/4
✓ Branch 20 → 21 taken 14 times.
✓ Branch 20 → 26 taken 12 times.
✓ Branch 21 → 22 taken 14 times.
✓ Branch 23 → 24 taken 14 times.
40 if (entry.state == QLatin1String("scheduled")) return QuickComposeBackend::tr("Scheduled for %1").arg(at);
684
3/7
✓ Branch 28 → 29 taken 12 times.
✗ Branch 28 → 32 not taken.
✗ Branch 31 → 32 not taken.
✓ Branch 31 → 33 taken 12 times.
✗ Branch 34 → 35 not taken.
✓ Branch 34 → 37 taken 12 times.
✗ Branch 35 → 36 not taken.
12 if (entry.state == QLatin1String("queued") || entry.state == QLatin1String("sending")) return QuickComposeBackend::tr("Sending…");
685
1/2
✗ Branch 39 → 40 not taken.
✓ Branch 39 → 53 taken 12 times.
12 if (entry.state == QLatin1String("retry"))
686 ✗ return entry.errorClass == QLatin1String("network") && entry.lastError.isEmpty()
687 ✗ ? QuickComposeBackend::tr("Waiting for the network")
688 ✗ : QuickComposeBackend::tr("Next try %1 — %2").arg(at, entry.lastError);
689
1/2
✓ Branch 55 → 56 taken 12 times.
✗ Branch 55 → 81 not taken.
12 if (entry.state == QLatin1String("waiting")) {
690 // §4.15: a refused login in words (SMTP 530/534/535/538 or the
691 // server's own words for it); other account problems keep the reply.
692 static const QRegularExpression refusedLogin(
693
4/7
✓ Branch 56 → 57 taken 2 times.
✓ Branch 56 → 69 taken 10 times.
✓ Branch 58 → 59 taken 2 times.
✗ Branch 58 → 69 not taken.
✓ Branch 63 → 64 taken 2 times.
✗ Branch 143 → 144 not taken.
✗ Branch 143 → 145 not taken.
14 QStringLiteral("^(530|534|535|538)\\b|authentication|password"), QRegularExpression::CaseInsensitiveOption);
694
3/4
✓ Branch 70 → 71 taken 12 times.
✓ Branch 71 → 72 taken 12 times.
✓ Branch 73 → 74 taken 12 times.
✗ Branch 73 → 76 not taken.
12 if (refusedLogin.match(entry.lastError).hasMatch())
695
1/1
✓ Branch 74 → 75 taken 12 times.
12 return QuickComposeBackend::tr("Waiting: the server refused the password");
696 ✗ return QuickComposeBackend::tr("Waiting for the account — %1").arg(entry.lastError);
697 }
698 ✗ if (entry.state == QLatin1String("sent") && !entry.refused.isEmpty())
699 ✗ return QuickComposeBackend::tr("Not delivered to %1").arg(entry.refused.join(QStringLiteral(", ")));
700 ✗ if (entry.state == QLatin1String("sent")) return QuickComposeBackend::tr("Sent — the copy for the Sent folder waits");
701 ✗ return QuickComposeBackend::tr("Failed: %1").arg(entry.lastError);
702 56 }
703 } // namespace
704
705 25 QVariantList QuickComposeBackend::outboxEntries() const {
706 25 QVariantList out;
707
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 25 times.
25 if (!m_outboxStore) return out;
708
1/1
✓ Branch 5 → 6 taken 25 times.
25 const QDateTime now = QDateTime::currentDateTimeUtc();
709
5/5
✓ Branch 7 → 8 taken 25 times.
✓ Branch 8 → 9 taken 25 times.
✓ Branch 9 → 10 taken 25 times.
✓ Branch 127 → 11 taken 30 times.
✓ Branch 127 → 128 taken 25 times.
55 for (const OutboxEntry &entry : m_outboxStore->entries()) {
710
1/1
✓ Branch 12 → 13 taken 30 times.
30 const auto identity = m_identityStore.find(entry.identityId);
711
1/1
✓ Branch 88 → 89 taken 30 times.
90 out.append(QVariantMap{{QStringLiteral("id"), entry.id},
712 60 {QStringLiteral("state"), entry.state},
713
1/1
✓ Branch 24 → 25 taken 30 times.
60 {QStringLiteral("stateText"), outboxStateText(entry, now)},
714
2/2
✓ Branch 34 → 35 taken 20 times.
✓ Branch 34 → 37 taken 10 times.
90 {QStringLiteral("failed"), entry.state == QLatin1String("failed") ||
715
2/4
✓ Branch 31 → 32 taken 30 times.
✗ Branch 31 → 37 not taken.
✗ Branch 36 → 37 not taken.
✓ Branch 36 → 38 taken 20 times.
60 entry.state == QLatin1String("waiting") || !entry.refused.isEmpty()},
716
1/1
✓ Branch 43 → 44 taken 30 times.
60 {QStringLiteral("due"), entry.due.toLocalTime()},
717 60 {QStringLiteral("summary"), entry.summaryTo},
718 60 {QStringLiteral("subject"), entry.subject},
719 60 {QStringLiteral("errorClass"), entry.errorClass},
720 60 {QStringLiteral("error"), entry.lastError},
721 60 {QStringLiteral("refused"), entry.refused},
722 60 {QStringLiteral("identityId"), entry.identityId},
723 60 {QStringLiteral("accountId"), entry.accountId},
724 90 {QStringLiteral("from"), identity ? identity->email : entry.envelopeFrom},
725
2/4
✓ Branch 92 → 93 taken 420 times.
✓ Branch 92 → 94 taken 30 times.
✗ Branch 137 → 138 not taken.
✗ Branch 137 → 139 not taken.
480 {QStringLiteral("invitation"), entry.invitation}});
726 55 }
727 25 return out;
728 505 }
729
730 26 QVariantMap QuickComposeBackend::outboxPreview(qint64 id) const {
731
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 26 times.
26 if (!m_outboxStore) return {};
732
1/1
✓ Branch 6 → 7 taken 26 times.
26 const auto entry = m_outboxStore->find(id);
733
1/2
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 26 times.
26 if (!entry) return {};
734 // The message as it will be sent: its own bytes, parsed like a received mail.
735
1/1
✓ Branch 11 → 12 taken 26 times.
26 const MimeMessage parsed = MimeParser::parse(entry->mime);
736 26 QStringList files;
737
2/2
✓ Branch 19 → 14 taken 48 times.
✓ Branch 19 → 20 taken 26 times.
74 for (const MimePart &part : parsed.attachments)
738
2/3
✓ Branch 15 → 16 taken 48 times.
✗ Branch 15 → 17 not taken.
✓ Branch 16 → 17 taken 48 times.
48 if (part.isAttachment) files.append(part.filename);
739 52 return {{QStringLiteral("to"), entry->summaryTo},
740 52 {QStringLiteral("subject"), entry->subject},
741 52 {QStringLiteral("text"), parsed.textPlain},
742 52 {QStringLiteral("files"), files},
743
4/6
✓ Branch 41 → 42 taken 26 times.
✓ Branch 43 → 44 taken 26 times.
✓ Branch 47 → 48 taken 130 times.
✓ Branch 47 → 49 taken 26 times.
✗ Branch 68 → 69 not taken.
✗ Branch 68 → 70 not taken.
182 {QStringLiteral("stateText"), outboxStateText(*entry, QDateTime::currentDateTimeUtc())}};
744 182 }
745
746
2/4
✓ Branch 3 → 4 taken 2 times.
✗ Branch 3 → 8 not taken.
✓ Branch 6 → 7 taken 2 times.
✗ Branch 6 → 8 not taken.
2 bool QuickComposeBackend::outboxSendNow(qint64 id) { return m_outbox && m_outbox->sendNow(id); }
747
748 ✗ bool QuickComposeBackend::outboxReschedule(qint64 id, const QDateTime &when) {
749 ✗ return m_outbox && m_outbox->reschedule(id, when);
750 }
751
752 ✗ bool QuickComposeBackend::outboxResendToRefused(qint64 id) { return m_outbox && m_outbox->resendToRefused(id); }
753
754 2 QObject *QuickComposeBackend::outboxEdit(qint64 id) {
755
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 2 times.
2 if (!m_outbox) return nullptr;
756
1/1
✓ Branch 6 → 7 taken 2 times.
2 const auto entry = m_outbox->takeBack(id);
757
1/2
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 2 times.
2 if (!entry) return nullptr;
758
2/3
✓ Branch 10 → 11 taken 2 times.
✓ Branch 11 → 12 taken 2 times.
✗ Branch 11 → 13 not taken.
2 if (ComposeSession *session = m_sessionStore.reopenFromOutbox(id)) return session;
759 // The session is gone (restart): the draft directory restores it.
760 ✗ if (const auto draft = ComposeDraftFiles::load(m_profileDirectory, entry->draftSession)) {
761 ✗ m_sessionStore.restoreOpenSessions();
762 ✗ if (auto *session = qobject_cast<ComposeSession *>(m_sessionStore.session(entry->draftSession))) {
763 ✗ m_sessionStore.setPlace(session->sessionId(),
764 ✗ session->mailReference().isEmpty() ? QStringLiteral("tab") : QStringLiteral("inline"));
765 ✗ return session;
766 }
767 ✗ }
768 ✗ return nullptr;
769 2 }
770
771 1 bool QuickComposeBackend::outboxDelete(qint64 id) {
772
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 1 time.
1 if (!m_outbox) return false;
773
1/1
✓ Branch 6 → 7 taken 1 time.
1 const auto entry = m_outbox->takeBack(id);
774
1/2
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 1 time.
1 if (!entry) return false;
775
1/1
✓ Branch 10 → 11 taken 1 time.
1 m_sessionStore.discardFromOutbox(id);
776 1 return true;
777 1 }
778
779 795 void QuickComposeBackend::flushOutboxOnQuit() {
780 // Every open composer's newest text is on disk before the process ends.
781
1/7
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 15 taken 795 times.
✗ Branch 4 → 5 not taken.
✗ Branch 8 → 9 not taken.
✗ Branch 9 → 10 not taken.
✗ Branch 13 → 6 not taken.
✗ Branch 13 → 14 not taken.
795 if (!m_sessionStore.saveAll()) qCWarning(lcCompose) << "a draft could not be saved at quit";
782
2/2
✓ Branch 16 → 17 taken 794 times.
✓ Branch 16 → 19 taken 1 time.
795 if (m_outbox) m_outbox->flushUndoWindow();
783 // A draft in the bin's undo window is gone for good at quit.
784 795 m_sessionStore.flushDiscard();
785 795 }
786