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: 90.3% 241 / 0 / 267
Functions: 92.3% 12 / 0 / 13
Branches: 73.5% 294 / 0 / 400

service/ImapLineReader.cpp
Line Branch Exec Source
1 #include "ImapLineReader.h"
2
3 #include "ImapCommandTable.h"
4 #include "ImapResponseParser.h"
5 #include "ImapService.h"
6
7 #include <QLoggingCategory>
8 #include <QRegularExpression>
9 #include <QSslSocket>
10
11 #include <limits>
12
13 Q_DECLARE_LOGGING_CATEGORY(lcImap)
14
15 namespace {
16 // F-02: only an untagged FETCH response carries a quoted BODY[] value.
17 3 bool isUntaggedFetchLine(const QString &line) {
18 static QRegularExpression rx(
19 2 QStringLiteral(R"(^\*\s+\d+\s+FETCH\s)"),
20
4/7
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 15 taken 2 times.
✓ Branch 4 → 5 taken 1 time.
✗ Branch 4 → 15 not taken.
✓ Branch 9 → 10 taken 1 time.
✗ Branch 29 → 30 not taken.
✗ Branch 29 → 31 not taken.
5 QRegularExpression::CaseInsensitiveOption);
21
2/2
✓ Branch 16 → 17 taken 3 times.
✓ Branch 17 → 18 taken 3 times.
3 return rx.match(line).hasMatch();
22 }
23 } // namespace
24
25 1401 ImapLineReader::ImapLineReader(QSslSocket *socket, ImapCommandTable &commands,
26 1401 BodyRequest body, Deps deps)
27 1401 : m_socket(socket), m_commands(commands), m_body(body), m_deps(std::move(deps)) {}
28
29 41 void ImapLineReader::completeBodyRequest(BodyOutcome outcome, QByteArray bytes,
30 const QString &error, qint64 maxBytes) {
31
1/1
✓ Branch 4 → 5 taken 41 times.
41 m_deps.completeBodyRequest(outcome, std::move(bytes), error, maxBytes);
32 41 }
33
34 // T-87.C2: body literals above the threshold drain into a private temp file
35 // instead of RAM. Returns false when the spool cannot be created (caller
36 // falls back to the in-memory literal).
37 3 bool ImapLineReader::startBodySpool() {
38
1/1
✓ Branch 2 → 3 taken 3 times.
3 auto spool = std::make_unique<QTemporaryFile>();
39
1/1
✓ Branch 4 → 5 taken 3 times.
3 spool->setAutoRemove(true);
40
2/3
✓ Branch 6 → 7 taken 3 times.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 19 taken 3 times.
3 if (!spool->open()) {
41 ✗ qCWarning(lcImap) << "Body spool unavailable — falling back to memory";
42 ✗ return false;
43 }
44 3 m_bodySpool = std::move(spool);
45 3 m_bodySpoolWriteFailed = false;
46 3 m_bodySpoolExpectedBytes = m_literalBytesRemaining;
47
1/1
✓ Branch 21 → 22 taken 3 times.
3 m_literalData.clear();
48
1/1
✓ Branch 23 → 24 taken 3 times.
3 m_literalData.reserve(int(qMin<qint64>(m_literalBytesRemaining,
49 3 qint64(64 * 1024))));
50 3 return true;
51 3 }
52
53 ✗ void ImapLineReader::discardBodySpool() {
54 ✗ if (m_bodySpool) m_bodySpool.reset();
55 ✗ }
56
57 100052 void ImapLineReader::completeLiteral() {
58
2/3
✓ Branch 2 → 3 taken 43 times.
✗ Branch 2 → 126 not taken.
✓ Branch 2 → 132 taken 100009 times.
100052 if (m_isBodyLiteral) {
59
2/2
✓ Branch 3 → 4 taken 4 times.
✓ Branch 3 → 18 taken 39 times.
43 if (m_discardingInvalidBody) {
60
5/5
✓ Branch 4 → 5 taken 4 times.
✓ Branch 8 → 9 taken 4 times.
✓ Branch 9 → 10 taken 4 times.
✓ Branch 16 → 6 taken 4 times.
✓ Branch 16 → 17 taken 4 times.
8 qCWarning(lcImap) << "Discarded BODY[] literal not matching active UID"
61
2/2
✓ Branch 10 → 11 taken 4 times.
✓ Branch 11 → 12 taken 4 times.
4 << m_body.fetchUid << "response UID"
62
1/1
✓ Branch 12 → 13 taken 4 times.
4 << m_bodyLiteralUid;
63 } else {
64 // Consume the request before emitting: direct signal handlers may queue
65 // another body fetch, which must not be overwritten after they return.
66 39 m_body.fetchUid = -1;
67
2/2
✓ Branch 18 → 19 taken 3 times.
✓ Branch 18 → 38 taken 36 times.
39 if (m_discardingOversizedBody) {
68
5/5
✓ Branch 19 → 20 taken 3 times.
✓ Branch 23 → 24 taken 3 times.
✓ Branch 24 → 25 taken 3 times.
✓ Branch 31 → 21 taken 3 times.
✓ Branch 31 → 32 taken 3 times.
6 qCWarning(lcImap) << "Discarded body above configured limit for UID"
69
2/2
✓ Branch 25 → 26 taken 3 times.
✓ Branch 26 → 27 taken 3 times.
3 << m_bodyLiteralUid << "limit"
70
1/1
✓ Branch 27 → 28 taken 3 times.
3 << m_body.fetchLimit;
71 3 const qint64 limit = m_body.fetchLimit;
72 3 m_body.fetchUid = m_bodyLiteralUid;
73
1/1
✓ Branch 34 → 35 taken 3 times.
3 completeBodyRequest(BodyOutcome::TooLarge, {}, {}, limit);
74 3 m_body.fetchUid = -1;
75
1/2
✗ Branch 37 → 123 not taken.
✓ Branch 37 → 129 taken 3 times.
3 m_deps.bodyFetchTooLarge(m_bodyLiteralUid, limit);
76 } else {
77
6/6
✓ Branch 38 → 39 taken 36 times.
✓ Branch 42 → 43 taken 36 times.
✓ Branch 43 → 44 taken 36 times.
✓ Branch 45 → 46 taken 36 times.
✓ Branch 51 → 40 taken 36 times.
✓ Branch 51 → 52 taken 36 times.
72 qCInfo(lcImap) << "Body literal complete:" << m_literalData.size()
78
2/2
✓ Branch 46 → 47 taken 36 times.
✓ Branch 47 → 48 taken 36 times.
36 << "bytes for UID" << m_bodyLiteralUid;
79 #ifdef MAILJD_E2E_TESTING
80
1/1
✓ Branch 55 → 56 taken 24 times.
24 m_deps.reportE2eTransportPhase(QStringLiteral("body-literal-complete"),
81 24 m_body.requestId);
82 #endif
83 36 m_body.fetchUid = m_bodyLiteralUid;
84
3/4
✗ Branch 53 → 54 not taken.
✓ Branch 53 → 115 taken 12 times.
✓ Branch 59 → 60 taken 3 times.
✓ Branch 59 → 121 taken 21 times.
36 if (m_bodySpool) {
85 // T-87.C2: the payload drained into the temp file during
86 // reception; it is read back once into one buffer, which the
87 // result then shares (QByteArray is implicitly shared — no deep
88 // copy). The temp file is removed with the spool object.
89 // S106-002: a short write (disk full, quota) fails the request
90 // instead of delivering a truncated body as Loaded.
91 // S106-014: every failure path falls through to the framing
92 // cleanup below.
93 3 const std::unique_ptr<QTemporaryFile> spool = std::move(m_bodySpool);
94
4/10
✗ Branch 56 → 57 not taken.
✗ Branch 56 → 64 not taken.
✗ Branch 58 → 59 not taken.
✗ Branch 59 → 60 not taken.
✗ Branch 59 → 64 not taken.
✓ Branch 62 → 63 taken 2 times.
✓ Branch 62 → 70 taken 1 time.
✓ Branch 64 → 65 taken 2 times.
✓ Branch 65 → 66 taken 2 times.
✗ Branch 65 → 70 not taken.
5 const bool written = !m_bodySpoolWriteFailed && spool->flush() &&
95
2/6
✗ Branch 61 → 62 not taken.
✗ Branch 62 → 63 not taken.
✗ Branch 62 → 64 not taken.
✓ Branch 67 → 68 taken 2 times.
✓ Branch 68 → 69 taken 2 times.
✗ Branch 68 → 70 not taken.
2 spool->size() == m_bodySpoolExpectedBytes;
96
6/13
✗ Branch 65 → 66 not taken.
✗ Branch 65 → 69 not taken.
✗ Branch 67 → 68 not taken.
✗ Branch 68 → 69 not taken.
✗ Branch 68 → 70 not taken.
✓ Branch 71 → 72 taken 2 times.
✓ Branch 71 → 75 taken 1 time.
✗ Branch 71 → 103 not taken.
✓ Branch 73 → 74 taken 2 times.
✗ Branch 74 → 75 not taken.
✓ Branch 74 → 76 taken 2 times.
✓ Branch 77 → 78 taken 1 time.
✓ Branch 77 → 109 taken 2 times.
3 if (!written || !spool->seek(0)) {
97
6/11
✗ Branch 72 → 73 not taken.
✗ Branch 76 → 77 not taken.
✗ Branch 77 → 78 not taken.
✓ Branch 78 → 79 taken 1 time.
✓ Branch 82 → 83 taken 1 time.
✓ Branch 83 → 84 taken 1 time.
✓ Branch 84 → 85 taken 1 time.
✗ Branch 93 → 74 not taken.
✗ Branch 93 → 94 not taken.
✓ Branch 99 → 80 taken 1 time.
✓ Branch 99 → 100 taken 1 time.
2 qCWarning(lcImap) << "Body spool incomplete for UID" << m_bodyLiteralUid
98
5/10
✗ Branch 79 → 80 not taken.
✗ Branch 81 → 82 not taken.
✗ Branch 82 → 83 not taken.
✗ Branch 83 → 84 not taken.
✗ Branch 84 → 85 not taken.
✓ Branch 85 → 86 taken 1 time.
✓ Branch 87 → 88 taken 1 time.
✓ Branch 88 → 89 taken 1 time.
✓ Branch 89 → 90 taken 1 time.
✓ Branch 90 → 91 taken 1 time.
1 << "- written" << spool->size() << "of" << m_bodySpoolExpectedBytes
99
3/6
✗ Branch 85 → 86 not taken.
✗ Branch 87 → 88 not taken.
✗ Branch 88 → 89 not taken.
✓ Branch 91 → 92 taken 1 time.
✓ Branch 93 → 94 taken 1 time.
✓ Branch 94 → 95 taken 1 time.
1 << "bytes:" << spool->errorString();
100
1/2
✗ Branch 98 → 99 not taken.
✓ Branch 104 → 105 taken 1 time.
1 completeBodyRequest(BodyOutcome::Failed, {},
101 2 QStringLiteral("Body spool write failed"));
102 } else {
103
1/2
✗ Branch 104 → 105 not taken.
✓ Branch 110 → 111 taken 2 times.
2 const QByteArray bytes = spool->readAll();
104
1/2
✗ Branch 105 → 106 not taken.
✓ Branch 111 → 112 taken 2 times.
2 m_deps.rawBodyReceived(m_bodyLiteralUid, bytes);
105
1/2
✗ Branch 108 → 109 not taken.
✓ Branch 114 → 115 taken 2 times.
2 completeBodyRequest(BodyOutcome::Loaded, bytes);
106 2 }
107 3 m_body.fetchUid = -1;
108 3 } else {
109 // Deliver to the settings-sync observers first (they read the bytes),
110 // then hand the sole owned buffer to the body request without a copy.
111
2/2
✓ Branch 115 → 116 taken 12 times.
✓ Branch 121 → 122 taken 21 times.
33 m_deps.rawBodyReceived(m_bodyLiteralUid, m_literalData);
112
2/2
✓ Branch 119 → 120 taken 12 times.
✓ Branch 125 → 126 taken 21 times.
33 completeBodyRequest(BodyOutcome::Loaded, std::move(m_literalData));
113 33 m_body.fetchUid = -1;
114 }
115 }
116 }
117
118
2/2
✓ Branch 123 → 124 taken 13 times.
✓ Branch 129 → 130 taken 30 times.
43 m_literalData.clear();
119 43 m_literalLine.clear();
120 43 m_isBodyLiteral = false;
121 43 m_bodyLiteralUid = -1;
122 43 m_discardingInvalidBody = false;
123 43 m_discardingOversizedBody = false;
124 // BODY FETCH has a closing parenthesis after the literal. It may arrive
125 // in a later socket chunk, so remember to skip that physical line.
126 43 m_skipFetchLiteralRemainder = true;
127 44 return;
128 }
129
130 // Non-body literals are folded into the logical parser line.
131
1/2
✗ Branch 126 → 127 not taken.
✓ Branch 132 → 133 taken 100009 times.
100009 QString literalStr = QString::fromUtf8(m_literalData);
132
2/4
✗ Branch 127 → 128 not taken.
✗ Branch 129 → 130 not taken.
✓ Branch 133 → 134 taken 100009 times.
✓ Branch 135 → 136 taken 100009 times.
100009 literalStr.replace('\\', "\\\\");
133
2/4
✗ Branch 131 → 132 not taken.
✗ Branch 133 → 134 not taken.
✓ Branch 137 → 138 taken 100009 times.
✓ Branch 139 → 140 taken 100009 times.
100009 literalStr.replace('"', "\\\"");
134 // SEC-2026-07-21-02: hard cap on the folded logical line. Without this, a
135 // malicious server can accumulate endless non-body literals (BODY[HEADER],
136 // BODY[1], ENVELOPE, …) into m_literalLine during pre-auth / IDLE / NOTIFY,
137 // growing RSS without bound. 8 MiB is far above any legitimate folded line.
138 100009 const qsizetype foldedAddition = literalStr.size() + 2; // +2 for the quotes
139
2/4
✗ Branch 137 → 138 not taken.
✗ Branch 137 → 145 not taken.
✓ Branch 143 → 144 taken 1 time.
✓ Branch 143 → 151 taken 100008 times.
100009 if (m_literalLine.size() + foldedAddition > ImapService::MAX_NON_BODY_LITERAL_LINE_SIZE) {
140
1/2
✗ Branch 141 → 142 not taken.
✓ Branch 147 → 148 taken 1 time.
2 m_deps.failConnection(QStringLiteral(
141 "IMAP folded literal line exceeded connection limit"));
142 1 return;
143 }
144
3/6
✗ Branch 147 → 148 not taken.
✗ Branch 148 → 149 not taken.
✗ Branch 149 → 150 not taken.
✓ Branch 153 → 154 taken 100008 times.
✓ Branch 154 → 155 taken 100008 times.
✓ Branch 155 → 156 taken 100008 times.
100008 m_literalLine.append('"' + literalStr + '"');
145
1/2
✗ Branch 152 → 153 not taken.
✓ Branch 158 → 159 taken 100008 times.
100008 m_literalData.clear();
146 100009 }
147
148
2/2
✓ Branch 2 → 3 taken 2964 times.
✓ Branch 3 → 4 taken 2964 times.
2964 void ImapLineReader::onReadyRead() { feed(m_socket->readAll()); }
149
150 2964 void ImapLineReader::feed(const QByteArray &incoming) {
151 2964 m_receivedBytes += quint64(incoming.size());
152
1/2
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 2964 times.
2964 if (!m_commands.accountCommandResponseBytes(incoming.size()))
153 ✗ return;
154 2964 m_readBuffer.append(incoming);
155
2/2
✓ Branch 9 → 10 taken 2934 times.
✓ Branch 9 → 13 taken 30 times.
2964 if (!incoming.isEmpty()) {
156 2934 m_commands.refreshCommandTimeout();
157 2934 m_lastReceiveMs = m_deps.nowMs(); // S92.5.2: connection quiet time
158 }
159
160 // Physical lines are bounded independently from literals. Literal payloads
161 // are drained incrementally below and therefore never need to collect in
162 // this parser buffer.
163
1/2
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 22 taken 2964 times.
2964 if (m_readBuffer.size() > MAX_READ_BUFFER_SIZE) {
164 ✗ m_deps.failConnection(QStringLiteral("Server response too large"));
165 ✗ return;
166 }
167
168 while (true) {
169
2/2
✓ Branch 22 → 23 taken 43 times.
✓ Branch 22 → 29 taken 206334 times.
206377 if (m_skipFetchLiteralRemainder) {
170 43 const int remainderEnd = m_readBuffer.indexOf("\r\n");
171
1/2
✗ Branch 25 → 26 not taken.
✓ Branch 25 → 27 taken 43 times.
43 if (remainderEnd < 0)
172 2963 break;
173
1/1
✓ Branch 27 → 28 taken 43 times.
43 m_readBuffer.remove(0, remainderEnd + 2);
174 43 m_skipFetchLiteralRemainder = false;
175 102696 continue;
176 43 }
177
178 // Mode 1: stream the current literal out of the parser buffer. This
179 // avoids the former read-buffer + literal-data double materialization.
180
2/2
✓ Branch 29 → 30 taken 5169 times.
✓ Branch 29 → 50 taken 201165 times.
206334 if (m_literalBytesRemaining > 0) {
181
2/2
✓ Branch 31 → 32 taken 2563 times.
✓ Branch 31 → 33 taken 2606 times.
5169 if (m_readBuffer.isEmpty())
182 2563 break;
183 const qsizetype bytesToConsume = static_cast<qsizetype>(
184 2606 qMin<qint64>(m_literalBytesRemaining, m_readBuffer.size()));
185
4/4
✓ Branch 35 → 36 taken 2346 times.
✓ Branch 35 → 46 taken 260 times.
✓ Branch 36 → 37 taken 2342 times.
✓ Branch 36 → 46 taken 4 times.
2606 if (!m_discardingOversizedBody && !m_discardingInvalidBody) {
186
2/2
✓ Branch 38 → 39 taken 2309 times.
✓ Branch 38 → 44 taken 33 times.
2342 if (m_bodySpool) {
187
3/3
✓ Branch 41 → 42 taken 2309 times.
✓ Branch 42 → 43 taken 1 time.
✓ Branch 42 → 46 taken 2308 times.
2309 if (m_bodySpool->write(m_readBuffer.constData(), bytesToConsume) != bytesToConsume)
188 1 m_bodySpoolWriteFailed = true; // S106-002: reported at completion
189 } else
190
1/1
✓ Branch 45 → 46 taken 33 times.
33 m_literalData.append(m_readBuffer.constData(), bytesToConsume);
191 }
192
1/1
✓ Branch 46 → 47 taken 2606 times.
2606 m_readBuffer.remove(0, bytesToConsume);
193 2606 m_literalBytesRemaining -= bytesToConsume;
194
2/2
✓ Branch 47 → 48 taken 43 times.
✓ Branch 47 → 49 taken 2563 times.
2606 if (m_literalBytesRemaining == 0)
195
1/1
✓ Branch 48 → 49 taken 43 times.
43 completeLiteral();
196 2606 continue;
197 2606 }
198
199 // Mode 2: Read next \r\n-delimited line
200 201165 int idx = m_readBuffer.indexOf("\r\n");
201
2/2
✓ Branch 52 → 53 taken 400 times.
✓ Branch 52 → 54 taken 200765 times.
201165 if (idx < 0)
202 400 break;
203
204
1/1
✓ Branch 54 → 55 taken 200765 times.
200765 auto lineBytes = m_readBuffer.left(idx);
205
1/1
✓ Branch 55 → 56 taken 200765 times.
200765 m_readBuffer.remove(0, idx + 2);
206
207
2/2
✓ Branch 56 → 57 taken 200765 times.
✓ Branch 57 → 58 taken 200765 times.
200765 auto line = QString::fromUtf8(lineBytes).trimmed();
208
1/2
✗ Branch 60 → 61 not taken.
✓ Branch 60 → 62 taken 200765 times.
200765 if (line.isEmpty())
209 ✗ continue;
210
211 // Check if this line ends with a literal marker {N}
212
5/8
✓ Branch 62 → 63 taken 7 times.
✓ Branch 62 → 72 taken 200758 times.
✓ Branch 64 → 65 taken 7 times.
✗ Branch 64 → 72 not taken.
✓ Branch 66 → 67 taken 7 times.
✓ Branch 67 → 68 taken 7 times.
✗ Branch 158 → 159 not taken.
✗ Branch 158 → 160 not taken.
200765 static QRegularExpression literalRx(R"(\{(\d+)\}$)");
213
1/1
✓ Branch 73 → 74 taken 200765 times.
200765 auto match = literalRx.match(line);
214
3/3
✓ Branch 74 → 75 taken 200765 times.
✓ Branch 75 → 76 taken 100045 times.
✓ Branch 75 → 80 taken 100720 times.
200765 if (match.hasMatch()) {
215
3/3
✓ Branch 76 → 77 taken 100045 times.
✓ Branch 77 → 78 taken 1 time.
✓ Branch 77 → 79 taken 100044 times.
100045 if (!startLiteral(line, match))
216 1 return;
217 100044 continue;
218 }
219
220 // M3 + F-02: a quoted BODY[] response (no literal) of the active body
221 // request. The item must be the trailing fetch item of an untagged
222 // FETCH response — detected structurally, so a "BODY[]" inside an
223 // ENVELOPE subject or a mailbox named BODY[] stays a normal line.
224
5/5
✓ Branch 80 → 81 taken 100720 times.
✓ Branch 81 → 82 taken 3 times.
✓ Branch 81 → 85 taken 100717 times.
✓ Branch 86 → 87 taken 3 times.
✓ Branch 86 → 91 taken 100717 times.
100723 if (ImapResponseParser::endsWithBodyFetchItem(line) &&
225
2/3
✓ Branch 82 → 83 taken 3 times.
✓ Branch 83 → 84 taken 3 times.
✗ Branch 83 → 85 not taken.
3 isUntaggedFetchLine(line)) {
226
2/3
✓ Branch 87 → 88 taken 3 times.
✗ Branch 88 → 89 not taken.
✓ Branch 88 → 90 taken 3 times.
3 if (!handleQuotedBody(lineBytes))
227 ✗ return;
228 3 continue;
229 }
230
231 // If we were building a multi-literal line, finalize it
232
2/2
✓ Branch 92 → 93 taken 100001 times.
✓ Branch 92 → 106 taken 716 times.
100717 if (!m_literalLine.isEmpty()) {
233
1/2
✗ Branch 95 → 96 not taken.
✓ Branch 95 → 103 taken 100001 times.
100001 if (m_literalLine.size() + line.size() >
234 ImapService::MAX_NON_BODY_LITERAL_LINE_SIZE) {
235 ✗ m_deps.failConnection(QStringLiteral(
236 "IMAP folded literal line exceeded connection limit"));
237 ✗ return;
238 }
239
1/1
✓ Branch 103 → 104 taken 100001 times.
100001 m_literalLine.append(line);
240 100001 line = m_literalLine;
241 100001 m_literalLine.clear();
242 }
243
244
7/7
✓ Branch 106 → 107 taken 100717 times.
✓ Branch 110 → 111 taken 100717 times.
✓ Branch 111 → 112 taken 100717 times.
✓ Branch 112 → 113 taken 100717 times.
✓ Branch 113 → 114 taken 100717 times.
✓ Branch 123 → 108 taken 100717 times.
✓ Branch 123 → 124 taken 100717 times.
201434 qCDebug(lcImap) << "<<<" << line.left(200)
245
3/3
✓ Branch 115 → 116 taken 63 times.
✓ Branch 115 → 117 taken 100654 times.
✓ Branch 118 → 119 taken 100717 times.
100717 << (line.length() > 200 ? "..." : "");
246
1/1
✓ Branch 124 → 125 taken 100717 times.
100717 processLine(line);
247 604274 }
248 }
249
250 // M3: a quoted BODY[] response (no literal) of the active body request.
251 // Returns false when the connection was failed.
252 3 bool ImapLineReader::handleQuotedBody(const QByteArray &lineBytes) {
253 3 const qint64 parserLimit =
254 3 m_body.requestBytes > 0
255
1/2
✓ Branch 2 → 3 taken 3 times.
✗ Branch 2 → 4 not taken.
3 ? m_body.requestBytes
256 : static_cast<qint64>(MAX_READ_BUFFER_SIZE);
257 const auto body = ImapResponseParser::parseFetchBodyResponse(
258
1/1
✓ Branch 5 → 6 taken 3 times.
3 lineBytes, parserLimit);
259
1/2
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 15 taken 3 times.
3 if (!body) {
260 ✗ m_deps.failConnection(QStringLiteral("Malformed quoted IMAP BODY[] response"));
261 ✗ return false;
262 }
263
264
1/1
✓ Branch 18 → 19 taken 3 times.
6 const bool fetchPending = m_commands.hasCommandType(QStringLiteral("FETCH_BODY"));
265
4/6
✓ Branch 21 → 22 taken 3 times.
✗ Branch 21 → 25 not taken.
✓ Branch 22 → 23 taken 3 times.
✗ Branch 22 → 25 not taken.
✓ Branch 27 → 28 taken 1 time.
✓ Branch 27 → 43 taken 2 times.
6 if (!fetchPending || m_body.requiresSelect ||
266
2/2
✓ Branch 24 → 25 taken 1 time.
✓ Branch 24 → 26 taken 2 times.
3 body->first != m_body.fetchUid) {
267
6/6
✓ Branch 28 → 29 taken 1 time.
✓ Branch 32 → 33 taken 1 time.
✓ Branch 33 → 34 taken 1 time.
✓ Branch 35 → 36 taken 1 time.
✓ Branch 41 → 30 taken 1 time.
✓ Branch 41 → 42 taken 1 time.
2 qCWarning(lcImap) << "Discarding quoted BODY[] for UID" << body->first
268
2/2
✓ Branch 36 → 37 taken 1 time.
✓ Branch 37 → 38 taken 1 time.
1 << "expected" << m_body.fetchUid;
269 } else {
270 2 const qint64 resultUid = body->first;
271
3/4
✓ Branch 44 → 45 taken 2 times.
✗ Branch 44 → 49 not taken.
✓ Branch 50 → 51 taken 1 time.
✓ Branch 50 → 57 taken 1 time.
4 if (m_body.fetchLimit > 0 &&
272
2/2
✓ Branch 47 → 48 taken 1 time.
✓ Branch 47 → 49 taken 1 time.
2 body->second.size() > m_body.fetchLimit) {
273 1 const qint64 limit = m_body.fetchLimit;
274 1 m_body.fetchUid = resultUid;
275
1/1
✓ Branch 53 → 54 taken 1 time.
1 completeBodyRequest(BodyOutcome::TooLarge, {}, {}, limit);
276 1 m_body.fetchUid = -1;
277
1/1
✓ Branch 56 → 65 taken 1 time.
1 m_deps.bodyFetchTooLarge(resultUid, limit);
278 } else {
279 1 m_body.fetchUid = resultUid;
280
1/1
✓ Branch 60 → 61 taken 1 time.
1 completeBodyRequest(BodyOutcome::Loaded, body->second);
281 1 m_body.fetchUid = -1;
282
1/1
✓ Branch 64 → 65 taken 1 time.
1 m_deps.rawBodyReceived(resultUid, body->second);
283 }
284 }
285 3 return true;
286 3 }
287
288 // A line ending in {N}: prepares the literal that follows (body literals
289 // keep bytes and may spool). Returns false when the connection was failed.
290 100045 bool ImapLineReader::startLiteral(const QString &line,
291 const QRegularExpressionMatch &match) {
292 // T-401/Bug 9: Use qint64 to avoid overflow with >2GB literals
293 bool ok;
294
2/2
✓ Branch 2 → 3 taken 100045 times.
✓ Branch 3 → 4 taken 100045 times.
100045 m_literalBytesRemaining = match.captured(1).toLongLong(&ok);
295
2/4
✓ Branch 5 → 6 taken 100045 times.
✗ Branch 5 → 7 not taken.
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 27 taken 100045 times.
100045 if (!ok || m_literalBytesRemaining < 0) {
296 ✗ qCWarning(lcImap) << "Invalid literal size:" << match.captured(1);
297 ✗ m_deps.failConnection(QStringLiteral("Invalid IMAP literal size"));
298 ✗ return false;
299 }
300
2/2
✓ Branch 27 → 28 taken 1 time.
✓ Branch 27 → 48 taken 100044 times.
100045 if (m_literalBytesRemaining > ImapService::MAX_LITERAL_SIZE) {
301
6/6
✓ Branch 28 → 29 taken 1 time.
✓ Branch 32 → 33 taken 1 time.
✓ Branch 33 → 34 taken 1 time.
✓ Branch 34 → 35 taken 1 time.
✓ Branch 40 → 30 taken 1 time.
✓ Branch 40 → 41 taken 1 time.
2 qCWarning(lcImap) << "Literal size exceeded" << ImapService::MAX_LITERAL_SIZE
302
2/2
✓ Branch 35 → 36 taken 1 time.
✓ Branch 36 → 37 taken 1 time.
1 << "bytes:" << m_literalBytesRemaining;
303
1/1
✓ Branch 44 → 45 taken 1 time.
2 m_deps.failConnection(QStringLiteral("Server literal too large"));
304 1 return false;
305 }
306
2/3
✓ Branch 48 → 49 taken 100044 times.
✗ Branch 49 → 50 not taken.
✓ Branch 49 → 51 taken 100044 times.
100044 if (!m_commands.accountCommandLiteral())
307 ✗ return false;
308
2/2
✓ Branch 51 → 52 taken 100044 times.
✓ Branch 52 → 53 taken 100044 times.
100044 const QString literalPrefix = line.left(match.capturedStart());
309 // F-02: structural detection — the BODY[…] token must be the fetch item
310 // directly before the literal marker, outside quoted strings (folded
311 // literal content is re-quoted on accumulation, so envelope subjects
312 // containing BODY[] cannot qualify). A subject "Why is BODY[] slower…"
313 // no longer turns the header literal into a discarded body literal.
314 const bool isBodyLiteral =
315
2/2
✓ Branch 53 → 54 taken 100044 times.
✓ Branch 54 → 55 taken 100044 times.
100044 ImapResponseParser::endsWithBodyFetchItem(m_literalLine + literalPrefix);
316
3/4
✓ Branch 56 → 57 taken 100001 times.
✓ Branch 56 → 62 taken 43 times.
✗ Branch 63 → 64 not taken.
✓ Branch 63 → 71 taken 100044 times.
200045 if (!isBodyLiteral &&
317
1/2
✓ Branch 57 → 58 taken 100001 times.
✗ Branch 57 → 61 not taken.
100001 (m_literalBytesRemaining > ImapService::MAX_NON_BODY_LITERAL_LINE_SIZE ||
318
1/2
✗ Branch 60 → 61 not taken.
✓ Branch 60 → 62 taken 100001 times.
100001 m_literalLine.size() + literalPrefix.size() >
319 ImapService::MAX_NON_BODY_LITERAL_LINE_SIZE)) {
320 ✗ m_deps.failConnection(QStringLiteral(
321 "IMAP folded literal line exceeded connection limit"));
322 ✗ return false;
323 }
324 // Remove the {N} from the line — it will be replaced by the literal
325 // content.
326
1/1
✓ Branch 71 → 72 taken 100044 times.
100044 m_literalLine.append(literalPrefix);
327
1/1
✓ Branch 72 → 73 taken 100044 times.
100044 m_literalData.clear();
328
329 // Detect if this is a BODY[] literal fetch
330 // The line looks like: "* N FETCH (UID 42 BODY[] {12345}"
331
2/2
✓ Branch 73 → 74 taken 43 times.
✓ Branch 73 → 167 taken 100001 times.
100044 if (isBodyLiteral) {
332 43 m_isBodyLiteral = true;
333 // Extract UID from the line
334
5/8
✓ Branch 74 → 75 taken 4 times.
✓ Branch 74 → 84 taken 39 times.
✓ Branch 76 → 77 taken 4 times.
✗ Branch 76 → 84 not taken.
✓ Branch 78 → 79 taken 4 times.
✓ Branch 79 → 80 taken 4 times.
✗ Branch 215 → 216 not taken.
✗ Branch 215 → 217 not taken.
43 static QRegularExpression uidRx(R"(UID\s+(\d+))");
335
1/1
✓ Branch 85 → 86 taken 43 times.
43 auto uidMatch = uidRx.match(m_literalLine);
336 43 bool uidOk = false;
337
2/3
✓ Branch 86 → 87 taken 43 times.
✓ Branch 87 → 88 taken 43 times.
✗ Branch 87 → 92 not taken.
43 if (uidMatch.hasMatch())
338
2/2
✓ Branch 88 → 89 taken 43 times.
✓ Branch 89 → 90 taken 43 times.
43 m_bodyLiteralUid = uidMatch.captured(1).toLongLong(&uidOk);
339 const bool fetchPending =
340
1/1
✓ Branch 95 → 96 taken 43 times.
86 m_commands.hasCommandType(QStringLiteral("FETCH_BODY"));
341 43 m_discardingInvalidBody =
342
2/4
✓ Branch 99 → 100 taken 40 times.
✗ Branch 99 → 103 not taken.
✓ Branch 100 → 101 taken 40 times.
✗ Branch 100 → 103 not taken.
40 !fetchPending || !uidOk || m_body.fetchUid <= 0 ||
343
4/4
✓ Branch 98 → 99 taken 40 times.
✓ Branch 98 → 103 taken 3 times.
✓ Branch 101 → 102 taken 39 times.
✓ Branch 101 → 103 taken 1 time.
122 m_bodyLiteralUid != m_body.fetchUid ||
344
1/2
✗ Branch 102 → 103 not taken.
✓ Branch 102 → 104 taken 39 times.
39 m_body.requiresSelect;
345
2/2
✓ Branch 105 → 106 taken 4 times.
✓ Branch 105 → 121 taken 39 times.
43 if (m_discardingInvalidBody) {
346
5/5
✓ Branch 106 → 107 taken 4 times.
✓ Branch 110 → 111 taken 4 times.
✓ Branch 111 → 112 taken 4 times.
✓ Branch 119 → 108 taken 4 times.
✓ Branch 119 → 120 taken 4 times.
8 qCWarning(lcImap) << "Discarding unsolicited or mismatched BODY[]"
347
3/3
✓ Branch 112 → 113 taken 4 times.
✓ Branch 113 → 114 taken 4 times.
✓ Branch 114 → 115 taken 4 times.
4 << "for UID" << m_bodyLiteralUid << "expected"
348
1/1
✓ Branch 115 → 116 taken 4 times.
4 << m_body.fetchUid;
349 4 m_discardingOversizedBody = false;
350
1/2
✗ Branch 121 → 122 not taken.
✓ Branch 121 → 129 taken 39 times.
39 } else if (m_literalBytesRemaining > m_body.requestBytes) {
351 ✗ m_deps.failConnection(
352 ✗ QStringLiteral("IMAP server exceeded requested body range"));
353 ✗ return false;
354 } else {
355 39 m_discardingOversizedBody =
356
1/2
✓ Branch 129 → 130 taken 39 times.
✗ Branch 129 → 132 not taken.
78 m_body.fetchLimit > 0 &&
357
2/2
✓ Branch 130 → 131 taken 3 times.
✓ Branch 130 → 132 taken 36 times.
39 m_literalBytesRemaining > m_body.fetchLimit;
358 }
359
6/6
✓ Branch 134 → 135 taken 43 times.
✓ Branch 138 → 139 taken 43 times.
✓ Branch 139 → 140 taken 43 times.
✓ Branch 140 → 141 taken 43 times.
✓ Branch 146 → 136 taken 43 times.
✓ Branch 146 → 147 taken 43 times.
86 qCInfo(lcImap) << "Collecting body literal:" << m_literalBytesRemaining
360
2/2
✓ Branch 141 → 142 taken 43 times.
✓ Branch 142 → 143 taken 43 times.
43 << "bytes for UID" << m_bodyLiteralUid;
361 // The server announces the literal size: large payloads spool to a
362 // private temp file so reception stays out of RAM (T-87.C2); small
363 // ones reserve once so the drain cannot double the allocation.
364
4/4
✓ Branch 147 → 148 taken 39 times.
✓ Branch 147 → 161 taken 4 times.
✓ Branch 148 → 149 taken 36 times.
✓ Branch 148 → 161 taken 3 times.
43 if (!m_discardingInvalidBody && !m_discardingOversizedBody) {
365
2/2
✓ Branch 149 → 150 taken 3 times.
✓ Branch 149 → 156 taken 33 times.
36 if (m_literalBytesRemaining >= m_bodySpoolThreshold) {
366
2/3
✓ Branch 150 → 151 taken 3 times.
✗ Branch 151 → 152 not taken.
✓ Branch 151 → 161 taken 3 times.
3 if (!startBodySpool())
367 ✗ m_literalData.reserve(
368 ✗ int(qMin<qint64>(m_literalBytesRemaining,
369 ✗ std::numeric_limits<int>::max())));
370 } else {
371
1/1
✓ Branch 156 → 157 taken 33 times.
33 m_literalData.clear();
372
1/1
✓ Branch 159 → 160 taken 33 times.
33 m_literalData.reserve(
373 33 int(qMin<qint64>(m_literalBytesRemaining,
374 66 std::numeric_limits<int>::max())));
375 }
376 }
377
1/2
✓ Branch 163 → 164 taken 43 times.
✗ Branch 163 → 166 not taken.
43 } else {
378 100001 m_isBodyLiteral = false;
379 100001 m_discardingInvalidBody = false;
380 100001 m_discardingOversizedBody = false;
381 }
382
383
2/2
✓ Branch 168 → 169 taken 100001 times.
✓ Branch 168 → 170 taken 43 times.
100044 if (m_literalBytesRemaining == 0)
384
1/1
✓ Branch 169 → 170 taken 100001 times.
100001 completeLiteral();
385 100044 return true;
386 100044 }
387
388 100732 void ImapLineReader::processLine(const QString &line) {
389
2/2
✓ Branch 3 → 4 taken 100387 times.
✓ Branch 3 → 5 taken 345 times.
100732 if (ImapResponseParser::isUntagged(line)) {
390 100387 m_deps.handleUntagged(line);
391
2/2
✓ Branch 6 → 7 taken 313 times.
✓ Branch 6 → 8 taken 32 times.
345 } else if (ImapResponseParser::isTagged(line)) {
392 313 m_deps.handleTagged(line);
393
2/2
✓ Branch 9 → 10 taken 31 times.
✓ Branch 9 → 11 taken 1 time.
32 } else if (ImapResponseParser::isContinuation(line)) {
394 31 m_deps.handleContinuation(line);
395 } else {
396
6/6
✓ Branch 11 → 12 taken 1 time.
✓ Branch 15 → 16 taken 1 time.
✓ Branch 16 → 17 taken 1 time.
✓ Branch 17 → 18 taken 1 time.
✓ Branch 21 → 13 taken 1 time.
✓ Branch 21 → 22 taken 1 time.
2 qCWarning(lcImap) << "Unrecognized response:" << line;
397 }
398 100732 }
399
400 2482 void ImapLineReader::resetFraming() {
401 2482 m_readBuffer.clear();
402 2482 m_lastReceiveMs = -1;
403 2482 m_literalBytesRemaining = 0;
404 2482 m_literalData.clear();
405 2482 m_bodySpool.reset();
406 2482 m_literalLine.clear();
407 2482 m_isBodyLiteral = false;
408 2482 m_bodyLiteralUid = -1;
409 2482 m_discardingInvalidBody = false;
410 2482 m_discardingOversizedBody = false;
411 2482 m_skipFetchLiteralRemainder = false;
412 2482 }
413
414 205 qint64 ImapLineReader::quietMs() const {
415
2/2
✓ Branch 2 → 3 taken 113 times.
✓ Branch 2 → 5 taken 92 times.
205 return m_lastReceiveMs < 0 ? -1 : m_deps.nowMs() - m_lastReceiveMs;
416 }
417