service/ImapResponseParser.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #include "ImapResponseParser.h" | ||
| 2 | |||
| 3 | #include "util/MessageId.h" | ||
| 4 | #include "util/SpamHeaders.h" | ||
| 5 | |||
| 6 | #include <QLoggingCategory> | ||
| 7 | #include <QRegularExpression> | ||
| 8 | #include <QStringDecoder> | ||
| 9 | |||
| 10 | #include <cstring> | ||
| 11 | #include <limits> | ||
| 12 | |||
| 13 |
4/7✓ Branch 2 → 3 taken 4 times.
✓ Branch 2 → 8 taken 65 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.
|
69 | Q_LOGGING_CATEGORY(lcImapParser, "mailjd.imap.parser") |
| 14 | |||
| 15 | namespace { | ||
| 16 | |||
| 17 | // One character of FETCH data as ASCII (non-ASCII never matches an item). | ||
| 18 | 25653 | inline char asciiAt(const QString &data, qsizetype i) { | |
| 19 | 25653 | const ushort u = data.at(i).unicode(); | |
| 20 |
1/2✓ Branch 4 → 5 taken 25653 times.
✗ Branch 4 → 6 not taken.
|
25653 | return u < 128 ? char(u) : char(0x7f); |
| 21 | } | ||
| 22 | 1054 | inline char asciiAt(const QByteArray &data, qsizetype i) { return data.at(i); } | |
| 23 | |||
| 24 | // S106-019/021: the position right after the FETCH attribute `name` on the | ||
| 25 | // top level of the attribute list, or -1. Quoted strings (with \-escapes) | ||
| 26 | // and nested lists are skipped, so an ENVELOPE subject "UUID 42" or a | ||
| 27 | // MYFLAGS item can never be taken for the UID or FLAGS item. The name must | ||
| 28 | // start a token (after '(' or a blank) and be followed by one of `follow`. | ||
| 29 | // Works on QString and QByteArray data alike. | ||
| 30 | template <typename Data> | ||
| 31 | 667 | qsizetype topLevelItem(const Data &data, const char *name, const char *follow = " (") { | |
| 32 | 667 | const qsizetype length = qsizetype(qstrlen(name)); | |
| 33 | // The attribute list is the first parenthesized level ("(UID 1 …)", | ||
| 34 | // "* 1 FETCH (UID 1 …)") or the bare text ("UID 1 …"): depth 0 or 1. | ||
| 35 | 667 | int depth = 0; | |
| 36 | 667 | bool quoted = false; | |
| 37 | 667 | bool escaped = false; | |
| 38 |
4/4long long (anonymous namespace)::topLevelItem<QByteArray>(QByteArray const&, char const*, char const*):
✓ Branch 55 → 4 taken 556 times.
✓ Branch 55 → 56 taken 5 times.
long long (anonymous namespace)::topLevelItem<QString>(QString const&, char const*, char const*):
✓ Branch 61 → 4 taken 20918 times.
✓ Branch 61 → 62 taken 185 times.
|
21664 | for (qsizetype i = 0; i < data.size(); ++i) { |
| 39 | 21474 | const char c = asciiAt(data, i); | |
| 40 |
4/4long long (anonymous namespace)::topLevelItem<QByteArray>(QByteArray const&, char const*, char const*):
✓ Branch 5 → 6 taken 56 times.
✓ Branch 5 → 13 taken 500 times.
long long (anonymous namespace)::topLevelItem<QString>(QString const&, char const*, char const*):
✓ Branch 5 → 6 taken 6176 times.
✓ Branch 5 → 13 taken 14742 times.
|
21474 | if (quoted) { |
| 41 |
4/4long long (anonymous namespace)::topLevelItem<QByteArray>(QByteArray const&, char const*, char const*):
✓ Branch 6 → 7 taken 4 times.
✓ Branch 6 → 8 taken 52 times.
long long (anonymous namespace)::topLevelItem<QString>(QString const&, char const*, char const*):
✓ Branch 6 → 7 taken 86 times.
✓ Branch 6 → 8 taken 6090 times.
|
6232 | if (escaped) |
| 42 | 90 | escaped = false; | |
| 43 |
4/4long long (anonymous namespace)::topLevelItem<QByteArray>(QByteArray const&, char const*, char const*):
✓ Branch 8 → 9 taken 4 times.
✓ Branch 8 → 10 taken 48 times.
long long (anonymous namespace)::topLevelItem<QString>(QString const&, char const*, char const*):
✓ Branch 8 → 9 taken 89 times.
✓ Branch 8 → 10 taken 6001 times.
|
6142 | else if (c == '\\') |
| 44 | 93 | escaped = true; | |
| 45 |
4/4long long (anonymous namespace)::topLevelItem<QByteArray>(QByteArray const&, char const*, char const*):
✓ Branch 10 → 11 taken 5 times.
✓ Branch 10 → 12 taken 43 times.
long long (anonymous namespace)::topLevelItem<QString>(QString const&, char const*, char const*):
✓ Branch 10 → 11 taken 487 times.
✓ Branch 10 → 12 taken 5514 times.
|
6049 | else if (c == '"') |
| 46 | 492 | quoted = false; | |
| 47 | 6232 | continue; | |
| 48 | } | ||
| 49 |
4/4long long (anonymous namespace)::topLevelItem<QByteArray>(QByteArray const&, char const*, char const*):
✓ Branch 13 → 14 taken 5 times.
✓ Branch 13 → 15 taken 495 times.
long long (anonymous namespace)::topLevelItem<QString>(QString const&, char const*, char const*):
✓ Branch 13 → 14 taken 493 times.
✓ Branch 13 → 15 taken 14249 times.
|
15242 | if (c == '"') { |
| 50 | 498 | quoted = true; | |
| 51 | 498 | continue; | |
| 52 | } | ||
| 53 |
4/4long long (anonymous namespace)::topLevelItem<QByteArray>(QByteArray const&, char const*, char const*):
✓ Branch 15 → 16 taken 71 times.
✓ Branch 15 → 17 taken 424 times.
long long (anonymous namespace)::topLevelItem<QString>(QString const&, char const*, char const*):
✓ Branch 15 → 16 taken 1053 times.
✓ Branch 15 → 17 taken 13196 times.
|
14744 | if (c == '(') { |
| 54 | 1124 | ++depth; | |
| 55 | 1124 | continue; | |
| 56 | } | ||
| 57 |
4/4long long (anonymous namespace)::topLevelItem<QByteArray>(QByteArray const&, char const*, char const*):
✓ Branch 17 → 18 taken 8 times.
✓ Branch 17 → 19 taken 416 times.
long long (anonymous namespace)::topLevelItem<QString>(QString const&, char const*, char const*):
✓ Branch 17 → 18 taken 626 times.
✓ Branch 17 → 19 taken 12570 times.
|
13620 | if (c == ')') { |
| 58 | 634 | --depth; | |
| 59 | 634 | continue; | |
| 60 | } | ||
| 61 |
22/24long long (anonymous namespace)::topLevelItem<QByteArray>(QByteArray const&, char const*, char const*):
✓ Branch 19 → 20 taken 403 times.
✓ Branch 19 → 27 taken 13 times.
✓ Branch 20 → 21 taken 395 times.
✓ Branch 20 → 28 taken 8 times.
✓ Branch 22 → 23 taken 327 times.
✓ Branch 22 → 28 taken 68 times.
✓ Branch 24 → 25 taken 248 times.
✓ Branch 24 → 28 taken 79 times.
✓ Branch 26 → 27 taken 247 times.
✓ Branch 26 → 28 taken 1 time.
✓ Branch 29 → 30 taken 260 times.
✓ Branch 29 → 31 taken 156 times.
long long (anonymous namespace)::topLevelItem<QString>(QString const&, char const*, char const*):
✓ Branch 19 → 20 taken 6422 times.
✓ Branch 19 → 33 taken 6148 times.
✓ Branch 20 → 21 taken 6422 times.
✗ Branch 20 → 34 not taken.
✓ Branch 24 → 25 taken 5825 times.
✓ Branch 24 → 34 taken 597 times.
✓ Branch 28 → 29 taken 4898 times.
✓ Branch 28 → 34 taken 927 times.
✓ Branch 32 → 33 taken 4898 times.
✗ Branch 32 → 34 not taken.
✓ Branch 35 → 36 taken 11046 times.
✓ Branch 35 → 37 taken 1524 times.
|
12986 | if (depth > 1 || (i > 0 && data.at(i - 1) != '(' && data.at(i - 1) != ' ' && data.at(i - 1) != '\t')) |
| 62 | 11306 | continue; | |
| 63 |
4/4long long (anonymous namespace)::topLevelItem<QByteArray>(QByteArray const&, char const*, char const*):
✓ Branch 32 → 33 taken 2 times.
✓ Branch 32 → 34 taken 154 times.
long long (anonymous namespace)::topLevelItem<QString>(QString const&, char const*, char const*):
✓ Branch 38 → 39 taken 19 times.
✓ Branch 38 → 40 taken 1505 times.
|
1680 | if (length > data.size() - i) |
| 64 | 21 | return -1; | |
| 65 | 1659 | bool same = true; | |
| 66 |
8/8long long (anonymous namespace)::topLevelItem<QByteArray>(QByteArray const&, char const*, char const*):
✓ Branch 40 → 41 taken 457 times.
✓ Branch 40 → 42 taken 65 times.
✓ Branch 41 → 35 taken 368 times.
✓ Branch 41 → 42 taken 89 times.
long long (anonymous namespace)::topLevelItem<QString>(QString const&, char const*, char const*):
✓ Branch 46 → 47 taken 4372 times.
✓ Branch 46 → 48 taken 393 times.
✓ Branch 47 → 41 taken 3260 times.
✓ Branch 47 → 48 taken 1112 times.
|
5287 | for (qsizetype k = 0; k < length && same; ++k) { |
| 67 | 3628 | char actual = asciiAt(data, i + k); | |
| 68 |
6/8long long (anonymous namespace)::topLevelItem<QByteArray>(QByteArray const&, char const*, char const*):
✓ Branch 36 → 37 taken 8 times.
✓ Branch 36 → 39 taken 360 times.
✓ Branch 37 → 38 taken 8 times.
✗ Branch 37 → 39 not taken.
long long (anonymous namespace)::topLevelItem<QString>(QString const&, char const*, char const*):
✓ Branch 42 → 43 taken 1 time.
✓ Branch 42 → 45 taken 3259 times.
✓ Branch 43 → 44 taken 1 time.
✗ Branch 43 → 45 not taken.
|
3628 | if (actual >= 'a' && actual <= 'z') actual = char(actual - ('a' - 'A')); |
| 69 | 3628 | same = actual == name[k]; | |
| 70 | } | ||
| 71 | 1659 | const qsizetype end = i + length; | |
| 72 |
15/20long long (anonymous namespace)::topLevelItem<QByteArray>(QByteArray const&, char const*, char const*):
✓ Branch 42 → 43 taken 65 times.
✓ Branch 42 → 50 taken 89 times.
✓ Branch 44 → 45 taken 65 times.
✗ Branch 44 → 50 not taken.
✓ Branch 46 → 47 taken 65 times.
✗ Branch 46 → 50 not taken.
✓ Branch 48 → 49 taken 63 times.
✓ Branch 48 → 50 taken 2 times.
✓ Branch 51 → 52 taken 63 times.
✓ Branch 51 → 53 taken 91 times.
long long (anonymous namespace)::topLevelItem<QString>(QString const&, char const*, char const*):
✓ Branch 48 → 49 taken 393 times.
✓ Branch 48 → 56 taken 1112 times.
✓ Branch 50 → 51 taken 393 times.
✗ Branch 50 → 56 not taken.
✓ Branch 52 → 53 taken 393 times.
✗ Branch 52 → 56 not taken.
✓ Branch 54 → 55 taken 393 times.
✗ Branch 54 → 56 not taken.
✓ Branch 57 → 58 taken 393 times.
✓ Branch 57 → 59 taken 1112 times.
|
1659 | if (same && end < data.size() && asciiAt(data, end) != 0 && std::strchr(follow, asciiAt(data, end))) |
| 73 | 456 | return end; | |
| 74 | } | ||
| 75 | 190 | return -1; | |
| 76 | } | ||
| 77 | |||
| 78 | // A positive 63-bit number right after `pos` (blanks skipped); 0 when the | ||
| 79 | // digits are missing, overflow (S106-018) or are not positive. | ||
| 80 | template <typename Data> | ||
| 81 | 285 | qint64 numberAfter(const Data &data, qsizetype pos) { | |
| 82 |
7/8✓ Branch 4 → 5 taken 550 times.
✓ Branch 4 → 12 taken 10 times.
✓ Branch 6 → 7 taken 550 times.
✗ Branch 6 → 12 not taken.
✓ Branch 10 → 11 taken 275 times.
✓ Branch 10 → 12 taken 275 times.
✓ Branch 13 → 3 taken 275 times.
✓ Branch 13 → 14 taken 285 times.
|
560 | while (pos >= 0 && pos < data.size() && data.at(pos) == ' ') |
| 83 | 275 | ++pos; | |
| 84 |
2/2✓ Branch 14 → 15 taken 10 times.
✓ Branch 14 → 16 taken 275 times.
|
285 | if (pos < 0) |
| 85 | 10 | return 0; | |
| 86 | 275 | qint64 value = 0; | |
| 87 | 275 | const qsizetype digitsStart = pos; | |
| 88 |
6/8✓ Branch 23 → 24 taken 962 times.
✗ Branch 23 → 35 not taken.
✓ Branch 28 → 29 taken 689 times.
✓ Branch 28 → 35 taken 273 times.
✓ Branch 33 → 34 taken 689 times.
✗ Branch 33 → 35 not taken.
✓ Branch 36 → 17 taken 689 times.
✓ Branch 36 → 37 taken 273 times.
|
962 | while (pos < data.size() && data.at(pos) >= '0' && data.at(pos) <= '9') { |
| 89 | 689 | const int digit = int(asciiAt(data, pos) - '0'); | |
| 90 |
2/2✓ Branch 19 → 20 taken 2 times.
✓ Branch 19 → 21 taken 687 times.
|
689 | if (value > (std::numeric_limits<qint64>::max() - digit) / 10) |
| 91 | 2 | return 0; | |
| 92 | 687 | value = value * 10 + digit; | |
| 93 | 687 | ++pos; | |
| 94 | } | ||
| 95 |
1/2✓ Branch 37 → 38 taken 273 times.
✗ Branch 37 → 39 not taken.
|
273 | return pos == digitsStart ? 0 : value; |
| 96 | } | ||
| 97 | |||
| 98 | // The parenthesized FLAGS list after `pos` ("" with ok=false when absent). | ||
| 99 | 185 | QString flagListAfter(const QString &data, qsizetype pos, bool *ok) { | |
| 100 | 185 | *ok = false; | |
| 101 |
7/8✓ Branch 4 → 5 taken 228 times.
✓ Branch 4 → 13 taken 71 times.
✓ Branch 6 → 7 taken 228 times.
✗ Branch 6 → 13 not taken.
✓ Branch 11 → 12 taken 114 times.
✓ Branch 11 → 13 taken 114 times.
✓ Branch 14 → 3 taken 114 times.
✓ Branch 14 → 15 taken 185 times.
|
299 | while (pos >= 0 && pos < data.size() && data.at(pos) == QLatin1Char(' ')) |
| 102 | 114 | ++pos; | |
| 103 |
6/8✓ Branch 15 → 16 taken 114 times.
✓ Branch 15 → 23 taken 71 times.
✓ Branch 17 → 18 taken 114 times.
✗ Branch 17 → 23 not taken.
✗ Branch 22 → 23 not taken.
✓ Branch 22 → 24 taken 114 times.
✓ Branch 25 → 26 taken 71 times.
✓ Branch 25 → 27 taken 114 times.
|
185 | if (pos < 0 || pos >= data.size() || data.at(pos) != QLatin1Char('(')) |
| 104 | 71 | return {}; | |
| 105 | 114 | const qsizetype close = data.indexOf(QLatin1Char(')'), pos); | |
| 106 |
1/2✗ Branch 30 → 31 not taken.
✓ Branch 30 → 32 taken 114 times.
|
114 | if (close < 0) |
| 107 | ✗ | return {}; | |
| 108 | 114 | *ok = true; | |
| 109 | 114 | return data.mid(pos + 1, close - pos - 1); | |
| 110 | } | ||
| 111 | |||
| 112 | // F-12 (SPRINT-104): the value of the last BODY[HEADER.FIELDS (…)] item | ||
| 113 | // of a FETCH response — the only part of the data that carries header | ||
| 114 | // fields. Returns an empty string when the item is absent (the ENVELOPE | ||
| 115 | // remains the source then); a subject or display name quoting | ||
| 116 | // "References:" can never leak into the header scan again. | ||
| 117 | 158 | QString headerFieldsBlock(const QString &data) { | |
| 118 | static QRegularExpression item( | ||
| 119 | 18 | QStringLiteral("BODY\\[HEADER\\.FIELDS"), | |
| 120 |
4/7✓ Branch 2 → 3 taken 9 times.
✓ Branch 2 → 15 taken 149 times.
✓ Branch 4 → 5 taken 9 times.
✗ Branch 4 → 15 not taken.
✓ Branch 9 → 10 taken 9 times.
✗ Branch 95 → 96 not taken.
✗ Branch 95 → 97 not taken.
|
176 | QRegularExpression::CaseInsensitiveOption); |
| 121 | 158 | qsizetype at = -1; | |
| 122 |
1/1✓ Branch 15 → 16 taken 158 times.
|
158 | QRegularExpressionMatch match; |
| 123 | 158 | qsizetype search = 0; | |
| 124 |
4/4✓ Branch 21 → 22 taken 187 times.
✓ Branch 23 → 24 taken 187 times.
✓ Branch 25 → 17 taken 29 times.
✓ Branch 25 → 26 taken 158 times.
|
345 | while ((match = item.match(data, search)).hasMatch()) { |
| 125 |
1/1✓ Branch 17 → 18 taken 29 times.
|
29 | at = match.capturedStart(); |
| 126 |
1/1✓ Branch 18 → 19 taken 29 times.
|
29 | search = match.capturedEnd(); |
| 127 | } | ||
| 128 | // S106-012: no BODY[HEADER.FIELDS item → no header fields. Every header | ||
| 129 | // sync requests the item (kHeaderFieldsItem in ImapService.cpp); the former | ||
| 130 | // whole-line fallback scanned quoted ENVELOPE/BODYSTRUCTURE strings of a | ||
| 131 | // response without it (unsolicited FETCH, another item set) as headers. | ||
| 132 |
2/2✓ Branch 26 → 27 taken 129 times.
✓ Branch 26 → 28 taken 29 times.
|
158 | if (at < 0) return {}; |
| 133 | 29 | const qsizetype close = data.indexOf(QLatin1Char(']'), search); | |
| 134 |
1/2✗ Branch 31 → 32 not taken.
✓ Branch 31 → 33 taken 29 times.
|
29 | if (close < 0) return {}; |
| 135 | 29 | qsizetype quote = close + 1; | |
| 136 |
5/6✓ Branch 36 → 37 taken 58 times.
✗ Branch 36 → 41 not taken.
✓ Branch 39 → 40 taken 29 times.
✓ Branch 39 → 41 taken 29 times.
✓ Branch 42 → 34 taken 29 times.
✓ Branch 42 → 43 taken 29 times.
|
58 | while (quote < data.size() && data.at(quote).isSpace()) ++quote; |
| 137 |
3/6✓ Branch 44 → 45 taken 29 times.
✗ Branch 44 → 50 not taken.
✗ Branch 49 → 50 not taken.
✓ Branch 49 → 51 taken 29 times.
✗ Branch 52 → 53 not taken.
✓ Branch 52 → 54 taken 29 times.
|
29 | if (quote >= data.size() || data.at(quote) != QLatin1Char('"')) return {}; |
| 138 | 29 | ++quote; | |
| 139 | 29 | QString value; | |
| 140 | 29 | bool escaped = false; | |
| 141 |
1/2✓ Branch 82 → 55 taken 2394 times.
✗ Branch 82 → 83 not taken.
|
2394 | for (qsizetype i = quote; i < data.size(); ++i) { |
| 142 | 2394 | const QChar c = data.at(i); | |
| 143 |
2/2✓ Branch 56 → 57 taken 46 times.
✓ Branch 56 → 67 taken 2348 times.
|
2394 | if (escaped) { |
| 144 | // Keep \r and \n as their escape pairs (the normalization below | ||
| 145 | // turns them into breaks); an escaped quote becomes the character. | ||
| 146 |
2/3✓ Branch 60 → 61 taken 46 times.
✗ Branch 60 → 65 not taken.
✓ Branch 63 → 64 taken 46 times.
|
46 | if (c != QLatin1Char('"')) value.append(QLatin1Char('\\')); |
| 147 |
1/1✓ Branch 65 → 66 taken 46 times.
|
46 | value.append(c); |
| 148 | 46 | escaped = false; | |
| 149 | 92 | continue; | |
| 150 | } | ||
| 151 |
2/2✓ Branch 70 → 71 taken 46 times.
✓ Branch 70 → 72 taken 2302 times.
|
2348 | if (c == QLatin1Char('\\')) { escaped = true; continue; } |
| 152 |
2/2✓ Branch 75 → 76 taken 29 times.
✓ Branch 75 → 77 taken 2273 times.
|
2302 | if (c == QLatin1Char('"')) break; |
| 153 |
1/1✓ Branch 77 → 78 taken 2273 times.
|
2273 | value.append(c); |
| 154 | } | ||
| 155 | 29 | return value; | |
| 156 | 158 | } | |
| 157 | } // namespace | ||
| 158 | |||
| 159 | 357 | bool ImapResponseParser::isTagged(const QString &line) { | |
| 160 | // Tagged responses start with a tag like "A001 " | ||
| 161 |
5/8✓ Branch 2 → 3 taken 10 times.
✓ Branch 2 → 12 taken 347 times.
✓ Branch 4 → 5 taken 10 times.
✗ Branch 4 → 12 not taken.
✓ Branch 6 → 7 taken 10 times.
✓ Branch 7 → 8 taken 10 times.
✗ Branch 23 → 24 not taken.
✗ Branch 23 → 25 not taken.
|
357 | static QRegularExpression rx(R"(^[A-Za-z]\d+\s)"); |
| 162 |
2/2✓ Branch 13 → 14 taken 357 times.
✓ Branch 14 → 15 taken 357 times.
|
357 | return rx.match(line).hasMatch(); |
| 163 | } | ||
| 164 | |||
| 165 | 100744 | bool ImapResponseParser::isUntagged(const QString &line) { | |
| 166 |
2/2✓ Branch 2 → 3 taken 100744 times.
✓ Branch 3 → 4 taken 100744 times.
|
100744 | return line.startsWith("* "); |
| 167 | } | ||
| 168 | |||
| 169 | 44 | bool ImapResponseParser::isContinuation(const QString &line) { | |
| 170 |
2/2✓ Branch 2 → 3 taken 44 times.
✓ Branch 3 → 4 taken 44 times.
|
44 | return line.startsWith("+"); |
| 171 | } | ||
| 172 | |||
| 173 | std::optional<TaggedResponse> | ||
| 174 | 487 | ImapResponseParser::parseTaggedResponse(const QString &line) { | |
| 175 | // Format: "A001 OK message text" or "A001 NO [CODE] message" | ||
| 176 |
5/8✓ Branch 2 → 3 taken 13 times.
✓ Branch 2 → 12 taken 474 times.
✓ Branch 4 → 5 taken 13 times.
✗ Branch 4 → 12 not taken.
✓ Branch 6 → 7 taken 13 times.
✓ Branch 7 → 8 taken 13 times.
✗ Branch 32 → 33 not taken.
✗ Branch 32 → 34 not taken.
|
487 | static QRegularExpression rx(R"(^([A-Za-z]\d+)\s+(OK|NO|BAD)\s*(.*)?$)"); |
| 177 |
1/1✓ Branch 13 → 14 taken 487 times.
|
487 | auto match = rx.match(line); |
| 178 |
3/3✓ Branch 14 → 15 taken 487 times.
✓ Branch 15 → 16 taken 4 times.
✓ Branch 15 → 17 taken 483 times.
|
487 | if (!match.hasMatch()) { |
| 179 | 4 | return std::nullopt; | |
| 180 | } | ||
| 181 | |||
| 182 |
3/7✓ Branch 17 → 18 taken 483 times.
✓ Branch 18 → 19 taken 483 times.
✓ Branch 20 → 21 taken 483 times.
✗ Branch 39 → 40 not taken.
✗ Branch 39 → 41 not taken.
✗ Branch 42 → 43 not taken.
✗ Branch 42 → 44 not taken.
|
1449 | return TaggedResponse{ |
| 183 | match.captured(1), // tag | ||
| 184 | match.captured(2), // status | ||
| 185 |
1/1✓ Branch 19 → 20 taken 483 times.
|
966 | match.captured(3).trimmed() // message |
| 186 | 483 | }; | |
| 187 | 487 | } | |
| 188 | |||
| 189 | std::optional<UntaggedResponse> | ||
| 190 | 100454 | ImapResponseParser::parseUntaggedResponse(const QString &line) { | |
| 191 |
4/4✓ Branch 2 → 3 taken 100454 times.
✓ Branch 3 → 4 taken 100454 times.
✓ Branch 5 → 6 taken 4 times.
✓ Branch 5 → 7 taken 100450 times.
|
100454 | if (!line.startsWith("* ")) { |
| 192 | 4 | return std::nullopt; | |
| 193 | } | ||
| 194 | |||
| 195 | // After "* ", the next token is the type | ||
| 196 | 100450 | const auto rest = QStringView(line).mid(2); | |
| 197 | 100450 | const auto spaceIdx = rest.indexOf(' '); | |
| 198 | |||
| 199 |
2/2✓ Branch 11 → 12 taken 3 times.
✓ Branch 11 → 16 taken 100447 times.
|
100450 | if (spaceIdx < 0) { |
| 200 | // Single-word untagged response (rare) | ||
| 201 |
1/1✓ Branch 12 → 13 taken 3 times.
|
3 | return UntaggedResponse{rest.toString(), {}}; |
| 202 | } | ||
| 203 | |||
| 204 |
2/4✓ Branch 17 → 18 taken 100447 times.
✓ Branch 19 → 20 taken 100447 times.
✗ Branch 30 → 31 not taken.
✗ Branch 30 → 32 not taken.
|
301341 | return UntaggedResponse{rest.left(spaceIdx).toString(), |
| 205 | 301341 | rest.mid(spaceIdx + 1).toString()}; | |
| 206 | } | ||
| 207 | |||
| 208 | std::optional<FolderInfo> | ||
| 209 | 43 | ImapResponseParser::parseListResponse(const QString &data) { | |
| 210 | // Format: "(\Flag1 \Flag2) "delimiter" "mailbox-name"" — the mailbox | ||
| 211 | // name is a quoted string with escapes or a bare astring. F-16: a | ||
| 212 | // "\\" delimiter (escaped backslash) and quoted names containing \" | ||
| 213 | // need a quote-aware scan; the former one-character regex dropped | ||
| 214 | // those folders from the list. | ||
| 215 | 43 | const int n = data.size(); | |
| 216 | 43 | int i = 0; | |
| 217 |
4/6✓ Branch 5 → 6 taken 42 times.
✓ Branch 5 → 10 taken 1 time.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 42 times.
✗ Branch 11 → 4 not taken.
✓ Branch 11 → 12 taken 43 times.
|
43 | while (i < n && data.at(i).isSpace()) |
| 218 | ✗ | ++i; | |
| 219 |
6/6✓ Branch 12 → 13 taken 42 times.
✓ Branch 12 → 17 taken 1 time.
✓ Branch 16 → 17 taken 2 times.
✓ Branch 16 → 18 taken 40 times.
✓ Branch 19 → 20 taken 3 times.
✓ Branch 19 → 32 taken 40 times.
|
43 | if (i >= n || data.at(i) != '(') { |
| 220 |
6/6✓ Branch 20 → 21 taken 3 times.
✓ Branch 24 → 25 taken 3 times.
✓ Branch 25 → 26 taken 3 times.
✓ Branch 26 → 27 taken 3 times.
✓ Branch 30 → 22 taken 3 times.
✓ Branch 30 → 31 taken 3 times.
|
6 | qCWarning(lcImapParser) << "Failed to parse LIST response:" << data; |
| 221 | 3 | return std::nullopt; | |
| 222 | } | ||
| 223 | 40 | const int flagsEnd = data.indexOf(')', i); | |
| 224 |
1/2✗ Branch 34 → 35 not taken.
✓ Branch 34 → 47 taken 40 times.
|
40 | if (flagsEnd < 0) { |
| 225 | ✗ | qCWarning(lcImapParser) << "Failed to parse LIST response:" << data; | |
| 226 | ✗ | return std::nullopt; | |
| 227 | } | ||
| 228 | 40 | FolderInfo info; | |
| 229 |
2/2✓ Branch 47 → 48 taken 40 times.
✓ Branch 48 → 49 taken 40 times.
|
40 | info.flags = parseFlags(data.mid(i, flagsEnd - i + 1)); |
| 230 | 40 | i = flagsEnd + 1; | |
| 231 |
5/6✓ Branch 54 → 55 taken 80 times.
✗ Branch 54 → 59 not taken.
✓ Branch 57 → 58 taken 40 times.
✓ Branch 57 → 59 taken 40 times.
✓ Branch 60 → 53 taken 40 times.
✓ Branch 60 → 61 taken 40 times.
|
80 | while (i < n && data.at(i).isSpace()) |
| 232 | 40 | ++i; | |
| 233 | |||
| 234 | // Delimiter: NIL, a quoted (escape-aware) string, or a bare character. | ||
| 235 |
3/3✓ Branch 61 → 62 taken 40 times.
✓ Branch 65 → 66 taken 3 times.
✓ Branch 65 → 67 taken 37 times.
|
40 | if (data.mid(i, 3).compare(QLatin1String("NIL"), Qt::CaseInsensitive) == 0) { |
| 236 | 3 | i += 3; | |
| 237 |
3/6✓ Branch 67 → 68 taken 37 times.
✗ Branch 67 → 73 not taken.
✓ Branch 71 → 72 taken 37 times.
✗ Branch 71 → 73 not taken.
✓ Branch 74 → 75 taken 37 times.
✗ Branch 74 → 96 not taken.
|
37 | } else if (i < n && data.at(i) == '"') { |
| 238 | 37 | ++i; | |
| 239 | 37 | bool escaped = false; | |
| 240 | 37 | QString delimiter; | |
| 241 |
1/2✓ Branch 92 → 76 taken 75 times.
✗ Branch 92 → 93 not taken.
|
75 | for (; i < n; ++i) { |
| 242 | 75 | const QChar c = data.at(i); | |
| 243 |
2/2✓ Branch 77 → 78 taken 1 time.
✓ Branch 77 → 80 taken 74 times.
|
75 | if (escaped) { |
| 244 |
1/1✓ Branch 78 → 79 taken 1 time.
|
1 | delimiter.append(c); |
| 245 | 1 | escaped = false; | |
| 246 |
2/2✓ Branch 83 → 84 taken 1 time.
✓ Branch 83 → 85 taken 73 times.
|
74 | } else if (c == QLatin1Char('\\')) { |
| 247 | 1 | escaped = true; | |
| 248 |
2/2✓ Branch 88 → 89 taken 37 times.
✓ Branch 88 → 90 taken 36 times.
|
73 | } else if (c == QLatin1Char('"')) { |
| 249 | 37 | ++i; | |
| 250 | 37 | break; | |
| 251 | } else { | ||
| 252 |
1/1✓ Branch 90 → 91 taken 36 times.
|
36 | delimiter.append(c); |
| 253 | } | ||
| 254 | } | ||
| 255 | 37 | info.delimiter = delimiter; | |
| 256 |
0/6✗ Branch 96 → 97 not taken.
✗ Branch 96 → 101 not taken.
✗ Branch 99 → 100 not taken.
✗ Branch 99 → 101 not taken.
✗ Branch 102 → 103 not taken.
✗ Branch 102 → 108 not taken.
|
37 | } else if (i < n && !data.at(i).isSpace()) { |
| 257 | ✗ | info.delimiter = QString(data.at(i)); | |
| 258 | ✗ | ++i; | |
| 259 | } | ||
| 260 |
5/6✓ Branch 110 → 111 taken 80 times.
✗ Branch 110 → 115 not taken.
✓ Branch 113 → 114 taken 40 times.
✓ Branch 113 → 115 taken 40 times.
✓ Branch 116 → 109 taken 40 times.
✓ Branch 116 → 117 taken 40 times.
|
80 | while (i < n && data.at(i).isSpace()) |
| 261 | 40 | ++i; | |
| 262 | |||
| 263 | // Mailbox name: quoted (escape-aware) or astring to the end. | ||
| 264 | 40 | QString path; | |
| 265 |
5/6✓ Branch 117 → 118 taken 40 times.
✗ Branch 117 → 123 not taken.
✓ Branch 121 → 122 taken 39 times.
✓ Branch 121 → 123 taken 1 time.
✓ Branch 124 → 125 taken 39 times.
✓ Branch 124 → 143 taken 1 time.
|
40 | if (i < n && data.at(i) == '"') { |
| 266 | 39 | ++i; | |
| 267 | 39 | bool escaped = false; | |
| 268 |
1/2✓ Branch 142 → 126 taken 440 times.
✗ Branch 142 → 149 not taken.
|
440 | for (; i < n; ++i) { |
| 269 | 440 | const QChar c = data.at(i); | |
| 270 |
2/2✓ Branch 127 → 128 taken 1 time.
✓ Branch 127 → 130 taken 439 times.
|
440 | if (escaped) { |
| 271 |
1/1✓ Branch 128 → 129 taken 1 time.
|
1 | path.append(c); |
| 272 | 1 | escaped = false; | |
| 273 |
2/2✓ Branch 133 → 134 taken 1 time.
✓ Branch 133 → 135 taken 438 times.
|
439 | } else if (c == QLatin1Char('\\')) { |
| 274 | 1 | escaped = true; | |
| 275 |
2/2✓ Branch 138 → 139 taken 39 times.
✓ Branch 138 → 140 taken 399 times.
|
438 | } else if (c == QLatin1Char('"')) { |
| 276 | 39 | ++i; | |
| 277 | 39 | break; | |
| 278 | } else { | ||
| 279 |
1/1✓ Branch 140 → 141 taken 399 times.
|
399 | path.append(c); |
| 280 | } | ||
| 281 | } | ||
| 282 | } else { | ||
| 283 |
2/2✓ Branch 143 → 144 taken 1 time.
✓ Branch 144 → 145 taken 1 time.
|
1 | path = data.mid(i).trimmed(); |
| 284 | } | ||
| 285 |
1/2✗ Branch 150 → 151 not taken.
✓ Branch 150 → 163 taken 40 times.
|
40 | if (path.isEmpty()) { |
| 286 | ✗ | qCWarning(lcImapParser) << "Failed to parse LIST response:" << data; | |
| 287 | ✗ | return std::nullopt; | |
| 288 | } | ||
| 289 | |||
| 290 | 40 | info.path = path; | |
| 291 |
1/1✓ Branch 164 → 165 taken 40 times.
|
40 | info.name = decodeMailboxName(info.path); |
| 292 | |||
| 293 | // Extract display name (last component after delimiter) | ||
| 294 |
7/7✓ Branch 168 → 169 taken 37 times.
✓ Branch 168 → 172 taken 3 times.
✓ Branch 169 → 170 taken 37 times.
✓ Branch 170 → 171 taken 5 times.
✓ Branch 170 → 172 taken 32 times.
✓ Branch 173 → 174 taken 5 times.
✓ Branch 173 → 179 taken 35 times.
|
40 | if (!info.delimiter.isEmpty() && info.name.contains(info.delimiter)) { |
| 295 |
1/1✓ Branch 175 → 176 taken 5 times.
|
5 | info.name = info.name.section(info.delimiter, -1); |
| 296 | } | ||
| 297 | |||
| 298 | 40 | return info; | |
| 299 | 40 | } | |
| 300 | |||
| 301 | 78 | QStringList ImapResponseParser::parseCapabilities(const QString &data) { | |
| 302 | // CAPABILITY data is space-separated tokens | ||
| 303 |
1/1✓ Branch 4 → 5 taken 78 times.
|
78 | return data.split(' ', Qt::SkipEmptyParts); |
| 304 | } | ||
| 305 | |||
| 306 | 40 | QStringList ImapResponseParser::parseFlags(const QString &flagStr) { | |
| 307 | // Input: "(\HasChildren \Sent)" → ["\\HasChildren", "\\Sent"] | ||
| 308 | 40 | auto inner = flagStr; | |
| 309 |
1/1✓ Branch 4 → 5 taken 40 times.
|
40 | inner.remove('('); |
| 310 |
1/1✓ Branch 6 → 7 taken 40 times.
|
40 | inner.remove(')'); |
| 311 |
1/1✓ Branch 7 → 8 taken 40 times.
|
40 | inner = inner.trimmed(); |
| 312 | |||
| 313 |
2/2✓ Branch 11 → 12 taken 15 times.
✓ Branch 11 → 13 taken 25 times.
|
40 | if (inner.isEmpty()) { |
| 314 | 15 | return {}; | |
| 315 | } | ||
| 316 | |||
| 317 |
1/1✓ Branch 15 → 16 taken 25 times.
|
25 | return inner.split(' ', Qt::SkipEmptyParts); |
| 318 | 40 | } | |
| 319 | |||
| 320 | 14 | quint32 ImapResponseParser::flagsToBitmask(const QStringList &flagStrings) { | |
| 321 |
1/1✓ Branch 2 → 3 taken 14 times.
|
14 | return flagsAndKeywords(flagStrings).first; |
| 322 | } | ||
| 323 | |||
| 324 | QPair<quint32, QStringList> | ||
| 325 | 144 | ImapResponseParser::flagsAndKeywords(const QStringList &flagStrings) { | |
| 326 | 144 | quint32 result = MailFlag::None; | |
| 327 | 144 | QStringList keywords; | |
| 328 |
2/2✓ Branch 41 → 4 taken 147 times.
✓ Branch 41 → 42 taken 144 times.
|
291 | for (const auto &flag : flagStrings) { |
| 329 |
3/3✓ Branch 5 → 6 taken 147 times.
✓ Branch 8 → 9 taken 101 times.
✓ Branch 8 → 10 taken 46 times.
|
147 | if (flag.compare("\\Seen", Qt::CaseInsensitive) == 0) |
| 330 | 101 | result |= MailFlag::Seen; | |
| 331 |
3/3✓ Branch 10 → 11 taken 46 times.
✓ Branch 13 → 14 taken 7 times.
✓ Branch 13 → 15 taken 39 times.
|
46 | else if (flag.compare("\\Answered", Qt::CaseInsensitive) == 0) |
| 332 | 7 | result |= MailFlag::Answered; | |
| 333 |
3/3✓ Branch 15 → 16 taken 39 times.
✓ Branch 18 → 19 taken 12 times.
✓ Branch 18 → 20 taken 27 times.
|
39 | else if (flag.compare("\\Flagged", Qt::CaseInsensitive) == 0) |
| 334 | 12 | result |= MailFlag::Flagged; | |
| 335 |
3/3✓ Branch 20 → 21 taken 27 times.
✓ Branch 23 → 24 taken 3 times.
✓ Branch 23 → 25 taken 24 times.
|
27 | else if (flag.compare("\\Deleted", Qt::CaseInsensitive) == 0) |
| 336 | 3 | result |= MailFlag::Deleted; | |
| 337 |
3/3✓ Branch 25 → 26 taken 24 times.
✓ Branch 28 → 29 taken 4 times.
✓ Branch 28 → 30 taken 20 times.
|
24 | else if (flag.compare("\\Draft", Qt::CaseInsensitive) == 0) |
| 338 | 4 | result |= MailFlag::Draft; | |
| 339 |
7/7✓ Branch 31 → 32 taken 20 times.
✓ Branch 32 → 33 taken 16 times.
✓ Branch 32 → 36 taken 4 times.
✓ Branch 34 → 35 taken 14 times.
✓ Branch 34 → 36 taken 2 times.
✓ Branch 37 → 38 taken 14 times.
✓ Branch 37 → 39 taken 6 times.
|
20 | else if (!flag.startsWith('\\') && !flag.isEmpty()) { |
| 340 | // Non-system flag = keyword (label) — store all, UI filters internal ones | ||
| 341 |
1/1✓ Branch 38 → 39 taken 14 times.
|
14 | keywords.append(flag); |
| 342 | } | ||
| 343 | } | ||
| 344 | 288 | return {result, keywords}; | |
| 345 | 144 | } | |
| 346 | |||
| 347 | 2979 | bool ImapResponseParser::isInternalKeyword(const QString &keyword) { | |
| 348 | static const QStringList internalKeywords = { | ||
| 349 | "NonJunk", "NotJunk", "$NotJunk", | ||
| 350 | "Junk", "$Junk", "$MDNSent", | ||
| 351 | "$Forwarded", "$SubmitPending", "$Submitted", | ||
| 352 |
5/10✓ Branch 2 → 3 taken 7 times.
✓ Branch 2 → 21 taken 2972 times.
✓ Branch 4 → 5 taken 7 times.
✗ Branch 4 → 21 not taken.
✓ Branch 18 → 19 taken 63 times.
✓ Branch 18 → 20 taken 7 times.
✗ Branch 38 → 39 not taken.
✗ Branch 38 → 40 not taken.
✗ Branch 47 → 48 not taken.
✗ Branch 47 → 49 not taken.
|
3049 | }; |
| 353 |
2/2✓ Branch 29 → 23 taken 26706 times.
✓ Branch 29 → 30 taken 2957 times.
|
29663 | for (const auto &kw : internalKeywords) { |
| 354 |
2/2✓ Branch 25 → 26 taken 22 times.
✓ Branch 25 → 27 taken 26684 times.
|
26706 | if (keyword.compare(kw, Qt::CaseInsensitive) == 0) { |
| 355 | 22 | return true; | |
| 356 | } | ||
| 357 | } | ||
| 358 | // SOGo/Mailcow internal annotations: x-me-annot-1, x-me-annot-2, etc. | ||
| 359 |
4/4✓ Branch 30 → 31 taken 2957 times.
✓ Branch 31 → 32 taken 2957 times.
✓ Branch 33 → 34 taken 3 times.
✓ Branch 33 → 35 taken 2954 times.
|
2957 | if (keyword.startsWith("x-me-annot", Qt::CaseInsensitive)) |
| 360 | 3 | return true; | |
| 361 | 2954 | return false; | |
| 362 | 7 | } | |
| 363 | |||
| 364 | std::optional<MailHeader> | ||
| 365 | 161 | ImapResponseParser::parseFetchHeaderResponse(const QString &data) { | |
| 366 | // Expected format (after "* N FETCH "): | ||
| 367 | // (UID 42 FLAGS (\Seen) RFC822.SIZE 1234 ENVELOPE ("date" "subject" | ||
| 368 | // (("from-name" NIL "user" "host")) ...)) | ||
| 369 | // | ||
| 370 | // We extract key fields using targeted regex patterns rather than | ||
| 371 | // implementing a full IMAP grammar parser. | ||
| 372 | |||
| 373 | 161 | MailHeader header; | |
| 374 | |||
| 375 | // Extract UID (top-level item only, S106-019; a positive 63-bit value, | ||
| 376 | // S106-018 — an overflowing UID used to become 0 and was accepted) | ||
| 377 | 161 | header.uid = numberAfter(data, topLevelItem(data, "UID", " ")); | |
| 378 |
2/2✓ Branch 5 → 6 taken 3 times.
✓ Branch 5 → 20 taken 158 times.
|
161 | if (header.uid <= 0) { |
| 379 |
7/7✓ Branch 6 → 7 taken 3 times.
✓ Branch 10 → 11 taken 3 times.
✓ Branch 11 → 12 taken 3 times.
✓ Branch 12 → 13 taken 3 times.
✓ Branch 13 → 14 taken 3 times.
✓ Branch 18 → 8 taken 3 times.
✓ Branch 18 → 19 taken 3 times.
|
6 | qCWarning(lcImapParser) << "No valid UID in FETCH response:" << data.left(100); |
| 380 | 3 | return std::nullopt; | |
| 381 | } | ||
| 382 | |||
| 383 | // Extract FLAGS | ||
| 384 | 158 | bool hasFlags = false; | |
| 385 |
1/1✓ Branch 21 → 22 taken 158 times.
|
158 | const QString flagText = flagListAfter(data, topLevelItem(data, "FLAGS"), &hasFlags); |
| 386 |
2/2✓ Branch 22 → 23 taken 89 times.
✓ Branch 22 → 33 taken 69 times.
|
158 | if (hasFlags) { |
| 387 |
1/1✓ Branch 25 → 26 taken 89 times.
|
89 | auto flagList = flagText.split(' ', Qt::SkipEmptyParts); |
| 388 |
1/1✓ Branch 26 → 27 taken 89 times.
|
89 | auto [flags, keywords] = flagsAndKeywords(flagList); |
| 389 | 89 | header.flags = flags; | |
| 390 | 89 | header.labels = keywords; | |
| 391 | 89 | } | |
| 392 | |||
| 393 | // Extract RFC822.SIZE | ||
| 394 | 158 | const qsizetype sizeItem = topLevelItem(data, "RFC822.SIZE", " "); | |
| 395 |
2/2✓ Branch 34 → 35 taken 88 times.
✓ Branch 34 → 37 taken 70 times.
|
158 | if (sizeItem >= 0) |
| 396 | 88 | header.size = numberAfter(data, sizeItem); | |
| 397 | |||
| 398 | // Extract ENVELOPE fields | ||
| 399 | // Envelope format: ("date" "subject" ((from)) ((sender)) ((reply-to)) | ||
| 400 | // ((to)) ((cc)) ((bcc)) "in-reply-to" "message-id") | ||
| 401 |
5/8✓ Branch 37 → 38 taken 9 times.
✓ Branch 37 → 47 taken 149 times.
✓ Branch 39 → 40 taken 9 times.
✗ Branch 39 → 47 not taken.
✓ Branch 41 → 42 taken 9 times.
✓ Branch 42 → 43 taken 9 times.
✗ Branch 701 → 702 not taken.
✗ Branch 701 → 703 not taken.
|
158 | static QRegularExpression envRx(R"(ENVELOPE\s*\()"); |
| 402 |
1/1✓ Branch 48 → 49 taken 158 times.
|
158 | auto envMatch = envRx.match(data); |
| 403 |
3/3✓ Branch 49 → 50 taken 158 times.
✓ Branch 50 → 51 taken 140 times.
✓ Branch 50 → 243 taken 18 times.
|
158 | if (envMatch.hasMatch()) { |
| 404 |
1/1✓ Branch 51 → 52 taken 140 times.
|
140 | int envStart = envMatch.capturedEnd(); |
| 405 | // Parse quoted fields from envelope | ||
| 406 | // Field 1: date string | ||
| 407 | // Field 2: subject | ||
| 408 | 1328 | auto extractQuoted = [&](int &pos) -> QString { | |
| 409 | // Skip whitespace | ||
| 410 |
6/6✓ Branch 5 → 6 taken 2286 times.
✓ Branch 5 → 10 taken 9 times.
✓ Branch 8 → 9 taken 967 times.
✓ Branch 8 → 10 taken 1319 times.
✓ Branch 11 → 3 taken 967 times.
✓ Branch 11 → 12 taken 1328 times.
|
2295 | while (pos < data.length() && data[pos].isSpace()) |
| 411 | 967 | pos++; | |
| 412 |
2/2✓ Branch 13 → 14 taken 9 times.
✓ Branch 13 → 15 taken 1319 times.
|
1328 | if (pos >= data.length()) |
| 413 | 9 | return {}; | |
| 414 | |||
| 415 |
3/3✓ Branch 15 → 16 taken 1319 times.
✓ Branch 18 → 19 taken 382 times.
✓ Branch 18 → 20 taken 937 times.
|
1319 | if (data.mid(pos, 3) == "NIL") { |
| 416 | 382 | pos += 3; | |
| 417 | 382 | return {}; | |
| 418 | } | ||
| 419 | |||
| 420 | // Handle IMAP literal syntax {N} — the transport layer normally converts | ||
| 421 | // these to quoted strings, but handle the case where they survive inline. | ||
| 422 | // SEC-2026-07-21-01: use qsizetype to avoid int overflow on unrealistic | ||
| 423 | // literal lengths; the transport budgets already reject >64 MiB literals, | ||
| 424 | // but this makes the parser self-defensive. | ||
| 425 |
2/2✓ Branch 23 → 24 taken 9 times.
✓ Branch 23 → 42 taken 928 times.
|
937 | if (data[pos] == '{') { |
| 426 | 9 | qsizetype braceEnd = data.indexOf('}', pos); | |
| 427 |
2/2✓ Branch 26 → 27 taken 8 times.
✓ Branch 26 → 42 taken 1 time.
|
9 | if (braceEnd > pos) { |
| 428 | bool ok; | ||
| 429 | qsizetype len = | ||
| 430 |
2/2✓ Branch 27 → 28 taken 8 times.
✓ Branch 28 → 29 taken 8 times.
|
8 | data.mid(pos + 1, braceEnd - pos - 1).toLongLong(&ok); |
| 431 | // S106-017: compared without the sum, which overflowed for a | ||
| 432 | // server length near INT64_MAX and wrapped the check true. | ||
| 433 |
8/8✓ Branch 30 → 31 taken 7 times.
✓ Branch 30 → 35 taken 1 time.
✓ Branch 31 → 32 taken 5 times.
✓ Branch 31 → 35 taken 2 times.
✓ Branch 33 → 34 taken 3 times.
✓ Branch 33 → 35 taken 2 times.
✓ Branch 36 → 37 taken 3 times.
✓ Branch 36 → 41 taken 5 times.
|
8 | if (ok && len >= 0 && len <= data.length() - (braceEnd + 1)) { |
| 434 |
1/1✓ Branch 37 → 38 taken 3 times.
|
3 | QString result = data.mid(braceEnd + 1, len); |
| 435 | 3 | pos = braceEnd + 1 + len; | |
| 436 | 3 | return result; | |
| 437 | 3 | } | |
| 438 | } | ||
| 439 | } | ||
| 440 | |||
| 441 |
2/2✓ Branch 45 → 46 taken 31 times.
✓ Branch 45 → 65 taken 903 times.
|
934 | if (data[pos] != '"') { |
| 442 |
4/4✓ Branch 46 → 47 taken 31 times.
✓ Branch 50 → 51 taken 31 times.
✓ Branch 63 → 48 taken 31 times.
✓ Branch 63 → 64 taken 31 times.
|
62 | qCWarning(lcImapParser) |
| 443 |
2/2✓ Branch 51 → 52 taken 31 times.
✓ Branch 53 → 54 taken 31 times.
|
31 | << "extractQuoted: unexpected char" << data[pos] |
| 444 |
5/5✓ Branch 54 → 55 taken 31 times.
✓ Branch 55 → 56 taken 31 times.
✓ Branch 56 → 57 taken 31 times.
✓ Branch 57 → 58 taken 31 times.
✓ Branch 58 → 59 taken 31 times.
|
31 | << "at pos" << pos << "context:" << data.mid(pos, 30); |
| 445 | // T-401/Bug 11: Advance pos to avoid infinite loop | ||
| 446 | 31 | pos++; | |
| 447 | 31 | return {}; | |
| 448 | } | ||
| 449 | |||
| 450 | 903 | pos++; // skip opening quote | |
| 451 | 903 | QString result; | |
| 452 |
6/6✓ Branch 80 → 81 taken 10110 times.
✓ Branch 80 → 86 taken 2 times.
✓ Branch 84 → 85 taken 9209 times.
✓ Branch 84 → 86 taken 901 times.
✓ Branch 87 → 66 taken 9209 times.
✓ Branch 87 → 88 taken 903 times.
|
10112 | while (pos < data.length() && data[pos] != '"') { |
| 453 |
6/6✓ Branch 69 → 70 taken 7 times.
✓ Branch 69 → 73 taken 9202 times.
✓ Branch 71 → 72 taken 6 times.
✓ Branch 71 → 73 taken 1 time.
✓ Branch 74 → 75 taken 6 times.
✓ Branch 74 → 76 taken 9203 times.
|
9209 | if (data[pos] == '\\' && pos + 1 < data.length()) { |
| 454 | 6 | pos++; | |
| 455 | } | ||
| 456 |
1/1✓ Branch 77 → 78 taken 9209 times.
|
9209 | result.append(data[pos]); |
| 457 | 9209 | pos++; | |
| 458 | } | ||
| 459 |
2/2✓ Branch 89 → 90 taken 901 times.
✓ Branch 89 → 91 taken 2 times.
|
903 | if (pos < data.length()) |
| 460 | 901 | pos++; // skip closing quote | |
| 461 | 903 | return result; | |
| 462 | 903 | }; | |
| 463 | |||
| 464 | 140 | int pos = envStart; | |
| 465 |
1/1✓ Branch 52 → 53 taken 140 times.
|
140 | QString dateStr = extractQuoted(pos); |
| 466 |
2/2✓ Branch 53 → 54 taken 140 times.
✓ Branch 54 → 55 taken 140 times.
|
140 | header.subject = decodeRfc2047(extractQuoted(pos)); |
| 467 |
6/6✓ Branch 59 → 60 taken 16 times.
✓ Branch 59 → 63 taken 124 times.
✓ Branch 61 → 62 taken 4 times.
✓ Branch 61 → 63 taken 12 times.
✓ Branch 64 → 65 taken 4 times.
✓ Branch 64 → 83 taken 136 times.
|
140 | if (header.subject.isEmpty() && !dateStr.isEmpty()) { |
| 468 |
4/4✓ Branch 65 → 66 taken 4 times.
✓ Branch 69 → 70 taken 4 times.
✓ Branch 81 → 67 taken 4 times.
✓ Branch 81 → 82 taken 4 times.
|
8 | qCWarning(lcImapParser) |
| 469 |
2/2✓ Branch 70 → 71 taken 4 times.
✓ Branch 71 → 72 taken 4 times.
|
4 | << "Empty subject for UID" << header.uid |
| 470 |
1/1✓ Branch 72 → 73 taken 4 times.
|
4 | << "— ENVELOPE near pos" << pos |
| 471 |
4/4✓ Branch 73 → 74 taken 4 times.
✓ Branch 74 → 75 taken 4 times.
✓ Branch 75 → 76 taken 4 times.
✓ Branch 76 → 77 taken 4 times.
|
4 | << "context:" << data.mid(envStart, 120); |
| 472 | } | ||
| 473 | |||
| 474 | // Parse date (RFC 2822 format) | ||
| 475 |
2/2✓ Branch 84 → 85 taken 110 times.
✓ Branch 84 → 194 taken 30 times.
|
140 | if (!dateStr.isEmpty()) { |
| 476 | // Strip RFC 2822 comments like "(UTC)" or "(CET)" before parsing | ||
| 477 |
5/8✓ Branch 85 → 86 taken 8 times.
✓ Branch 85 → 95 taken 102 times.
✓ Branch 87 → 88 taken 8 times.
✗ Branch 87 → 95 not taken.
✓ Branch 89 → 90 taken 8 times.
✓ Branch 90 → 91 taken 8 times.
✗ Branch 721 → 722 not taken.
✗ Branch 721 → 723 not taken.
|
110 | static QRegularExpression commentRx(R"(\([^)]*\))"); |
| 478 | 110 | QString cleanDate = dateStr; | |
| 479 |
1/1✓ Branch 96 → 97 taken 110 times.
|
110 | cleanDate.remove(commentRx); |
| 480 |
1/1✓ Branch 97 → 98 taken 110 times.
|
110 | cleanDate = cleanDate.simplified(); // collapse whitespace |
| 481 | |||
| 482 | // Replace named timezones that Qt may not recognize. | ||
| 483 | // T-79.A3/M2: token-exact (trailing token only) — a plain substring | ||
| 484 | // replace of " UT" turned " UTC" into " +0000C", breaking every | ||
| 485 | // subsequent parse attempt. | ||
| 486 | static QRegularExpression namedZoneRx( | ||
| 487 |
4/7✓ Branch 100 → 101 taken 8 times.
✓ Branch 100 → 113 taken 102 times.
✓ Branch 102 → 103 taken 8 times.
✗ Branch 102 → 113 not taken.
✓ Branch 107 → 108 taken 8 times.
✗ Branch 732 → 733 not taken.
✗ Branch 732 → 734 not taken.
|
118 | QStringLiteral(" (?:UTC|GMT|UT)$")); |
| 488 |
1/1✓ Branch 116 → 117 taken 110 times.
|
110 | cleanDate.replace(namedZoneRx, QStringLiteral(" +0000")); |
| 489 | |||
| 490 | // Try common date formats (use QLocale::c() for English month names) | ||
| 491 |
1/1✓ Branch 119 → 120 taken 110 times.
|
110 | QLocale cLocale = QLocale::c(); |
| 492 | |||
| 493 |
1/1✓ Branch 123 → 124 taken 110 times.
|
220 | header.date = cLocale.toDateTime(cleanDate, |
| 494 | 330 | QStringLiteral("ddd, d MMM yyyy H:mm:ss t")); | |
| 495 |
3/3✓ Branch 128 → 129 taken 110 times.
✓ Branch 129 → 130 taken 26 times.
✓ Branch 129 → 134 taken 84 times.
|
110 | if (!header.date.isValid()) { |
| 496 |
1/1✓ Branch 130 → 131 taken 26 times.
|
26 | header.date = QDateTime::fromString(cleanDate, Qt::RFC2822Date); |
| 497 | } | ||
| 498 |
3/3✓ Branch 134 → 135 taken 110 times.
✓ Branch 135 → 136 taken 10 times.
✓ Branch 135 → 140 taken 100 times.
|
110 | if (!header.date.isValid()) { |
| 499 |
1/1✓ Branch 136 → 137 taken 10 times.
|
10 | header.date = QDateTime::fromString(cleanDate, Qt::ISODate); |
| 500 | } | ||
| 501 |
3/3✓ Branch 140 → 141 taken 110 times.
✓ Branch 141 → 142 taken 9 times.
✓ Branch 141 → 151 taken 101 times.
|
110 | if (!header.date.isValid()) { |
| 502 | // Without timezone: "Mon, 16 Feb 2026 10:58:01" | ||
| 503 |
1/1✓ Branch 145 → 146 taken 9 times.
|
18 | header.date = cLocale.toDateTime(cleanDate, |
| 504 | 27 | QStringLiteral("ddd, d MMM yyyy H:mm:ss")); | |
| 505 | } | ||
| 506 |
3/3✓ Branch 151 → 152 taken 110 times.
✓ Branch 152 → 153 taken 9 times.
✓ Branch 152 → 162 taken 101 times.
|
110 | if (!header.date.isValid()) { |
| 507 | // Without weekday: "16 Feb 2026 10:58:01 +0000" | ||
| 508 |
1/1✓ Branch 156 → 157 taken 9 times.
|
18 | header.date = cLocale.toDateTime(cleanDate, |
| 509 | 27 | QStringLiteral("d MMM yyyy H:mm:ss t")); | |
| 510 | } | ||
| 511 |
3/3✓ Branch 162 → 163 taken 110 times.
✓ Branch 163 → 164 taken 9 times.
✓ Branch 163 → 173 taken 101 times.
|
110 | if (!header.date.isValid()) { |
| 512 | // Without weekday or timezone: "16 Feb 2026 10:58:01" | ||
| 513 |
1/1✓ Branch 167 → 168 taken 9 times.
|
18 | header.date = cLocale.toDateTime(cleanDate, |
| 514 | 27 | QStringLiteral("d MMM yyyy H:mm:ss")); | |
| 515 | } | ||
| 516 |
3/3✓ Branch 173 → 174 taken 110 times.
✓ Branch 174 → 175 taken 9 times.
✓ Branch 174 → 191 taken 101 times.
|
110 | if (!header.date.isValid()) { |
| 517 |
6/6✓ Branch 175 → 176 taken 9 times.
✓ Branch 179 → 180 taken 9 times.
✓ Branch 180 → 181 taken 9 times.
✓ Branch 181 → 182 taken 9 times.
✓ Branch 189 → 177 taken 9 times.
✓ Branch 189 → 190 taken 9 times.
|
18 | qCWarning(lcImapParser) << "Date parse failed for UID" << header.uid |
| 518 |
2/2✓ Branch 182 → 183 taken 9 times.
✓ Branch 183 → 184 taken 9 times.
|
9 | << "dateStr:" << dateStr |
| 519 |
2/2✓ Branch 184 → 185 taken 9 times.
✓ Branch 185 → 186 taken 9 times.
|
9 | << "cleanDate:" << cleanDate; |
| 520 | } | ||
| 521 | 110 | } | |
| 522 | |||
| 523 | // Helper: skip a parenthesized group (NIL or (...)) entirely | ||
| 524 | 794 | auto skipGroup = [&](int &pos) { | |
| 525 |
6/6✓ Branch 5 → 6 taken 1154 times.
✓ Branch 5 → 10 taken 15 times.
✓ Branch 8 → 9 taken 375 times.
✓ Branch 8 → 10 taken 779 times.
✓ Branch 11 → 3 taken 375 times.
✓ Branch 11 → 12 taken 794 times.
|
1169 | while (pos < data.length() && data[pos].isSpace()) |
| 526 | 375 | pos++; | |
| 527 |
2/2✓ Branch 13 → 14 taken 15 times.
✓ Branch 13 → 15 taken 779 times.
|
794 | if (pos >= data.length()) |
| 528 | 15 | return; | |
| 529 |
3/3✓ Branch 15 → 16 taken 779 times.
✓ Branch 18 → 19 taken 530 times.
✓ Branch 18 → 20 taken 249 times.
|
779 | if (data.mid(pos, 3) == "NIL") { |
| 530 | 530 | pos += 3; | |
| 531 | 530 | return; | |
| 532 | } | ||
| 533 |
2/2✓ Branch 23 → 24 taken 189 times.
✓ Branch 23 → 68 taken 60 times.
|
249 | if (data[pos] == '(') { |
| 534 | 189 | int depth = 0; | |
| 535 |
1/2✓ Branch 67 → 25 taken 2365 times.
✗ Branch 67 → 68 not taken.
|
2365 | while (pos < data.length()) { |
| 536 | // T-401/Bug 12: Skip quoted strings to avoid counting | ||
| 537 | // parentheses inside them (e.g. "Smith (CTO)") | ||
| 538 |
2/2✓ Branch 28 → 29 taken 571 times.
✓ Branch 28 → 54 taken 1794 times.
|
2365 | if (data[pos] == '"') { |
| 539 | 571 | pos++; // skip opening quote | |
| 540 |
5/6✓ Branch 42 → 43 taken 3672 times.
✗ Branch 42 → 48 not taken.
✓ Branch 46 → 47 taken 3101 times.
✓ Branch 46 → 48 taken 571 times.
✓ Branch 49 → 30 taken 3101 times.
✓ Branch 49 → 50 taken 571 times.
|
3672 | while (pos < data.length() && data[pos] != '"') { |
| 541 |
5/6✓ Branch 33 → 34 taken 6 times.
✓ Branch 33 → 37 taken 3095 times.
✓ Branch 35 → 36 taken 6 times.
✗ Branch 35 → 37 not taken.
✓ Branch 38 → 39 taken 6 times.
✓ Branch 38 → 40 taken 3095 times.
|
3101 | if (data[pos] == '\\' && pos + 1 < data.length()) |
| 542 | 6 | pos++; | |
| 543 | 3101 | pos++; | |
| 544 | } | ||
| 545 |
1/2✓ Branch 51 → 52 taken 571 times.
✗ Branch 51 → 53 not taken.
|
571 | if (pos < data.length()) |
| 546 | 571 | pos++; // skip closing quote | |
| 547 | 571 | continue; | |
| 548 | } | ||
| 549 |
2/2✓ Branch 57 → 58 taken 291 times.
✓ Branch 57 → 59 taken 1503 times.
|
1794 | if (data[pos] == '(') |
| 550 | 291 | depth++; | |
| 551 |
2/2✓ Branch 62 → 63 taken 291 times.
✓ Branch 62 → 65 taken 1212 times.
|
1503 | else if (data[pos] == ')') { |
| 552 | 291 | depth--; | |
| 553 |
2/2✓ Branch 63 → 64 taken 189 times.
✓ Branch 63 → 65 taken 102 times.
|
291 | if (depth == 0) { |
| 554 | 189 | pos++; | |
| 555 | 189 | return; | |
| 556 | } | ||
| 557 | } | ||
| 558 | 1605 | pos++; | |
| 559 | } | ||
| 560 | } | ||
| 561 | 140 | }; | |
| 562 | |||
| 563 | // Helper: extract first address from an address list. | ||
| 564 | // IMAP address list formats: | ||
| 565 | // NIL → no addresses | ||
| 566 | // ((name NIL user host)) → single address | ||
| 567 | // ((name NIL user host)(name NIL user host)) → multiple addresses | ||
| 568 | // ((NIL NIL group NIL)(name NIL u h)(NIL NIL NIL NIL)) → group syntax | ||
| 569 | 140 | auto extractAddress = [&](int &pos) -> std::pair<QString, QString> { | |
| 570 |
6/6✓ Branch 5 → 6 taken 267 times.
✓ Branch 5 → 10 taken 3 times.
✓ Branch 8 → 9 taken 130 times.
✓ Branch 8 → 10 taken 137 times.
✓ Branch 11 → 3 taken 130 times.
✓ Branch 11 → 12 taken 140 times.
|
270 | while (pos < data.length() && data[pos].isSpace()) |
| 571 | 130 | pos++; | |
| 572 |
2/2✓ Branch 13 → 14 taken 3 times.
✓ Branch 13 → 15 taken 137 times.
|
140 | if (pos >= data.length()) |
| 573 | 3 | return {}; | |
| 574 | |||
| 575 | // NIL = no address list | ||
| 576 |
3/3✓ Branch 15 → 16 taken 137 times.
✓ Branch 18 → 19 taken 26 times.
✓ Branch 18 → 20 taken 111 times.
|
137 | if (data.mid(pos, 3) == "NIL") { |
| 577 | 26 | pos += 3; | |
| 578 | 26 | return {}; | |
| 579 | } | ||
| 580 | |||
| 581 | // Must start with '(' (outer list) | ||
| 582 |
2/2✓ Branch 23 → 24 taken 9 times.
✓ Branch 23 → 25 taken 102 times.
|
111 | if (data[pos] != '(') { |
| 583 | 9 | return {}; | |
| 584 | } | ||
| 585 | |||
| 586 | // Save start position for fallback | ||
| 587 | 102 | int listStart = pos; | |
| 588 | |||
| 589 | // Check if we have "((" (normal case) or just "(" | ||
| 590 |
5/6✓ Branch 26 → 27 taken 102 times.
✗ Branch 26 → 32 not taken.
✓ Branch 30 → 31 taken 100 times.
✓ Branch 30 → 32 taken 2 times.
✓ Branch 33 → 34 taken 100 times.
✓ Branch 33 → 110 taken 2 times.
|
102 | if (pos + 1 < data.length() && data[pos + 1] == '(') { |
| 591 | // Standard format: ((name NIL user host) ...) | ||
| 592 | 100 | pos += 2; // skip (( | |
| 593 |
2/2✓ Branch 34 → 35 taken 100 times.
✓ Branch 35 → 36 taken 100 times.
|
100 | QString name = decodeRfc2047(extractQuoted(pos)); |
| 594 |
1/1✓ Branch 37 → 38 taken 100 times.
|
100 | extractQuoted(pos); // skip at-domain-list (NIL) |
| 595 |
1/1✓ Branch 39 → 40 taken 100 times.
|
100 | QString user = extractQuoted(pos); |
| 596 |
1/1✓ Branch 40 → 41 taken 100 times.
|
100 | QString host = extractQuoted(pos); |
| 597 | |||
| 598 | // Skip to end of address list (balance all parens from listStart) | ||
| 599 | 100 | int depth = 2; // we opened (( | |
| 600 |
5/6✓ Branch 83 → 84 taken 300 times.
✗ Branch 83 → 86 not taken.
✓ Branch 84 → 85 taken 200 times.
✓ Branch 84 → 86 taken 100 times.
✓ Branch 87 → 42 taken 200 times.
✓ Branch 87 → 88 taken 100 times.
|
300 | while (pos < data.length() && depth > 0) { |
| 601 | // T-79.A4/M1: skip quoted strings so parentheses inside display | ||
| 602 | // names (e.g. "John (Work)") don't desynchronize the balance — | ||
| 603 | // same walk as skipGroup() above (T-401/Bug 12). | ||
| 604 |
1/2✗ Branch 45 → 46 not taken.
✓ Branch 45 → 71 taken 200 times.
|
200 | if (data[pos] == '"') { |
| 605 | ✗ | pos++; // skip opening quote | |
| 606 | ✗ | while (pos < data.length() && data[pos] != '"') { | |
| 607 | ✗ | if (data[pos] == '\\' && pos + 1 < data.length()) | |
| 608 | ✗ | pos++; | |
| 609 | ✗ | pos++; | |
| 610 | } | ||
| 611 | ✗ | if (pos < data.length()) | |
| 612 | ✗ | pos++; // skip closing quote | |
| 613 | ✗ | continue; | |
| 614 | } | ||
| 615 |
1/2✗ Branch 74 → 75 not taken.
✓ Branch 74 → 76 taken 200 times.
|
200 | if (data[pos] == '(') |
| 616 | ✗ | depth++; | |
| 617 |
1/2✓ Branch 79 → 80 taken 200 times.
✗ Branch 79 → 81 not taken.
|
200 | else if (data[pos] == ')') |
| 618 | 200 | depth--; | |
| 619 | 200 | pos++; | |
| 620 | } | ||
| 621 | |||
| 622 | 100 | QString email; | |
| 623 |
6/6✓ Branch 89 → 90 taken 99 times.
✓ Branch 89 → 93 taken 1 time.
✓ Branch 91 → 92 taken 97 times.
✓ Branch 91 → 93 taken 2 times.
✓ Branch 94 → 95 taken 97 times.
✓ Branch 94 → 101 taken 3 times.
|
100 | if (!user.isEmpty() && !host.isEmpty()) { |
| 624 |
2/2✓ Branch 95 → 96 taken 97 times.
✓ Branch 96 → 97 taken 97 times.
|
97 | email = user + "@" + host; |
| 625 |
2/2✓ Branch 102 → 103 taken 2 times.
✓ Branch 102 → 104 taken 1 time.
|
3 | } else if (!user.isEmpty()) { |
| 626 | 2 | email = user; // partial address | |
| 627 | } | ||
| 628 | 100 | return {name, email}; | |
| 629 | 100 | } else { | |
| 630 | // Unexpected format – skip the whole group safely | ||
| 631 | 2 | skipGroup(pos); | |
| 632 | 2 | return {}; | |
| 633 | } | ||
| 634 | 140 | }; | |
| 635 | |||
| 636 | // from address | ||
| 637 |
1/1✓ Branch 194 → 195 taken 140 times.
|
140 | auto [fromName, fromEmail] = extractAddress(pos); |
| 638 |
2/2✓ Branch 198 → 199 taken 99 times.
✓ Branch 198 → 212 taken 41 times.
|
140 | if (!fromEmail.isEmpty()) { |
| 639 | header.from = | ||
| 640 |
9/13✓ Branch 200 → 201 taken 3 times.
✓ Branch 200 → 202 taken 96 times.
✓ Branch 202 → 203 taken 96 times.
✓ Branch 203 → 204 taken 96 times.
✓ Branch 204 → 205 taken 96 times.
✓ Branch 207 → 208 taken 96 times.
✓ Branch 207 → 209 taken 3 times.
✓ Branch 209 → 210 taken 96 times.
✓ Branch 209 → 211 taken 3 times.
✗ Branch 781 → 782 not taken.
✗ Branch 781 → 783 not taken.
✗ Branch 785 → 786 not taken.
✗ Branch 785 → 787 not taken.
|
99 | fromName.isEmpty() ? fromEmail : fromName + " <" + fromEmail + ">"; |
| 641 | } | ||
| 642 | |||
| 643 | // Skip sender (usually same as from) | ||
| 644 |
1/1✓ Branch 212 → 213 taken 140 times.
|
140 | skipGroup(pos); |
| 645 | |||
| 646 | // Skip reply-to | ||
| 647 |
1/1✓ Branch 213 → 214 taken 140 times.
|
140 | skipGroup(pos); |
| 648 | |||
| 649 | // Sprint 95 S95.1.11: every To and Cc address ("Name <a@b>, c@d"), so | ||
| 650 | // server rules can be previewed and applied on cached mail. Each entry | ||
| 651 | // is bounded by the balanced-group skip, so a malformed entry cannot | ||
| 652 | // desynchronize the rest of the envelope. | ||
| 653 | 280 | auto extractAddressList = [&](int &pos) -> QString { | |
| 654 |
6/6✓ Branch 5 → 6 taken 524 times.
✓ Branch 5 → 10 taken 6 times.
✓ Branch 8 → 9 taken 250 times.
✓ Branch 8 → 10 taken 274 times.
✓ Branch 11 → 3 taken 250 times.
✓ Branch 11 → 12 taken 280 times.
|
530 | while (pos < data.length() && data[pos].isSpace()) |
| 655 | 250 | pos++; | |
| 656 | 280 | QStringList addresses; | |
| 657 |
6/6✓ Branch 13 → 14 taken 274 times.
✓ Branch 13 → 18 taken 6 times.
✓ Branch 17 → 18 taken 189 times.
✓ Branch 17 → 19 taken 85 times.
✓ Branch 20 → 21 taken 195 times.
✓ Branch 20 → 23 taken 85 times.
|
280 | if (pos >= data.length() || data[pos] != '(') { |
| 658 |
1/1✓ Branch 21 → 22 taken 195 times.
|
195 | skipGroup(pos); // NIL or unexpected |
| 659 | 195 | return {}; | |
| 660 | } | ||
| 661 | 85 | int listEnd = pos; | |
| 662 |
1/1✓ Branch 23 → 24 taken 85 times.
|
85 | skipGroup(listEnd); |
| 663 | 85 | int entry = pos + 1; | |
| 664 |
1/2✓ Branch 79 → 25 taken 177 times.
✗ Branch 79 → 80 not taken.
|
177 | while (entry < listEnd) { |
| 665 |
3/6✓ Branch 27 → 28 taken 177 times.
✗ Branch 27 → 32 not taken.
✗ Branch 30 → 31 not taken.
✓ Branch 30 → 32 taken 177 times.
✗ Branch 33 → 26 not taken.
✓ Branch 33 → 34 taken 177 times.
|
177 | while (entry < listEnd && data[entry].isSpace()) |
| 666 | ✗ | entry++; | |
| 667 |
5/6✓ Branch 34 → 35 taken 177 times.
✗ Branch 34 → 39 not taken.
✓ Branch 38 → 39 taken 85 times.
✓ Branch 38 → 40 taken 92 times.
✓ Branch 41 → 42 taken 85 times.
✓ Branch 41 → 43 taken 92 times.
|
177 | if (entry >= listEnd || data[entry] != '(') |
| 668 | 85 | break; | |
| 669 | 92 | int entryEnd = entry; | |
| 670 |
1/1✓ Branch 43 → 44 taken 92 times.
|
92 | skipGroup(entryEnd); |
| 671 | 92 | int field = entry + 1; | |
| 672 |
2/2✓ Branch 44 → 45 taken 92 times.
✓ Branch 45 → 46 taken 92 times.
|
92 | const QString name = decodeRfc2047(extractQuoted(field)); |
| 673 |
1/1✓ Branch 47 → 48 taken 92 times.
|
92 | extractQuoted(field); // at-domain-list |
| 674 |
1/1✓ Branch 49 → 50 taken 92 times.
|
92 | const QString user = extractQuoted(field); |
| 675 |
1/1✓ Branch 50 → 51 taken 92 times.
|
92 | const QString host = extractQuoted(field); |
| 676 | // Group markers have no host (RFC 3501 §7.4.2); they are skipped. | ||
| 677 |
6/6✓ Branch 52 → 53 taken 91 times.
✓ Branch 52 → 56 taken 1 time.
✓ Branch 54 → 55 taken 90 times.
✓ Branch 54 → 56 taken 1 time.
✓ Branch 57 → 58 taken 90 times.
✓ Branch 57 → 75 taken 2 times.
|
92 | if (!user.isEmpty() && !host.isEmpty()) { |
| 678 |
2/2✓ Branch 58 → 59 taken 90 times.
✓ Branch 59 → 60 taken 90 times.
|
90 | const QString email = user + "@" + host; |
| 679 |
10/14✓ Branch 62 → 63 taken 1 time.
✓ Branch 62 → 64 taken 89 times.
✓ Branch 64 → 65 taken 89 times.
✓ Branch 65 → 66 taken 89 times.
✓ Branch 66 → 67 taken 89 times.
✓ Branch 67 → 68 taken 90 times.
✓ Branch 69 → 70 taken 89 times.
✓ Branch 69 → 71 taken 1 time.
✓ Branch 71 → 72 taken 89 times.
✓ Branch 71 → 73 taken 1 time.
✗ Branch 100 → 101 not taken.
✗ Branch 100 → 102 not taken.
✗ Branch 104 → 105 not taken.
✗ Branch 104 → 106 not taken.
|
90 | addresses.append(name.isEmpty() ? email : name + " <" + email + ">"); |
| 680 | 90 | } | |
| 681 | 92 | entry = entryEnd; | |
| 682 | 92 | } | |
| 683 | 85 | pos = listEnd; | |
| 684 |
1/1✓ Branch 83 → 84 taken 85 times.
|
85 | return addresses.join(QStringLiteral(", ")); |
| 685 | 280 | }; | |
| 686 | |||
| 687 |
1/1✓ Branch 214 → 215 taken 140 times.
|
140 | header.to = extractAddressList(pos); |
| 688 |
1/1✓ Branch 217 → 218 taken 140 times.
|
140 | header.cc = extractAddressList(pos); |
| 689 | 140 | header.recipientsKnown = true; | |
| 690 | |||
| 691 | // Skip bcc (address list) | ||
| 692 |
1/1✓ Branch 220 → 221 taken 140 times.
|
140 | skipGroup(pos); |
| 693 | |||
| 694 | // in-reply-to (quoted string or NIL) | ||
| 695 |
1/1✓ Branch 221 → 222 taken 140 times.
|
140 | QString rawInReplyTo = extractQuoted(pos); |
| 696 |
2/2✓ Branch 223 → 224 taken 7 times.
✓ Branch 223 → 231 taken 133 times.
|
140 | if (!rawInReplyTo.isEmpty()) { |
| 697 |
1/1✓ Branch 224 → 225 taken 7 times.
|
7 | header.inReplyToIds = MessageId::parseTokens(rawInReplyTo); |
| 698 |
2/2✓ Branch 228 → 229 taken 6 times.
✓ Branch 228 → 231 taken 1 time.
|
7 | if (!header.inReplyToIds.isEmpty()) |
| 699 |
1/1✓ Branch 229 → 230 taken 6 times.
|
6 | header.inReplyTo = header.inReplyToIds.first(); |
| 700 | } | ||
| 701 | |||
| 702 | // message-id (quoted string or NIL) | ||
| 703 |
1/1✓ Branch 231 → 232 taken 140 times.
|
140 | QString rawMessageId = extractQuoted(pos); |
| 704 |
2/2✓ Branch 233 → 234 taken 97 times.
✓ Branch 233 → 238 taken 43 times.
|
140 | if (!rawMessageId.isEmpty()) { |
| 705 |
1/1✓ Branch 234 → 235 taken 97 times.
|
97 | header.messageId = MessageId::canonicalFirst(rawMessageId); |
| 706 | } | ||
| 707 | 140 | } | |
| 708 | |||
| 709 | // Fallback: if ENVELOPE date is missing/unparseable, use INTERNALDATE | ||
| 710 |
3/3✓ Branch 243 → 244 taken 158 times.
✓ Branch 244 → 245 taken 57 times.
✓ Branch 244 → 323 taken 101 times.
|
158 | if (!header.date.isValid()) { |
| 711 |
4/7✓ Branch 245 → 246 taken 5 times.
✓ Branch 245 → 258 taken 52 times.
✓ Branch 247 → 248 taken 5 times.
✗ Branch 247 → 258 not taken.
✓ Branch 252 → 253 taken 5 times.
✗ Branch 810 → 811 not taken.
✗ Branch 810 → 812 not taken.
|
62 | static QRegularExpression idateRx(QStringLiteral(R"~~(^\s*"([^"]+)")~~")); |
| 712 | 57 | const qsizetype idateItem = topLevelItem(data, "INTERNALDATE", " "); | |
| 713 |
4/4✓ Branch 260 → 261 taken 53 times.
✓ Branch 260 → 262 taken 4 times.
✓ Branch 262 → 263 taken 4 times.
✓ Branch 263 → 264 taken 57 times.
|
57 | auto idateMatch = idateRx.match(idateItem < 0 ? QString() : data.mid(idateItem)); |
| 714 |
3/3✓ Branch 265 → 266 taken 57 times.
✓ Branch 266 → 267 taken 4 times.
✓ Branch 266 → 321 taken 53 times.
|
57 | if (idateMatch.hasMatch()) { |
| 715 |
1/1✓ Branch 267 → 268 taken 4 times.
|
4 | QString idateStr = idateMatch.captured(1); |
| 716 | // INTERNALDATE format: "16-Feb-2026 21:44:53 +0100" | ||
| 717 |
1/1✓ Branch 268 → 269 taken 4 times.
|
4 | QLocale cLocale = QLocale::c(); |
| 718 |
1/1✓ Branch 272 → 273 taken 4 times.
|
8 | header.date = cLocale.toDateTime( |
| 719 | 12 | idateStr, QStringLiteral("d-MMM-yyyy H:mm:ss t")); | |
| 720 |
3/3✓ Branch 277 → 278 taken 4 times.
✓ Branch 278 → 279 taken 1 time.
✓ Branch 278 → 288 taken 3 times.
|
4 | if (!header.date.isValid()) { |
| 721 |
1/1✓ Branch 282 → 283 taken 1 time.
|
2 | header.date = cLocale.toDateTime( |
| 722 | 3 | idateStr, QStringLiteral("dd-MMM-yyyy HH:mm:ss t")); | |
| 723 | } | ||
| 724 |
3/3✓ Branch 288 → 289 taken 4 times.
✓ Branch 289 → 290 taken 3 times.
✓ Branch 289 → 304 taken 1 time.
|
4 | if (header.date.isValid()) { |
| 725 |
5/5✓ Branch 290 → 291 taken 3 times.
✓ Branch 294 → 295 taken 3 times.
✓ Branch 295 → 296 taken 3 times.
✓ Branch 302 → 292 taken 3 times.
✓ Branch 302 → 303 taken 3 times.
|
6 | qCInfo(lcImapParser) << "Used INTERNALDATE fallback for UID" |
| 726 |
3/3✓ Branch 296 → 297 taken 3 times.
✓ Branch 297 → 298 taken 3 times.
✓ Branch 298 → 299 taken 3 times.
|
3 | << header.uid << ":" << idateStr; |
| 727 | } else { | ||
| 728 |
5/5✓ Branch 304 → 305 taken 1 time.
✓ Branch 308 → 309 taken 1 time.
✓ Branch 309 → 310 taken 1 time.
✓ Branch 316 → 306 taken 1 time.
✓ Branch 316 → 317 taken 1 time.
|
2 | qCWarning(lcImapParser) << "INTERNALDATE parse also failed for UID" |
| 729 |
3/3✓ Branch 310 → 311 taken 1 time.
✓ Branch 311 → 312 taken 1 time.
✓ Branch 312 → 313 taken 1 time.
|
1 | << header.uid << ":" << idateStr; |
| 730 | } | ||
| 731 | 4 | } | |
| 732 | 57 | } | |
| 733 | |||
| 734 | // Parse the requested header block as logical fields. IMAP test fixtures and | ||
| 735 | // quoted transports may contain visible "\\r\\n" escapes, while literal | ||
| 736 | // responses contain real line breaks; normalize both without treating an | ||
| 737 | // arbitrary later header as part of References. | ||
| 738 |
1/1✓ Branch 323 → 324 taken 158 times.
|
158 | QString headerBlock = headerFieldsBlock(data); |
| 739 |
1/1✓ Branch 330 → 331 taken 158 times.
|
316 | headerBlock.replace(QStringLiteral("\\r\\n"), QStringLiteral("\n")); |
| 740 |
1/1✓ Branch 341 → 342 taken 158 times.
|
316 | headerBlock.replace(QStringLiteral("\\n"), QStringLiteral("\n")); |
| 741 |
1/1✓ Branch 352 → 353 taken 158 times.
|
316 | headerBlock.replace(QStringLiteral("\r\n"), QStringLiteral("\n")); |
| 742 |
1/1✓ Branch 361 → 362 taken 158 times.
|
158 | headerBlock.replace(QLatin1Char('\r'), QLatin1Char('\n')); |
| 743 | |||
| 744 | // ENVELOPE permits only a single in-reply-to string and some servers omit | ||
| 745 | // or simplify it. Prefer the explicitly requested raw header so folded and | ||
| 746 | // multi-ID values survive incremental header synchronization. | ||
| 747 | static QRegularExpression inReplyToStart( | ||
| 748 | 18 | QStringLiteral(R"(In-Reply-To\s*:)"), | |
| 749 |
4/7✓ Branch 362 → 363 taken 9 times.
✓ Branch 362 → 375 taken 149 times.
✓ Branch 364 → 365 taken 9 times.
✗ Branch 364 → 375 not taken.
✓ Branch 369 → 370 taken 9 times.
✗ Branch 896 → 897 not taken.
✗ Branch 896 → 898 not taken.
|
176 | QRegularExpression::CaseInsensitiveOption); |
| 750 |
1/1✓ Branch 375 → 376 taken 158 times.
|
158 | QRegularExpressionMatch inReplyToMatch; |
| 751 | 158 | qsizetype inReplyToOffset = 0; | |
| 752 | 158 | while ((inReplyToMatch = | |
| 753 |
4/4✓ Branch 394 → 395 taken 160 times.
✓ Branch 396 → 397 taken 160 times.
✓ Branch 398 → 377 taken 6 times.
✓ Branch 398 → 399 taken 154 times.
|
160 | inReplyToStart.match(headerBlock, inReplyToOffset)).hasMatch()) { |
| 754 |
1/1✓ Branch 377 → 378 taken 6 times.
|
6 | const qsizetype start = inReplyToMatch.capturedStart(); |
| 755 |
2/2✓ Branch 378 → 379 taken 3 times.
✓ Branch 378 → 387 taken 3 times.
|
9 | if (start == 0 || |
| 756 |
2/2✓ Branch 381 → 382 taken 2 times.
✓ Branch 381 → 388 taken 1 time.
|
3 | (!headerBlock.at(start - 1).isLetterOrNumber() && |
| 757 |
4/4✓ Branch 386 → 387 taken 1 time.
✓ Branch 386 → 388 taken 1 time.
✓ Branch 389 → 390 taken 4 times.
✓ Branch 389 → 391 taken 2 times.
|
8 | headerBlock.at(start - 1) != QLatin1Char('-'))) { |
| 758 | 4 | break; | |
| 759 | } | ||
| 760 |
1/1✓ Branch 391 → 392 taken 2 times.
|
2 | inReplyToOffset = inReplyToMatch.capturedEnd(); |
| 761 | } | ||
| 762 |
3/3✓ Branch 399 → 400 taken 158 times.
✓ Branch 400 → 401 taken 4 times.
✓ Branch 400 → 454 taken 154 times.
|
158 | if (inReplyToMatch.hasMatch()) { |
| 763 |
1/1✓ Branch 401 → 402 taken 4 times.
|
4 | const qsizetype valueStart = inReplyToMatch.capturedEnd(); |
| 764 | 4 | qsizetype lineEnd = headerBlock.indexOf(QLatin1Char('\n'), valueStart); | |
| 765 |
2/2✓ Branch 405 → 406 taken 1 time.
✓ Branch 405 → 407 taken 3 times.
|
4 | if (lineEnd < 0) |
| 766 | 1 | lineEnd = headerBlock.size(); | |
| 767 |
1/1✓ Branch 407 → 408 taken 4 times.
|
4 | QString value = headerBlock.mid(valueStart, lineEnd - valueStart); |
| 768 |
2/2✓ Branch 441 → 409 taken 4 times.
✓ Branch 441 → 442 taken 1 time.
|
5 | while (lineEnd < headerBlock.size()) { |
| 769 | 4 | const qsizetype continuationStart = lineEnd + 1; | |
| 770 |
2/2✓ Branch 410 → 411 taken 3 times.
✓ Branch 410 → 421 taken 1 time.
|
7 | if (continuationStart >= headerBlock.size() || |
| 771 |
1/2✓ Branch 415 → 416 taken 3 times.
✗ Branch 415 → 422 not taken.
|
3 | (headerBlock.at(continuationStart) != QLatin1Char(' ') && |
| 772 |
4/4✓ Branch 420 → 421 taken 2 times.
✓ Branch 420 → 422 taken 1 time.
✓ Branch 423 → 424 taken 3 times.
✓ Branch 423 → 425 taken 1 time.
|
7 | headerBlock.at(continuationStart) != QLatin1Char('\t'))) { |
| 773 | 3 | break; | |
| 774 | } | ||
| 775 | qsizetype continuationEnd = | ||
| 776 | 1 | headerBlock.indexOf(QLatin1Char('\n'), continuationStart); | |
| 777 |
1/2✗ Branch 428 → 429 not taken.
✓ Branch 428 → 430 taken 1 time.
|
1 | if (continuationEnd < 0) |
| 778 | ✗ | continuationEnd = headerBlock.size(); | |
| 779 | 2 | value += QLatin1Char(' ') + | |
| 780 | headerBlock | ||
| 781 |
1/1✓ Branch 430 → 431 taken 1 time.
|
2 | .mid(continuationStart, |
| 782 | continuationEnd - continuationStart) | ||
| 783 |
3/3✓ Branch 431 → 432 taken 1 time.
✓ Branch 434 → 435 taken 1 time.
✓ Branch 435 → 436 taken 1 time.
|
3 | .trimmed(); |
| 784 | 1 | lineEnd = continuationEnd; | |
| 785 | } | ||
| 786 |
1/1✓ Branch 442 → 443 taken 4 times.
|
4 | header.inReplyToIds = MessageId::parseTokens(value); |
| 787 | 4 | header.inReplyTo = header.inReplyToIds.isEmpty() | |
| 788 |
2/2✓ Branch 446 → 447 taken 1 time.
✓ Branch 446 → 448 taken 3 times.
|
8 | ? QString() |
| 789 |
1/1✓ Branch 448 → 449 taken 3 times.
|
4 | : header.inReplyToIds.first(); |
| 790 | 4 | } | |
| 791 | |||
| 792 | static QRegularExpression refsStart( | ||
| 793 | 18 | QStringLiteral(R"(References\s*:)"), | |
| 794 |
4/7✓ Branch 454 → 455 taken 9 times.
✓ Branch 454 → 467 taken 149 times.
✓ Branch 456 → 457 taken 9 times.
✗ Branch 456 → 467 not taken.
✓ Branch 461 → 462 taken 9 times.
✗ Branch 926 → 927 not taken.
✗ Branch 926 → 928 not taken.
|
176 | QRegularExpression::CaseInsensitiveOption); |
| 795 |
1/1✓ Branch 467 → 468 taken 158 times.
|
158 | QRegularExpressionMatch refsMatch; |
| 796 | 158 | qsizetype searchOffset = 0; | |
| 797 |
4/4✓ Branch 486 → 487 taken 160 times.
✓ Branch 488 → 489 taken 160 times.
✓ Branch 490 → 469 taken 11 times.
✓ Branch 490 → 491 taken 149 times.
|
318 | while ((refsMatch = refsStart.match(headerBlock, searchOffset)).hasMatch()) { |
| 798 |
1/1✓ Branch 469 → 470 taken 11 times.
|
11 | const qsizetype start = refsMatch.capturedStart(); |
| 799 |
2/2✓ Branch 470 → 471 taken 4 times.
✓ Branch 470 → 479 taken 7 times.
|
15 | if (start == 0 || |
| 800 |
2/2✓ Branch 473 → 474 taken 3 times.
✓ Branch 473 → 480 taken 1 time.
|
4 | (!headerBlock.at(start - 1).isLetterOrNumber() && |
| 801 |
4/4✓ Branch 478 → 479 taken 2 times.
✓ Branch 478 → 480 taken 1 time.
✓ Branch 481 → 482 taken 9 times.
✓ Branch 481 → 483 taken 2 times.
|
14 | headerBlock.at(start - 1) != QLatin1Char('-'))) { |
| 802 | 9 | break; | |
| 803 | } | ||
| 804 |
1/1✓ Branch 483 → 484 taken 2 times.
|
2 | searchOffset = refsMatch.capturedEnd(); |
| 805 | } | ||
| 806 |
3/3✓ Branch 491 → 492 taken 158 times.
✓ Branch 492 → 493 taken 9 times.
✓ Branch 492 → 539 taken 149 times.
|
158 | if (refsMatch.hasMatch()) { |
| 807 |
1/1✓ Branch 493 → 494 taken 9 times.
|
9 | qsizetype valueStart = refsMatch.capturedEnd(); |
| 808 | 9 | qsizetype lineEnd = headerBlock.indexOf(QLatin1Char('\n'), valueStart); | |
| 809 |
2/2✓ Branch 497 → 498 taken 2 times.
✓ Branch 497 → 499 taken 7 times.
|
9 | if (lineEnd < 0) |
| 810 | 2 | lineEnd = headerBlock.size(); | |
| 811 |
1/1✓ Branch 499 → 500 taken 9 times.
|
9 | QString refsValue = headerBlock.mid(valueStart, lineEnd - valueStart); |
| 812 | |||
| 813 |
2/2✓ Branch 533 → 501 taken 9 times.
✓ Branch 533 → 534 taken 3 times.
|
12 | while (lineEnd < headerBlock.size()) { |
| 814 | 9 | const qsizetype continuationStart = lineEnd + 1; | |
| 815 |
2/2✓ Branch 502 → 503 taken 6 times.
✓ Branch 502 → 513 taken 3 times.
|
15 | if (continuationStart >= headerBlock.size() || |
| 816 |
1/2✓ Branch 507 → 508 taken 6 times.
✗ Branch 507 → 514 not taken.
|
6 | (headerBlock.at(continuationStart) != QLatin1Char(' ') && |
| 817 |
4/4✓ Branch 512 → 513 taken 3 times.
✓ Branch 512 → 514 taken 3 times.
✓ Branch 515 → 516 taken 6 times.
✓ Branch 515 → 517 taken 3 times.
|
15 | headerBlock.at(continuationStart) != QLatin1Char('\t'))) { |
| 818 | 6 | break; | |
| 819 | } | ||
| 820 | qsizetype continuationEnd = | ||
| 821 | 3 | headerBlock.indexOf(QLatin1Char('\n'), continuationStart); | |
| 822 |
2/2✓ Branch 520 → 521 taken 1 time.
✓ Branch 520 → 522 taken 2 times.
|
3 | if (continuationEnd < 0) |
| 823 | 1 | continuationEnd = headerBlock.size(); | |
| 824 | 6 | refsValue += QLatin1Char(' ') + | |
| 825 | headerBlock | ||
| 826 |
1/1✓ Branch 522 → 523 taken 3 times.
|
6 | .mid(continuationStart, |
| 827 | continuationEnd - continuationStart) | ||
| 828 |
3/3✓ Branch 523 → 524 taken 3 times.
✓ Branch 526 → 527 taken 3 times.
✓ Branch 527 → 528 taken 3 times.
|
9 | .trimmed(); |
| 829 | 3 | lineEnd = continuationEnd; | |
| 830 | } | ||
| 831 | |||
| 832 |
1/1✓ Branch 534 → 535 taken 9 times.
|
9 | header.references = MessageId::parseTokens(refsValue); |
| 833 | 9 | } | |
| 834 | |||
| 835 | // Sprint 95 S95.1.11: List-Id (RFC 2919), requested with the header | ||
| 836 | // fields above; stored as the bare id ("dev.lists.example.org"). | ||
| 837 | 18 | static QRegularExpression listIdStart(QStringLiteral(R"((?:^|\n|")List-Id[ \t]*:)"), | |
| 838 |
4/7✓ Branch 539 → 540 taken 9 times.
✓ Branch 539 → 552 taken 149 times.
✓ Branch 541 → 542 taken 9 times.
✗ Branch 541 → 552 not taken.
✓ Branch 546 → 547 taken 9 times.
✗ Branch 955 → 956 not taken.
✗ Branch 955 → 957 not taken.
|
176 | QRegularExpression::CaseInsensitiveOption); |
| 839 |
1/1✓ Branch 553 → 554 taken 158 times.
|
158 | const QRegularExpressionMatch listIdMatch = listIdStart.match(headerBlock); |
| 840 |
3/3✓ Branch 554 → 555 taken 158 times.
✓ Branch 555 → 556 taken 1 time.
✓ Branch 555 → 618 taken 157 times.
|
158 | if (listIdMatch.hasMatch()) { |
| 841 |
1/1✓ Branch 556 → 557 taken 1 time.
|
1 | qsizetype lineEnd = headerBlock.indexOf(QLatin1Char('\n'), listIdMatch.capturedEnd()); |
| 842 |
1/2✗ Branch 560 → 561 not taken.
✓ Branch 560 → 562 taken 1 time.
|
1 | if (lineEnd < 0) |
| 843 | ✗ | lineEnd = headerBlock.size(); | |
| 844 |
3/3✓ Branch 562 → 563 taken 1 time.
✓ Branch 563 → 564 taken 1 time.
✓ Branch 564 → 565 taken 1 time.
|
1 | QString value = headerBlock.mid(listIdMatch.capturedEnd(), lineEnd - listIdMatch.capturedEnd()); |
| 845 |
3/4✓ Branch 582 → 583 taken 2 times.
✗ Branch 582 → 594 not taken.
✓ Branch 587 → 588 taken 1 time.
✓ Branch 587 → 593 taken 1 time.
|
5 | while (lineEnd + 1 < headerBlock.size() && |
| 846 |
3/4✗ Branch 592 → 593 not taken.
✓ Branch 592 → 594 taken 1 time.
✓ Branch 595 → 566 taken 1 time.
✓ Branch 595 → 596 taken 1 time.
|
5 | (headerBlock.at(lineEnd + 1) == QLatin1Char(' ') || headerBlock.at(lineEnd + 1) == QLatin1Char('\t'))) { |
| 847 | 1 | qsizetype next = headerBlock.indexOf(QLatin1Char('\n'), lineEnd + 1); | |
| 848 |
1/2✗ Branch 569 → 570 not taken.
✓ Branch 569 → 571 taken 1 time.
|
1 | if (next < 0) |
| 849 | ✗ | next = headerBlock.size(); | |
| 850 |
4/4✓ Branch 571 → 572 taken 1 time.
✓ Branch 572 → 573 taken 1 time.
✓ Branch 575 → 576 taken 1 time.
✓ Branch 576 → 577 taken 1 time.
|
1 | value += QLatin1Char(' ') + headerBlock.mid(lineEnd + 1, next - lineEnd - 1).trimmed(); |
| 851 | 1 | lineEnd = next; | |
| 852 | } | ||
| 853 | 1 | const qsizetype open = value.lastIndexOf(QLatin1Char('<')); | |
| 854 | 1 | const qsizetype close = value.indexOf(QLatin1Char('>'), open + 1); | |
| 855 |
3/5✓ Branch 602 → 603 taken 1 time.
✗ Branch 602 → 606 not taken.
✓ Branch 603 → 604 taken 1 time.
✗ Branch 603 → 606 not taken.
✓ Branch 604 → 605 taken 1 time.
|
2 | header.listId = (open >= 0 && close > open ? value.mid(open + 1, close - open - 1) : value) |
| 856 |
3/3✓ Branch 607 → 608 taken 1 time.
✓ Branch 610 → 611 taken 1 time.
✓ Branch 611 → 612 taken 1 time.
|
1 | .trimmed().remove(QLatin1Char('"')).toLower(); |
| 857 | 1 | } | |
| 858 | |||
| 859 | // Sprint 105 S105.2.2: one parser for the sync and the reader | ||
| 860 | // (SpamHeaders, fuzzed by fuzz_spam_headers) — the verdict, score, | ||
| 861 | // threshold and tool of the requested filter fields. Only a response | ||
| 862 | // with the HEADER.FIELDS item knows them; others keep the stored verdict. | ||
| 863 | 18 | static const QRegularExpression fieldsItem(QStringLiteral("BODY\\[HEADER\\.FIELDS"), | |
| 864 |
4/7✓ Branch 618 → 619 taken 9 times.
✓ Branch 618 → 631 taken 149 times.
✓ Branch 620 → 621 taken 9 times.
✗ Branch 620 → 631 not taken.
✓ Branch 625 → 626 taken 9 times.
✗ Branch 989 → 990 not taken.
✗ Branch 989 → 991 not taken.
|
176 | QRegularExpression::CaseInsensitiveOption); |
| 865 |
2/2✓ Branch 632 → 633 taken 158 times.
✓ Branch 633 → 634 taken 158 times.
|
158 | header.spamKnown = fieldsItem.match(data).hasMatch(); |
| 866 |
2/2✓ Branch 635 → 636 taken 129 times.
✓ Branch 635 → 654 taken 29 times.
|
158 | if (!header.spamKnown) { |
| 867 | // The header-only transport shape without the item (see | ||
| 868 | // headerFieldsBlock): the former tolerant yes/no scan, no score. | ||
| 869 | 16 | static const QRegularExpression spamYes(QStringLiteral(R"(X-Spam(?:-Flag|-Status)?:\s*Yes)"), | |
| 870 |
4/7✓ Branch 636 → 637 taken 8 times.
✓ Branch 636 → 649 taken 121 times.
✓ Branch 638 → 639 taken 8 times.
✗ Branch 638 → 649 not taken.
✓ Branch 643 → 644 taken 8 times.
✗ Branch 1003 → 1004 not taken.
✗ Branch 1003 → 1005 not taken.
|
145 | QRegularExpression::CaseInsensitiveOption); |
| 871 |
2/2✓ Branch 650 → 651 taken 129 times.
✓ Branch 651 → 652 taken 129 times.
|
129 | header.isSpam = spamYes.match(headerBlock).hasMatch(); |
| 872 | 129 | return header; | |
| 873 | } | ||
| 874 |
4/4✓ Branch 654 → 655 taken 29 times.
✓ Branch 655 → 656 taken 29 times.
✓ Branch 656 → 657 taken 29 times.
✓ Branch 657 → 658 taken 29 times.
|
29 | const SpamHeaders::Verdict spam = SpamHeaders::parse(SpamHeaders::extract(headerBlock.toUtf8() + "\n\n")); |
| 875 | 29 | header.isSpam = spam.spam; | |
| 876 | 29 | header.spamScore = spam.score; | |
| 877 | 29 | header.spamThreshold = spam.threshold; | |
| 878 |
6/6✓ Branch 661 → 662 taken 13 times.
✓ Branch 661 → 668 taken 16 times.
✓ Branch 663 → 664 taken 7 times.
✓ Branch 663 → 667 taken 6 times.
✓ Branch 671 → 672 taken 7 times.
✓ Branch 671 → 673 taken 22 times.
|
36 | header.spamTool = spam.known ? (spam.tool.isEmpty() ? QStringLiteral("other") : spam.tool) : QString(); |
| 879 | |||
| 880 | 29 | return header; | |
| 881 | 161 | } | |
| 882 | |||
| 883 | std::optional<QPair<qint64, QByteArray>> | ||
| 884 | 40 | ImapResponseParser::parseFetchBodyResponse(const QByteArray &data, | |
| 885 | qint64 maxBodyBytes) { | ||
| 886 |
2/2✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 4 taken 39 times.
|
40 | if (maxBodyBytes < 0) |
| 887 | 1 | return std::nullopt; | |
| 888 | |||
| 889 | // S106-019: top-level items only — the former byte scan matched "UID" | ||
| 890 | // inside a quoted ENVELOPE subject or another item's name. | ||
| 891 | 39 | const qsizetype uidItem = topLevelItem(data, "UID", " \t"); | |
| 892 |
2/2✓ Branch 5 → 6 taken 4 times.
✓ Branch 5 → 7 taken 35 times.
|
39 | if (uidItem < 0) |
| 893 | 4 | return std::nullopt; | |
| 894 | 35 | qsizetype pos = uidItem; | |
| 895 |
5/6✓ Branch 10 → 11 taken 70 times.
✗ Branch 10 → 16 not taken.
✓ Branch 12 → 13 taken 36 times.
✓ Branch 12 → 15 taken 34 times.
✓ Branch 17 → 8 taken 35 times.
✓ Branch 17 → 18 taken 35 times.
|
176 | while (pos < data.size() && |
| 896 |
2/2✓ Branch 14 → 15 taken 1 time.
✓ Branch 14 → 16 taken 35 times.
|
106 | (data.at(pos) == ' ' || data.at(pos) == '\t')) |
| 897 | 35 | ++pos; | |
| 898 | |||
| 899 | 35 | qint64 uid = 0; | |
| 900 | 35 | const qsizetype uidStart = pos; | |
| 901 |
7/8✓ Branch 25 → 26 taken 114 times.
✗ Branch 25 → 31 not taken.
✓ Branch 27 → 28 taken 82 times.
✓ Branch 27 → 31 taken 32 times.
✓ Branch 29 → 30 taken 81 times.
✓ Branch 29 → 31 taken 1 time.
✓ Branch 32 → 19 taken 81 times.
✓ Branch 32 → 33 taken 33 times.
|
114 | while (pos < data.size() && data.at(pos) >= '0' && data.at(pos) <= '9') { |
| 902 | 81 | const int digit = data.at(pos) - '0'; | |
| 903 |
2/2✓ Branch 21 → 22 taken 2 times.
✓ Branch 21 → 23 taken 79 times.
|
81 | if (uid > (std::numeric_limits<qint64>::max() - digit) / 10) |
| 904 | 2 | return std::nullopt; | |
| 905 | 79 | uid = uid * 10 + digit; | |
| 906 | 79 | ++pos; | |
| 907 | } | ||
| 908 |
4/4✓ Branch 33 → 34 taken 32 times.
✓ Branch 33 → 35 taken 1 time.
✓ Branch 34 → 35 taken 1 time.
✓ Branch 34 → 36 taken 31 times.
|
33 | if (pos == uidStart || uid <= 0) |
| 909 | 2 | return std::nullopt; | |
| 910 | |||
| 911 | 31 | const qsizetype bodyItem = topLevelItem(data, "BODY[]", " \t<\"{"); | |
| 912 |
2/2✓ Branch 37 → 38 taken 3 times.
✓ Branch 37 → 39 taken 28 times.
|
31 | if (bodyItem < 0) |
| 913 | 3 | return std::nullopt; | |
| 914 | 28 | pos = bodyItem; | |
| 915 | |||
| 916 | // A partial fetch response may echo BODY[]<0>. The requested offset must be | ||
| 917 | // zero; non-zero origins cannot represent the complete raw message. | ||
| 918 |
5/6✓ Branch 40 → 41 taken 28 times.
✗ Branch 40 → 44 not taken.
✓ Branch 42 → 43 taken 7 times.
✓ Branch 42 → 44 taken 21 times.
✓ Branch 45 → 46 taken 7 times.
✓ Branch 45 → 78 taken 21 times.
|
28 | if (pos < data.size() && data.at(pos) == '<') { |
| 919 | 7 | ++pos; | |
| 920 | 7 | const qsizetype offsetStart = pos; | |
| 921 |
7/8✓ Branch 49 → 50 taken 13 times.
✗ Branch 49 → 55 not taken.
✓ Branch 51 → 52 taken 11 times.
✓ Branch 51 → 55 taken 2 times.
✓ Branch 53 → 54 taken 6 times.
✓ Branch 53 → 55 taken 5 times.
✓ Branch 56 → 47 taken 6 times.
✓ Branch 56 → 57 taken 7 times.
|
13 | while (pos < data.size() && data.at(pos) >= '0' && data.at(pos) <= '9') |
| 922 | 6 | ++pos; | |
| 923 |
5/6✓ Branch 57 → 58 taken 5 times.
✓ Branch 57 → 67 taken 2 times.
✓ Branch 59 → 60 taken 5 times.
✗ Branch 59 → 67 not taken.
✓ Branch 61 → 62 taken 4 times.
✓ Branch 61 → 67 taken 1 time.
|
11 | if (pos == offsetStart || pos >= data.size() || data.at(pos) != '>' || |
| 924 |
11/17✓ Branch 64 → 65 taken 4 times.
✓ Branch 66 → 67 taken 2 times.
✓ Branch 66 → 68 taken 2 times.
✓ Branch 69 → 70 taken 4 times.
✓ Branch 69 → 71 taken 3 times.
✓ Branch 71 → 72 taken 4 times.
✓ Branch 71 → 73 taken 3 times.
✓ Branch 73 → 74 taken 4 times.
✓ Branch 73 → 75 taken 3 times.
✓ Branch 75 → 76 taken 5 times.
✓ Branch 75 → 77 taken 2 times.
✗ Branch 127 → 128 not taken.
✗ Branch 127 → 129 not taken.
✗ Branch 131 → 132 not taken.
✗ Branch 131 → 133 not taken.
✗ Branch 135 → 136 not taken.
✗ Branch 135 → 137 not taken.
|
11 | data.mid(offsetStart, pos - offsetStart) != QByteArrayLiteral("0")) { |
| 925 | 5 | return std::nullopt; | |
| 926 | } | ||
| 927 | 2 | ++pos; | |
| 928 | } | ||
| 929 | |||
| 930 |
5/6✓ Branch 81 → 82 taken 46 times.
✗ Branch 81 → 87 not taken.
✓ Branch 83 → 84 taken 24 times.
✓ Branch 83 → 86 taken 22 times.
✓ Branch 88 → 79 taken 23 times.
✓ Branch 88 → 89 taken 23 times.
|
116 | while (pos < data.size() && |
| 931 |
2/2✓ Branch 85 → 86 taken 1 time.
✓ Branch 85 → 87 taken 23 times.
|
70 | (data.at(pos) == ' ' || data.at(pos) == '\t')) |
| 932 | 23 | ++pos; | |
| 933 |
5/6✓ Branch 90 → 91 taken 23 times.
✗ Branch 90 → 93 not taken.
✓ Branch 92 → 93 taken 3 times.
✓ Branch 92 → 94 taken 20 times.
✓ Branch 95 → 96 taken 3 times.
✓ Branch 95 → 97 taken 20 times.
|
23 | if (pos >= data.size() || data.at(pos) != '"') |
| 934 | 3 | return std::nullopt; | |
| 935 | 20 | ++pos; | |
| 936 | |||
| 937 | 20 | QByteArray body; | |
| 938 |
1/1✓ Branch 99 → 100 taken 20 times.
|
20 | body.reserve(static_cast<qsizetype>( |
| 939 | 20 | qMin<qint64>(maxBodyBytes, data.size() - pos))); | |
| 940 | 20 | bool closed = false; | |
| 941 |
2/2✓ Branch 117 → 101 taken 150 times.
✓ Branch 117 → 118 taken 3 times.
|
153 | while (pos < data.size()) { |
| 942 | 150 | uchar byte = static_cast<uchar>(data.at(pos++)); | |
| 943 |
2/2✓ Branch 102 → 103 taken 13 times.
✓ Branch 102 → 104 taken 137 times.
|
150 | if (byte == '"') { |
| 944 | 13 | closed = true; | |
| 945 | 13 | break; | |
| 946 | } | ||
| 947 |
2/2✓ Branch 104 → 105 taken 9 times.
✓ Branch 104 → 112 taken 128 times.
|
137 | if (byte == '\\') { |
| 948 |
1/2✗ Branch 106 → 107 not taken.
✓ Branch 106 → 108 taken 9 times.
|
9 | if (pos >= data.size()) |
| 949 | ✗ | return std::nullopt; | |
| 950 | 9 | byte = static_cast<uchar>(data.at(pos++)); | |
| 951 |
4/4✓ Branch 109 → 110 taken 6 times.
✓ Branch 109 → 112 taken 3 times.
✓ Branch 110 → 111 taken 2 times.
✓ Branch 110 → 112 taken 4 times.
|
9 | if (byte != '"' && byte != '\\') |
| 952 | 2 | return std::nullopt; | |
| 953 | } | ||
| 954 |
2/2✓ Branch 113 → 114 taken 2 times.
✓ Branch 113 → 115 taken 133 times.
|
135 | if (body.size() >= maxBodyBytes) |
| 955 | 2 | return std::nullopt; | |
| 956 |
1/1✓ Branch 115 → 116 taken 133 times.
|
133 | body.append(static_cast<char>(byte)); |
| 957 | } | ||
| 958 | |||
| 959 |
2/2✓ Branch 118 → 119 taken 3 times.
✓ Branch 118 → 120 taken 13 times.
|
16 | if (!closed) |
| 960 | 3 | return std::nullopt; | |
| 961 | 13 | return QPair<qint64, QByteArray>{uid, body}; | |
| 962 | 20 | } | |
| 963 | |||
| 964 | 200777 | bool ImapResponseParser::endsWithBodyFetchItem(const QString &line) { | |
| 965 | // F-02: only the empty-section body item — BODY[] or BODY[]<partial> — | ||
| 966 | // carries the full raw message and qualifies as the reader's body | ||
| 967 | // literal/quoted body. Section literals (BODY[HEADER], BODY[1.TEXT], …) | ||
| 968 | // are ordinary folded literals. The token must sit outside quoted | ||
| 969 | // strings as the line's trailing item, so a "BODY[]" inside an ENVELOPE | ||
| 970 | // subject or display name — always quoted — can never match, and only a | ||
| 971 | // partial <a.b>, whitespace, closing parens or one quoted value may | ||
| 972 | // follow it. | ||
| 973 | 200777 | const int n = line.size(); | |
| 974 |
2/2✓ Branch 3 → 4 taken 100001 times.
✓ Branch 3 → 5 taken 100776 times.
|
200777 | if (n < 6) |
| 975 | 100001 | return false; | |
| 976 | |||
| 977 | // Last "BODY[]" outside quotes. | ||
| 978 | 100776 | int bracket = -1; | |
| 979 | { | ||
| 980 | 100776 | const QChar body[4] = {QLatin1Char('B'), QLatin1Char('O'), | |
| 981 | QLatin1Char('D'), QLatin1Char('Y')}; | ||
| 982 | 100776 | bool inQuote = false; | |
| 983 | 100776 | bool escaped = false; | |
| 984 |
2/2✓ Branch 58 → 6 taken 828850 times.
✓ Branch 58 → 59 taken 100776 times.
|
929626 | for (int i = 0; i + 6 <= n; ++i) { |
| 985 | 828850 | const QChar c = line.at(i); | |
| 986 |
2/2✓ Branch 7 → 8 taken 8655 times.
✓ Branch 7 → 21 taken 820195 times.
|
828850 | if (inQuote) { |
| 987 |
2/2✓ Branch 8 → 9 taken 2 times.
✓ Branch 8 → 10 taken 8653 times.
|
8655 | if (escaped) |
| 988 | 2 | escaped = false; | |
| 989 |
2/2✓ Branch 13 → 14 taken 2 times.
✓ Branch 13 → 15 taken 8651 times.
|
8653 | else if (c == QLatin1Char('\\')) |
| 990 | 2 | escaped = true; | |
| 991 |
2/2✓ Branch 18 → 19 taken 614 times.
✓ Branch 18 → 20 taken 8037 times.
|
8651 | else if (c == QLatin1Char('"')) |
| 992 | 614 | inQuote = false; | |
| 993 | 9358 | continue; | |
| 994 | } | ||
| 995 |
2/2✓ Branch 24 → 25 taken 703 times.
✓ Branch 24 → 26 taken 819492 times.
|
820195 | if (c == QLatin1Char('"')) { |
| 996 | 703 | inQuote = true; | |
| 997 | 703 | continue; | |
| 998 | } | ||
| 999 |
4/4✓ Branch 32 → 33 taken 56 times.
✓ Branch 32 → 52 taken 104 times.
✓ Branch 36 → 37 taken 54 times.
✓ Branch 36 → 52 taken 2 times.
|
819708 | if (c.toUpper() == body[0] && line.at(i + 1).toUpper() == body[1] && |
| 1000 |
1/2✓ Branch 40 → 41 taken 54 times.
✗ Branch 40 → 52 not taken.
|
110 | line.at(i + 2).toUpper() == body[2] && |
| 1001 |
1/2✓ Branch 45 → 46 taken 54 times.
✗ Branch 45 → 52 not taken.
|
108 | line.at(i + 3).toUpper() == body[3] && |
| 1002 |
4/4✓ Branch 28 → 29 taken 160 times.
✓ Branch 28 → 52 taken 819332 times.
✓ Branch 50 → 51 taken 51 times.
✓ Branch 50 → 52 taken 3 times.
|
819706 | line.at(i + 4) == QLatin1Char('[') && |
| 1003 |
2/2✓ Branch 53 → 54 taken 51 times.
✓ Branch 53 → 55 taken 819441 times.
|
819546 | line.at(i + 5) == QLatin1Char(']')) { |
| 1004 | 51 | bracket = i + 5; // index of ']' | |
| 1005 | 51 | i += 5; | |
| 1006 | } | ||
| 1007 | } | ||
| 1008 | } | ||
| 1009 |
2/2✓ Branch 59 → 60 taken 100725 times.
✓ Branch 59 → 61 taken 51 times.
|
100776 | if (bracket < 0) |
| 1010 | 100725 | return false; | |
| 1011 | |||
| 1012 | 51 | int i = bracket + 1; | |
| 1013 | |||
| 1014 | // Optional <partial> echo (BODY[]<0>). | ||
| 1015 |
5/6✓ Branch 61 → 62 taken 51 times.
✗ Branch 61 → 68 not taken.
✓ Branch 66 → 67 taken 8 times.
✓ Branch 66 → 68 taken 43 times.
✓ Branch 69 → 70 taken 8 times.
✓ Branch 69 → 76 taken 43 times.
|
51 | if (i < n && line.at(i) == QLatin1Char('<')) { |
| 1016 | 8 | const int close = line.indexOf(QLatin1Char('>'), i); | |
| 1017 |
1/2✗ Branch 73 → 74 not taken.
✓ Branch 73 → 75 taken 8 times.
|
8 | if (close < 0) |
| 1018 | ✗ | return false; | |
| 1019 | 8 | i = close + 1; | |
| 1020 | } | ||
| 1021 | |||
| 1022 | // Optional quoted value of the item (the quoted-body form). | ||
| 1023 | 51 | int j = i; | |
| 1024 |
6/6✓ Branch 78 → 79 taken 56 times.
✓ Branch 78 → 83 taken 46 times.
✓ Branch 81 → 82 taken 51 times.
✓ Branch 81 → 83 taken 5 times.
✓ Branch 84 → 77 taken 51 times.
✓ Branch 84 → 85 taken 51 times.
|
102 | while (j < n && line.at(j).isSpace()) |
| 1025 | 51 | ++j; | |
| 1026 |
5/6✓ Branch 85 → 86 taken 5 times.
✓ Branch 85 → 92 taken 46 times.
✓ Branch 90 → 91 taken 5 times.
✗ Branch 90 → 92 not taken.
✓ Branch 93 → 94 taken 5 times.
✓ Branch 93 → 130 taken 46 times.
|
51 | if (j < n && line.at(j) == QLatin1Char('"')) { |
| 1027 | 5 | bool q = false; | |
| 1028 | 5 | bool esc = false; | |
| 1029 |
1/2✓ Branch 126 → 95 taken 60 times.
✗ Branch 126 → 127 not taken.
|
60 | for (; j < n; ++j) { |
| 1030 | 60 | const QChar c = line.at(j); | |
| 1031 |
2/2✓ Branch 96 → 97 taken 50 times.
✓ Branch 96 → 110 taken 10 times.
|
60 | if (q) { |
| 1032 |
2/2✓ Branch 97 → 98 taken 2 times.
✓ Branch 97 → 99 taken 48 times.
|
50 | if (esc) |
| 1033 | 2 | esc = false; | |
| 1034 |
2/2✓ Branch 102 → 103 taken 2 times.
✓ Branch 102 → 104 taken 46 times.
|
48 | else if (c == QLatin1Char('\\')) |
| 1035 | 2 | esc = true; | |
| 1036 |
2/2✓ Branch 107 → 108 taken 5 times.
✓ Branch 107 → 109 taken 41 times.
|
46 | else if (c == QLatin1Char('"')) |
| 1037 | 5 | q = false; | |
| 1038 | 55 | continue; | |
| 1039 | } | ||
| 1040 |
2/2✓ Branch 113 → 114 taken 5 times.
✓ Branch 113 → 115 taken 5 times.
|
10 | if (c == QLatin1Char('"')) { |
| 1041 | 5 | q = true; | |
| 1042 | 5 | continue; | |
| 1043 | } | ||
| 1044 |
1/2✓ Branch 118 → 119 taken 5 times.
✗ Branch 118 → 120 not taken.
|
5 | if (c == QLatin1Char(')')) |
| 1045 | 5 | break; // closing FETCH paren after the quoted value | |
| 1046 | ✗ | if (!c.isSpace()) | |
| 1047 | ✗ | return false; | |
| 1048 | } | ||
| 1049 |
1/2✗ Branch 127 → 128 not taken.
✓ Branch 127 → 129 taken 5 times.
|
5 | if (q) |
| 1050 | ✗ | return false; // unterminated value | |
| 1051 | 5 | i = j; | |
| 1052 | } | ||
| 1053 | |||
| 1054 | // Only whitespace and ')' may remain. | ||
| 1055 |
2/2✓ Branch 143 → 131 taken 51 times.
✓ Branch 143 → 144 taken 51 times.
|
102 | for (; i < n; ++i) { |
| 1056 | 51 | const QChar c = line.at(i); | |
| 1057 |
4/6✓ Branch 133 → 134 taken 5 times.
✓ Branch 133 → 139 taken 46 times.
✗ Branch 137 → 138 not taken.
✓ Branch 137 → 139 taken 5 times.
✗ Branch 140 → 141 not taken.
✓ Branch 140 → 142 taken 51 times.
|
51 | if (!c.isSpace() && c != QLatin1Char(')')) |
| 1058 | ✗ | return false; | |
| 1059 | } | ||
| 1060 | 51 | return true; | |
| 1061 | } | ||
| 1062 | |||
| 1063 | std::optional<int> | ||
| 1064 | 7 | ImapResponseParser::parseExistsResponse(const QString &data) { | |
| 1065 | // Untagged data format: "N EXISTS" (the "* " prefix is already stripped) | ||
| 1066 | // After parseUntaggedResponse, type="N" and data="EXISTS" | ||
| 1067 | // But we also handle full "42 EXISTS" format | ||
| 1068 |
5/8✓ Branch 2 → 3 taken 2 times.
✓ Branch 2 → 12 taken 5 times.
✓ Branch 4 → 5 taken 2 times.
✗ Branch 4 → 12 not taken.
✓ Branch 6 → 7 taken 2 times.
✓ Branch 7 → 8 taken 2 times.
✗ Branch 32 → 33 not taken.
✗ Branch 32 → 34 not taken.
|
7 | static QRegularExpression rx(R"(^(\d+)\s+EXISTS)"); |
| 1069 |
1/1✓ Branch 13 → 14 taken 7 times.
|
7 | auto match = rx.match(data); |
| 1070 |
3/3✓ Branch 14 → 15 taken 7 times.
✓ Branch 15 → 16 taken 3 times.
✓ Branch 15 → 22 taken 4 times.
|
7 | if (match.hasMatch()) { |
| 1071 |
2/2✓ Branch 16 → 17 taken 3 times.
✓ Branch 17 → 18 taken 3 times.
|
3 | return match.captured(1).toInt(); |
| 1072 | } | ||
| 1073 | 4 | return std::nullopt; | |
| 1074 | 7 | } | |
| 1075 | |||
| 1076 | std::optional<quint32> | ||
| 1077 | 72 | ImapResponseParser::parseUidValidity(const QString &data) { | |
| 1078 | // Format: "OK [UIDVALIDITY 12345] ..." or just "[UIDVALIDITY 12345]" | ||
| 1079 |
5/8✓ Branch 2 → 3 taken 6 times.
✓ Branch 2 → 12 taken 66 times.
✓ Branch 4 → 5 taken 6 times.
✗ Branch 4 → 12 not taken.
✓ Branch 6 → 7 taken 6 times.
✓ Branch 7 → 8 taken 6 times.
✗ Branch 32 → 33 not taken.
✗ Branch 32 → 34 not taken.
|
72 | static QRegularExpression rx(R"(\[UIDVALIDITY\s+(\d+)\])"); |
| 1080 |
1/1✓ Branch 13 → 14 taken 72 times.
|
72 | auto match = rx.match(data); |
| 1081 |
3/3✓ Branch 14 → 15 taken 72 times.
✓ Branch 15 → 16 taken 42 times.
✓ Branch 15 → 22 taken 30 times.
|
72 | if (match.hasMatch()) { |
| 1082 |
2/2✓ Branch 16 → 17 taken 42 times.
✓ Branch 17 → 18 taken 42 times.
|
42 | return match.captured(1).toUInt(); |
| 1083 | } | ||
| 1084 | 30 | return std::nullopt; | |
| 1085 | 72 | } | |
| 1086 | |||
| 1087 | // Modified UTF-7 decoding (RFC 3501 Section 5.1.3) | ||
| 1088 | // In Modified UTF-7: | ||
| 1089 | // - '&' starts a shifted section (Base64-encoded UTF-16BE) | ||
| 1090 | // - '-' ends the shifted section | ||
| 1091 | // - '&-' represents a literal '&' | ||
| 1092 | // - All other printable ASCII is literal | ||
| 1093 | 18168 | QString ImapResponseParser::decodeMailboxName(const QString &encoded) { | |
| 1094 | 18168 | QString result; | |
| 1095 | 18168 | int i = 0; | |
| 1096 | |||
| 1097 |
2/2✓ Branch 48 → 3 taken 164948 times.
✓ Branch 48 → 49 taken 17939 times.
|
182887 | while (i < encoded.length()) { |
| 1098 |
2/2✓ Branch 6 → 7 taken 256 times.
✓ Branch 6 → 44 taken 164692 times.
|
164948 | if (encoded[i] == '&') { |
| 1099 | // Find the closing '-' | ||
| 1100 | 256 | int end = encoded.indexOf('-', i + 1); | |
| 1101 |
2/2✓ Branch 9 → 10 taken 229 times.
✓ Branch 9 → 14 taken 27 times.
|
256 | if (end < 0) { |
| 1102 | // Malformed: no closing '-', just append the rest literally | ||
| 1103 |
2/2✓ Branch 10 → 11 taken 229 times.
✓ Branch 11 → 12 taken 229 times.
|
229 | result.append(encoded.mid(i)); |
| 1104 | 229 | break; | |
| 1105 | } | ||
| 1106 | |||
| 1107 |
2/2✓ Branch 14 → 15 taken 7 times.
✓ Branch 14 → 18 taken 20 times.
|
27 | if (end == i + 1) { |
| 1108 | // "&-" represents a literal '&' | ||
| 1109 |
1/1✓ Branch 16 → 17 taken 7 times.
|
7 | result.append('&'); |
| 1110 | } else { | ||
| 1111 | // Base64-encoded UTF-16BE between '&' and '-' | ||
| 1112 |
1/1✓ Branch 18 → 19 taken 20 times.
|
20 | auto base64Str = encoded.mid(i + 1, end - i - 1); |
| 1113 | // Modified UTF-7 uses ',' instead of '/' in Base64 | ||
| 1114 |
1/1✓ Branch 21 → 22 taken 20 times.
|
20 | base64Str.replace(',', '/'); |
| 1115 | // Add padding if needed | ||
| 1116 |
2/2✓ Branch 27 → 23 taken 19 times.
✓ Branch 27 → 28 taken 20 times.
|
39 | while (base64Str.length() % 4 != 0) { |
| 1117 |
1/1✓ Branch 24 → 25 taken 19 times.
|
19 | base64Str.append('='); |
| 1118 | } | ||
| 1119 | |||
| 1120 |
2/2✓ Branch 29 → 30 taken 20 times.
✓ Branch 30 → 31 taken 20 times.
|
20 | QByteArray decoded = QByteArray::fromBase64(base64Str.toLatin1()); |
| 1121 | // Decode UTF-16BE | ||
| 1122 |
2/2✓ Branch 39 → 33 taken 27 times.
✓ Branch 39 → 40 taken 20 times.
|
47 | for (int j = 0; j + 1 < decoded.size(); j += 2) { |
| 1123 |
1/1✓ Branch 33 → 34 taken 27 times.
|
27 | ushort ch = (static_cast<uchar>(decoded[j]) << 8) | |
| 1124 |
1/1✓ Branch 34 → 35 taken 27 times.
|
27 | static_cast<uchar>(decoded[j + 1]); |
| 1125 |
1/1✓ Branch 36 → 37 taken 27 times.
|
27 | result.append(QChar(ch)); |
| 1126 | } | ||
| 1127 | 20 | } | |
| 1128 | 27 | i = end + 1; | |
| 1129 | } else { | ||
| 1130 |
1/1✓ Branch 45 → 46 taken 164692 times.
|
164692 | result.append(encoded[i]); |
| 1131 | 164692 | i++; | |
| 1132 | } | ||
| 1133 | } | ||
| 1134 | |||
| 1135 | 18168 | return result; | |
| 1136 | ✗ | } | |
| 1137 | |||
| 1138 | // A mailbox-name argument: the path in its wire form (modified UTF-7, RFC | ||
| 1139 | // 3501 §5.1.3 — the form LIST returns and the cache and UI keep) inside an | ||
| 1140 | // escaped quoted string. S106-968: the path is NOT encoded again — F-03 | ||
| 1141 | // encoded every path, so a listed "Entw&APw-rfe" went out as | ||
| 1142 | // "Entw&-APw-rfe". Names typed by the user are encoded once where they | ||
| 1143 | // become a path (encodeMailboxName, QuickFolderActions). | ||
| 1144 | 170 | QString ImapResponseParser::quoteMailboxName(const QString &path) { | |
| 1145 | 170 | QString escaped = path; | |
| 1146 |
1/1✓ Branch 4 → 5 taken 170 times.
|
170 | escaped.remove('\r'); |
| 1147 |
1/1✓ Branch 6 → 7 taken 170 times.
|
170 | escaped.remove('\n'); |
| 1148 |
1/1✓ Branch 8 → 9 taken 170 times.
|
170 | escaped.remove(QChar(0)); |
| 1149 |
2/2✓ Branch 9 → 10 taken 170 times.
✓ Branch 11 → 12 taken 170 times.
|
170 | escaped.replace('\\', "\\\\"); |
| 1150 |
2/2✓ Branch 13 → 14 taken 170 times.
✓ Branch 15 → 16 taken 170 times.
|
170 | escaped.replace('"', "\\\""); |
| 1151 |
2/2✓ Branch 19 → 20 taken 170 times.
✓ Branch 20 → 21 taken 170 times.
|
510 | return '"' + escaped + '"'; |
| 1152 | 170 | } | |
| 1153 | |||
| 1154 | 26 | QString ImapResponseParser::encodeMailboxName(const QString &decoded) { | |
| 1155 | 26 | QString result; | |
| 1156 | |||
| 1157 | 26 | int i = 0; | |
| 1158 |
2/2✓ Branch 55 → 3 taken 142 times.
✓ Branch 55 → 56 taken 26 times.
|
168 | while (i < decoded.length()) { |
| 1159 | 142 | QChar ch = decoded[i]; | |
| 1160 | |||
| 1161 |
2/2✓ Branch 6 → 7 taken 5 times.
✓ Branch 6 → 9 taken 137 times.
|
142 | if (ch == '&') { |
| 1162 |
1/1✓ Branch 7 → 8 taken 5 times.
|
5 | result.append("&-"); |
| 1163 | 5 | i++; | |
| 1164 |
6/6✓ Branch 10 → 11 taken 135 times.
✓ Branch 10 → 14 taken 2 times.
✓ Branch 12 → 13 taken 121 times.
✓ Branch 12 → 14 taken 14 times.
✓ Branch 15 → 16 taken 121 times.
✓ Branch 15 → 18 taken 16 times.
|
137 | } else if (ch.unicode() >= 0x20 && ch.unicode() <= 0x7E) { |
| 1165 | // Printable ASCII – literal | ||
| 1166 |
1/1✓ Branch 16 → 17 taken 121 times.
|
121 | result.append(ch); |
| 1167 | 121 | i++; | |
| 1168 | } else { | ||
| 1169 | // Non-ASCII: collect consecutive non-ASCII chars and encode as UTF-16BE | ||
| 1170 | 16 | QByteArray utf16be; | |
| 1171 |
2/2✓ Branch 37 → 19 taken 33 times.
✓ Branch 37 → 38 taken 7 times.
|
40 | while (i < decoded.length()) { |
| 1172 | 33 | QChar c = decoded[i]; | |
| 1173 |
8/8✓ Branch 22 → 23 taken 32 times.
✓ Branch 22 → 27 taken 1 time.
✓ Branch 24 → 25 taken 30 times.
✓ Branch 24 → 28 taken 2 times.
✓ Branch 26 → 27 taken 8 times.
✓ Branch 26 → 28 taken 22 times.
✓ Branch 29 → 30 taken 9 times.
✓ Branch 29 → 31 taken 24 times.
|
33 | if (c == '&' || (c.unicode() >= 0x20 && c.unicode() <= 0x7E)) { |
| 1174 | 9 | break; | |
| 1175 | } | ||
| 1176 |
1/1✓ Branch 32 → 33 taken 24 times.
|
24 | utf16be.append(static_cast<char>(c.unicode() >> 8)); |
| 1177 |
1/1✓ Branch 34 → 35 taken 24 times.
|
24 | utf16be.append(static_cast<char>(c.unicode() & 0xFF)); |
| 1178 | 24 | i++; | |
| 1179 | } | ||
| 1180 | |||
| 1181 | auto base64 = utf16be.toBase64(QByteArray::Base64Encoding | | ||
| 1182 |
1/1✓ Branch 39 → 40 taken 16 times.
|
16 | QByteArray::OmitTrailingEquals); |
| 1183 |
1/1✓ Branch 40 → 41 taken 16 times.
|
16 | QString b64str = QString::fromLatin1(base64); |
| 1184 |
1/1✓ Branch 43 → 44 taken 16 times.
|
16 | b64str.replace('/', ','); |
| 1185 |
1/1✓ Branch 45 → 46 taken 16 times.
|
16 | result.append('&'); |
| 1186 |
1/1✓ Branch 46 → 47 taken 16 times.
|
16 | result.append(b64str); |
| 1187 |
1/1✓ Branch 48 → 49 taken 16 times.
|
16 | result.append('-'); |
| 1188 | 16 | } | |
| 1189 | } | ||
| 1190 | |||
| 1191 | 26 | return result; | |
| 1192 | ✗ | } | |
| 1193 | |||
| 1194 | std::optional<QPair<qint64, quint32>> | ||
| 1195 | 10 | ImapResponseParser::parseFetchFlagsResponse(const QString &data) { | |
| 1196 |
1/1✓ Branch 2 → 3 taken 10 times.
|
10 | const auto snapshot = parseFetchFlagSnapshot(data); |
| 1197 |
2/2✓ Branch 4 → 5 taken 5 times.
✓ Branch 4 → 6 taken 5 times.
|
10 | if (!snapshot) |
| 1198 | 5 | return std::nullopt; | |
| 1199 | 5 | return QPair<qint64, quint32>{snapshot->uid, snapshot->flags}; | |
| 1200 | 10 | } | |
| 1201 | |||
| 1202 | std::optional<ImapFlagSnapshot> | ||
| 1203 | 36 | ImapResponseParser::parseFetchFlagSnapshot(const QString &data) { | |
| 1204 | // Expected input (after "* N FETCH "): "(UID 123 FLAGS (\Seen \Flagged))" | ||
| 1205 | // or "(FLAGS (\Seen) UID 123)" – order may vary. | ||
| 1206 | |||
| 1207 | // Extract UID — S106-021: anchored on the top-level item (not inside | ||
| 1208 | // X-UID or MYFLAGS) and refused when it overflows instead of becoming 0. | ||
| 1209 | 36 | const qint64 uid = numberAfter(data, topLevelItem(data, "UID", " ")); | |
| 1210 |
2/2✓ Branch 4 → 5 taken 9 times.
✓ Branch 4 → 19 taken 27 times.
|
36 | if (uid <= 0) { |
| 1211 | // Unsolicited FETCH without UID is valid IMAP (another client changed | ||
| 1212 | // flags); callers resolve it through its sequence number. One per mail. | ||
| 1213 |
4/4✓ Branch 5 → 6 taken 9 times.
✓ Branch 9 → 10 taken 9 times.
✓ Branch 17 → 7 taken 9 times.
✓ Branch 17 → 18 taken 9 times.
|
18 | qCDebug(lcImapParser) |
| 1214 |
3/3✓ Branch 10 → 11 taken 9 times.
✓ Branch 11 → 12 taken 9 times.
✓ Branch 12 → 13 taken 9 times.
|
9 | << "No UID in FETCH FLAGS response:" << data.left(100); |
| 1215 | 9 | return std::nullopt; | |
| 1216 | } | ||
| 1217 | |||
| 1218 | // Extract FLAGS | ||
| 1219 | 27 | bool hasFlags = false; | |
| 1220 |
1/1✓ Branch 20 → 21 taken 27 times.
|
27 | const QString flagText = flagListAfter(data, topLevelItem(data, "FLAGS"), &hasFlags); |
| 1221 |
2/2✓ Branch 21 → 22 taken 2 times.
✓ Branch 21 → 36 taken 25 times.
|
27 | if (!hasFlags) { |
| 1222 |
4/4✓ Branch 22 → 23 taken 2 times.
✓ Branch 26 → 27 taken 2 times.
✓ Branch 34 → 24 taken 2 times.
✓ Branch 34 → 35 taken 2 times.
|
4 | qCWarning(lcImapParser) |
| 1223 |
3/3✓ Branch 27 → 28 taken 2 times.
✓ Branch 28 → 29 taken 2 times.
✓ Branch 29 → 30 taken 2 times.
|
2 | << "No FLAGS in FETCH FLAGS response:" << data.left(100); |
| 1224 | 2 | return std::nullopt; | |
| 1225 | } | ||
| 1226 |
1/1✓ Branch 38 → 39 taken 25 times.
|
25 | auto flagList = flagText.split(' ', Qt::SkipEmptyParts); |
| 1227 |
1/1✓ Branch 39 → 40 taken 25 times.
|
25 | const auto [flags, keywords] = flagsAndKeywords(flagList); |
| 1228 | 25 | return ImapFlagSnapshot{uid, flags, keywords}; | |
| 1229 | 27 | } | |
| 1230 | |||
| 1231 | 9 | std::optional<QList<qint64>> ImapResponseParser::expandUidSet(const QString &set, | |
| 1232 | qint64 maxItems) { | ||
| 1233 | 9 | QList<qint64> uids; | |
| 1234 |
2/2✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 5 taken 8 times.
|
9 | if (set.isEmpty()) return uids; |
| 1235 |
5/5✓ Branch 8 → 9 taken 8 times.
✓ Branch 9 → 10 taken 8 times.
✓ Branch 10 → 11 taken 8 times.
✓ Branch 57 → 12 taken 11 times.
✓ Branch 57 → 58 taken 3 times.
|
14 | for (const auto &part : set.split(QLatin1Char(','))) { |
| 1236 |
1/1✓ Branch 16 → 17 taken 11 times.
|
11 | const auto bounds = part.split(QLatin1Char(':')); |
| 1237 |
2/2✓ Branch 18 → 19 taken 1 time.
✓ Branch 18 → 21 taken 10 times.
|
11 | if (bounds.size() > 2) return std::nullopt; |
| 1238 | 10 | bool okFirst = false, okLast = false; | |
| 1239 |
1/1✓ Branch 22 → 23 taken 10 times.
|
10 | const qint64 first = bounds.first().toLongLong(&okFirst); |
| 1240 |
3/3✓ Branch 24 → 25 taken 8 times.
✓ Branch 24 → 28 taken 2 times.
✓ Branch 26 → 27 taken 8 times.
|
10 | const qint64 last = bounds.size() == 2 ? bounds.last().toLongLong(&okLast) : first; |
| 1241 |
10/12✓ Branch 29 → 30 taken 10 times.
✗ Branch 29 → 35 not taken.
✓ Branch 31 → 32 taken 8 times.
✓ Branch 31 → 33 taken 2 times.
✓ Branch 32 → 33 taken 6 times.
✓ Branch 32 → 35 taken 2 times.
✓ Branch 33 → 34 taken 7 times.
✓ Branch 33 → 35 taken 1 time.
✗ Branch 34 → 35 not taken.
✓ Branch 34 → 36 taken 7 times.
✓ Branch 37 → 38 taken 3 times.
✓ Branch 37 → 40 taken 7 times.
|
10 | if (!okFirst || (bounds.size() == 2 && !okLast) || first <= 0 || last <= 0) |
| 1242 | 3 | return std::nullopt; | |
| 1243 | 7 | const qint64 low = qMin(first, last), high = qMax(first, last); | |
| 1244 |
2/2✓ Branch 43 → 44 taken 1 time.
✓ Branch 43 → 46 taken 6 times.
|
7 | if (high - low + 1 > maxItems - uids.size()) return std::nullopt; |
| 1245 |
3/3✓ Branch 47 → 48 taken 17 times.
✓ Branch 49 → 47 taken 17 times.
✓ Branch 49 → 50 taken 6 times.
|
23 | for (qint64 uid = low; uid <= high; ++uid) uids.append(uid); |
| 1246 | 19 | } | |
| 1247 | 3 | return uids; | |
| 1248 | 9 | } | |
| 1249 | |||
| 1250 | std::optional<StatusResult> | ||
| 1251 | 34 | ImapResponseParser::parseStatusResponse(const QString &data) { | |
| 1252 | // Expected input (after "* STATUS "): '"INBOX" (MESSAGES 42 UNSEEN 3 RECENT | ||
| 1253 | // 0)' or: 'INBOX (MESSAGES 42 UNSEEN 3 RECENT 0)' | ||
| 1254 | |||
| 1255 | 34 | StatusResult result; | |
| 1256 | 34 | result.hasMessages = false; | |
| 1257 | 34 | result.hasUnseen = false; | |
| 1258 | |||
| 1259 | // Extract folder name (quoted or unquoted). F-08: a quoted name may | ||
| 1260 | // itself contain '(' ("Projekte (alt)") — the status parens start after | ||
| 1261 | // the closing quote, not at the first parenthesis. | ||
| 1262 | 34 | int parenIdx = -1; | |
| 1263 | 34 | QString folderPart; | |
| 1264 |
1/1✓ Branch 2 → 3 taken 34 times.
|
34 | const QString trimmed = data.trimmed(); |
| 1265 |
3/3✓ Branch 4 → 5 taken 34 times.
✓ Branch 5 → 6 taken 29 times.
✓ Branch 5 → 27 taken 5 times.
|
34 | if (trimmed.startsWith('"')) { |
| 1266 | 29 | bool escaped = false; | |
| 1267 |
2/2✓ Branch 26 → 7 taken 193 times.
✓ Branch 26 → 38 taken 1 time.
|
194 | for (int i = 1; i < trimmed.size(); ++i) { |
| 1268 | 193 | const QChar c = trimmed.at(i); | |
| 1269 |
1/2✗ Branch 8 → 9 not taken.
✓ Branch 8 → 11 taken 193 times.
|
193 | if (escaped) { |
| 1270 | ✗ | folderPart.append(c); | |
| 1271 | ✗ | escaped = false; | |
| 1272 |
1/2✗ Branch 14 → 15 not taken.
✓ Branch 14 → 16 taken 193 times.
|
193 | } else if (c == QLatin1Char('\\')) { |
| 1273 | ✗ | escaped = true; | |
| 1274 |
2/2✓ Branch 19 → 20 taken 28 times.
✓ Branch 19 → 23 taken 165 times.
|
193 | } else if (c == QLatin1Char('"')) { |
| 1275 | 28 | parenIdx = trimmed.indexOf('(', i); | |
| 1276 | 28 | break; | |
| 1277 | } else { | ||
| 1278 |
1/1✓ Branch 23 → 24 taken 165 times.
|
165 | folderPart.append(c); |
| 1279 | } | ||
| 1280 | } | ||
| 1281 | } else { | ||
| 1282 | 5 | parenIdx = trimmed.indexOf('('); | |
| 1283 |
4/4✓ Branch 29 → 30 taken 3 times.
✓ Branch 29 → 31 taken 2 times.
✓ Branch 32 → 33 taken 5 times.
✓ Branch 33 → 34 taken 5 times.
|
5 | folderPart = trimmed.left(parenIdx < 0 ? trimmed.size() : parenIdx).trimmed(); |
| 1284 | } | ||
| 1285 |
2/2✓ Branch 38 → 39 taken 4 times.
✓ Branch 38 → 51 taken 30 times.
|
34 | if (parenIdx < 0) { |
| 1286 |
4/4✓ Branch 39 → 40 taken 4 times.
✓ Branch 43 → 44 taken 4 times.
✓ Branch 49 → 41 taken 4 times.
✓ Branch 49 → 50 taken 4 times.
|
8 | qCWarning(lcImapParser) |
| 1287 |
2/2✓ Branch 44 → 45 taken 4 times.
✓ Branch 45 → 46 taken 4 times.
|
4 | << "No parenthesized data in STATUS response:" << data; |
| 1288 | 4 | return std::nullopt; | |
| 1289 | } | ||
| 1290 | |||
| 1291 | 30 | result.folderPath = folderPart; | |
| 1292 | |||
| 1293 | // Extract the parenthesized status values | ||
| 1294 |
1/1✓ Branch 52 → 53 taken 30 times.
|
30 | QString statusPart = trimmed.mid(parenIdx); |
| 1295 | // Remove outer parens | ||
| 1296 |
1/1✓ Branch 54 → 55 taken 30 times.
|
30 | statusPart.remove('('); |
| 1297 |
1/1✓ Branch 56 → 57 taken 30 times.
|
30 | statusPart.remove(')'); |
| 1298 |
1/1✓ Branch 57 → 58 taken 30 times.
|
30 | statusPart = statusPart.trimmed(); |
| 1299 | |||
| 1300 | // Parse key-value pairs: "MESSAGES 42 UNSEEN 3 RECENT 0" | ||
| 1301 |
1/1✓ Branch 62 → 63 taken 30 times.
|
30 | auto tokens = statusPart.split(' ', Qt::SkipEmptyParts); |
| 1302 |
2/2✓ Branch 88 → 64 taken 59 times.
✓ Branch 88 → 89 taken 30 times.
|
89 | for (int i = 0; i + 1 < tokens.size(); i += 2) { |
| 1303 |
1/1✓ Branch 64 → 65 taken 59 times.
|
59 | const auto &key = tokens[i]; |
| 1304 | 59 | bool ok = false; | |
| 1305 |
2/2✓ Branch 65 → 66 taken 59 times.
✓ Branch 66 → 67 taken 59 times.
|
59 | int value = tokens[i + 1].toInt(&ok); |
| 1306 |
4/4✓ Branch 67 → 68 taken 58 times.
✓ Branch 67 → 69 taken 1 time.
✓ Branch 68 → 69 taken 2 times.
✓ Branch 68 → 70 taken 56 times.
|
59 | if (!ok || value < 0) |
| 1307 | 3 | continue; | |
| 1308 | |||
| 1309 |
3/3✓ Branch 70 → 71 taken 56 times.
✓ Branch 73 → 74 taken 24 times.
✓ Branch 73 → 75 taken 32 times.
|
56 | if (key.compare("MESSAGES", Qt::CaseInsensitive) == 0) { |
| 1310 | 24 | result.messages = value; | |
| 1311 | 24 | result.hasMessages = true; | |
| 1312 |
3/3✓ Branch 75 → 76 taken 32 times.
✓ Branch 78 → 79 taken 21 times.
✓ Branch 78 → 80 taken 11 times.
|
32 | } else if (key.compare("UNSEEN", Qt::CaseInsensitive) == 0) { |
| 1313 | 21 | result.unseen = value; | |
| 1314 | 21 | result.hasUnseen = true; | |
| 1315 |
3/3✓ Branch 80 → 81 taken 11 times.
✓ Branch 83 → 84 taken 5 times.
✓ Branch 83 → 85 taken 6 times.
|
11 | } else if (key.compare("RECENT", Qt::CaseInsensitive) == 0) { |
| 1316 | 5 | result.recent = value; | |
| 1317 | } | ||
| 1318 | } | ||
| 1319 | |||
| 1320 | 30 | return result; | |
| 1321 | 34 | } | |
| 1322 | |||
| 1323 | 8252 | QString ImapResponseParser::decodeRfc2047(const QString &input) { | |
| 1324 |
4/4✓ Branch 2 → 3 taken 8252 times.
✓ Branch 3 → 4 taken 8252 times.
✓ Branch 5 → 6 taken 8191 times.
✓ Branch 5 → 7 taken 61 times.
|
8252 | if (!input.contains("=?")) |
| 1325 | 8191 | return input; | |
| 1326 | |||
| 1327 | // RFC 2047 pattern: =?charset?encoding?encoded_text?= | ||
| 1328 | static QRegularExpression rx(R"(=\?([^?]+)\?([BbQq])\?([^?]*)\?=)", | ||
| 1329 |
5/8✓ Branch 7 → 8 taken 7 times.
✓ Branch 7 → 17 taken 54 times.
✓ Branch 9 → 10 taken 7 times.
✗ Branch 9 → 17 not taken.
✓ Branch 11 → 12 taken 7 times.
✓ Branch 12 → 13 taken 7 times.
✗ Branch 175 → 176 not taken.
✗ Branch 175 → 177 not taken.
|
61 | QRegularExpression::CaseInsensitiveOption); |
| 1330 | |||
| 1331 | 61 | QString result; | |
| 1332 | 61 | int lastEnd = 0; | |
| 1333 | 61 | bool lastWasEncoded = false; | |
| 1334 | |||
| 1335 |
1/1✓ Branch 18 → 19 taken 61 times.
|
61 | auto it = rx.globalMatch(input); |
| 1336 |
3/3✓ Branch 129 → 130 taken 125 times.
✓ Branch 130 → 20 taken 64 times.
✓ Branch 130 → 131 taken 61 times.
|
125 | while (it.hasNext()) { |
| 1337 |
1/1✓ Branch 20 → 21 taken 64 times.
|
64 | auto match = it.next(); |
| 1338 |
1/1✓ Branch 21 → 22 taken 64 times.
|
64 | int matchStart = match.capturedStart(); |
| 1339 | |||
| 1340 | // Text between encoded words | ||
| 1341 |
1/1✓ Branch 22 → 23 taken 64 times.
|
64 | QString between = input.mid(lastEnd, matchStart - lastEnd); |
| 1342 | |||
| 1343 | // RFC 2047 §6.2: whitespace between adjacent encoded words is ignored | ||
| 1344 |
9/11✓ Branch 23 → 24 taken 4 times.
✓ Branch 23 → 28 taken 60 times.
✓ Branch 24 → 25 taken 4 times.
✓ Branch 26 → 27 taken 3 times.
✓ Branch 26 → 28 taken 1 time.
✓ Branch 29 → 30 taken 4 times.
✓ Branch 29 → 31 taken 60 times.
✓ Branch 31 → 32 taken 61 times.
✓ Branch 31 → 33 taken 3 times.
✗ Branch 179 → 180 not taken.
✗ Branch 179 → 181 not taken.
|
64 | if (lastWasEncoded && between.trimmed().isEmpty()) { |
| 1345 | // Skip whitespace between consecutive encoded words | ||
| 1346 | } else { | ||
| 1347 |
1/1✓ Branch 32 → 33 taken 61 times.
|
61 | result.append(between); |
| 1348 | } | ||
| 1349 | |||
| 1350 |
2/2✓ Branch 33 → 34 taken 64 times.
✓ Branch 34 → 35 taken 64 times.
|
64 | QString charset = match.captured(1).toLower(); |
| 1351 |
2/2✓ Branch 36 → 37 taken 64 times.
✓ Branch 37 → 38 taken 64 times.
|
64 | QChar encoding = match.captured(2).toUpper().at(0); |
| 1352 |
1/1✓ Branch 41 → 42 taken 64 times.
|
64 | QString encodedText = match.captured(3); |
| 1353 | |||
| 1354 | // T-272: WHATWG Encoding Standard mapping — ISO-8859-1 → Windows-1252. | ||
| 1355 | // Senders routinely mislabel CP1252 as ISO-8859-1. Bytes 0x80-0x9F are | ||
| 1356 | // C1 control chars in ISO-8859-1 (rendering as rectangles) but printable | ||
| 1357 | // glyphs (smart quotes, dashes, etc.) in Windows-1252. | ||
| 1358 |
2/2✓ Branch 47 → 48 taken 56 times.
✓ Branch 47 → 51 taken 3 times.
|
123 | if (charset == QLatin1String("iso-8859-1") || |
| 1359 |
4/4✓ Branch 44 → 45 taken 59 times.
✓ Branch 44 → 51 taken 5 times.
✓ Branch 50 → 51 taken 2 times.
✓ Branch 50 → 52 taken 54 times.
|
123 | charset == QLatin1String("latin1") || |
| 1360 |
2/2✓ Branch 53 → 54 taken 10 times.
✓ Branch 53 → 61 taken 54 times.
|
120 | charset == QLatin1String("latin-1")) { |
| 1361 | 10 | charset = QStringLiteral("windows-1252"); | |
| 1362 | } | ||
| 1363 | |||
| 1364 | 64 | QByteArray decoded; | |
| 1365 |
2/2✓ Branch 63 → 64 taken 26 times.
✓ Branch 63 → 71 taken 38 times.
|
64 | if (encoding == 'B') { |
| 1366 | // Base64 | ||
| 1367 |
2/2✓ Branch 65 → 66 taken 26 times.
✓ Branch 66 → 67 taken 26 times.
|
26 | decoded = QByteArray::fromBase64(encodedText.toLatin1()); |
| 1368 |
1/2✓ Branch 73 → 74 taken 38 times.
✗ Branch 73 → 102 not taken.
|
38 | } else if (encoding == 'Q') { |
| 1369 | // Quoted-Printable (RFC 2047 variant: _ = space, =XX = hex) | ||
| 1370 |
2/2✓ Branch 101 → 75 taken 322 times.
✓ Branch 101 → 102 taken 38 times.
|
360 | for (int i = 0; i < encodedText.length(); i++) { |
| 1371 |
1/1✓ Branch 75 → 76 taken 322 times.
|
322 | QChar c = encodedText[i]; |
| 1372 |
2/2✓ Branch 78 → 79 taken 26 times.
✓ Branch 78 → 80 taken 296 times.
|
322 | if (c == '_') { |
| 1373 |
1/1✓ Branch 79 → 99 taken 26 times.
|
26 | decoded.append(' '); |
| 1374 |
6/6✓ Branch 82 → 83 taken 55 times.
✓ Branch 82 → 86 taken 241 times.
✓ Branch 84 → 85 taken 53 times.
✓ Branch 84 → 86 taken 2 times.
✓ Branch 87 → 88 taken 53 times.
✓ Branch 87 → 97 taken 243 times.
|
296 | } else if (c == '=' && i + 2 < encodedText.length()) { |
| 1375 | bool ok; | ||
| 1376 |
2/2✓ Branch 88 → 89 taken 53 times.
✓ Branch 89 → 90 taken 53 times.
|
53 | int byte = encodedText.mid(i + 1, 2).toInt(&ok, 16); |
| 1377 |
2/2✓ Branch 91 → 92 taken 51 times.
✓ Branch 91 → 94 taken 2 times.
|
53 | if (ok) { |
| 1378 |
1/1✓ Branch 92 → 93 taken 51 times.
|
51 | decoded.append(static_cast<char>(byte)); |
| 1379 | 51 | i += 2; | |
| 1380 | } else { | ||
| 1381 |
1/1✓ Branch 95 → 96 taken 2 times.
|
2 | decoded.append(c.toLatin1()); |
| 1382 | } | ||
| 1383 | } else { | ||
| 1384 |
1/1✓ Branch 98 → 99 taken 243 times.
|
243 | decoded.append(c.toLatin1()); |
| 1385 | } | ||
| 1386 | } | ||
| 1387 | } | ||
| 1388 | |||
| 1389 | // RFC 2047 §5: CR and LF are forbidden inside encoded text and MUST be | ||
| 1390 | // removed. Some senders (notably Google Play) smuggle =0D=0A into the | ||
| 1391 | // Q-encoded payload, which otherwise surfaces as a stray leading blank | ||
| 1392 | // line in subjects and sender names. Replace the bytes with a space so | ||
| 1393 | // adjacent words cannot merge; runs of whitespace are collapsed later. | ||
| 1394 |
1/1✓ Branch 102 → 103 taken 64 times.
|
64 | decoded.replace('\r', ' '); |
| 1395 |
1/1✓ Branch 103 → 104 taken 64 times.
|
64 | decoded.replace('\n', ' '); |
| 1396 | |||
| 1397 | // Convert from charset to QString | ||
| 1398 |
2/2✓ Branch 105 → 106 taken 64 times.
✓ Branch 108 → 109 taken 64 times.
|
64 | auto toUtf16 = QStringDecoder(charset.toLatin1().constData()); |
| 1399 |
2/2✓ Branch 111 → 112 taken 62 times.
✓ Branch 111 → 117 taken 2 times.
|
64 | if (toUtf16.isValid()) { |
| 1400 |
2/2✓ Branch 113 → 114 taken 62 times.
✓ Branch 114 → 115 taken 62 times.
|
62 | result.append(toUtf16(decoded)); |
| 1401 | } else { | ||
| 1402 | // Fallback: assume UTF-8 | ||
| 1403 |
2/2✓ Branch 117 → 118 taken 2 times.
✓ Branch 118 → 119 taken 2 times.
|
2 | result.append(QString::fromUtf8(decoded)); |
| 1404 | } | ||
| 1405 | |||
| 1406 |
1/1✓ Branch 121 → 122 taken 64 times.
|
64 | lastEnd = match.capturedEnd(); |
| 1407 | 64 | lastWasEncoded = true; | |
| 1408 | 64 | } | |
| 1409 | |||
| 1410 | // Append any trailing non-encoded text | ||
| 1411 |
2/2✓ Branch 132 → 133 taken 4 times.
✓ Branch 132 → 137 taken 57 times.
|
61 | if (lastEnd < input.length()) { |
| 1412 |
2/2✓ Branch 133 → 134 taken 4 times.
✓ Branch 134 → 135 taken 4 times.
|
4 | result.append(input.mid(lastEnd)); |
| 1413 | } | ||
| 1414 | |||
| 1415 | // Robustness: collapse any residual CR/LF that leaked through unencoded | ||
| 1416 | // portions (replace, do not drop, to keep adjacent words separated), squash | ||
| 1417 | // runs of internal whitespace to a single space, and trim the result. | ||
| 1418 | // Header values such as Subject / display name / filename should never | ||
| 1419 | // carry wrapping whitespace. | ||
| 1420 |
4/7✓ Branch 137 → 138 taken 7 times.
✓ Branch 137 → 150 taken 54 times.
✓ Branch 139 → 140 taken 7 times.
✗ Branch 139 → 150 not taken.
✓ Branch 144 → 145 taken 7 times.
✗ Branch 236 → 237 not taken.
✗ Branch 236 → 238 not taken.
|
68 | static const QRegularExpression kWhitespaceRun(QStringLiteral("\\s+")); |
| 1421 |
1/1✓ Branch 152 → 153 taken 61 times.
|
61 | result.replace('\r', ' '); |
| 1422 |
1/1✓ Branch 155 → 156 taken 61 times.
|
61 | result.replace('\n', ' '); |
| 1423 |
1/1✓ Branch 159 → 160 taken 61 times.
|
61 | result.replace(kWhitespaceRun, QStringLiteral(" ")); |
| 1424 |
1/1✓ Branch 162 → 163 taken 61 times.
|
61 | return result.trimmed(); |
| 1425 | 61 | } | |
| 1426 |