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% 56 / 0 / 56
Functions: 100.0% 46 / 0 / 46
Branches: 83.3% 10 / 0 / 12

presentation/ComposeSession.h
Line Branch Exec Source
1 #pragma once
2
3 #include "ComposeAttachmentModel.h"
4 #include "ComposeHighlighter.h"
5 #include "ComposeMessage.h"
6 #include "ComposePasteJob.h"
7 #include "RecipientChipModel.h"
8 #include "RichTextController.h"
9 #include "data/IdentityStore.h"
10
11 #include <QDateTime>
12 #include <QJsonObject>
13 #include <QObject>
14 #include <QPointer>
15 #include <QSet>
16 #include <QThreadPool>
17 #include <QTimer>
18 #include <QVariantList>
19 #include <functional>
20 #include <limits>
21 #include <memory>
22
23
24
25 // Sprint 108 (§5.1): one draft — a new mail, a reply, a forward or a resumed
26 // draft — wherever it is shown (a tab, inline above its mail, a window).
27 // Owns its recipients (chips), its editor engine, its attachments and its
28 // draft directory; saves itself (3 s after the last change locally, the
29 // server copy after 2 minutes idle or on close, D-21) and hands a finished
30 // message to the outbox on *Send* (D-20). QML reaches it through
31 // ComposeSessionStore (`backend.compose.sessions`).
32 class ComposeSession : public QObject {
33 1232 Q_OBJECT
34 Q_PROPERTY(QString sessionId READ sessionId CONSTANT)
35 Q_PROPERTY(QString kind READ kind CONSTANT) // new, reply, replyAll, forward, resumed
36 Q_PROPERTY(QString place READ place NOTIFY placeChanged) // tab, inline, window
37 Q_PROPERTY(QString title READ title NOTIFY subjectChanged)
38 Q_PROPERTY(QString subject READ subject WRITE setSubject NOTIFY subjectChanged)
39 Q_PROPERTY(bool formatted READ formatted NOTIFY formattedChanged)
40 Q_PROPERTY(QString identityId READ identityId NOTIFY identityChanged)
41 Q_PROPERTY(QVariantMap identity READ identityMap NOTIFY identityChanged)
42 Q_PROPERTY(QVariantList identityChoices READ identityChoices NOTIFY identityChoicesChanged)
43 Q_PROPERTY(QString signatureId READ signatureId NOTIFY signatureChanged)
44 Q_PROPERTY(QVariantList signatureChoices READ signatureChoices NOTIFY identityChanged)
45 Q_PROPERTY(QObject *to READ toModel CONSTANT)
46 Q_PROPERTY(QObject *cc READ ccModel CONSTANT)
47 Q_PROPERTY(QObject *bcc READ bccModel CONSTANT)
48 Q_PROPERTY(QObject *attachments READ attachmentModel CONSTANT)
49 Q_PROPERTY(QObject *editor READ editorObject CONSTANT)
50 Q_PROPERTY(bool dirty READ dirty NOTIFY saveStateChanged)
51 Q_PROPERTY(QString saveState READ saveState NOTIFY saveStateChanged)
52 Q_PROPERTY(bool saveFailed READ saveFailed NOTIFY saveStateChanged)
53 Q_PROPERTY(QVariantList checks READ checks NOTIFY checksChanged)
54 Q_PROPERTY(bool sending READ sending NOTIFY sendingChanged)
55 // A large HTML paste or drop is going in (ComposePasteJob): the text is read-only meanwhile.
56 Q_PROPERTY(bool pasting READ pasting NOTIFY pastingChanged)
57 Q_PROPERTY(int priority READ priority WRITE setPriority NOTIFY optionsChanged)
58 Q_PROPERTY(bool readReceipt READ readReceipt WRITE setReadReceipt NOTIFY optionsChanged)
59 Q_PROPERTY(bool hasQuote READ hasQuote NOTIFY quoteChanged)
60 Q_PROPERTY(bool quoteInEditor READ quoteInEditor NOTIFY quoteChanged)
61 Q_PROPERTY(QString quoteAttribution READ quoteAttribution NOTIFY quoteChanged)
62 Q_PROPERTY(QVariantMap mailReference READ mailReference NOTIFY placeChanged)
63 Q_PROPERTY(QString notice READ notice NOTIFY noticeChanged)
64 Q_PROPERTY(QString language READ language NOTIFY languageChanged)
65 // D-26: a reply started from the quick reply field — the view shows the
66 // text first, where the field was (runtime only, not part of the draft).
67 Q_PROPERTY(bool quickStart READ quickStart WRITE setQuickStart NOTIFY placeChanged)
68 // D-23: the meeting this message invites to — {active, start (QDateTime),
69 // durationMinutes, location, calendarAccount, calendarPath, uid, and for a
70 // stored event's invitation rrule and sequence}; empty for a plain mail.
71 Q_PROPERTY(QVariantMap meeting READ meeting NOTIFY meetingChanged)
72 // Phase 4 (D-11): spelling — available when Sonnet has a dictionary;
73 // spellLanguage is the checked language ("" = from the message language,
74 // "off" = no checking for this message).
75 Q_PROPERTY(bool spellAvailable READ spellAvailable CONSTANT)
76 Q_PROPERTY(QString spellLanguage READ spellLanguage WRITE setSpellLanguage NOTIFY languageChanged)
77 Q_PROPERTY(QString checkedLanguage READ checkedLanguage NOTIFY languageChanged)
78 // D-4 (CJ18): a format key in a plain-text mail changes nothing and shows
79 // the quiet *Switch* line, once per mail; switching to Formatted then
80 // applies the key to the text it was pressed on.
81 Q_PROPERTY(bool formatHint READ formatHint NOTIFY formatHintChanged)
82
83 public:
84 struct Deps {
85 QString profileDirectory;
86 std::function<QList<Identity>()> identities;
87 std::function<bool(const QString &address)> knownAddress;
88 std::function<qint64(const QString &accountId)> serverSizeLimit;
89 std::function<QStringList()> ownDomains;
90 std::function<QStringList()> attachmentWords;
91 std::function<QString()> uiLanguage;
92 // Hands a finished message to the outbox; false when it could not be queued.
93 std::function<bool(ComposeSession *, const ComposeMessage &, const QDateTime &due, bool scheduled)> enqueue;
94 // Queues the server copy of the draft (journaled APPEND to Drafts).
95 std::function<void(ComposeSession *, const ComposeMessage &)> saveServerDraft;
96 int localSaveDelayMs = 3000;
97 int serverSaveIdleMs = 120000;
98 // Settings → Writing *Undo send* delay (0–30 s; D-20 default 10).
99 4 std::function<int()> undoDelaySeconds = [] { return 10; };
100 // D-7: Markdown-style typing in formatted mode (Settings → Writing).
101 std::function<bool()> markdownTyping;
102 // Phase 4: Sonnet's spellers (nullptr or unavailable: no spelling).
103 class SpellService *spell = nullptr;
104 // D-12: the spelling language last used with an address, and the
105 // remembering of it when a message leaves (SpellLanguageMemory).
106 std::function<QString(const QString &address)> rememberedLanguage;
107 std::function<void(const QString &address, const QString &language)> rememberLanguage;
108 };
109 struct Quote {
110 QString attribution; // in the message language
111 QString text; // the original's text (unquoted)
112 QString html; // the original's HTML body fragment (MailHtmlWriter output), "" = plain
113 };
114
115 ComposeSession(const QString &sessionId, const QString &kind, Deps deps, QObject *parent = nullptr);
116 ~ComposeSession() override;
117
118 3969 QString sessionId() const { return m_id; }
119 619 QString kind() const { return m_kind; }
120 1964 QString place() const { return m_place; }
121 void setPlace(const QString &place, const QVariantMap &mailReference = {});
122 QString title() const;
123 339 QString subject() const { return m_subject; }
124 void setSubject(const QString &subject);
125 484 bool formatted() const { return m_formatted; }
126 55 QString identityId() const { return m_identityId; }
127 QVariantMap identityMap() const;
128 QVariantList identityChoices() const;
129 226 QString signatureId() const { return m_signatureId; }
130 QVariantList signatureChoices() const;
131 2089 QObject *toModel() { return &m_to; }
132 478 QObject *ccModel() { return &m_cc; }
133 439 QObject *bccModel() { return &m_bcc; }
134 144 RecipientChipModel &to() { return m_to; }
135 37 RecipientChipModel &cc() { return m_cc; }
136 14 RecipientChipModel &bcc() { return m_bcc; }
137 204 QObject *attachmentModel() { return m_attachments.get(); }
138 289 ComposeAttachmentModel &attachments() { return *m_attachments; }
139 463 QObject *editorObject() { return &m_editor; }
140 186 RichTextController &editor() { return m_editor; }
141 399 QTextDocument *document() const { return m_document; }
142 // The text as plain text, also while no view shows the session.
143 QString plainText() const;
144 162 bool dirty() const { return m_revision != m_savedRevision; }
145 929 QString saveState() const { return m_saveState; }
146 1003 bool saveFailed() const { return m_saveFailed; }
147 1063 QVariantList checks() const { return m_checks; }
148 2025 bool sending() const { return m_sending; }
149
4/4
✓ Branch 2 → 3 taken 21 times.
✓ Branch 2 → 6 taken 205 times.
✓ Branch 4 → 5 taken 14 times.
✓ Branch 4 → 6 taken 7 times.
226 bool pasting() const { return m_pasteJob && m_pasteJob->running(); }
150 2 int priority() const { return m_priority; }
151 void setPriority(int priority);
152 1 bool readReceipt() const { return m_readReceipt; }
153 void setReadReceipt(bool on);
154
3/4
✓ Branch 3 → 4 taken 306 times.
✓ Branch 3 → 6 taken 47 times.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 306 times.
353 bool hasQuote() const { return !m_quote.text.isEmpty() || !m_quote.html.isEmpty(); }
155 205 bool quoteInEditor() const { return m_quoteInEditor; }
156 204 QString quoteAttribution() const { return m_quote.attribution; }
157 454 QVariantMap mailReference() const { return m_mailReference; }
158 214 QString notice() const { return m_notice; }
159 472 QString language() const { return m_language; }
160 // The UID of this draft's newest copy in the Drafts folder (0: none yet),
161 // kept with the draft so a restart still replaces it instead of adding one.
162 12 qint64 serverDraftUid() const { return m_serverDraftUid; }
163 7 quint32 serverDraftUidValidity() const { return m_serverDraftUidValidity; }
164 4 void setServerDraftUid(qint64 uid, quint32 uidValidity) {
165 4 m_serverDraftUid = uid;
166 4 m_serverDraftUidValidity = uidValidity;
167 4 m_localTimer.start(); // written with the draft, not a change of it
168 4 }
169
170 bool spellAvailable() const;
171 1 QString spellLanguage() const { return m_spellLanguage; }
172 void setSpellLanguage(const QString &language);
173 // The language actually checked ("" when spelling is off or unavailable).
174 QString checkedLanguage() const;
175 // The misspelled word at a document position: {start, length, word,
176 // suggestions}; empty when there is none.
177 Q_INVOKABLE QVariantMap misspellingAt(int position) const;
178 // The next misspelled word after `position` (wrapping once): {start, length}.
179 Q_INVOKABLE QVariantMap nextMisspelling(int position) const;
180 Q_INVOKABLE void replaceRange(int start, int length, const QString &text);
181 Q_INVOKABLE void ignoreWord(const QString &word); // this message only
182 Q_INVOKABLE void addToDictionary(const QString &word); // the personal dictionary
183 // The editor reports the caret while typing: its word is judged after a
184 // 400 ms pause (D-12).
185 Q_INVOKABLE void caretMoved(int position, bool typing);
186 910 QVariantMap meeting() const { return m_meeting; }
187
2/2
✓ Branch 6 → 7 taken 160 times.
✓ Branch 7 → 8 taken 160 times.
320 bool isInvitation() const { return m_meeting.value(QStringLiteral("active")).toBool(); }
188 // Turns the message into an invitation (D-23) or updates it; the uid is
189 // kept across edits so the event and every later update share it.
190 Q_INVOKABLE void setMeeting(const QVariantMap &meeting);
191 Q_INVOKABLE void clearMeeting();
192 // The meeting as an iMIP REQUEST of this draft (organizer = the identity,
193 // To = required, Cc = optional attendees, the new text as description).
194 QByteArray meetingRequest() const;
195 65 bool quickStart() const { return m_quickStart; }
196 2 void setQuickStart(bool on) {
197
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 2 times.
2 if (m_quickStart == on) return;
198 2 m_quickStart = on;
199 2 emit placeChanged();
200 }
201 // D-26: the characters typed into the quick reply field go to the start
202 // of the text (now, or when the editor attaches); takeStartCaret() then
203 // answers the caret after them once (-1: nothing pending).
204 Q_INVOKABLE void startWithText(const QString &text);
205 2 Q_INVOKABLE int takeStartCaret() { return std::exchange(m_startCaret, -1); }
206 // The editor's document: QML passes TextEdit.textDocument once.
207 // Identities or signatures were edited in the settings.
208 9 void identitiesChanged() {
209 9 emit identityChoicesChanged();
210 9 emit identityChanged();
211 9 }
212 Q_INVOKABLE void attachEditor(QObject *quickTextDocument);
213 // The view is going away: the session keeps the text (see keepTextOf).
214 Q_INVOKABLE void detachEditor(QObject *quickTextDocument);
215 // Tests and windows without QML: a plain document owned by the session.
216 void attachDocument(QTextDocument *document);
217 // The display layer (headings, code, dark-theme contrast, spelling):
218 // the view passes the token palette (text, secondary, surface,
219 // codeBackground, spellError, dark) and the base font size.
220 Q_INVOKABLE void setDisplayPalette(const QVariantMap &palette, qreal basePointSize);
221 1 ComposeHighlighter *highlighter() const { return m_highlighter; }
222 // Paste from the clipboard (D-9, S108.3.3): files are attached, an image
223 // is placed (formatted) or attached (plain), HTML goes through
224 // ComposeHtmlImport, text is inserted as text. Returns the caret position
225 // after the inserted text, or -1 when nothing went into the text.
226 Q_INVOKABLE int paste(bool plainTextOnly = false);
227 // Dropped content (DropArea): URLs, HTML or text at the caret.
228 Q_INVOKABLE int dropContent(const QList<QUrl> &urls, const QString &html, const QString &text, bool placeImages);
229 // Places an attached image in the text at the caret (formatted mode).
230 Q_INVOKABLE void placeImage(const QString &attachmentId);
231
232 // Prefill of a new composer: to, cc, bcc, subject, body (plain), html
233 // (formatted), inReplyTo, references, identityId, formatted, quote.
234 void prefill(const QVariantMap &draft, const Quote &quote = {});
235 Q_INVOKABLE void setIdentity(const QString &identityId);
236 Q_INVOKABLE void setSignature(const QString &signatureId, bool replaceEdited = false);
237 Q_INVOKABLE void cycleSignature();
238 // D-4: Formatted ↔ Plain text. Going plain with losses needs `confirmed`.
239 Q_INVOKABLE QStringList formatLosses() const;
240 Q_INVOKABLE void setFormatted(bool formatted, bool confirmed = false);
241 // The format keys of D-28 by name: bold, italic, underline, strike, code,
242 // bullets, numbered, quote (applied), link (formatted mode: the view's).
243 Q_INVOKABLE void format(const QString &action);
244 214 bool formatHint() const { return m_formatHint; }
245 Q_INVOKABLE void dismissFormatHint();
246 // §4.3a: the full quote into the editor (*Edit quote*); the hidden quote ends.
247 Q_INVOKABLE void includeQuote();
248
249 // *Send*: runs the checks (D-19). Returns "checks" when the check bar
250 // shows findings, "queued" when the message is in the outbox, "error".
251 Q_INVOKABLE QString send(bool anyway = false);
252 Q_INVOKABLE QString sendLater(const QDateTime &when, bool anyway = false);
253 // *Undo* took the message back from the outbox: editable again.
254 void reopenAfterUndo();
255 Q_INVOKABLE void dismissCheck(int index);
256 Q_INVOKABLE void clearChecks();
257 // The *Move to Bcc* fix (ManyWithoutBcc): every To/Cc chip moves to Bcc;
258 // the own identity stays in To so the mail has a visible recipient.
259 Q_INVOKABLE void moveRecipientsToBcc();
260 // Explicit save (Ctrl+S): local now, server copy queued.
261 Q_INVOKABLE bool save();
262 // Close keeping the draft (local save now, server copy queued when dirty).
263 Q_INVOKABLE bool closeKeepingDraft();
264 // Discard: the draft directory goes (the store keeps an undo copy).
265 Q_INVOKABLE void discard();
266
267 // The message as it would leave now (Preview, outbox, server draft).
268 ComposeMessage message(ComposeMessage::Form form) const;
269 QJsonObject snapshot() const;
270 bool restore(const QJsonObject &draft);
271 // The new text only (no quote, no signature) — the attachment-word check.
272 QString newText(int limit = std::numeric_limits<int>::max()) const; // stops after about `limit` characters
273 // S108.9.5 templates: {subject, html ("" in plain mode), text} of the new
274 // text — without signature, quote and images (an image keeps its name).
275 Q_INVOKABLE QVariantMap templateContent() const;
276 // Inserts a template at the caret (its HTML in formatted mode, else its
277 // text); an empty subject takes the template's.
278 Q_INVOKABLE bool insertTemplate(const QVariantMap &content);
279 void markClean() { m_savedRevision = m_revision; emit saveStateChanged(); }
280
281 signals:
282 void placeChanged();
283 void subjectChanged();
284 void formattedChanged();
285 void identityChanged();
286 void identityChoicesChanged();
287 void signatureChanged();
288 void saveStateChanged();
289 void checksChanged();
290 void sendingChanged();
291 void pastingChanged();
292 // A paste in parts is in: the view puts the caret after it.
293 void caretRequested(int position);
294 void optionsChanged();
295 void quoteChanged();
296 void noticeChanged();
297 void languageChanged();
298 void meetingChanged();
299 void formatHintChanged();
300 // The composer leaves its place (sent, discarded or closed).
301 void finished(const QString &reason); // "sent", "scheduled", "discarded", "closed"
302 // A signature swap would replace an edited one: the view asks (D-16).
303 void signatureReplaceRequested(const QString &signatureId);
304
305 private:
306 QString m_id;
307 QString m_kind;
308 QString m_place = QStringLiteral("tab");
309 Deps m_deps;
310 RecipientChipModel m_to;
311 RecipientChipModel m_cc;
312 RecipientChipModel m_bcc;
313 std::unique_ptr<ComposeAttachmentModel> m_attachments;
314 RichTextController m_editor;
315 QPointer<QTextDocument> m_document;
316 std::unique_ptr<QTextDocument> m_ownDocument;
317 QString m_subject;
318 bool m_formatted = false;
319 QString m_identityId;
320 QString m_signatureId;
321 QString m_placedSignatureText;
322 QString m_language;
323 int m_priority = 3;
324 bool m_readReceipt = false;
325 QString m_inReplyTo;
326 QStringList m_references;
327 QString m_originalSentTo;
328 QVariantMap m_mailReference;
329 Quote m_quote;
330 bool m_quoteInEditor = false;
331 bool m_quickStart = false;
332 QVariantMap m_meeting;
333 bool m_formatHint = false;
334 bool m_formatHintShown = false; // once per mail
335 QString m_pendingFormat; // the key pressed in plain text
336 int m_pendingFormatStart = 0;
337 int m_pendingFormatEnd = 0;
338 void showFormatHint();
339 bool applyFormat(const QString &action);
340 QString m_spellLanguage;
341 QString m_guessedLanguage; // D-12: from the text (its first two words on)
342 QString m_guessedFrom; // the text (first 1 000 characters) the guess was made from
343 QString firstRecipient() const;
344 QSet<QString> m_ignoredWords; // lower case
345 QTimer m_typingPause;
346 void applySpellCheck();
347 QString m_startText; // quick reply characters waiting for the editor
348 int m_startCaret = -1;
349 bool m_closed = false; // closed keeping the draft: not reopened after a restart
350 QJsonObject m_pendingRestore; // applied when the editor attaches
351 quint64 m_revision = 0;
352 quint64 m_savedRevision = 0;
353 quint64 m_serverRevision = 0;
354 QString m_saveState;
355 bool m_saveFailed = false;
356 QVariantList m_checks;
357 bool m_anyway = false;
358 bool m_sending = false;
359 QString m_notice;
360 QPointer<ComposeHighlighter> m_highlighter;
361 int insertHtml(const QByteArray &html);
362 // Paste and drop: HTML over ComposePasteJob::AsyncBytes goes in parts
363 // (returns -1 while pasting()); `text` is the fallback when the import
364 // refuses it. finishPaste inserts what is still waiting at once.
365 int insertPastedHtml(const QByteArray &html, const QString &text);
366 void adoptImported(const ComposeHtmlImport::Result &imported);
367 void finishPaste();
368 ComposePasteJob *m_pasteJob = nullptr;
369 QString m_pasteText;
370 int insertImageData(const QString &name, const QByteArray &data, const QString &mimeType);
371 QTimer m_localTimer;
372 QTimer m_serverTimer;
373 QThreadPool m_savePool;
374
375 std::optional<Identity> currentIdentity() const;
376 QString submit(bool anyway, const QDateTime &due, bool scheduled);
377 void changed();
378 void fieldsMayHaveChanged();
379 QString m_fieldsKey; // recipients and files as last seen (fieldsMayHaveChanged)
380 qint64 m_serverDraftUid = 0;
381 quint32 m_serverDraftUidValidity = 0;
382 void saveLocalNow();
383 bool saveLocalSync();
384 void queueServerDraft();
385 void setNotice(const QString &notice);
386 QString checkSentence(const QVariantMap &finding) const;
387 void placeSignature(const QString &signatureId);
388 void applyPendingRestore();
389 void keepTextOf(QTextDocument *document);
390 QJsonObject snapshotText() const;
391 };
392