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: 100.0% 25 / 0 / 25
Functions: 100.0% 22 / 0 / 22
Branches: 33.3% 1 / 0 / 3

presentation/QuickComposeBackend.h
Line Branch Exec Source
1 #pragma once
2
3 #include "data/FreeSlots.h"
4
5 #include "ComposeRecipients.h"
6 #include "ComposeReplyPrefill.h"
7 #include "ComposeSessionStore.h"
8 #include "MailOutboxSender.h"
9 #include "data/IdentityStore.h"
10 #include "data/TemplateStore.h"
11 #include "data/SpellLanguageMemory.h"
12 #include "AddressPickerModel.h"
13 #include "ComposePreview.h"
14 #include "SpellService.h"
15 #include "data/MailOutboxStore.h"
16 #include "ServerDraftCleanup.h"
17 #include "data/MailRequest.h"
18 #include <QHash>
19 #include <QSet>
20 #include <QObject>
21 #include <QStringList>
22 #include <QThreadPool>
23 #include <QVariantMap>
24 #include <functional>
25 #include <memory>
26
27 class MailSessionCore;
28 class WorkspaceState;
29
30 // Compose surface of the Quick frontend (Sprint 91 S91.3.1, rebuilt in
31 // Sprint 108): the composer sessions (ComposeSessionStore), their server
32 // drafts, the outbox, identities, templates, spelling, the address book
33 // sheet, the preview, suggest times and invitations, and the reply/forward
34 // prefill. QML reaches it as `backend.compose`.
35 //
36 // Owner: QuickMailBackend (value member). GUI thread; MIME builds run on the
37 // backend's single-thread pool (passed by reference), reply sources on the
38 // prefill's own pool. The backend calls connectComposeSource() and
39 // connectServerDrafts() at the positions of the former wire blocks, so
40 // receiver order is unchanged (cancelReplyDraft stays the first
41 // WorkspaceState::selectionChanged receiver).
42 namespace Imip {
43 struct Meeting;
44 struct Attendee;
45 } // namespace Imip
46
47 class QuickComposeBackend final : public QObject {
48 232 Q_OBJECT
49 Q_PROPERTY(bool composeWindowed READ composeWindowed WRITE setComposeWindowed NOTIFY composeWindowedChanged)
50 // Sprint 108: every open composer (ComposeSessionStore), the message in
51 // its undo window ({id, summary, deadline}) and the outbox.
52 Q_PROPERTY(QObject *sessions READ sessionsObject CONSTANT)
53 // S108.5.3 (D-14): the rows of the address book sheet.
54 Q_PROPERTY(QObject *addressPicker READ addressPickerObject CONSTANT)
55 // §4.14: ⋯ › Preview of the focused composer.
56 Q_PROPERTY(QObject *preview READ previewObject CONSTANT)
57 // Phase 4: Sonnet's languages and dictionaries.
58 Q_PROPERTY(QObject *spell READ spellObject CONSTANT)
59 Q_PROPERTY(QVariantMap pendingSend READ pendingSend NOTIFY pendingSendChanged)
60 Q_PROPERTY(int outboxCount READ outboxCount NOTIFY outboxChanged)
61 Q_PROPERTY(bool outboxHasFailures READ outboxHasFailures NOTIFY outboxChanged)
62 // D-20: seconds a sent message waits for *Undo* (0–30).
63 Q_PROPERTY(int undoDelaySeconds READ undoDelaySeconds WRITE setUndoDelaySeconds NOTIFY undoDelayChanged)
64 // D-7: Markdown-style typing in formatted mails (Settings → Mail → Writing).
65 Q_PROPERTY(bool markdownTyping READ markdownTyping WRITE setMarkdownTyping NOTIFY markdownTypingChanged)
66 // D-26: the quick reply field above the shown message (Settings → Mail).
67 Q_PROPERTY(bool quickReply READ quickReply WRITE setQuickReply NOTIFY quickReplyChanged)
68 // Bumped when an identity or signature changed (Settings → account page).
69 Q_PROPERTY(int identitiesRevision READ identitiesRevision NOTIFY identitiesChanged)
70 public:
71 struct Deps {
72 std::function<bool(const MailId &)> acceptsIdentity;
73 std::function<QString()> accountName;
74 std::function<QString()> accountEmail;
75 std::function<QString()> activeAccountId;
76 std::function<void(const QString &)> setStatus;
77 std::function<void(const QString &)> reportError;
78 };
79 QuickComposeBackend(MailSessionCore &session, WorkspaceState &workspace,
80 const QString &profileDirectory, QThreadPool &threadPool,
81 const QString &draftsFolder, const QString &sentFolder,
82 const QHash<qint64, QString> &folderPathById,
83 const QHash<qint64, quint32> &folderEpochById, Deps deps,
84 QObject *parent = nullptr);
85 void connectComposeSource();
86 void connectServerDrafts();
87 // Backend transitions: the compose part of an account reload / an account
88 // switch, and the durable teardown at the start of ~QuickMailBackend().
89 void resetForAccountReload();
90 void resetForAccountSwitch();
91 void shutdown();
92 31 bool draftCleanupActive() const { return m_draftCleanup.active(); }
93
94 // F3.2: compose opens in its own top-level window when true.
95 1596 bool composeWindowed() const { return m_composeWindowed; }
96 void setComposeWindowed(bool on);
97 // F-20 (SPRINT-104): the subject of a reply — every reply prefix counts,
98 // so "AW: Termin" becomes "Re: Termin" instead of "Re: AW: Termin", and a
99 // subject without a prefix gets "Re: ". Recognizes the
100 // common German (AW, WG, SV, FW), English (Re, Fwd), French (REF/RE),
101 // Dutch (VB) and Scandinavian (SV, VS) abbreviations, case-insensitive,
102 // repeated prefixes collapse to one "Re:".
103 5 Q_INVOKABLE static QString normalizeReplySubject(const QString &subject) {
104 5 return ComposeReplyPrefill::normalizeReplySubject(subject);
105 }
106
107 // Prefill for replies from the selected mail's header: recipient, a
108 // Re:-normalized subject, the quoted original body and the threading
109 // headers for the outgoing mail. With all=true the original recipients
110 // join the recipient field (reply-all).
111 10 Q_INVOKABLE QVariantMap replyDraft(bool all = false) const { return m_prefill.replyDraft(all); }
112 // Forward prefill: "Fwd:" subject plus the quoted original message.
113 1 Q_INVOKABLE QVariantMap forwardDraft() const { return m_prefill.forwardDraft(); }
114 // Interactive path: reserve a Compose intent before the read and deliver
115 // its bounded quote only while account, mail and epoch remain current.
116 30 Q_INVOKABLE bool requestReplyDraft(int requestToken, bool all = false,
117 bool forward = false) {
118 30 return m_prefill.requestReplyDraft(requestToken, all, forward);
119 }
120 7 Q_INVOKABLE void cancelReplyDraft() { m_prefill.cancelReplyDraft(); }
121 // S108.7.2 (D-21): the shown draft of the drafts folder as a prefill,
122 // delivered through replyDraftReady like a reply.
123 2 Q_INVOKABLE bool requestDraftResume(int requestToken) { return m_prefill.requestDraftResume(requestToken); }
124 // A recipient-field entry for a name and address (S106-373/384).
125 16 Q_INVOKABLE QString mailbox(const QString &name, const QString &address) const {
126 16 return ComposeRecipients::mailbox(name, address);
127 }
128 // F1: total budget across a message's attachments. Together with the
129 // per-file budget in Rfc2822Builder this bounds worst-case build RAM to
130 // 100 MiB even if every file sits exactly at its limit. Public:
131 // QuickComposeForwardAttachments enforces it on forwards and resumes.
132 static constexpr qint64 MaxComposeAttachmentsBytes = 100LL * 1024 * 1024;
133
134 // ─── Sprint 108: sessions, identities, outbox (QuickComposeBackendSessions.cpp) ───
135 765 QObject *sessionsObject() { return &m_sessionStore; }
136 505 ComposeSessionStore &sessionStore() { return m_sessionStore; }
137 IdentityStore &identityStore() { return m_identityStore; }
138 818 AddressPickerModel &addressPicker() { return m_addressPicker; }
139 142 QObject *addressPickerObject() { return &m_addressPicker; }
140 160 QObject *previewObject() { return &m_preview; }
141 477 QObject *spellObject() { return &m_spell; }
142 MailOutboxSender *outbox() const { return m_outbox.get(); }
143 27 QVariantMap pendingSend() const { return m_pendingSend; }
144 36 int undoDelaySeconds() const { return m_undoDelaySeconds; }
145 void setUndoDelaySeconds(int seconds);
146 36 bool markdownTyping() const { return m_markdownTyping; }
147 void setMarkdownTyping(bool on);
148 46 bool quickReply() const { return m_quickReply; }
149 void setQuickReply(bool on);
150 // Whether a mailbox ("Name <address>" or an address) is one of the
151 // user's own addresses (D-26: *Follow up* instead of *Reply*).
152 Q_INVOKABLE bool isOwnAddress(const QString &mailbox) const;
153 28 int identitiesRevision() const { return m_identitiesRevision; }
154 // S108.5.4/5.6 (D-15, D-16): the identities of one account for its
155 // settings page — {id, name, email, replyTo, formattedByDefault,
156 // wrapPlainText, bccSelf, isDefault, newMailSignatureId,
157 // replySignatureId, signatures: [{id, name, text}]}.
158 Q_INVOKABLE QVariantList identitiesFor(const QString &accountId) const;
159 // Validates and stores one identity of the map's shape (a new one when
160 // its id is empty); {ok, id} or {ok: false, error}.
161 Q_INVOKABLE QVariantMap saveIdentity(const QVariantMap &identity);
162 // Another address of the account; the account's default stays.
163 Q_INVOKABLE bool removeIdentity(const QString &identityId);
164 // S108.9.5 templates (QuickComposeBackendTemplates.cpp): [{id, name,
165 // subject, formatted}] sorted by name; save from a session's new text
166 // (the same name replaces), insert by id or name into a session, rename,
167 // delete. Saving and renaming answer {ok, id} or {ok: false, error}.
168 Q_INVOKABLE QVariantList templates() const;
169 Q_INVOKABLE QVariantMap saveTemplate(QObject *session, const QString &name);
170 Q_INVOKABLE bool insertTemplate(QObject *session, const QString &idOrName);
171 Q_INVOKABLE QVariantMap renameTemplate(const QString &id, const QString &name);
172 Q_INVOKABLE bool removeTemplate(const QString &id);
173 int outboxCount() const;
174 bool outboxHasFailures() const;
175 // A new mail in a tab or window; prefill keys as ComposeSession::prefill
176 // plus "attachments" (file paths copied into the draft).
177 Q_INVOKABLE QObject *openNewMessage(const QVariantMap &prefill, const QString &place = QStringLiteral("tab"));
178 // A reply/forward from a replyDraftReady() map (quote, format, identity
179 // inputs); place inline (with the mail), tab or window.
180 Q_INVOKABLE QObject *openReply(const QVariantMap &draft, const QString &kind, const QString &place,
181 const QVariantMap &mailReference = {});
182 // A resumed draft (D-21): a new session (new id) that replaces the
183 // folder copy `resumedUid` on its next save and deletes it once sent.
184 Q_INVOKABLE QObject *openResumed(const QVariantMap &draft, const QString &place = QStringLiteral("tab"));
185 // The open session that already continues the draft with this UID.
186 Q_INVOKABLE QObject *sessionForServerDraft(qint64 uid) const;
187 // A message built elsewhere (iMIP replies, S93.5.2) through the outbox:
188 // sent at once from the primary account, retried like any send; the
189 // status line says `doneText` when it left.
190 // `identityId` sends through that identity's account (default: the primary).
191 bool enqueuePrepared(const QStringList &recipients, const QByteArray &message, const QString &doneText,
192 const QString &identityId = {});
193 // S108.8.8/8.9 (QuickComposeBackendScheduling.cpp): meetings the user
194 // organizes, from the calendar. organizedEvent: {organized, foreign
195 // (someone else organizes it), organizerEmail, sequence, attendees:
196 // [{email, name, partstat, role}]}. newRecipientField: a chip model owned
197 // by `owner` (the event editor's attendees). sendMeetingChange: kind
198 // "update" (REQUEST with SEQUENCE+1 to `attendees`, CANCEL to removed
199 // ones; the stored event takes the new list), "silent" (the stored event
200 // takes the list, no mail), "removed" (the stored event takes the list,
201 // CANCEL to the removed ones only — the invitation composer follows) or
202 // "cancel" (CANCEL to every attendee; the caller deletes the event).
203 Q_INVOKABLE QVariantMap organizedEvent(const QString &uid, const QString &accountId, const QString &calendarPath) const;
204 Q_INVOKABLE QObject *newRecipientField(QObject *owner, const QString &label);
205 Q_INVOKABLE bool sendMeetingChange(const QString &uid, const QString &accountId, const QString &calendarPath,
206 const QString &kind, const QVariantList &attendees = {});
207 // S108.8.1/8.2 (D-22, QuickComposeBackendMeeting.cpp): free times of the
208 // next 14 days over the shown calendars, within working hours and days —
209 // [{start, end, day, time}] — and the block that offers up to five of them
210 // at the caret, in the message language with the time zone.
211 Q_INVOKABLE QVariantList freeSlots(int durationMinutes, int dayStartHour, int dayEndHour, const QVariantList &workdays,
212 const QStringList &hiddenCalendars) const;
213 Q_INVOKABLE bool insertSuggestedTimes(QObject *session, const QVariantList &chosen);
214 // S108.8.6 (D-25): the scheduling grid. ownBusy: the user's busy times of
215 // the next `days` days ([{start, end}], local time). requestAttendeeBusy:
216 // the attendees' (To and Cc) through the calendar account's free/busy —
217 // attendeeBusyReady(sessionId, [{email, name, known, busy}]) follows.
218 // nextFreeForAll: the first `minutes` long time after `after` within
219 // working hours and days that overlaps none of `busy`; {} when none in 14 days.
220 // daySpans: what one row draws — `busy` within the working hours of
221 // `day`'s date as [{from, to}] fractions of them (FreeSlots::daySpans).
222 Q_INVOKABLE QVariantList ownBusy(const QStringList &hiddenCalendars, int days = 14) const;
223 Q_INVOKABLE QVariantList daySpans(const QVariantList &busy, const QDateTime &day, int dayStartHour,
224 int dayEndHour) const;
225 // §4.11: the same day as words for a screen reader — "busy 13:00 to
226 // 14:00, free otherwise", "free all day" or "busy all day".
227 Q_INVOKABLE QString busyDescription(const QVariantList &busy, const QDateTime &day, int dayStartHour,
228 int dayEndHour) const;
229 Q_INVOKABLE void requestAttendeeBusy(QObject *session, int days = 14);
230 Q_INVOKABLE QVariantMap nextFreeForAll(const QVariantList &busy, const QDateTime &after, int minutes,
231 int dayStartHour, int dayEndHour, const QVariantList &workdays) const;
232 // D-23: the event of an invitation that just left (QuickComposeBackendMeeting.cpp).
233 // `serverSchedules`: the server delivers (D-24) — the event is written
234 // without SCHEDULE-AGENT=CLIENT, so the server sends the invitations.
235 bool writeOrganizedEvent(const OutboxEntry &entry, const QByteArray &sentBytes, bool serverSchedules = false);
236 // S108.8.4 (D-24): who delivers a meeting of `organizer` in a calendar of
237 // `calendarAccount` — "server", "client" or "fallback" (Imip::deliveryMode
238 // with the *Invitations and replies* setting). invitationDelivery adds the
239 // meeting panel's note: {mode, note}.
240 QString invitationDeliveryMode(const QString &calendarAccount, const QString &organizer) const;
241 Q_INVOKABLE QVariantMap invitationDelivery(const QString &calendarAccount, const QString &organizer) const;
242 // *Undo* of a send (0 = the newest message in its undo window).
243 Q_INVOKABLE bool undoSend(qint64 outboxId = 0);
244 Q_INVOKABLE QVariantList outboxEntries() const;
245 // The Outbox view's reading pane: to, subject, text, files, state.
246 Q_INVOKABLE QVariantMap outboxPreview(qint64 id) const;
247 Q_INVOKABLE bool outboxSendNow(qint64 id);
248 Q_INVOKABLE bool outboxReschedule(qint64 id, const QDateTime &when);
249 Q_INVOKABLE bool outboxResendToRefused(qint64 id);
250 // Back to an editable draft (Outbox *Edit*, a refused message).
251 Q_INVOKABLE QObject *outboxEdit(qint64 id);
252 Q_INVOKABLE bool outboxDelete(qint64 id);
253 // Quit: messages in their undo window leave now (D-20).
254 void flushOutboxOnQuit();
255
256 signals:
257 void pendingSendChanged();
258 void undoDelayChanged();
259 void markdownTypingChanged();
260 void quickReplyChanged();
261 void identitiesChanged();
262 void templatesChanged();
263 void outboxChanged();
264 // A sent session that came back from the undo window or the outbox.
265 void sessionReturned(QObject *session);
266 void composeWindowedChanged();
267 void replyDraftReady(int requestToken, const QVariantMap &draft);
268 void attendeeBusyReady(const QString &sessionId, const QVariantList &rows);
269
270 private:
271 // connectServerDrafts() step: completions and failures of the sessions'
272 // journaled Drafts appends.
273 void connectDraftAppendResults();
274 MailSessionCore &m_session;
275 WorkspaceState &m_workspace;
276 const QString &m_profileDirectory; // owned by QuickMailBackend
277 QThreadPool &m_threadPool; // QuickMailBackend's shared pool
278 const QString &m_draftsFolder; // QuickMailBackend (folder list)
279 const QString &m_sentFolder; // QuickMailBackend (folder list)
280 const QHash<qint64, QString> &m_folderPathById; // QuickMailBackend identity cache
281 const QHash<qint64, quint32> &m_folderEpochById;
282 Deps m_deps;
283 // Per-session server-draft revisions: the newest journaled append token
284 // and its completed UIDPLUS UID (0 = none yet). A newer revision's
285 // completion removes the previous one; sends and closes clean up.
286 QHash<QString, qint64> m_sessionDraftTokens;
287 QHash<QString, qint64> m_sessionDraftUids;
288 QHash<qint64, quint32> m_draftUidValidity; // APPENDUID of Drafts copies
289 // A session's previous server copy goes; its mailbox identity may come
290 // from the draft file (after a restart) instead of this run's APPENDUID.
291 void retireSessionServerCopy(qint64 uid, quint32 uidValidity);
292 // S106-259: appends a newer revision (or a closed composer) superseded
293 // while they were still in flight; their revision is deleted on arrival.
294 QSet<qint64> m_supersededDraftTokens;
295
1/3
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 5 taken 26 times.
✗ Branch 3 → 4 not taken.
26 void retireDraftToken(qint64 token) { if (token > 0) m_supersededDraftTokens.insert(token); }
296 ServerDraftCleanup m_draftCleanup; // Sprint 102 S102.5.1: exact UID EXPUNGE
297 // STORE \Deleted + UID EXPUNGE of one Drafts revision (ServerDraftCleanup).
298 void deleteServerDraftRevision(qint64 draftUid);
299 bool m_composeWindowed = false;
300 ComposeReplyPrefill m_prefill; // S108.0.2: reply/forward prefill unit
301 // Sprint 108 sessions and the outbox (QuickComposeBackendSessions.cpp).
302 void initComposeSessions();
303 ComposeSession::Deps sessionDeps();
304 MailOutboxSender::Deps outboxDeps();
305 void reconcileIdentities();
306 void migrateOldDrafts();
307 QList<FreeSlots::Interval> ownBusyIntervals(const QDate &from, int days, const QStringList &hiddenCalendars) const;
308 QStringList ownAddresses() const;
309 bool sendMeetingMail(Imip::Meeting meeting, const QString &method, const QList<Imip::Attendee> &to);
310 IdentityStore m_identityStore;
311 TemplateStore m_templateStore;
312 AddressPickerModel m_addressPicker;
313 ComposePreview m_preview;
314 SpellService m_spell;
315 SpellLanguageMemory m_spellMemory; // D-12: <profile>/spell-languages.json
316 QHash<qint64, QString> m_preparedDoneTexts; // outbox id → status when it left
317 ComposeSessionStore m_sessionStore;
318 std::unique_ptr<MailOutboxStore> m_outboxStore;
319 std::unique_ptr<MailOutboxSender> m_outbox;
320 QVariantMap m_pendingSend;
321 int m_undoDelaySeconds = 10;
322 bool m_markdownTyping = true;
323 bool m_quickReply = true;
324 int m_identitiesRevision = 0;
325 void identitiesEdited();
326 QHash<QString, qint64> m_sizeLimits; // account id → SMTP SIZE
327 };
328