service/MimeParser.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #include "util/CanonicalContentId.h" | ||
| 2 | #include "MimeParser.h" | ||
| 3 | |||
| 4 | #include <QHash> | ||
| 5 | #include <QLoggingCategory> | ||
| 6 | #include <QStringDecoder> | ||
| 7 | #include <QUrl> | ||
| 8 | |||
| 9 | #include "service/ImapResponseParser.h" | ||
| 10 | #include "util/AttachmentFileSecurity.h" | ||
| 11 | |||
| 12 |
4/7✓ Branch 2 → 3 taken 4 times.
✓ Branch 2 → 8 taken 10 times.
✓ Branch 4 → 5 taken 4 times.
✗ Branch 4 → 8 not taken.
✓ Branch 5 → 6 taken 4 times.
✗ Branch 10 → 11 not taken.
✗ Branch 10 → 12 not taken.
|
14 | Q_LOGGING_CATEGORY(lcMime, "mailjd.mime") |
| 13 | |||
| 14 | namespace { | ||
| 15 | // S106-033: the media type is the token before the first ';' — "multipart/" | ||
| 16 | // inside a parameter value (name="multipart/x") made a text part a | ||
| 17 | // multipart container. | ||
| 18 | 7077 | bool isMultipartType(const QString &contentTypeLower) { | |
| 19 |
3/3✓ Branch 5 → 6 taken 7077 times.
✓ Branch 6 → 7 taken 7077 times.
✓ Branch 10 → 11 taken 7077 times.
|
14154 | return contentTypeLower.section(QLatin1Char(';'), 0, 0).trimmed().startsWith(QStringLiteral("multipart/")); |
| 20 | } | ||
| 21 | |||
| 22 | } // namespace | ||
| 23 | |||
| 24 | // --- Public API --- | ||
| 25 | |||
| 26 | 447 | MimeMessage MimeParser::parse(const QByteArray &rawMessage) { | |
| 27 | 447 | MimeMessage result; | |
| 28 | |||
| 29 |
1/2✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 447 times.
|
447 | if (rawMessage.isEmpty()) { |
| 30 | ✗ | return result; | |
| 31 | } | ||
| 32 |
2/2✓ Branch 6 → 7 taken 2 times.
✓ Branch 6 → 19 taken 445 times.
|
447 | if (rawMessage.size() > kMaxRawMessageBytes) { |
| 33 | 2 | result.limitExceeded = true; | |
| 34 |
5/5✓ Branch 7 → 8 taken 2 times.
✓ Branch 11 → 12 taken 2 times.
✓ Branch 12 → 13 taken 2 times.
✓ Branch 17 → 9 taken 2 times.
✓ Branch 17 → 18 taken 2 times.
|
4 | qCWarning(lcMime) << "MIME raw-message budget exceeded" |
| 35 |
1/1✓ Branch 13 → 14 taken 2 times.
|
2 | << kMaxRawMessageBytes; |
| 36 | 2 | return result; | |
| 37 | } | ||
| 38 | |||
| 39 | // Split into headers and body | ||
| 40 |
1/1✓ Branch 19 → 20 taken 445 times.
|
445 | auto [headerBlock, bodyBlock] = splitHeaderBody(rawMessage); |
| 41 |
1/1✓ Branch 22 → 23 taken 445 times.
|
445 | auto headers = parseHeaders(headerBlock); |
| 42 | |||
| 43 | 890 | QString contentType = headers.value(QStringLiteral("content-type"), | |
| 44 |
1/1✓ Branch 29 → 30 taken 445 times.
|
1335 | QStringLiteral("text/plain")); |
| 45 | QString transferEncoding = headers.value( | ||
| 46 |
1/1✓ Branch 40 → 41 taken 445 times.
|
890 | QStringLiteral("content-transfer-encoding"), QStringLiteral("7bit")); |
| 47 | |||
| 48 | // Check if this is a multipart message | ||
| 49 |
1/1✓ Branch 45 → 46 taken 445 times.
|
445 | QString ctLower = contentType.toLower(); |
| 50 |
3/3✓ Branch 46 → 47 taken 445 times.
✓ Branch 47 → 48 taken 142 times.
✓ Branch 47 → 119 taken 303 times.
|
445 | if (isMultipartType(ctLower)) { |
| 51 |
1/1✓ Branch 48 → 49 taken 142 times.
|
142 | QByteArray boundary = extractBoundary(contentType); |
| 52 |
2/2✓ Branch 50 → 51 taken 1 time.
✓ Branch 50 → 65 taken 141 times.
|
142 | if (boundary.isEmpty()) { |
| 53 | // Malformed: no boundary → treat entire body as plain text | ||
| 54 |
4/4✓ Branch 51 → 52 taken 1 time.
✓ Branch 55 → 56 taken 1 time.
✓ Branch 60 → 53 taken 1 time.
✓ Branch 60 → 61 taken 1 time.
|
2 | qCWarning(lcMime) << "Multipart message without boundary, falling back " |
| 55 |
1/1✓ Branch 56 → 57 taken 1 time.
|
1 | "to plain text"; |
| 56 |
1/1✓ Branch 61 → 62 taken 1 time.
|
1 | result.textPlain = QString::fromUtf8(bodyBlock); |
| 57 | 1 | return result; | |
| 58 | } | ||
| 59 | |||
| 60 | 141 | ParseBudget budget; | |
| 61 | // F-07: the top level applies the same rule as nested parts — an | ||
| 62 | // alternative container keeps first-wins, everything else appends. | ||
| 63 | const bool alternative = | ||
| 64 |
1/1✓ Branch 68 → 69 taken 141 times.
|
141 | ctLower.startsWith(QStringLiteral("multipart/alternative")); |
| 65 | auto parts = | ||
| 66 | splitMultipartBody(bodyBlock, boundary, remainingParts(budget), | ||
| 67 |
3/3✓ Branch 71 → 72 taken 141 times.
✓ Branch 72 → 73 taken 141 times.
✓ Branch 73 → 74 taken 141 times.
|
141 | remainingMaterializedBytes(budget), &budget.limitExceeded); |
| 68 | // S106-036: a boundary that never appears yields no part — the body was | ||
| 69 | // silently not shown. Like a missing boundary, it is shown as text. | ||
| 70 |
5/6✓ Branch 75 → 76 taken 1 time.
✓ Branch 75 → 78 taken 140 times.
✓ Branch 76 → 77 taken 1 time.
✗ Branch 76 → 78 not taken.
✓ Branch 79 → 80 taken 1 time.
✓ Branch 79 → 94 taken 140 times.
|
141 | if (parts.isEmpty() && !budget.limitExceeded) { |
| 71 |
5/5✓ Branch 80 → 81 taken 1 time.
✓ Branch 84 → 85 taken 1 time.
✓ Branch 85 → 86 taken 1 time.
✓ Branch 89 → 82 taken 1 time.
✓ Branch 89 → 90 taken 1 time.
|
2 | qCWarning(lcMime) << "Multipart boundary never found, falling back to plain text"; |
| 72 |
1/1✓ Branch 90 → 91 taken 1 time.
|
1 | result.textPlain = QString::fromUtf8(bodyBlock); |
| 73 | 1 | return result; | |
| 74 | } | ||
| 75 |
4/4✓ Branch 94 → 95 taken 140 times.
✓ Branch 95 → 96 taken 140 times.
✓ Branch 104 → 97 taken 5302 times.
✓ Branch 104 → 105 taken 140 times.
|
5442 | for (const auto &part : parts) { |
| 76 |
2/3✓ Branch 98 → 99 taken 5302 times.
✗ Branch 99 → 100 not taken.
✓ Branch 99 → 101 taken 5302 times.
|
5302 | if (!consumePartBudget(part, budget)) { |
| 77 | ✗ | break; | |
| 78 | } | ||
| 79 |
1/1✓ Branch 101 → 102 taken 5302 times.
|
5302 | parsePart(part, result, budget, 0, alternative); |
| 80 | } | ||
| 81 |
4/4✓ Branch 105 → 106 taken 136 times.
✓ Branch 105 → 107 taken 4 times.
✓ Branch 106 → 107 taken 4 times.
✓ Branch 106 → 108 taken 132 times.
|
140 | result.limitExceeded = result.limitExceeded || budget.limitExceeded; |
| 82 |
4/4✓ Branch 111 → 112 taken 140 times.
✓ Branch 111 → 113 taken 1 time.
✓ Branch 115 → 116 taken 140 times.
✓ Branch 115 → 118 taken 2 times.
|
143 | } else { |
| 83 | // Single-part message | ||
| 84 |
1/1✓ Branch 119 → 120 taken 303 times.
|
303 | QByteArray decoded = decodeTransferEncoding(bodyBlock, transferEncoding); |
| 85 |
1/1✓ Branch 120 → 121 taken 303 times.
|
303 | QString charset = extractCharset(contentType); |
| 86 | |||
| 87 |
3/3✓ Branch 124 → 125 taken 303 times.
✓ Branch 127 → 128 taken 4 times.
✓ Branch 127 → 132 taken 299 times.
|
303 | if (ctLower.startsWith(QStringLiteral("text/html"))) { |
| 88 |
1/1✓ Branch 128 → 129 taken 4 times.
|
4 | result.textHtml = convertCharset(decoded, charset); |
| 89 |
6/11✓ Branch 135 → 136 taken 299 times.
✓ Branch 136 → 137 taken 291 times.
✓ Branch 136 → 143 taken 8 times.
✓ Branch 150 → 151 taken 299 times.
✗ Branch 150 → 152 not taken.
✓ Branch 152 → 153 taken 291 times.
✓ Branch 152 → 157 taken 8 times.
✗ Branch 307 → 308 not taken.
✗ Branch 307 → 309 not taken.
✗ Branch 311 → 312 not taken.
✗ Branch 311 → 313 not taken.
|
1188 | } else if (ctLower.startsWith(QStringLiteral("text/")) && |
| 90 |
7/13✓ Branch 140 → 141 taken 291 times.
✓ Branch 141 → 142 taken 291 times.
✗ Branch 141 → 143 not taken.
✓ Branch 144 → 145 taken 291 times.
✓ Branch 144 → 146 taken 8 times.
✓ Branch 146 → 147 taken 291 times.
✓ Branch 146 → 148 taken 8 times.
✓ Branch 148 → 149 taken 299 times.
✗ Branch 148 → 150 not taken.
✗ Branch 299 → 300 not taken.
✗ Branch 299 → 301 not taken.
✗ Branch 303 → 304 not taken.
✗ Branch 303 → 305 not taken.
|
590 | !ctLower.startsWith(QStringLiteral("text/calendar"))) { |
| 91 |
1/1✓ Branch 153 → 154 taken 291 times.
|
291 | result.textPlain = convertCharset(decoded, charset); |
| 92 | } else { | ||
| 93 | // Non-text single part (rare but possible) | ||
| 94 | 8 | MimePart part; | |
| 95 |
3/3✓ Branch 159 → 160 taken 8 times.
✓ Branch 160 → 161 taken 8 times.
✓ Branch 161 → 162 taken 8 times.
|
8 | part.contentType = contentType.section(';', 0, 0).trimmed().toLower(); |
| 96 | 8 | part.body = decoded; | |
| 97 | 8 | part.isAttachment = true; | |
| 98 | const QString disposition = | ||
| 99 |
1/1✓ Branch 171 → 172 taken 8 times.
|
16 | headers.value(QStringLiteral("content-disposition")); |
| 100 |
1/1✓ Branch 175 → 176 taken 8 times.
|
8 | part.filename = decodeAndSanitizeFilename(disposition, contentType); |
| 101 |
4/6✓ Branch 179 → 180 taken 3 times.
✓ Branch 179 → 184 taken 5 times.
✗ Branch 182 → 183 not taken.
✓ Branch 182 → 184 taken 3 times.
✗ Branch 185 → 186 not taken.
✓ Branch 185 → 193 taken 8 times.
|
8 | if (part.filename.isEmpty() && part.contentType == QLatin1String("text/calendar")) |
| 102 | ✗ | part.filename = QStringLiteral("invite.ics"); | |
| 103 | part.contentId = | ||
| 104 |
2/2✓ Branch 197 → 198 taken 8 times.
✓ Branch 198 → 199 taken 8 times.
|
16 | normalizeContentId(headers.value(QStringLiteral("content-id"))); |
| 105 |
2/2✓ Branch 206 → 207 taken 3 times.
✓ Branch 206 → 223 taken 5 times.
|
8 | if (!part.contentId.isEmpty()) { |
| 106 | 3 | part.presentation = | |
| 107 |
2/2✓ Branch 207 → 208 taken 3 times.
✓ Branch 211 → 212 taken 3 times.
|
9 | disposition.trimmed().startsWith(QStringLiteral("attachment"), |
| 108 | Qt::CaseInsensitive) | ||
| 109 |
2/2✓ Branch 212 → 213 taken 1 time.
✓ Branch 212 → 214 taken 2 times.
|
5 | ? AttachmentPresentation::ExplicitAttachmentWithContentId |
| 110 |
1/1✓ Branch 214 → 215 taken 2 times.
|
2 | : (isAllowedInlineImage(part.contentType, part.body) |
| 111 |
2/2✓ Branch 215 → 216 taken 1 time.
✓ Branch 215 → 217 taken 1 time.
|
2 | ? AttachmentPresentation::InlineCandidate |
| 112 | : AttachmentPresentation::RejectedInline); | ||
| 113 | } | ||
| 114 |
1/1✓ Branch 223 → 224 taken 8 times.
|
8 | result.attachments.append(part); |
| 115 | 8 | } | |
| 116 | 303 | } | |
| 117 | |||
| 118 |
1/1✓ Branch 230 → 231 taken 443 times.
|
443 | finalizeInlineParts(result); |
| 119 | 443 | return result; | |
| 120 | 445 | } | |
| 121 | |||
| 122 | // --- Encoding utilities --- | ||
| 123 | |||
| 124 | 397 | QByteArray MimeParser::decodeQuotedPrintable(const QByteArray &input) { | |
| 125 | 397 | QByteArray output; | |
| 126 |
1/1✓ Branch 3 → 4 taken 397 times.
|
397 | output.reserve(input.size()); |
| 127 | |||
| 128 |
2/2✓ Branch 44 → 5 taken 6713693 times.
✓ Branch 44 → 45 taken 397 times.
|
6714090 | for (int i = 0; i < input.size(); ++i) { |
| 129 | 6713693 | char c = input.at(i); | |
| 130 |
2/2✓ Branch 6 → 7 taken 217710 times.
✓ Branch 6 → 41 taken 6495983 times.
|
6713693 | if (c == '=') { |
| 131 | // Check for soft line break: =\r\n or =\n | ||
| 132 |
3/6✓ Branch 8 → 9 taken 217710 times.
✗ Branch 8 → 12 not taken.
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 217710 times.
✗ Branch 13 → 14 not taken.
✓ Branch 13 → 15 taken 217710 times.
|
217710 | if (i + 1 < input.size() && input.at(i + 1) == '\n') { |
| 133 | ✗ | i += 1; // skip \n | |
| 134 | ✗ | continue; | |
| 135 | } | ||
| 136 |
6/8✓ Branch 16 → 17 taken 217710 times.
✗ Branch 16 → 22 not taken.
✓ Branch 18 → 19 taken 16747 times.
✓ Branch 18 → 22 taken 200963 times.
✓ Branch 20 → 21 taken 16747 times.
✗ Branch 20 → 22 not taken.
✓ Branch 23 → 24 taken 16747 times.
✓ Branch 23 → 25 taken 200963 times.
|
234457 | if (i + 2 < input.size() && input.at(i + 1) == '\r' && |
| 137 | 16747 | input.at(i + 2) == '\n') { | |
| 138 | 16747 | i += 2; // skip \r\n | |
| 139 | 16747 | continue; | |
| 140 | } | ||
| 141 | // Hex-encoded byte: =XX | ||
| 142 |
1/2✓ Branch 26 → 27 taken 200963 times.
✗ Branch 26 → 40 not taken.
|
200963 | if (i + 2 < input.size()) { |
| 143 | 200963 | char hi = input.at(i + 1); | |
| 144 | 200963 | char lo = input.at(i + 2); | |
| 145 | 200963 | bool okHi = false, okLo = false; | |
| 146 |
2/2✓ Branch 29 → 30 taken 200963 times.
✓ Branch 30 → 31 taken 200963 times.
|
200963 | int hiVal = QByteArray(1, hi).toInt(&okHi, 16); |
| 147 |
2/2✓ Branch 32 → 33 taken 200963 times.
✓ Branch 33 → 34 taken 200963 times.
|
200963 | int loVal = QByteArray(1, lo).toInt(&okLo, 16); |
| 148 |
3/4✓ Branch 35 → 36 taken 200962 times.
✓ Branch 35 → 39 taken 1 time.
✓ Branch 36 → 37 taken 200962 times.
✗ Branch 36 → 39 not taken.
|
200963 | if (okHi && okLo) { |
| 149 |
1/1✓ Branch 37 → 38 taken 200962 times.
|
200962 | output.append(static_cast<char>((hiVal << 4) | loVal)); |
| 150 | 200962 | i += 2; | |
| 151 | 200962 | continue; | |
| 152 | } | ||
| 153 | } | ||
| 154 | // Malformed =, pass through | ||
| 155 |
1/1✓ Branch 40 → 42 taken 1 time.
|
1 | output.append(c); |
| 156 | } else { | ||
| 157 |
1/1✓ Branch 41 → 42 taken 6495983 times.
|
6495983 | output.append(c); |
| 158 | } | ||
| 159 | } | ||
| 160 | |||
| 161 | 397 | return output; | |
| 162 | ✗ | } | |
| 163 | |||
| 164 | 6613 | QByteArray MimeParser::decodeTransferEncoding(const QByteArray &data, | |
| 165 | const QString &encoding) { | ||
| 166 |
2/2✓ Branch 2 → 3 taken 6613 times.
✓ Branch 3 → 4 taken 6613 times.
|
6613 | QString enc = encoding.trimmed().toLower(); |
| 167 |
2/2✓ Branch 11 → 12 taken 395 times.
✓ Branch 11 → 14 taken 6218 times.
|
6613 | if (enc == QStringLiteral("quoted-printable")) { |
| 168 |
1/1✓ Branch 12 → 13 taken 395 times.
|
395 | return decodeQuotedPrintable(data); |
| 169 | } | ||
| 170 |
2/2✓ Branch 20 → 21 taken 145 times.
✓ Branch 20 → 24 taken 6073 times.
|
6218 | if (enc == QStringLiteral("base64")) { |
| 171 |
1/1✓ Branch 22 → 23 taken 145 times.
|
145 | return QByteArray::fromBase64(data); |
| 172 | } | ||
| 173 | // 7bit, 8bit, binary → no transformation | ||
| 174 | 6073 | return data; | |
| 175 | 6613 | } | |
| 176 | |||
| 177 | 470 | QString MimeParser::convertCharset(const QByteArray &data, | |
| 178 | const QString &charset) { | ||
| 179 |
2/2✓ Branch 3 → 4 taken 21 times.
✓ Branch 3 → 5 taken 449 times.
|
470 | if (data.isEmpty()) { |
| 180 | 21 | return {}; | |
| 181 | } | ||
| 182 | |||
| 183 |
2/2✓ Branch 5 → 6 taken 449 times.
✓ Branch 6 → 7 taken 449 times.
|
449 | QString cs = charset.trimmed().toLower(); |
| 184 |
7/8✓ Branch 14 → 15 taken 13 times.
✓ Branch 14 → 25 taken 436 times.
✓ Branch 19 → 20 taken 8 times.
✓ Branch 19 → 25 taken 5 times.
✓ Branch 37 → 38 taken 449 times.
✗ Branch 37 → 39 not taken.
✓ Branch 39 → 40 taken 441 times.
✓ Branch 39 → 42 taken 8 times.
|
911 | if (cs.isEmpty() || cs == QStringLiteral("utf-8") || |
| 185 |
11/14✓ Branch 9 → 10 taken 449 times.
✗ Branch 9 → 25 not taken.
✗ Branch 24 → 25 not taken.
✓ Branch 24 → 26 taken 8 times.
✓ Branch 27 → 28 taken 8 times.
✓ Branch 27 → 29 taken 441 times.
✓ Branch 29 → 30 taken 8 times.
✓ Branch 29 → 31 taken 441 times.
✓ Branch 31 → 32 taken 13 times.
✓ Branch 31 → 33 taken 436 times.
✓ Branch 33 → 34 taken 13 times.
✓ Branch 33 → 35 taken 436 times.
✓ Branch 35 → 36 taken 449 times.
✗ Branch 35 → 37 not taken.
|
911 | cs == QStringLiteral("us-ascii") || cs == QStringLiteral("ascii")) { |
| 186 |
1/1✓ Branch 40 → 41 taken 441 times.
|
441 | return QString::fromUtf8(data); |
| 187 | } | ||
| 188 | |||
| 189 | // Try QStringDecoder for the given charset | ||
| 190 |
2/2✓ Branch 43 → 44 taken 8 times.
✓ Branch 46 → 47 taken 8 times.
|
8 | auto decoder = QStringDecoder(cs.toLatin1().constData()); |
| 191 |
2/2✓ Branch 49 → 50 taken 7 times.
✓ Branch 49 → 54 taken 1 time.
|
8 | if (decoder.isValid()) { |
| 192 |
1/1✓ Branch 51 → 52 taken 7 times.
|
7 | return decoder.decode(data); |
| 193 | } | ||
| 194 | |||
| 195 | // Fallback: try Latin-1 | ||
| 196 |
6/6✓ Branch 54 → 55 taken 1 time.
✓ Branch 58 → 59 taken 1 time.
✓ Branch 59 → 60 taken 1 time.
✓ Branch 60 → 61 taken 1 time.
✓ Branch 65 → 56 taken 1 time.
✓ Branch 65 → 66 taken 1 time.
|
2 | qCWarning(lcMime) << "Unknown charset" << charset |
| 197 |
1/1✓ Branch 61 → 62 taken 1 time.
|
1 | << ", falling back to Latin-1"; |
| 198 |
1/1✓ Branch 66 → 67 taken 1 time.
|
1 | return QString::fromLatin1(data); |
| 199 | 449 | } | |
| 200 | |||
| 201 | // --- Private helpers --- | ||
| 202 | |||
| 203 | QPair<QByteArray, QByteArray> | ||
| 204 | 7077 | MimeParser::splitHeaderBody(const QByteArray &raw) { | |
| 205 | // Headers and body are separated by the FIRST blank line, whichever line | ||
| 206 | // ending it uses. S106-034: "\r\n\r\n" used to win over an earlier | ||
| 207 | // "\n\n", so body lines up to a later CRLF blank line were read as | ||
| 208 | // headers (body text could set Content-Type). messageHeaders() already | ||
| 209 | // took the earliest. | ||
| 210 |
1/2✗ Branch 4 → 5 not taken.
✓ Branch 4 → 11 taken 7077 times.
|
7077 | if (raw.startsWith("\r\n")) |
| 211 | ✗ | return {{}, raw.mid(2)}; | |
| 212 |
1/2✗ Branch 12 → 13 not taken.
✓ Branch 12 → 19 taken 7077 times.
|
7077 | if (raw.startsWith('\n')) |
| 213 | ✗ | return {{}, raw.mid(1)}; | |
| 214 |
1/2✓ Branch 54 → 20 taken 34481 times.
✗ Branch 54 → 55 not taken.
|
34481 | for (qsizetype i = raw.indexOf('\n'); i >= 0; i = raw.indexOf('\n', i + 1)) { |
| 215 |
3/4✓ Branch 20 → 21 taken 34481 times.
✗ Branch 20 → 24 not taken.
✓ Branch 22 → 23 taken 34478 times.
✓ Branch 22 → 24 taken 3 times.
|
34481 | const qsizetype headerEnd = i > 0 && raw.at(i - 1) == '\r' ? i - 1 : i; |
| 216 |
5/6✓ Branch 26 → 27 taken 34481 times.
✗ Branch 26 → 30 not taken.
✓ Branch 28 → 29 taken 2 times.
✓ Branch 28 → 30 taken 34479 times.
✓ Branch 31 → 32 taken 2 times.
✓ Branch 31 → 38 taken 34479 times.
|
34481 | if (i + 1 < raw.size() && raw.at(i + 1) == '\n') |
| 217 |
2/2✓ Branch 32 → 33 taken 2 times.
✓ Branch 33 → 34 taken 2 times.
|
2 | return {raw.left(headerEnd), raw.mid(i + 2)}; |
| 218 |
6/8✓ Branch 39 → 40 taken 34479 times.
✗ Branch 39 → 45 not taken.
✓ Branch 41 → 42 taken 7075 times.
✓ Branch 41 → 45 taken 27404 times.
✓ Branch 43 → 44 taken 7075 times.
✗ Branch 43 → 45 not taken.
✓ Branch 46 → 47 taken 7075 times.
✓ Branch 46 → 53 taken 27404 times.
|
34479 | if (i + 2 < raw.size() && raw.at(i + 1) == '\r' && raw.at(i + 2) == '\n') |
| 219 |
2/2✓ Branch 47 → 48 taken 7075 times.
✓ Branch 48 → 49 taken 7075 times.
|
7075 | return {raw.left(headerEnd), raw.mid(i + 3)}; |
| 220 | } | ||
| 221 | |||
| 222 | // No blank line → entire message is headers (no body) | ||
| 223 | ✗ | return {raw, {}}; | |
| 224 | } | ||
| 225 | |||
| 226 | 343 | QMap<QString, QString> MimeParser::messageHeaders(const QByteArray &rawMessage) { | |
| 227 |
1/1✓ Branch 4 → 5 taken 343 times.
|
343 | const auto prefix = rawMessage.first(qMin(rawMessage.size(), MaximumHeaderBytes + 4)); |
| 228 | 343 | auto end = prefix.indexOf("\r\n\r\n"); | |
| 229 | 343 | const auto lfEnd = prefix.indexOf("\n\n"); | |
| 230 |
6/6✓ Branch 9 → 10 taken 228 times.
✓ Branch 9 → 13 taken 115 times.
✓ Branch 10 → 11 taken 227 times.
✓ Branch 10 → 12 taken 1 time.
✓ Branch 11 → 12 taken 1 time.
✓ Branch 11 → 13 taken 226 times.
|
343 | if (lfEnd >= 0 && (end < 0 || lfEnd < end)) end = lfEnd; |
| 231 |
3/4✓ Branch 13 → 14 taken 330 times.
✓ Branch 13 → 15 taken 13 times.
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 16 taken 330 times.
|
343 | if (end < 0 || end > MaximumHeaderBytes) return {}; |
| 232 |
2/2✓ Branch 16 → 17 taken 330 times.
✓ Branch 17 → 18 taken 330 times.
|
330 | return parseHeaders(prefix.first(end)); |
| 233 | 343 | } | |
| 234 | |||
| 235 | 7407 | QMap<QString, QString> MimeParser::parseHeaders(const QByteArray &headerBlock) { | |
| 236 | 7407 | QMap<QString, QString> headers; | |
| 237 |
1/2✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 7407 times.
|
7407 | if (headerBlock.isEmpty()) { |
| 238 | ✗ | return headers; | |
| 239 | } | ||
| 240 | |||
| 241 | 7407 | QString currentKey; | |
| 242 | 7407 | QString currentValue; | |
| 243 | |||
| 244 | // S106-035 (INV-14): lines are read one at a time and their number is | ||
| 245 | // bounded — splitting a 64 MB header-only message materialized tens of | ||
| 246 | // millions of line objects (plus a QString each) before any check. Lines | ||
| 247 | // past MaximumHeaderLines are ignored; a long folded field is still read | ||
| 248 | // whole (its size is bounded by the raw-message budget). | ||
| 249 | 7407 | qsizetype lineCount = 0; | |
| 250 | 7407 | qsizetype lineStart = 0; | |
| 251 |
2/2✓ Branch 87 → 6 taken 26468 times.
✓ Branch 87 → 88 taken 7406 times.
|
33874 | while (lineStart < headerBlock.size()) { |
| 252 |
2/2✓ Branch 6 → 7 taken 1 time.
✓ Branch 6 → 20 taken 26467 times.
|
26468 | if (++lineCount > MaximumHeaderLines) { |
| 253 |
7/7✓ Branch 7 → 8 taken 1 time.
✓ Branch 11 → 12 taken 1 time.
✓ Branch 12 → 13 taken 1 time.
✓ Branch 13 → 14 taken 1 time.
✓ Branch 14 → 15 taken 1 time.
✓ Branch 18 → 9 taken 1 time.
✓ Branch 18 → 19 taken 1 time.
|
2 | qCWarning(lcMime) << "Header block has more than" << MaximumHeaderLines << "lines; the rest is ignored"; |
| 254 | 1 | break; | |
| 255 | } | ||
| 256 | 26467 | qsizetype lineEnd = headerBlock.indexOf('\n', lineStart); | |
| 257 |
2/2✓ Branch 21 → 22 taken 7406 times.
✓ Branch 21 → 23 taken 19061 times.
|
26467 | const qsizetype next = lineEnd < 0 ? headerBlock.size() : lineEnd + 1; |
| 258 |
2/2✓ Branch 24 → 25 taken 7406 times.
✓ Branch 24 → 26 taken 19061 times.
|
26467 | if (lineEnd < 0) lineEnd = headerBlock.size(); |
| 259 |
5/6✓ Branch 26 → 27 taken 26467 times.
✗ Branch 26 → 30 not taken.
✓ Branch 28 → 29 taken 19060 times.
✓ Branch 28 → 30 taken 7407 times.
✓ Branch 31 → 32 taken 19060 times.
✓ Branch 31 → 33 taken 7407 times.
|
26467 | if (lineEnd > lineStart && headerBlock.at(lineEnd - 1) == '\r') --lineEnd; |
| 260 |
1/1✓ Branch 34 → 35 taken 26467 times.
|
26467 | const QString line = QString::fromUtf8(headerBlock.constData() + lineStart, lineEnd - lineStart); |
| 261 | 26467 | lineStart = next; | |
| 262 | |||
| 263 | // Continuation line: starts with space or tab | ||
| 264 |
7/8✓ Branch 36 → 37 taken 26467 times.
✗ Branch 36 → 46 not taken.
✓ Branch 40 → 41 taken 26037 times.
✓ Branch 40 → 45 taken 430 times.
✓ Branch 44 → 45 taken 4 times.
✓ Branch 44 → 46 taken 26033 times.
✓ Branch 47 → 48 taken 434 times.
✓ Branch 47 → 58 taken 26033 times.
|
26467 | if (!line.isEmpty() && (line.at(0) == ' ' || line.at(0) == '\t')) { |
| 265 |
1/2✓ Branch 49 → 50 taken 434 times.
✗ Branch 49 → 57 not taken.
|
434 | if (!currentKey.isEmpty()) { |
| 266 |
3/3✓ Branch 50 → 51 taken 434 times.
✓ Branch 52 → 53 taken 434 times.
✓ Branch 53 → 54 taken 434 times.
|
434 | currentValue += ' ' + line.trimmed(); |
| 267 | } | ||
| 268 | 434 | continue; | |
| 269 | } | ||
| 270 | |||
| 271 | // Save previous header | ||
| 272 |
2/2✓ Branch 59 → 60 taken 18626 times.
✓ Branch 59 → 61 taken 7407 times.
|
26033 | if (!currentKey.isEmpty()) { |
| 273 |
1/1✓ Branch 60 → 61 taken 18626 times.
|
18626 | headers.insert(currentKey, currentValue); |
| 274 | } | ||
| 275 | |||
| 276 | // Parse new header line: "Key: Value" | ||
| 277 | 26033 | int colonPos = line.indexOf(':'); | |
| 278 |
1/2✓ Branch 63 → 64 taken 26033 times.
✗ Branch 63 → 77 not taken.
|
26033 | if (colonPos > 0) { |
| 279 |
3/3✓ Branch 64 → 65 taken 26033 times.
✓ Branch 65 → 66 taken 26033 times.
✓ Branch 66 → 67 taken 26033 times.
|
26033 | currentKey = line.left(colonPos).trimmed().toLower(); |
| 280 |
2/2✓ Branch 71 → 72 taken 26033 times.
✓ Branch 72 → 73 taken 26033 times.
|
26033 | currentValue = line.mid(colonPos + 1).trimmed(); |
| 281 | } else { | ||
| 282 | ✗ | currentKey.clear(); | |
| 283 | ✗ | currentValue.clear(); | |
| 284 | } | ||
| 285 | 26467 | } | |
| 286 | |||
| 287 | // Save last header | ||
| 288 |
1/2✓ Branch 89 → 90 taken 7407 times.
✗ Branch 89 → 91 not taken.
|
7407 | if (!currentKey.isEmpty()) { |
| 289 |
1/1✓ Branch 90 → 91 taken 7407 times.
|
7407 | headers.insert(currentKey, currentValue); |
| 290 | } | ||
| 291 | |||
| 292 | 7407 | return headers; | |
| 293 | 7407 | } | |
| 294 | |||
| 295 | 464 | QByteArray MimeParser::extractBoundary(const QString &contentType) { | |
| 296 |
1/1✓ Branch 5 → 6 taken 464 times.
|
464 | QString boundary = extractParam(contentType, QStringLiteral("boundary")); |
| 297 |
1/1✓ Branch 8 → 9 taken 464 times.
|
928 | return boundary.toUtf8(); |
| 298 | 464 | } | |
| 299 | |||
| 300 | 6613 | QString MimeParser::extractCharset(const QString &contentType) { | |
| 301 |
1/1✓ Branch 5 → 6 taken 6613 times.
|
6613 | QString charset = extractParam(contentType, QStringLiteral("charset")); |
| 302 |
2/2✓ Branch 9 → 10 taken 6161 times.
✓ Branch 9 → 15 taken 452 times.
|
6613 | if (charset.isEmpty()) { |
| 303 | 6161 | return QStringLiteral("utf-8"); // Default | |
| 304 | } | ||
| 305 | // Remove surrounding quotes | ||
| 306 |
3/8✓ Branch 16 → 17 taken 452 times.
✗ Branch 17 → 18 not taken.
✓ Branch 17 → 22 taken 452 times.
✗ Branch 19 → 20 not taken.
✗ Branch 20 → 21 not taken.
✗ Branch 20 → 22 not taken.
✗ Branch 23 → 24 not taken.
✓ Branch 23 → 29 taken 452 times.
|
452 | if (charset.startsWith('"') && charset.endsWith('"')) { |
| 307 | ✗ | charset = charset.mid(1, charset.length() - 2); | |
| 308 | } | ||
| 309 | 452 | return charset; | |
| 310 | 6613 | } | |
| 311 | |||
| 312 | 13413 | QString MimeParser::extractParam(const QString &headerValue, | |
| 313 | const QString ¶m) { | ||
| 314 | 13413 | QMap<QString, QString> parameters; | |
| 315 | 13413 | qsizetype position = 0; | |
| 316 | 13413 | const qsizetype firstSeparator = headerValue.indexOf(QLatin1Char(';')); | |
| 317 | 13413 | const qsizetype firstEquals = headerValue.indexOf(QLatin1Char('=')); | |
| 318 | // Be tolerant of a bare parameter string ("boundary=value"), which is | ||
| 319 | // useful for isolated header fragments. A regular media/disposition token | ||
| 320 | // has no '=' before its first ';' and is skipped. | ||
| 321 |
4/4✓ Branch 8 → 9 taken 5295 times.
✓ Branch 8 → 11 taken 8118 times.
✓ Branch 9 → 10 taken 5294 times.
✓ Branch 9 → 14 taken 1 time.
|
13413 | if (firstEquals < 0 || |
| 322 |
1/2✓ Branch 10 → 11 taken 5294 times.
✗ Branch 10 → 14 not taken.
|
5294 | (firstSeparator >= 0 && firstEquals > firstSeparator)) { |
| 323 |
2/2✓ Branch 11 → 12 taken 8118 times.
✓ Branch 11 → 13 taken 5294 times.
|
13412 | if (firstSeparator < 0) |
| 324 | 8118 | return {}; | |
| 325 | 5294 | position = firstSeparator + 1; | |
| 326 | } | ||
| 327 | |||
| 328 |
2/2✓ Branch 121 → 15 taken 5375 times.
✓ Branch 121 → 122 taken 5295 times.
|
10670 | while (position < headerValue.size()) { |
| 329 |
3/4✓ Branch 18 → 19 taken 10745 times.
✗ Branch 18 → 28 not taken.
✓ Branch 21 → 22 taken 5375 times.
✓ Branch 21 → 27 taken 5370 times.
|
26865 | while (position < headerValue.size() && |
| 330 |
1/2✗ Branch 26 → 27 not taken.
✓ Branch 26 → 28 taken 5375 times.
|
16120 | (headerValue.at(position).isSpace() || |
| 331 |
2/2✓ Branch 29 → 16 taken 5370 times.
✓ Branch 29 → 30 taken 5375 times.
|
16120 | headerValue.at(position) == QLatin1Char(';'))) |
| 332 | 5370 | ++position; | |
| 333 | 5375 | const qsizetype equals = headerValue.indexOf(QLatin1Char('='), position); | |
| 334 |
1/2✗ Branch 33 → 34 not taken.
✓ Branch 33 → 35 taken 5375 times.
|
5375 | if (equals < 0) |
| 335 | ✗ | break; | |
| 336 | const qsizetype separator = | ||
| 337 | 5375 | headerValue.indexOf(QLatin1Char(';'), position); | |
| 338 |
3/4✓ Branch 38 → 39 taken 80 times.
✓ Branch 38 → 41 taken 5295 times.
✗ Branch 39 → 40 not taken.
✓ Branch 39 → 41 taken 80 times.
|
5375 | if (separator >= 0 && separator < equals) { |
| 339 | ✗ | position = separator + 1; | |
| 340 | ✗ | continue; | |
| 341 | } | ||
| 342 | const QString name = | ||
| 343 |
3/3✓ Branch 41 → 42 taken 5375 times.
✓ Branch 42 → 43 taken 5375 times.
✓ Branch 43 → 44 taken 5375 times.
|
5375 | headerValue.mid(position, equals - position).trimmed().toLower(); |
| 344 | 5375 | position = equals + 1; | |
| 345 | |||
| 346 | 5375 | QString value; | |
| 347 |
3/4✓ Branch 47 → 48 taken 5375 times.
✗ Branch 47 → 54 not taken.
✓ Branch 52 → 53 taken 1527 times.
✓ Branch 52 → 54 taken 3848 times.
|
10750 | if (position < headerValue.size() && |
| 348 |
2/2✓ Branch 55 → 56 taken 1527 times.
✓ Branch 55 → 87 taken 3848 times.
|
10750 | headerValue.at(position) == QLatin1Char('"')) { |
| 349 | 1527 | ++position; | |
| 350 | 1527 | bool escaped = false; | |
| 351 |
1/2✓ Branch 74 → 57 taken 20821 times.
✗ Branch 74 → 75 not taken.
|
20821 | while (position < headerValue.size()) { |
| 352 | 20821 | const QChar character = headerValue.at(position++); | |
| 353 |
2/2✓ Branch 58 → 59 taken 1 time.
✓ Branch 58 → 61 taken 20820 times.
|
20821 | if (escaped) { |
| 354 |
1/1✓ Branch 59 → 60 taken 1 time.
|
1 | value.append(character); |
| 355 | 1 | escaped = false; | |
| 356 |
2/2✓ Branch 64 → 65 taken 1 time.
✓ Branch 64 → 66 taken 20819 times.
|
20820 | } else if (character == QLatin1Char('\\')) { |
| 357 | 1 | escaped = true; | |
| 358 |
2/2✓ Branch 69 → 70 taken 1527 times.
✓ Branch 69 → 71 taken 19292 times.
|
20819 | } else if (character == QLatin1Char('"')) { |
| 359 | 1527 | break; | |
| 360 | } else { | ||
| 361 |
1/1✓ Branch 71 → 72 taken 19292 times.
|
19292 | value.append(character); |
| 362 | } | ||
| 363 | } | ||
| 364 |
2/2✓ Branch 78 → 79 taken 12 times.
✓ Branch 78 → 85 taken 1515 times.
|
1539 | while (position < headerValue.size() && |
| 365 |
2/4✗ Branch 83 → 84 not taken.
✓ Branch 83 → 85 taken 12 times.
✗ Branch 86 → 76 not taken.
✓ Branch 86 → 98 taken 1527 times.
|
1539 | headerValue.at(position) != QLatin1Char(';')) |
| 366 | ✗ | ++position; | |
| 367 | } else { | ||
| 368 | 3848 | qsizetype end = headerValue.indexOf(QLatin1Char(';'), position); | |
| 369 |
2/2✓ Branch 90 → 91 taken 3780 times.
✓ Branch 90 → 92 taken 68 times.
|
3848 | if (end < 0) |
| 370 | 3780 | end = headerValue.size(); | |
| 371 |
2/2✓ Branch 92 → 93 taken 3848 times.
✓ Branch 93 → 94 taken 3848 times.
|
3848 | value = headerValue.mid(position, end - position).trimmed(); |
| 372 | 3848 | position = end; | |
| 373 | } | ||
| 374 |
4/7✓ Branch 99 → 100 taken 5375 times.
✗ Branch 99 → 103 not taken.
✓ Branch 100 → 101 taken 5375 times.
✓ Branch 101 → 102 taken 5375 times.
✗ Branch 101 → 103 not taken.
✓ Branch 104 → 105 taken 5375 times.
✗ Branch 104 → 106 not taken.
|
5375 | if (!name.isEmpty() && !parameters.contains(name)) |
| 375 |
1/1✓ Branch 105 → 106 taken 5375 times.
|
5375 | parameters.insert(name, value); |
| 376 |
3/4✓ Branch 107 → 108 taken 80 times.
✓ Branch 107 → 114 taken 5295 times.
✓ Branch 112 → 113 taken 80 times.
✗ Branch 112 → 114 not taken.
|
5455 | if (position < headerValue.size() && |
| 377 |
2/2✓ Branch 115 → 116 taken 80 times.
✓ Branch 115 → 117 taken 5295 times.
|
5455 | headerValue.at(position) == QLatin1Char(';')) |
| 378 | 80 | ++position; | |
| 379 | 5375 | } | |
| 380 | |||
| 381 |
1/1✓ Branch 122 → 123 taken 5295 times.
|
5295 | const QString baseName = param.toLower(); |
| 382 | // RFC 2231: an extended value (name* or name*N*) is charset'lang'%XX… — | ||
| 383 | // the charset/language prefix only in the first extended section — and is | ||
| 384 | // percent-decoded; a plain continuation (name*N) is literal text. | ||
| 385 | // S106-037: the sections were concatenated and then decoded as a whole, | ||
| 386 | // so a plain section with "%41" or "'" corrupted the name. | ||
| 387 | 5295 | QString charset; | |
| 388 | 5295 | QByteArray bytes; | |
| 389 | 5295 | bool any = false; | |
| 390 | 21 | const auto extended = [&](const QString &value, bool first) { | |
| 391 | 21 | QString payload = value; | |
| 392 |
2/2✓ Branch 3 → 4 taken 9 times.
✓ Branch 3 → 19 taken 12 times.
|
21 | if (first) { |
| 393 | 9 | const qsizetype firstTick = payload.indexOf(QLatin1Char('\'')); | |
| 394 | 9 | const qsizetype secondTick = payload.indexOf(QLatin1Char('\''), firstTick + 1); | |
| 395 |
2/4✓ Branch 10 → 11 taken 9 times.
✗ Branch 10 → 19 not taken.
✓ Branch 11 → 12 taken 9 times.
✗ Branch 11 → 19 not taken.
|
9 | if (firstTick >= 0 && secondTick > firstTick) { |
| 396 |
1/1✓ Branch 12 → 13 taken 9 times.
|
9 | charset = payload.left(firstTick); |
| 397 |
1/1✓ Branch 15 → 16 taken 9 times.
|
9 | payload = payload.mid(secondTick + 1); |
| 398 | } | ||
| 399 | } | ||
| 400 |
3/3✓ Branch 19 → 20 taken 21 times.
✓ Branch 20 → 21 taken 21 times.
✓ Branch 21 → 22 taken 21 times.
|
21 | bytes += QByteArray::fromPercentEncoding(payload.toLatin1()); |
| 401 | 21 | }; | |
| 402 |
4/4✓ Branch 125 → 126 taken 5295 times.
✓ Branch 126 → 127 taken 5295 times.
✓ Branch 128 → 129 taken 6 times.
✓ Branch 128 → 139 taken 5289 times.
|
5295 | if (parameters.contains(baseName + QLatin1Char('*'))) { |
| 403 |
3/3✓ Branch 132 → 133 taken 6 times.
✓ Branch 133 → 134 taken 6 times.
✓ Branch 134 → 135 taken 6 times.
|
6 | extended(parameters.value(baseName + QLatin1Char('*')), true); |
| 404 | 6 | any = true; | |
| 405 | } else { | ||
| 406 | 5289 | for (int section = 0;; ++section) { | |
| 407 | const QString plainKey = | ||
| 408 |
3/3✓ Branch 140 → 141 taken 5307 times.
✓ Branch 143 → 144 taken 5307 times.
✓ Branch 144 → 145 taken 5307 times.
|
5307 | baseName + QLatin1Char('*') + QString::number(section); |
| 409 |
1/1✓ Branch 149 → 150 taken 5307 times.
|
5307 | const QString encodedKey = plainKey + QLatin1Char('*'); |
| 410 |
3/3✓ Branch 150 → 151 taken 5307 times.
✓ Branch 151 → 152 taken 15 times.
✓ Branch 151 → 158 taken 5292 times.
|
5307 | if (parameters.contains(encodedKey)) |
| 411 |
2/2✓ Branch 153 → 154 taken 15 times.
✓ Branch 154 → 155 taken 15 times.
|
15 | extended(parameters.value(encodedKey), section == 0); |
| 412 |
3/3✓ Branch 158 → 159 taken 5292 times.
✓ Branch 159 → 160 taken 3 times.
✓ Branch 159 → 168 taken 5289 times.
|
5292 | else if (parameters.contains(plainKey)) |
| 413 |
3/3✓ Branch 161 → 162 taken 3 times.
✓ Branch 162 → 163 taken 3 times.
✓ Branch 163 → 164 taken 3 times.
|
3 | bytes += parameters.value(plainKey).toUtf8(); |
| 414 | else | ||
| 415 | 5289 | break; | |
| 416 | 18 | any = true; | |
| 417 | 10614 | } | |
| 418 | } | ||
| 419 | |||
| 420 |
2/2✓ Branch 179 → 180 taken 10 times.
✓ Branch 179 → 192 taken 5285 times.
|
5295 | if (any) |
| 421 |
5/7✓ Branch 181 → 182 taken 1 time.
✓ Branch 181 → 185 taken 9 times.
✓ Branch 186 → 187 taken 10 times.
✓ Branch 189 → 190 taken 1 time.
✓ Branch 189 → 191 taken 9 times.
✗ Branch 267 → 268 not taken.
✗ Branch 267 → 269 not taken.
|
11 | return convertCharset(bytes, charset.isEmpty() ? QStringLiteral("utf-8") : charset); |
| 422 |
1/1✓ Branch 193 → 194 taken 5285 times.
|
5285 | return parameters.value(baseName); |
| 423 | 13413 | } | |
| 424 | |||
| 425 | 6318 | QString MimeParser::extractFilename(const QString &contentDisposition, | |
| 426 | const QString &contentType) { | ||
| 427 | 6318 | QString fn; | |
| 428 | // Try Content-Disposition first | ||
| 429 |
2/2✓ Branch 3 → 4 taken 4121 times.
✓ Branch 3 → 13 taken 2197 times.
|
6318 | if (!contentDisposition.isEmpty()) { |
| 430 |
1/1✓ Branch 7 → 8 taken 4121 times.
|
4121 | fn = extractParam(contentDisposition, QStringLiteral("filename")); |
| 431 | } | ||
| 432 | |||
| 433 | // Fallback: Content-Type name= parameter | ||
| 434 |
5/6✓ Branch 14 → 15 taken 2206 times.
✓ Branch 14 → 18 taken 4112 times.
✓ Branch 16 → 17 taken 2206 times.
✗ Branch 16 → 18 not taken.
✓ Branch 19 → 20 taken 2206 times.
✓ Branch 19 → 29 taken 4112 times.
|
6318 | if (fn.isEmpty() && !contentType.isEmpty()) { |
| 435 |
1/1✓ Branch 23 → 24 taken 2206 times.
|
2206 | fn = extractParam(contentType, QStringLiteral("name")); |
| 436 | } | ||
| 437 | |||
| 438 | 6318 | return fn; | |
| 439 | ✗ | } | |
| 440 | |||
| 441 | 6318 | QString MimeParser::decodeAndSanitizeFilename( | |
| 442 | const QString &contentDisposition, const QString &contentType) { | ||
| 443 | const QString decoded = ImapResponseParser::decodeRfc2047( | ||
| 444 |
2/2✓ Branch 2 → 3 taken 6318 times.
✓ Branch 3 → 4 taken 6318 times.
|
6318 | extractFilename(contentDisposition, contentType)); |
| 445 |
2/2✓ Branch 6 → 7 taken 2204 times.
✓ Branch 6 → 8 taken 4114 times.
|
6318 | if (decoded.isEmpty()) |
| 446 | 2204 | return {}; | |
| 447 |
1/1✓ Branch 8 → 9 taken 4114 times.
|
4114 | return AttachmentFileSecurity::normalizedFileName(decoded); |
| 448 | 6318 | } | |
| 449 | |||
| 450 | 463 | int MimeParser::remainingParts(const ParseBudget &budget) { | |
| 451 | 463 | return qMax(0, kMaxTotalParts - budget.totalParts); | |
| 452 | } | ||
| 453 | |||
| 454 | 463 | qint64 MimeParser::remainingMaterializedBytes(const ParseBudget &budget) { | |
| 455 | 926 | return qMax<qint64>(0, | |
| 456 | 926 | kMaxMaterializedPartBytes - | |
| 457 | 463 | budget.materializedPartBytes); | |
| 458 | } | ||
| 459 | |||
| 460 | 6636 | bool MimeParser::consumePartBudget(const QByteArray &partData, | |
| 461 | ParseBudget &budget) { | ||
| 462 |
1/2✗ Branch 2 → 3 not taken.
✓ Branch 2 → 16 taken 6636 times.
|
6636 | if (budget.totalParts >= kMaxTotalParts) { |
| 463 | ✗ | budget.limitExceeded = true; | |
| 464 | ✗ | qCWarning(lcMime) << "MIME total parts limit exceeded" << kMaxTotalParts | |
| 465 | ✗ | << "- skipping remaining"; | |
| 466 | ✗ | return false; | |
| 467 | } | ||
| 468 | |||
| 469 |
1/2✗ Branch 17 → 18 not taken.
✓ Branch 17 → 31 taken 6636 times.
|
6636 | if (budget.materializedPartBytes + partData.size() > |
| 470 | kMaxMaterializedPartBytes) { | ||
| 471 | ✗ | budget.limitExceeded = true; | |
| 472 | ✗ | qCWarning(lcMime) << "MIME materialized part byte limit exceeded" | |
| 473 | ✗ | << kMaxMaterializedPartBytes << "- skipping remaining"; | |
| 474 | ✗ | return false; | |
| 475 | } | ||
| 476 | |||
| 477 | 6636 | ++budget.totalParts; | |
| 478 | 6636 | budget.materializedPartBytes += partData.size(); | |
| 479 | 6636 | return true; | |
| 480 | } | ||
| 481 | |||
| 482 | 463 | QList<QByteArray> MimeParser::splitMultipartBody(const QByteArray &body, | |
| 483 | const QByteArray &boundary, | ||
| 484 | int maxParts, | ||
| 485 | qint64 maxPartBytes, | ||
| 486 | bool *limitExceeded) { | ||
| 487 | 463 | QList<QByteArray> parts; | |
| 488 |
2/4✓ Branch 2 → 3 taken 463 times.
✗ Branch 2 → 4 not taken.
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 12 taken 463 times.
|
463 | if (maxParts == 0 || maxPartBytes == 0) { |
| 489 | ✗ | if (limitExceeded && !body.isEmpty()) | |
| 490 | ✗ | *limitExceeded = true; | |
| 491 | ✗ | return parts; | |
| 492 | } | ||
| 493 | |||
| 494 | 463 | qint64 materializedBytes = 0; | |
| 495 | |||
| 496 | 6640 | auto appendPart = [&](const QByteArray &part) { | |
| 497 |
5/6✓ Branch 2 → 3 taken 6640 times.
✗ Branch 2 → 6 not taken.
✓ Branch 4 → 5 taken 3 times.
✓ Branch 4 → 6 taken 6637 times.
✓ Branch 7 → 8 taken 3 times.
✓ Branch 7 → 23 taken 6637 times.
|
6640 | if (maxParts >= 0 && parts.size() >= maxParts) { |
| 498 |
1/2✓ Branch 8 → 9 taken 3 times.
✗ Branch 8 → 10 not taken.
|
3 | if (limitExceeded) |
| 499 | 3 | *limitExceeded = true; | |
| 500 |
5/5✓ Branch 10 → 11 taken 3 times.
✓ Branch 14 → 15 taken 3 times.
✓ Branch 15 → 16 taken 3 times.
✓ Branch 21 → 12 taken 3 times.
✓ Branch 21 → 22 taken 3 times.
|
6 | qCWarning(lcMime) << "MIME multipart split part limit reached" |
| 501 |
2/2✓ Branch 16 → 17 taken 3 times.
✓ Branch 17 → 18 taken 3 times.
|
3 | << maxParts << "- skipping remaining"; |
| 502 | 3 | return false; | |
| 503 | } | ||
| 504 |
5/6✓ Branch 23 → 24 taken 6637 times.
✗ Branch 23 → 27 not taken.
✓ Branch 25 → 26 taken 1 time.
✓ Branch 25 → 27 taken 6636 times.
✓ Branch 28 → 29 taken 1 time.
✓ Branch 28 → 44 taken 6636 times.
|
6637 | if (maxPartBytes >= 0 && materializedBytes + part.size() > maxPartBytes) { |
| 505 |
1/2✓ Branch 29 → 30 taken 1 time.
✗ Branch 29 → 31 not taken.
|
1 | if (limitExceeded) |
| 506 | 1 | *limitExceeded = true; | |
| 507 |
5/5✓ Branch 31 → 32 taken 1 time.
✓ Branch 35 → 36 taken 1 time.
✓ Branch 36 → 37 taken 1 time.
✓ Branch 42 → 33 taken 1 time.
✓ Branch 42 → 43 taken 1 time.
|
2 | qCWarning(lcMime) << "MIME multipart split byte limit reached" |
| 508 |
2/2✓ Branch 37 → 38 taken 1 time.
✓ Branch 38 → 39 taken 1 time.
|
1 | << maxPartBytes << "- skipping remaining"; |
| 509 | 1 | return false; | |
| 510 | } | ||
| 511 | |||
| 512 | 6636 | materializedBytes += part.size(); | |
| 513 | 6636 | parts.append(part); | |
| 514 | 6636 | return true; | |
| 515 | 463 | }; | |
| 516 | |||
| 517 |
1/1✓ Branch 12 → 13 taken 463 times.
|
463 | const QByteArray delimiter = "--" + boundary; |
| 518 | 463 | qsizetype partStart = -1; | |
| 519 | 463 | qsizetype lineStart = 0; | |
| 520 |
1/2✓ Branch 86 → 14 taken 896549 times.
✗ Branch 86 → 87 not taken.
|
896549 | while (lineStart <= body.size()) { |
| 521 | 896549 | const qsizetype newline = body.indexOf('\n', lineStart); | |
| 522 |
2/2✓ Branch 15 → 16 taken 76 times.
✓ Branch 15 → 17 taken 896473 times.
|
896549 | const qsizetype nextLine = newline < 0 ? body.size() : newline + 1; |
| 523 |
2/2✓ Branch 18 → 19 taken 76 times.
✓ Branch 18 → 20 taken 896473 times.
|
896549 | qsizetype lineEnd = newline < 0 ? body.size() : newline; |
| 524 |
6/6✓ Branch 21 → 22 taken 896547 times.
✓ Branch 21 → 25 taken 2 times.
✓ Branch 23 → 24 taken 896467 times.
✓ Branch 23 → 25 taken 80 times.
✓ Branch 26 → 27 taken 896467 times.
✓ Branch 26 → 28 taken 82 times.
|
896549 | if (lineEnd > lineStart && body.at(lineEnd - 1) == '\r') |
| 525 | 896467 | --lineEnd; | |
| 526 | 1793098 | const QByteArrayView line(body.constData() + lineStart, | |
| 527 | 896549 | lineEnd - lineStart); | |
| 528 | |||
| 529 | 896549 | bool closing = false; | |
| 530 | 896549 | bool legalBoundary = false; | |
| 531 |
2/2✓ Branch 32 → 33 taken 7682 times.
✓ Branch 32 → 47 taken 888867 times.
|
896549 | if (line.startsWith(delimiter)) { |
| 532 |
1/1✓ Branch 34 → 35 taken 7682 times.
|
7682 | QByteArrayView suffix = line.sliced(delimiter.size()); |
| 533 |
2/2✓ Branch 37 → 38 taken 460 times.
✓ Branch 37 → 39 taken 7222 times.
|
7682 | if (suffix.startsWith(QByteArrayView("--"))) { |
| 534 | 460 | closing = true; | |
| 535 |
1/1✓ Branch 38 → 39 taken 460 times.
|
460 | suffix = suffix.sliced(2); |
| 536 | } | ||
| 537 | 7682 | legalBoundary = true; | |
| 538 |
2/2✓ Branch 45 → 41 taken 585 times.
✓ Branch 45 → 46 taken 7101 times.
|
7686 | for (const char character : suffix) { |
| 539 |
4/4✓ Branch 41 → 42 taken 583 times.
✓ Branch 41 → 44 taken 2 times.
✓ Branch 42 → 43 taken 581 times.
✓ Branch 42 → 44 taken 2 times.
|
585 | if (character != ' ' && character != '\t') { |
| 540 | 581 | legalBoundary = false; | |
| 541 | 581 | break; | |
| 542 | } | ||
| 543 | } | ||
| 544 | } | ||
| 545 | |||
| 546 |
2/2✓ Branch 47 → 48 taken 7101 times.
✓ Branch 47 → 71 taken 889448 times.
|
896549 | if (legalBoundary) { |
| 547 |
2/2✓ Branch 48 → 49 taken 6639 times.
✓ Branch 48 → 68 taken 462 times.
|
7101 | if (partStart >= 0) { |
| 548 | 6639 | qsizetype partEnd = lineStart; | |
| 549 |
3/6✓ Branch 49 → 50 taken 6639 times.
✗ Branch 49 → 53 not taken.
✓ Branch 51 → 52 taken 6639 times.
✗ Branch 51 → 53 not taken.
✓ Branch 54 → 55 taken 6639 times.
✗ Branch 54 → 56 not taken.
|
6639 | if (partEnd > partStart && body.at(partEnd - 1) == '\n') |
| 550 | 6639 | --partEnd; | |
| 551 |
5/6✓ Branch 56 → 57 taken 6639 times.
✗ Branch 56 → 60 not taken.
✓ Branch 58 → 59 taken 6638 times.
✓ Branch 58 → 60 taken 1 time.
✓ Branch 61 → 62 taken 6638 times.
✓ Branch 61 → 63 taken 1 time.
|
6639 | if (partEnd > partStart && body.at(partEnd - 1) == '\r') |
| 552 | 6638 | --partEnd; | |
| 553 |
4/4✓ Branch 63 → 64 taken 6639 times.
✓ Branch 64 → 65 taken 6639 times.
✓ Branch 66 → 67 taken 4 times.
✓ Branch 66 → 68 taken 6635 times.
|
6639 | if (!appendPart(body.mid(partStart, partEnd - partStart))) |
| 554 | 463 | break; | |
| 555 | } | ||
| 556 |
2/2✓ Branch 68 → 69 taken 457 times.
✓ Branch 68 → 70 taken 6640 times.
|
7097 | if (closing) |
| 557 | 457 | break; | |
| 558 | 6640 | partStart = nextLine; | |
| 559 | } | ||
| 560 | |||
| 561 |
2/2✓ Branch 71 → 72 taken 2 times.
✓ Branch 71 → 83 taken 896086 times.
|
896088 | if (newline < 0) { |
| 562 |
5/6✓ Branch 72 → 73 taken 1 time.
✓ Branch 72 → 76 taken 1 time.
✓ Branch 74 → 75 taken 1 time.
✗ Branch 74 → 76 not taken.
✓ Branch 77 → 78 taken 1 time.
✓ Branch 77 → 82 taken 1 time.
|
2 | if (partStart >= 0 && partStart < body.size()) |
| 563 |
2/2✓ Branch 78 → 79 taken 1 time.
✓ Branch 79 → 80 taken 1 time.
|
1 | appendPart(body.mid(partStart)); |
| 564 | 2 | break; | |
| 565 | } | ||
| 566 | 896086 | lineStart = nextLine; | |
| 567 | } | ||
| 568 | |||
| 569 | 463 | return parts; | |
| 570 | 463 | } | |
| 571 | |||
| 572 | 6636 | void MimeParser::parsePart(const QByteArray &partData, MimeMessage &result, | |
| 573 | ParseBudget &budget, int depth, bool alternative) { | ||
| 574 |
1/2✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 6636 times.
|
6636 | if (partData.isEmpty()) { |
| 575 | 490 | return; | |
| 576 | } | ||
| 577 | |||
| 578 | // T-405/Bug 19: Prevent stack overflow from deeply nested MIME | ||
| 579 |
2/2✓ Branch 5 → 6 taken 4 times.
✓ Branch 5 → 19 taken 6632 times.
|
6636 | if (depth >= kMaxMimeDepth) { |
| 580 | 4 | result.limitExceeded = true; | |
| 581 |
6/6✓ Branch 6 → 7 taken 4 times.
✓ Branch 10 → 11 taken 4 times.
✓ Branch 11 → 12 taken 4 times.
✓ Branch 12 → 13 taken 4 times.
✓ Branch 17 → 8 taken 4 times.
✓ Branch 17 → 18 taken 4 times.
|
8 | qCWarning(lcMime) << "MIME nesting depth exceeded" << kMaxMimeDepth |
| 582 |
1/1✓ Branch 13 → 14 taken 4 times.
|
4 | << "— skipping further parts"; |
| 583 | 4 | return; | |
| 584 | } | ||
| 585 | |||
| 586 |
1/1✓ Branch 19 → 20 taken 6632 times.
|
6632 | auto [headerBlock, bodyBlock] = splitHeaderBody(partData); |
| 587 |
1/1✓ Branch 22 → 23 taken 6632 times.
|
6632 | auto headers = parseHeaders(headerBlock); |
| 588 | |||
| 589 | 13264 | QString contentType = headers.value(QStringLiteral("content-type"), | |
| 590 |
1/1✓ Branch 29 → 30 taken 6632 times.
|
19896 | QStringLiteral("text/plain")); |
| 591 | QString transferEncoding = headers.value( | ||
| 592 |
1/1✓ Branch 40 → 41 taken 6632 times.
|
13264 | QStringLiteral("content-transfer-encoding"), QStringLiteral("7bit")); |
| 593 | QString contentDisposition = | ||
| 594 |
1/1✓ Branch 49 → 50 taken 6632 times.
|
13264 | headers.value(QStringLiteral("content-disposition")); |
| 595 | QString contentId = | ||
| 596 |
2/2✓ Branch 57 → 58 taken 6632 times.
✓ Branch 58 → 59 taken 6632 times.
|
13264 | normalizeContentId(headers.value(QStringLiteral("content-id"))); |
| 597 | |||
| 598 |
1/1✓ Branch 63 → 64 taken 6632 times.
|
6632 | QString ctLower = contentType.toLower(); |
| 599 | |||
| 600 | // Recursive: this part is itself multipart | ||
| 601 |
3/3✓ Branch 64 → 65 taken 6632 times.
✓ Branch 65 → 66 taken 322 times.
✓ Branch 65 → 93 taken 6310 times.
|
6632 | if (isMultipartType(ctLower)) { |
| 602 |
1/1✓ Branch 66 → 67 taken 322 times.
|
322 | QByteArray boundary = extractBoundary(contentType); |
| 603 |
1/2✓ Branch 68 → 69 taken 322 times.
✗ Branch 68 → 91 not taken.
|
322 | if (!boundary.isEmpty()) { |
| 604 | // F-07 (SPRINT-104): inside multipart/alternative only the first | ||
| 605 | // part of a type is kept (the alternatives show the same text); | ||
| 606 | // every other multipart — mixed above all — has its parts follow | ||
| 607 | // each other: Apple Mail writes text, inline image, more text, and | ||
| 608 | // mailing lists append a footer part. Those texts are appended. | ||
| 609 | const bool alternative = | ||
| 610 |
1/1✓ Branch 72 → 73 taken 322 times.
|
322 | ctLower.startsWith(QStringLiteral("multipart/alternative")); |
| 611 | auto subParts = | ||
| 612 | splitMultipartBody(bodyBlock, boundary, remainingParts(budget), | ||
| 613 |
3/3✓ Branch 75 → 76 taken 322 times.
✓ Branch 76 → 77 taken 322 times.
✓ Branch 77 → 78 taken 322 times.
|
322 | remainingMaterializedBytes(budget), &budget.limitExceeded); |
| 614 |
4/4✓ Branch 78 → 79 taken 322 times.
✓ Branch 79 → 80 taken 322 times.
✓ Branch 88 → 81 taken 1334 times.
✓ Branch 88 → 89 taken 322 times.
|
1656 | for (const auto &sub : subParts) { |
| 615 |
2/3✓ Branch 82 → 83 taken 1334 times.
✗ Branch 83 → 84 not taken.
✓ Branch 83 → 85 taken 1334 times.
|
1334 | if (!consumePartBudget(sub, budget)) { |
| 616 | ✗ | break; | |
| 617 | } | ||
| 618 |
1/1✓ Branch 85 → 86 taken 1334 times.
|
1334 | parsePart(sub, result, budget, depth + 1, alternative); |
| 619 | } | ||
| 620 | 322 | } | |
| 621 | 322 | return; | |
| 622 | 322 | } | |
| 623 | |||
| 624 | // Decode the body | ||
| 625 |
1/1✓ Branch 93 → 94 taken 6310 times.
|
6310 | QByteArray decoded = decodeTransferEncoding(bodyBlock, transferEncoding); |
| 626 |
1/1✓ Branch 94 → 95 taken 6310 times.
|
6310 | QString charset = extractCharset(contentType); |
| 627 | QString filename = | ||
| 628 |
1/1✓ Branch 95 → 96 taken 6310 times.
|
6310 | decodeAndSanitizeFilename(contentDisposition, contentType); |
| 629 | |||
| 630 | // Determine if this is an attachment. | ||
| 631 | // Note: Content-ID alone does NOT mean attachment for text/* parts. | ||
| 632 | // Many mailers (e.g. LinkedIn) set Content-ID on body parts. | ||
| 633 |
2/2✓ Branch 96 → 97 taken 6310 times.
✓ Branch 100 → 101 taken 6310 times.
|
12620 | const bool isExplicitAttachment = contentDisposition.trimmed().startsWith( |
| 634 | 12620 | QStringLiteral("attachment"), Qt::CaseInsensitive); | |
| 635 | 6310 | const bool hasAttachmentFilename = !filename.isEmpty(); | |
| 636 | |||
| 637 | // Sprint 93 S93.5.2: an iMIP text/calendar body part is kept as an | ||
| 638 | // attachment so the reader can show the invitation card. | ||
| 639 |
1/1✓ Branch 108 → 109 taken 6310 times.
|
6310 | const bool isCalendar = ctLower.startsWith(QStringLiteral("text/calendar")); |
| 640 | |||
| 641 | // Text parts (not explicitly marked as attachment) | ||
| 642 |
5/10✓ Branch 116 → 117 taken 172 times.
✓ Branch 116 → 120 taken 21 times.
✓ Branch 123 → 124 taken 6310 times.
✗ Branch 123 → 125 not taken.
✓ Branch 125 → 126 taken 164 times.
✓ Branch 125 → 162 taken 6146 times.
✗ Branch 320 → 321 not taken.
✗ Branch 320 → 322 not taken.
✗ Branch 324 → 325 not taken.
✗ Branch 324 → 326 not taken.
|
12813 | if (ctLower.startsWith(QStringLiteral("text/")) && !isCalendar && |
| 643 |
7/9✓ Branch 114 → 115 taken 6310 times.
✓ Branch 115 → 116 taken 193 times.
✓ Branch 115 → 120 taken 6117 times.
✓ Branch 117 → 118 taken 164 times.
✓ Branch 117 → 120 taken 8 times.
✓ Branch 118 → 119 taken 164 times.
✗ Branch 118 → 120 not taken.
✓ Branch 121 → 122 taken 6310 times.
✗ Branch 121 → 123 not taken.
|
12813 | !isExplicitAttachment && !hasAttachmentFilename) { |
| 644 |
1/1✓ Branch 126 → 127 taken 164 times.
|
164 | QString text = convertCharset(decoded, charset); |
| 645 |
3/3✓ Branch 130 → 131 taken 164 times.
✓ Branch 133 → 134 taken 44 times.
✓ Branch 133 → 147 taken 120 times.
|
164 | if (ctLower.startsWith(QStringLiteral("text/html"))) { |
| 646 | // Keep first HTML part (or best one from multipart/alternative) | ||
| 647 |
1/2✓ Branch 135 → 136 taken 44 times.
✗ Branch 135 → 137 not taken.
|
44 | if (result.textHtml.isEmpty()) { |
| 648 | 44 | result.textHtml = text; | |
| 649 | ✗ | } else if (!alternative) { | |
| 650 | // F-07: a later HTML part of a mixed chain follows the first. | ||
| 651 | ✗ | result.textHtml += QStringLiteral("\n<hr>\n") + text; | |
| 652 | } | ||
| 653 | } else { | ||
| 654 | // text/plain or other text types | ||
| 655 |
2/2✓ Branch 148 → 149 taken 118 times.
✓ Branch 148 → 150 taken 2 times.
|
120 | if (result.textPlain.isEmpty()) { |
| 656 | 118 | result.textPlain = text; | |
| 657 |
2/2✓ Branch 150 → 151 taken 1 time.
✓ Branch 150 → 160 taken 1 time.
|
2 | } else if (!alternative) { |
| 658 | // F-07: a later plain-text part of a mixed chain follows the | ||
| 659 | // first (text, inline image, more text; list footers). | ||
| 660 |
2/2✓ Branch 154 → 155 taken 1 time.
✓ Branch 155 → 156 taken 1 time.
|
1 | result.textPlain += QStringLiteral("\n\n") + text; |
| 661 | } | ||
| 662 | } | ||
| 663 | 164 | return; | |
| 664 | 164 | } | |
| 665 | |||
| 666 | // Everything else is an attachment | ||
| 667 | 6146 | MimePart part; | |
| 668 |
2/2✓ Branch 164 → 165 taken 6146 times.
✓ Branch 165 → 166 taken 6146 times.
|
6146 | part.contentType = ctLower.section(';', 0, 0).trimmed(); |
| 669 | 6146 | part.charset = charset; | |
| 670 |
2/2✓ Branch 170 → 171 taken 6146 times.
✓ Branch 171 → 172 taken 6146 times.
|
6146 | part.transferEncoding = transferEncoding.trimmed().toLower(); |
| 671 | 6146 | part.body = decoded; | |
| 672 |
6/6✓ Branch 177 → 178 taken 2037 times.
✓ Branch 177 → 182 taken 4109 times.
✓ Branch 178 → 179 taken 21 times.
✓ Branch 178 → 182 taken 2016 times.
✓ Branch 185 → 186 taken 21 times.
✓ Branch 185 → 187 taken 6125 times.
|
6167 | part.filename = filename.isEmpty() && isCalendar ? QStringLiteral("invite.ics") : filename; |
| 673 | 6146 | part.contentId = contentId; | |
| 674 | 6146 | part.isAttachment = true; | |
| 675 |
2/2✓ Branch 189 → 190 taken 27 times.
✓ Branch 189 → 197 taken 6119 times.
|
6146 | if (!contentId.isEmpty()) { |
| 676 |
2/2✓ Branch 190 → 191 taken 2 times.
✓ Branch 190 → 192 taken 25 times.
|
27 | if (isExplicitAttachment) { |
| 677 | 2 | part.presentation = | |
| 678 | AttachmentPresentation::ExplicitAttachmentWithContentId; | ||
| 679 | } else { | ||
| 680 |
1/1✓ Branch 192 → 193 taken 25 times.
|
50 | part.presentation = isAllowedInlineImage(part.contentType, part.body) |
| 681 |
2/2✓ Branch 193 → 194 taken 20 times.
✓ Branch 193 → 195 taken 5 times.
|
25 | ? AttachmentPresentation::InlineCandidate |
| 682 | : AttachmentPresentation::RejectedInline; | ||
| 683 | } | ||
| 684 | } | ||
| 685 | |||
| 686 |
1/1✓ Branch 197 → 198 taken 6146 times.
|
6146 | result.attachments.append(part); |
| 687 | 10040 | } | |
| 688 | |||
| 689 | 12813 | QString MimeParser::normalizeContentId(const QString &contentId) { | |
| 690 | 12813 | return canonicalContentId(contentId); | |
| 691 | } | ||
| 692 | |||
| 693 | 63 | bool MimeParser::isAllowedInlineImage(const QString &contentType, | |
| 694 | const QByteArray &body) { | ||
| 695 |
6/6✓ Branch 3 → 4 taken 61 times.
✓ Branch 3 → 6 taken 2 times.
✓ Branch 5 → 6 taken 2 times.
✓ Branch 5 → 7 taken 59 times.
✓ Branch 8 → 9 taken 4 times.
✓ Branch 8 → 10 taken 59 times.
|
63 | if (body.isEmpty() || body.size() > MaxInlineImageBytes) |
| 696 | 4 | return false; | |
| 697 | |||
| 698 |
3/3✓ Branch 12 → 13 taken 59 times.
✓ Branch 13 → 14 taken 59 times.
✓ Branch 14 → 15 taken 59 times.
|
59 | const QString type = contentType.section(';', 0, 0).trimmed().toLower(); |
| 699 |
2/2✓ Branch 19 → 20 taken 42 times.
✓ Branch 19 → 23 taken 17 times.
|
59 | if (type == QLatin1String("image/png")) |
| 700 | 42 | return body.startsWith("\x89PNG\r\n\x1a\n"); | |
| 701 |
2/2✓ Branch 25 → 26 taken 5 times.
✓ Branch 25 → 37 taken 12 times.
|
17 | if (type == QLatin1String("image/jpeg")) |
| 702 |
4/4✓ Branch 29 → 30 taken 3 times.
✓ Branch 29 → 35 taken 1 time.
✓ Branch 31 → 32 taken 2 times.
✓ Branch 31 → 35 taken 1 time.
|
12 | return body.size() >= 3 && static_cast<uchar>(body.at(0)) == 0xFF && |
| 703 |
4/4✓ Branch 27 → 28 taken 4 times.
✓ Branch 27 → 35 taken 1 time.
✓ Branch 33 → 34 taken 1 time.
✓ Branch 33 → 35 taken 1 time.
|
12 | static_cast<uchar>(body.at(1)) == 0xD8 && |
| 704 | 7 | static_cast<uchar>(body.at(2)) == 0xFF; | |
| 705 |
2/2✓ Branch 39 → 40 taken 3 times.
✓ Branch 39 → 50 taken 9 times.
|
12 | if (type == QLatin1String("image/gif")) |
| 706 |
4/4✓ Branch 42 → 43 taken 2 times.
✓ Branch 42 → 46 taken 1 time.
✓ Branch 45 → 46 taken 1 time.
✓ Branch 45 → 47 taken 1 time.
|
3 | return body.startsWith("GIF87a") || body.startsWith("GIF89a"); |
| 707 |
2/2✓ Branch 52 → 53 taken 4 times.
✓ Branch 52 → 72 taken 5 times.
|
9 | if (type == QLatin1String("image/webp")) |
| 708 |
6/6✓ Branch 54 → 55 taken 3 times.
✓ Branch 54 → 64 taken 1 time.
✓ Branch 57 → 58 taken 2 times.
✓ Branch 57 → 64 taken 1 time.
✓ Branch 62 → 63 taken 1 time.
✓ Branch 62 → 64 taken 1 time.
|
6 | return body.size() >= 12 && body.startsWith("RIFF") && |
| 709 |
7/13✓ Branch 60 → 61 taken 2 times.
✓ Branch 65 → 66 taken 2 times.
✓ Branch 65 → 67 taken 2 times.
✓ Branch 67 → 68 taken 2 times.
✓ Branch 67 → 69 taken 2 times.
✓ Branch 69 → 70 taken 2 times.
✓ Branch 69 → 71 taken 2 times.
✗ Branch 90 → 91 not taken.
✗ Branch 90 → 92 not taken.
✗ Branch 94 → 95 not taken.
✗ Branch 94 → 96 not taken.
✗ Branch 98 → 99 not taken.
✗ Branch 98 → 100 not taken.
|
6 | body.mid(8, 4) == QByteArrayLiteral("WEBP"); |
| 710 |
2/2✓ Branch 74 → 75 taken 2 times.
✓ Branch 74 → 78 taken 3 times.
|
5 | if (type == QLatin1String("image/bmp")) |
| 711 | 2 | return body.startsWith("BM"); | |
| 712 | 3 | return false; | |
| 713 | 59 | } | |
| 714 | |||
| 715 | 443 | void MimeParser::finalizeInlineParts(MimeMessage &result) { | |
| 716 | 443 | QHash<QString, int> contentIdCounts; | |
| 717 |
4/4✓ Branch 2 → 3 taken 443 times.
✓ Branch 3 → 4 taken 443 times.
✓ Branch 15 → 5 taken 6154 times.
✓ Branch 15 → 16 taken 443 times.
|
6597 | for (MimePart &part : result.attachments) { |
| 718 |
1/1✓ Branch 6 → 7 taken 6154 times.
|
6154 | part.contentId = normalizeContentId(part.contentId); |
| 719 |
2/2✓ Branch 10 → 11 taken 30 times.
✓ Branch 10 → 13 taken 6124 times.
|
6154 | if (!part.contentId.isEmpty()) |
| 720 |
1/1✓ Branch 11 → 12 taken 30 times.
|
30 | ++contentIdCounts[part.contentId]; |
| 721 | } | ||
| 722 | |||
| 723 | 443 | qint64 aggregateBytes = 0; | |
| 724 |
4/4✓ Branch 16 → 17 taken 443 times.
✓ Branch 17 → 18 taken 443 times.
✓ Branch 34 → 19 taken 6154 times.
✓ Branch 34 → 35 taken 443 times.
|
6597 | for (MimePart &part : result.attachments) { |
| 725 |
2/2✓ Branch 20 → 21 taken 6133 times.
✓ Branch 20 → 22 taken 21 times.
|
6154 | if (part.presentation != AttachmentPresentation::InlineCandidate) |
| 726 | 6133 | continue; | |
| 727 |
4/4✓ Branch 23 → 24 taken 17 times.
✓ Branch 23 → 26 taken 4 times.
✓ Branch 28 → 29 taken 5 times.
✓ Branch 28 → 30 taken 16 times.
|
38 | if (contentIdCounts.value(part.contentId) != 1 || |
| 728 | 17 | part.body.size() > | |
| 729 |
2/2✓ Branch 25 → 26 taken 1 time.
✓ Branch 25 → 27 taken 16 times.
|
17 | MaxInlineImageAggregateBytes - aggregateBytes) { |
| 730 | 5 | part.presentation = AttachmentPresentation::RejectedInline; | |
| 731 | 5 | continue; | |
| 732 | } | ||
| 733 | 16 | aggregateBytes += part.body.size(); | |
| 734 | } | ||
| 735 | 443 | } | |
| 736 |