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 |