MailJDTourDownloadDocsQualityTestsCoverage

Which source lines and branches the tests execute. Lines 91.5 %, branches 75.0 % (minimum 85 % and 72 %). main @ 399ffe3d · 21 October 2026 · pipeline 7238.

GCC Code Coverage Report


Directory: src/
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 90.9% 120 / 0 / 132
Functions: 90.9% 10 / 0 / 11
Branches: 80.2% 219 / 0 / 273

presentation/ComposeRecipients.cpp
Line Branch Exec Source
1 #include "ComposeRecipients.h"
2 #include "service/MimeParser.h"
3 #include "util/MailboxText.h"
4 #include "service/SmtpService.h"
5 #include "service/ImapResponseParser.h"
6 #include <QSet>
7 #include <algorithm>
8 #include <QUrl>
9
10 8575 std::optional<ComposeRecipients> ComposeRecipients::parse(const QString &field) {
11
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 8575 times.
8575 if (field.size() > 65536) return std::nullopt;
12 8575 ComposeRecipients result;
13 8575 QString chunk;
14 8575 bool quoted = false, escaped = false, angle = false;
15 8590 const auto append = [&]() {
16
1/1
✓ Branch 2 → 3 taken 8590 times.
8590 const auto value = chunk.trimmed();
17 8590 chunk.clear();
18
2/2
✓ Branch 5 → 6 taken 4003 times.
✓ Branch 5 → 7 taken 4587 times.
8590 if (value.isEmpty()) return true;
19 4587 QString address = value;
20 // S106-233: the angle address is the one outside a quoted display name —
21 // '"Boss <b@x>" <real@y>' names real@y, not b@x.
22 4587 qsizetype left = -1, right = -1;
23 4587 bool inQuotes = false, escapedHere = false;
24
2/2
✓ Branch 50 → 9 taken 98949 times.
✓ Branch 50 → 51 taken 4587 times.
103536 for (qsizetype i = 0; i < value.size(); ++i) {
25 98949 const QChar c = value.at(i);
26
2/2
✓ Branch 10 → 11 taken 3 times.
✓ Branch 10 → 12 taken 98946 times.
99099 if (escapedHere) { escapedHere = false; continue; }
27
6/6
✓ Branch 12 → 13 taken 137 times.
✓ Branch 12 → 18 taken 98809 times.
✓ Branch 16 → 17 taken 3 times.
✓ Branch 16 → 18 taken 134 times.
✓ Branch 19 → 20 taken 3 times.
✓ Branch 19 → 21 taken 98943 times.
98946 if (inQuotes && c == QLatin1Char('\\')) { escapedHere = true; continue; }
28
2/2
✓ Branch 24 → 25 taken 26 times.
✓ Branch 24 → 26 taken 98917 times.
98943 if (c == QLatin1Char('"')) { inQuotes = !inQuotes; continue; }
29
2/2
✓ Branch 26 → 27 taken 121 times.
✓ Branch 26 → 28 taken 98796 times.
98917 if (inQuotes) continue;
30
5/6
✓ Branch 31 → 32 taken 1138 times.
✓ Branch 31 → 34 taken 97658 times.
✓ Branch 32 → 33 taken 1138 times.
✗ Branch 32 → 34 not taken.
✓ Branch 35 → 36 taken 1138 times.
✓ Branch 35 → 37 taken 97658 times.
98796 if (c == QLatin1Char('<') && left < 0) left = i;
31
5/6
✓ Branch 40 → 41 taken 1138 times.
✓ Branch 40 → 43 taken 96520 times.
✓ Branch 41 → 42 taken 1138 times.
✗ Branch 41 → 43 not taken.
✓ Branch 44 → 45 taken 1138 times.
✓ Branch 44 → 46 taken 96520 times.
97658 else if (c == QLatin1Char('>') && right < 0) right = i;
32 }
33
2/2
✓ Branch 51 → 52 taken 1138 times.
✓ Branch 51 → 71 taken 3449 times.
4587 if (left >= 0) {
34
9/16
✓ Branch 52 → 53 taken 1138 times.
✗ Branch 52 → 57 not taken.
✓ Branch 53 → 54 taken 1138 times.
✓ Branch 54 → 55 taken 1138 times.
✓ Branch 56 → 57 taken 1 time.
✓ Branch 56 → 58 taken 1137 times.
✓ Branch 59 → 60 taken 1138 times.
✗ Branch 59 → 61 not taken.
✓ Branch 61 → 62 taken 1138 times.
✗ Branch 61 → 63 not taken.
✓ Branch 63 → 64 taken 1 time.
✓ Branch 63 → 65 taken 1137 times.
✗ Branch 140 → 141 not taken.
✗ Branch 140 → 142 not taken.
✗ Branch 144 → 145 not taken.
✗ Branch 144 → 146 not taken.
1138 if (right <= left || !value.mid(right + 1).trimmed().isEmpty()) return false;
35
2/2
✓ Branch 65 → 66 taken 1137 times.
✓ Branch 66 → 67 taken 1137 times.
1137 address = value.mid(left + 1, right - left - 1).trimmed();
36
1/2
✗ Branch 71 → 72 not taken.
✓ Branch 71 → 73 taken 3449 times.
3449 } else if (right >= 0) return false;
37 // F-21 (SPRINT-104): an internationalized domain (name@bücher.de) is
38 // valid mail — the envelope carries its Punycode ACE form (RFC 5891),
39 // the header keeps what the user typed.
40 4586 QString envelope = address;
41 4586 const auto at = address.lastIndexOf(QLatin1Char('@'));
42
1/1
✓ Branch 77 → 78 taken 4586 times.
4586 const auto domain = address.mid(at + 1);
43
2/3
✓ Branch 80 → 81 taken 4586 times.
✓ Branch 81 → 82 taken 4586 times.
✗ Branch 81 → 115 not taken.
4586 if (!domain.startsWith(QLatin1Char('['))) {
44 4586 bool nonAscii = false;
45
2/2
✓ Branch 88 → 84 taken 55644 times.
✓ Branch 88 → 89 taken 4583 times.
60227 for (const auto &ch : domain)
46
2/2
✓ Branch 85 → 86 taken 3 times.
✓ Branch 85 → 87 taken 55641 times.
55644 if (ch.unicode() > 127) { nonAscii = true; break; }
47
2/2
✓ Branch 89 → 90 taken 3 times.
✓ Branch 89 → 115 taken 4583 times.
4586 if (nonAscii) {
48
2/2
✓ Branch 91 → 92 taken 3 times.
✓ Branch 92 → 93 taken 3 times.
3 const QString ace = QUrl::toAce(domain);
49
6/7
✓ Branch 95 → 96 taken 2 times.
✓ Branch 95 → 100 taken 1 time.
✓ Branch 98 → 99 taken 2 times.
✗ Branch 99 → 100 not taken.
✓ Branch 99 → 101 taken 2 times.
✓ Branch 102 → 103 taken 1 time.
✓ Branch 102 → 104 taken 2 times.
3 if (ace.isEmpty() || ace.contains(QLatin1Char(' '))) return false;
50
2/2
✓ Branch 104 → 105 taken 2 times.
✓ Branch 105 → 106 taken 2 times.
2 envelope = address.left(at + 1) + ace;
51 3 }
52 }
53
1/1
✓ Branch 115 → 116 taken 4585 times.
4585 if (!SmtpService::isValidSmtpAddress(envelope) ||
54
10/12
✓ Branch 116 → 117 taken 4571 times.
✓ Branch 116 → 127 taken 14 times.
✓ Branch 119 → 120 taken 4571 times.
✓ Branch 120 → 121 taken 3 times.
✓ Branch 120 → 125 taken 4568 times.
✓ Branch 123 → 124 taken 3 times.
✗ Branch 124 → 125 not taken.
✓ Branch 124 → 127 taken 3 times.
✗ Branch 126 → 127 not taken.
✓ Branch 126 → 128 taken 4568 times.
✓ Branch 129 → 130 taken 17 times.
✓ Branch 129 → 131 taken 4568 times.
9153 (!domain.contains(QLatin1Char('.')) && !domain.startsWith(QLatin1Char('['))) ||
55 4585 result.envelope.size() >= 1000) return false;
56
1/1
✓ Branch 131 → 132 taken 4568 times.
4568 result.headers.append(value);
57
1/1
✓ Branch 132 → 133 taken 4568 times.
4568 result.envelope.append(envelope);
58 4568 return true;
59 8590 };
60
2/2
✓ Branch 66 → 7 taken 99082 times.
✓ Branch 66 → 67 taken 8574 times.
107656 for (const auto ch : field) {
61
5/6
✓ Branch 8 → 9 taken 99081 times.
✓ Branch 8 → 11 taken 1 time.
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 99081 times.
✓ Branch 13 → 14 taken 1 time.
✓ Branch 13 → 15 taken 99081 times.
99082 if (ch.unicode() < 0x20 || ch.unicode() == 0x7f) return std::nullopt;
62
3/3
✓ Branch 15 → 16 taken 3 times.
✓ Branch 15 → 18 taken 99078 times.
✓ Branch 16 → 17 taken 3 times.
99081 if (escaped) { escaped = false; chunk.append(ch); continue; }
63
7/7
✓ Branch 18 → 19 taken 168 times.
✓ Branch 18 → 24 taken 98910 times.
✓ Branch 22 → 23 taken 3 times.
✓ Branch 22 → 24 taken 165 times.
✓ Branch 25 → 26 taken 3 times.
✓ Branch 25 → 28 taken 99075 times.
✓ Branch 26 → 27 taken 3 times.
99078 if (quoted && ch == QLatin1Char('\\')) { escaped = true; chunk.append(ch); continue; }
64
2/2
✓ Branch 31 → 32 taken 27 times.
✓ Branch 31 → 33 taken 99048 times.
99075 if (ch == QLatin1Char('"')) quoted = !quoted;
65
2/2
✓ Branch 33 → 34 taken 98909 times.
✓ Branch 33 → 64 taken 166 times.
99075 if (!quoted) {
66
2/2
✓ Branch 37 → 38 taken 1138 times.
✓ Branch 37 → 41 taken 97771 times.
98909 if (ch == QLatin1Char('<')) {
67
1/2
✗ Branch 38 → 39 not taken.
✓ Branch 38 → 40 taken 1138 times.
1138 if (angle) return std::nullopt;
68 1138 angle = true;
69
2/2
✓ Branch 44 → 45 taken 1138 times.
✓ Branch 44 → 48 taken 96633 times.
97771 } else if (ch == QLatin1Char('>')) {
70
1/2
✗ Branch 45 → 46 not taken.
✓ Branch 45 → 47 taken 1138 times.
1138 if (!angle) return std::nullopt;
71 1138 angle = false;
72
7/8
✓ Branch 48 → 49 taken 75120 times.
✓ Branch 48 → 58 taken 21513 times.
✓ Branch 52 → 53 taken 75103 times.
✓ Branch 52 → 57 taken 17 times.
✗ Branch 56 → 57 not taken.
✓ Branch 56 → 58 taken 75103 times.
✓ Branch 59 → 60 taken 17 times.
✓ Branch 59 → 64 taken 96616 times.
96633 } else if (!angle && (ch == QLatin1Char(',') || ch == QLatin1Char(';'))) {
73
2/3
✓ Branch 60 → 61 taken 17 times.
✗ Branch 61 → 62 not taken.
✓ Branch 61 → 63 taken 17 times.
17 if (!append()) return std::nullopt;
74 17 continue;
75 }
76 }
77
1/1
✓ Branch 64 → 65 taken 99058 times.
99058 chunk.append(ch);
78 }
79
9/11
✓ Branch 67 → 69 taken 8573 times.
✓ Branch 67 → 73 taken 1 time.
✓ Branch 69 → 70 taken 8573 times.
✗ Branch 69 → 73 not taken.
✓ Branch 70 → 71 taken 8573 times.
✗ Branch 70 → 73 not taken.
✓ Branch 71 → 72 taken 8573 times.
✓ Branch 72 → 73 taken 19 times.
✓ Branch 72 → 74 taken 8554 times.
✓ Branch 75 → 76 taken 20 times.
✓ Branch 75 → 77 taken 8554 times.
8574 if (quoted || escaped || angle || !append()) return std::nullopt;
80 8554 return result;
81 8575 }
82 ✗ QStringList ComposeRecipients::combinedEnvelope(const ComposeRecipients &to,
83 const ComposeRecipients &cc, const ComposeRecipients &bcc) {
84 ✗ QStringList result;
85 ✗ QSet<QString> seen;
86 ✗ for (const auto *field : {&to, &cc, &bcc}) {
87 ✗ for (const auto &address : field->envelope) {
88 ✗ const auto key = addressKey(address);
89 ✗ if (seen.contains(key)) continue;
90 ✗ seen.insert(key);
91 ✗ result.append(address);
92 ✗ }
93 }
94 ✗ return result;
95 ✗ }
96
97 1068 QString ComposeRecipients::mailbox(const QString &name, const QString &address) {
98 1068 return MailboxText::mailbox(name, address);
99 }
100
101 174 QString ComposeRecipients::addressKey(const QString &address) {
102 174 const auto at = address.lastIndexOf(QLatin1Char('@'));
103
5/5
✓ Branch 5 → 6 taken 174 times.
✓ Branch 6 → 7 taken 174 times.
✓ Branch 9 → 10 taken 174 times.
✓ Branch 10 → 11 taken 174 times.
✓ Branch 11 → 12 taken 174 times.
174 return address.left(at) + QLatin1Char('@') + address.mid(at + 1).toLower();
104 }
105 namespace {
106
107 // One mailbox of a received header as the composer shows it: an encoded-word
108 // name decoded, unless the decoded name holds angle brackets or control
109 // characters (S106-233) — even quoted, '<' '>' read as an address to other
110 // programs, and a control character makes the whole field unusable.
111 48 QString displayEntry(QString display, const QString &address) {
112 48 const auto left = display.lastIndexOf(QLatin1Char('<'));
113
13/17
✓ Branch 5 → 6 taken 32 times.
✓ Branch 5 → 10 taken 16 times.
✓ Branch 6 → 7 taken 32 times.
✓ Branch 7 → 8 taken 32 times.
✓ Branch 8 → 9 taken 32 times.
✓ Branch 9 → 10 taken 28 times.
✓ Branch 9 → 11 taken 4 times.
✓ Branch 12 → 13 taken 32 times.
✓ Branch 12 → 14 taken 16 times.
✓ Branch 14 → 15 taken 32 times.
✓ Branch 14 → 16 taken 16 times.
✓ Branch 16 → 17 taken 44 times.
✓ Branch 16 → 18 taken 4 times.
✗ Branch 50 → 51 not taken.
✗ Branch 50 → 52 not taken.
✗ Branch 54 → 55 not taken.
✗ Branch 54 → 56 not taken.
48 if (left <= 0 || !display.left(left).contains("=?")) return display;
114
3/3
✓ Branch 18 → 19 taken 4 times.
✓ Branch 19 → 20 taken 4 times.
✓ Branch 20 → 21 taken 4 times.
4 auto name = ImapResponseParser::decodeRfc2047(display.left(left).trimmed());
115
1/1
✓ Branch 25 → 26 taken 4 times.
4 const bool plain = std::none_of(name.cbegin(), name.cend(), [](QChar c) {
116
5/8
✓ Branch 3 → 4 taken 22 times.
✗ Branch 3 → 14 not taken.
✓ Branch 5 → 6 taken 22 times.
✗ Branch 5 → 14 not taken.
✓ Branch 9 → 10 taken 21 times.
✓ Branch 9 → 14 taken 1 time.
✗ Branch 13 → 14 not taken.
✓ Branch 13 → 15 taken 21 times.
22 return c.unicode() < 0x20 || c.unicode() == 0x7f || c == QLatin1Char('<') || c == QLatin1Char('>');
117 });
118
2/2
✓ Branch 26 → 27 taken 1 time.
✓ Branch 26 → 28 taken 3 times.
4 if (!plain) return display;
119
3/3
✓ Branch 28 → 29 taken 3 times.
✓ Branch 29 → 30 taken 3 times.
✓ Branch 30 → 31 taken 3 times.
3 name.replace("\\", "\\\\");
120
3/3
✓ Branch 33 → 34 taken 3 times.
✓ Branch 34 → 35 taken 3 times.
✓ Branch 35 → 36 taken 3 times.
3 name.replace("\"", "\\\"");
121
4/4
✓ Branch 38 → 39 taken 3 times.
✓ Branch 39 → 40 taken 3 times.
✓ Branch 40 → 41 taken 3 times.
✓ Branch 41 → 42 taken 3 times.
3 return "\"" + name + "\" <" + address + ">";
122 4 }
123
124 } // namespace
125
126 10 QString ComposeRecipients::decodedField(const QString &field) {
127
1/1
✓ Branch 2 → 3 taken 10 times.
10 const auto parsed = parse(field);
128
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 10 times.
10 if (!parsed) return field;
129 10 QStringList entries;
130
4/4
✓ Branch 12 → 13 taken 6 times.
✓ Branch 13 → 14 taken 6 times.
✓ Branch 19 → 7 taken 6 times.
✓ Branch 19 → 20 taken 10 times.
16 for (int i = 0; i < parsed->envelope.size(); ++i) entries.append(displayEntry(parsed->headers.at(i), parsed->envelope.at(i)));
131
1/1
✓ Branch 23 → 24 taken 10 times.
10 return entries.join(QStringLiteral(", "));
132 10 }
133
134 2 QStringList ComposeRecipients::sentHeaders(const QByteArray &message, const QStringList &bcc) {
135 2 QStringList headers;
136
1/1
✓ Branch 2 → 3 taken 2 times.
2 const auto fields = MimeParser::messageHeaders(message);
137
2/2
✓ Branch 26 → 13 taken 4 times.
✓ Branch 26 → 27 taken 2 times.
14 for (const QString &name : {QStringLiteral("to"), QStringLiteral("cc")})
138
7/10
✓ Branch 14 → 15 taken 4 times.
✓ Branch 15 → 16 taken 4 times.
✓ Branch 16 → 17 taken 4 times.
✓ Branch 21 → 22 taken 4 times.
✗ Branch 21 → 24 not taken.
✓ Branch 23 → 24 taken 4 times.
✓ Branch 28 → 29 taken 4 times.
✓ Branch 28 → 32 taken 2 times.
✗ Branch 61 → 62 not taken.
✗ Branch 61 → 65 not taken.
10 if (const auto parsed = parse(decodedField(fields.value(name)))) headers += parsed->headers;
139
2/2
✓ Branch 44 → 35 taken 1 time.
✓ Branch 44 → 45 taken 2 times.
3 for (const QString &mailbox : bcc)
140
3/4
✓ Branch 36 → 37 taken 1 time.
✓ Branch 38 → 39 taken 1 time.
✗ Branch 38 → 41 not taken.
✓ Branch 40 → 41 taken 1 time.
1 if (const auto parsed = parse(mailbox)) headers += parsed->headers;
141 2 return headers;
142 2 }
143
144 39 ComposeReplyRecipients ComposeRecipients::replyTargets(const QString &from, const QString &to,
145 const QString &cc, const QString &replyTo, const QString &ownAddress, bool all) {
146
1/1
✓ Branch 2 → 3 taken 39 times.
39 const auto own = addressKey(ownAddress);
147
1/1
✓ Branch 3 → 4 taken 39 times.
39 const auto fromField = parse(from);
148 39 bool ownSender = false;
149
1/2
✓ Branch 5 → 6 taken 39 times.
✗ Branch 5 → 22 not taken.
39 if (fromField) {
150
2/2
✓ Branch 20 → 9 taken 39 times.
✓ Branch 20 → 21 taken 39 times.
78 for (const auto &address : fromField->envelope)
151
5/9
✓ Branch 10 → 11 taken 39 times.
✗ Branch 10 → 14 not taken.
✓ Branch 11 → 12 taken 39 times.
✓ Branch 13 → 14 taken 2 times.
✓ Branch 13 → 15 taken 37 times.
✓ Branch 16 → 17 taken 39 times.
✗ Branch 16 → 18 not taken.
✗ Branch 56 → 57 not taken.
✗ Branch 56 → 58 not taken.
39 ownSender = ownSender || addressKey(address) == own;
152 }
153
2/4
✓ Branch 25 → 26 taken 39 times.
✓ Branch 25 → 27 taken 39 times.
✗ Branch 62 → 63 not taken.
✗ Branch 62 → 64 not taken.
78 QSet<QString> seen{own};
154 39 QStringList targets, copies;
155 50 const auto add = [&](const QString &field, QStringList &destination) {
156
1/1
✓ Branch 2 → 3 taken 50 times.
50 const auto parsed = parse(field);
157
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 50 times.
50 if (!parsed) return;
158
2/2
✓ Branch 32 → 7 taken 53 times.
✓ Branch 32 → 33 taken 50 times.
103 for (int i = 0; i < parsed->envelope.size(); ++i) {
159
1/1
✓ Branch 9 → 10 taken 53 times.
53 const auto key = addressKey(parsed->envelope.at(i));
160
2/2
✓ Branch 11 → 12 taken 11 times.
✓ Branch 11 → 13 taken 42 times.
53 if (seen.contains(key)) continue;
161
1/1
✓ Branch 13 → 14 taken 42 times.
42 seen.insert(key);
162
1/1
✓ Branch 19 → 20 taken 42 times.
42 const QString display = displayEntry(parsed->headers.at(i), parsed->envelope.at(i));
163
1/1
✓ Branch 21 → 22 taken 42 times.
42 destination.append(display);
164 53 }
165
1/2
✓ Branch 35 → 36 taken 50 times.
✗ Branch 35 → 38 not taken.
50 };
166
8/10
✓ Branch 27 → 28 taken 2 times.
✓ Branch 27 → 29 taken 37 times.
✓ Branch 29 → 30 taken 37 times.
✓ Branch 31 → 32 taken 35 times.
✓ Branch 31 → 33 taken 2 times.
✓ Branch 35 → 36 taken 39 times.
✓ Branch 36 → 37 taken 37 times.
✓ Branch 36 → 38 taken 2 times.
✗ Branch 71 → 72 not taken.
✗ Branch 71 → 73 not taken.
39 add(ownSender ? to : replyTo.trimmed().isEmpty() ? from : replyTo, targets);
167
2/2
✓ Branch 38 → 39 taken 6 times.
✓ Branch 38 → 42 taken 33 times.
39 if (all) {
168
3/3
✓ Branch 39 → 40 taken 5 times.
✓ Branch 39 → 41 taken 1 time.
✓ Branch 40 → 41 taken 5 times.
6 if (!ownSender) add(to, targets);
169
1/1
✓ Branch 41 → 42 taken 6 times.
6 add(cc, copies);
170 }
171
1/1
✓ Branch 42 → 43 taken 39 times.
117 return {targets.join(", "), copies.join(", ")};
172 117 }
173