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: 98.0% 450 / 0 / 459
Functions: 100.0% 32 / 0 / 32
Branches: 88.4% 772 / 0 / 873

service/Rfc2822Builder.cpp
Line Branch Exec Source
1 #include "Rfc2822Builder.h"
2
3 #include <QFile>
4 #include <QFileInfo>
5 #include <QRegularExpression>
6 #include <QLocale>
7 #include <QMimeDatabase>
8 #include <QUrl>
9 #include <QUuid>
10
11 #include <utility>
12
13 namespace {
14
15 // A header value without CR, LF (header injection, T-400/T-509) and NUL
16 // (RFC 5322 §2.2, S106-030), as UTF-8.
17 1988 QByteArray headerSafe(const QString &value) {
18 1988 QString safe = value;
19
1/1
✓ Branch 5 → 6 taken 1988 times.
1988 safe.remove(QLatin1Char('\r'));
20
1/1
✓ Branch 8 → 9 taken 1988 times.
1988 safe.remove(QLatin1Char('\n'));
21
1/1
✓ Branch 10 → 11 taken 1988 times.
1988 safe.remove(QChar::Null);
22
1/1
✓ Branch 11 → 12 taken 1988 times.
3976 return safe.toUtf8();
23 1988 }
24
25 // A message id from another mail (In-Reply-To, References) as written into
26 // ours: visible ASCII only (RFC 5322 msg-id) and at most 900 bytes, so an
27 // untrusted original cannot make a header line exceed 998 bytes
28 // (Sprint 108, fuzz_rfc2822_builder). Whitespace splits ids.
29 // The Content-Type of a leaf part (an attachment, an inline image): a
30 // plain type/subtype token pair. Anything else — header injection (T-614),
31 // parameters, or a container type (multipart/*, message/*) that would make
32 // a reader parse the file's bytes as MIME — becomes
33 // application/octet-stream (Sprint 108, fuzz_rfc2822_builder).
34 66 QByteArray leafMimeType(const QByteArray &mimeType) {
35
4/7
✓ Branch 2 → 3 taken 11 times.
✓ Branch 2 → 15 taken 55 times.
✓ Branch 4 → 5 taken 11 times.
✗ Branch 4 → 15 not taken.
✓ Branch 9 → 10 taken 11 times.
✗ Branch 51 → 52 not taken.
✗ Branch 51 → 53 not taken.
77 static const QRegularExpression token(QStringLiteral("^[a-z0-9][a-z0-9!#$&^_.+-]{0,63}/[a-z0-9][a-z0-9!#$&^_.+-]{0,126}$"));
36
3/3
✓ Branch 15 → 16 taken 66 times.
✓ Branch 16 → 17 taken 66 times.
✓ Branch 17 → 18 taken 66 times.
66 const QString type = QString::fromLatin1(mimeType).trimmed().toLower();
37
8/11
✓ Branch 21 → 22 taken 66 times.
✓ Branch 22 → 23 taken 66 times.
✓ Branch 23 → 24 taken 62 times.
✓ Branch 23 → 30 taken 4 times.
✓ Branch 25 → 26 taken 62 times.
✓ Branch 26 → 27 taken 62 times.
✗ Branch 26 → 30 not taken.
✓ Branch 34 → 35 taken 4 times.
✓ Branch 34 → 39 taken 62 times.
✗ Branch 62 → 63 not taken.
✗ Branch 62 → 64 not taken.
194 if (!token.match(type).hasMatch() || type.startsWith(QLatin1String("multipart/")) ||
38
3/5
✓ Branch 28 → 29 taken 62 times.
✗ Branch 29 → 30 not taken.
✓ Branch 29 → 31 taken 62 times.
✓ Branch 32 → 33 taken 66 times.
✗ Branch 32 → 34 not taken.
128 type.startsWith(QLatin1String("message/")))
39 4 return QByteArrayLiteral("application/octet-stream");
40
1/1
✓ Branch 39 → 40 taken 62 times.
62 return type.toLatin1();
41 66 }
42
43 622 QByteArrayList messageIdsSafe(const QString &value) {
44 622 QByteArrayList ids;
45
6/6
✓ Branch 2 → 3 taken 622 times.
✓ Branch 3 → 4 taken 622 times.
✓ Branch 5 → 6 taken 622 times.
✓ Branch 6 → 7 taken 622 times.
✓ Branch 25 → 8 taken 625 times.
✓ Branch 25 → 26 taken 622 times.
1247 for (const QByteArray &token : headerSafe(value).split(' ')) {
46 625 QByteArray id;
47
2/2
✓ Branch 15 → 11 taken 14451 times.
✓ Branch 15 → 16 taken 625 times.
15076 for (const char c : token)
48
4/5
✓ Branch 11 → 12 taken 12951 times.
✓ Branch 11 → 14 taken 1500 times.
✓ Branch 12 → 13 taken 12951 times.
✗ Branch 12 → 14 not taken.
✓ Branch 13 → 14 taken 12951 times.
14451 if (c > 0x20 && c < 0x7f) id.append(c);
49
4/4
✓ Branch 17 → 18 taken 264 times.
✓ Branch 17 → 22 taken 361 times.
✓ Branch 18 → 19 taken 264 times.
✓ Branch 19 → 20 taken 264 times.
625 if (!id.isEmpty()) ids.append(id.left(900));
50 1247 }
51 622 return ids;
52 ✗ }
53
54 QByteArray foldHeader(const QByteArray &name, const QByteArray &value);
55
56 132 QByteArray encodeMimeFilenameParameters(const QByteArray &parameter,
57 const QString &filename) {
58 132 QString sanitized = filename;
59
1/1
✓ Branch 5 → 6 taken 132 times.
132 sanitized.remove(QLatin1Char('\r'));
60
1/1
✓ Branch 8 → 9 taken 132 times.
132 sanitized.remove(QLatin1Char('\n'));
61
1/1
✓ Branch 10 → 11 taken 132 times.
132 sanitized.remove(QChar::Null);
62
63 132 QByteArray fallback;
64
4/4
✓ Branch 11 → 12 taken 132 times.
✓ Branch 12 → 13 taken 132 times.
✓ Branch 28 → 14 taken 2230 times.
✓ Branch 28 → 29 taken 132 times.
2362 for (const QChar character : sanitized) {
65 2230 const ushort code = character.unicode();
66
6/8
✓ Branch 15 → 16 taken 2230 times.
✗ Branch 15 → 21 not taken.
✓ Branch 16 → 17 taken 2046 times.
✓ Branch 16 → 21 taken 184 times.
✓ Branch 17 → 18 taken 2042 times.
✓ Branch 17 → 21 taken 4 times.
✓ Branch 18 → 19 taken 2042 times.
✗ Branch 18 → 21 not taken.
2230 if (code >= 0x20 && code <= 0x7e && code != '"' && code != '\\')
67
1/1
✓ Branch 19 → 20 taken 2042 times.
2042 fallback.append(static_cast<char>(code));
68
2/2
✓ Branch 21 → 22 taken 4 times.
✓ Branch 21 → 23 taken 184 times.
188 else if (code == '"')
69
1/1
✓ Branch 22 → 24 taken 4 times.
4 fallback.append('\'');
70 else
71
1/1
✓ Branch 23 → 24 taken 184 times.
184 fallback.append('_');
72
1/2
✗ Branch 25 → 26 not taken.
✓ Branch 25 → 27 taken 2230 times.
2230 if (fallback.size() >= 120)
73 ✗ break;
74 }
75
2/2
✓ Branch 30 → 31 taken 4 times.
✓ Branch 30 → 32 taken 128 times.
132 if (fallback.isEmpty())
76
1/1
✓ Branch 31 → 32 taken 4 times.
4 fallback = "attachment";
77
78
4/4
✓ Branch 32 → 33 taken 132 times.
✓ Branch 33 → 34 taken 132 times.
✓ Branch 34 → 35 taken 132 times.
✓ Branch 35 → 36 taken 132 times.
132 QByteArray result = " " + parameter + "=\"" + fallback + "\"";
79 132 const QByteArray encoded = QUrl::toPercentEncoding(
80
1/1
✓ Branch 42 → 43 taken 132 times.
132 sanitized, QByteArrayLiteral("!#$&+-.^_`|~"));
81
4/7
✓ Branch 46 → 47 taken 132 times.
✓ Branch 48 → 49 taken 118 times.
✓ Branch 48 → 51 taken 14 times.
✗ Branch 50 → 51 not taken.
✓ Branch 50 → 52 taken 118 times.
✗ Branch 142 → 143 not taken.
✗ Branch 142 → 144 not taken.
382 const bool needsExtended = sanitized.toUtf8() != fallback ||
82
1/2
✓ Branch 53 → 54 taken 132 times.
✗ Branch 53 → 55 not taken.
250 encoded.size() > 60;
83
2/2
✓ Branch 55 → 56 taken 118 times.
✓ Branch 55 → 57 taken 14 times.
132 if (!needsExtended)
84 118 return result;
85
86
2/2
✓ Branch 58 → 59 taken 8 times.
✓ Branch 58 → 67 taken 6 times.
14 if (encoded.size() <= 60) {
87
4/4
✓ Branch 59 → 60 taken 8 times.
✓ Branch 60 → 61 taken 8 times.
✓ Branch 61 → 62 taken 8 times.
✓ Branch 62 → 63 taken 8 times.
8 result += ";\r\n " + parameter + "*=UTF-8''" + encoded;
88 8 return result;
89 }
90
91 6 qsizetype position = 0;
92 6 int section = 0;
93
2/2
✓ Branch 111 → 68 taken 34 times.
✓ Branch 111 → 112 taken 6 times.
40 while (position < encoded.size()) {
94 34 qsizetype end = qMin<qsizetype>(position + 48, encoded.size());
95
5/6
✓ Branch 72 → 73 taken 50 times.
✗ Branch 72 → 79 not taken.
✓ Branch 74 → 75 taken 42 times.
✓ Branch 74 → 78 taken 8 times.
✓ Branch 80 → 71 taken 16 times.
✓ Branch 80 → 81 taken 34 times.
142 while (end > position &&
96 50 (encoded.at(end - 1) == '%' ||
97
3/4
✓ Branch 75 → 76 taken 42 times.
✗ Branch 75 → 79 not taken.
✓ Branch 77 → 78 taken 8 times.
✓ Branch 77 → 79 taken 34 times.
42 (end - position >= 2 && encoded.at(end - 2) == '%'))) {
98 16 --end;
99 }
100
1/2
✗ Branch 81 → 82 not taken.
✓ Branch 81 → 85 taken 34 times.
34 if (end == position)
101 ✗ end = qMin<qsizetype>(position + 3, encoded.size());
102 const QByteArray prefix =
103
4/4
✓ Branch 85 → 86 taken 6 times.
✓ Branch 85 → 88 taken 28 times.
✓ Branch 89 → 90 taken 6 times.
✓ Branch 89 → 91 taken 28 times.
34 section == 0 ? QByteArrayLiteral("UTF-8''") : QByteArray();
104
4/4
✓ Branch 92 → 93 taken 34 times.
✓ Branch 93 → 94 taken 34 times.
✓ Branch 94 → 95 taken 34 times.
✓ Branch 95 → 96 taken 34 times.
68 result += ";\r\n " + parameter + "*" + QByteArray::number(section) +
105
5/5
✓ Branch 91 → 92 taken 34 times.
✓ Branch 96 → 97 taken 34 times.
✓ Branch 97 → 98 taken 34 times.
✓ Branch 98 → 99 taken 34 times.
✓ Branch 99 → 100 taken 34 times.
102 "*=" + prefix + encoded.mid(position, end - position);
106 34 position = end;
107 34 ++section;
108 34 }
109 6 return result;
110 132 }
111
112 } // namespace
113
114 // ═══════════════════════════════════════════════════════
115 // Header setters
116 // ═══════════════════════════════════════════════════════
117
118 403 void Rfc2822Builder::setFrom(const QString &name, const QString &email) {
119 403 m_fromName = name;
120 403 m_fromEmail = email;
121 403 }
122
123 392 void Rfc2822Builder::setTo(const QStringList &recipients) {
124 392 m_to = recipients;
125 392 }
126
127 80 void Rfc2822Builder::setCc(const QStringList &cc) { m_cc = cc; }
128
129 75 void Rfc2822Builder::setBcc(const QStringList &bcc) { m_bcc = bcc; }
130
131 393 void Rfc2822Builder::setSubject(const QString &subject) {
132 393 m_subject = subject;
133 393 }
134
135 283 void Rfc2822Builder::setDate(const QDateTime &date) { m_date = date; }
136
137 361 void Rfc2822Builder::setMessageId(const QString &messageId) {
138 361 m_messageId = messageId;
139 361 }
140
141 61 void Rfc2822Builder::setInReplyTo(const QString &messageId) {
142 61 m_inReplyTo = messageId;
143 61 }
144
145 59 void Rfc2822Builder::setReferences(const QStringList &messageIds) {
146 59 m_references = messageIds;
147 59 }
148
149 396 void Rfc2822Builder::setBodyText(const QString &plainText) {
150 396 m_bodyText = plainText;
151 396 }
152
153 52 void Rfc2822Builder::addAttachment(const Attachment &attachment) {
154 52 m_attachments.append(attachment);
155 52 }
156
157 4 bool Rfc2822Builder::addAttachmentFromFile(const QString &filePath,
158 qint64 maxBytes,
159 qint64 *loadedBytes) {
160
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 4 times.
4 if (loadedBytes) *loadedBytes = 0;
161
2/2
✓ Branch 4 → 5 taken 4 times.
✓ Branch 5 → 6 taken 4 times.
4 const qint64 onDisk = QFileInfo(filePath).size();
162
3/6
✓ Branch 7 → 8 taken 4 times.
✗ Branch 7 → 11 not taken.
✓ Branch 8 → 9 taken 4 times.
✗ Branch 8 → 11 not taken.
✓ Branch 9 → 10 taken 4 times.
✗ Branch 9 → 11 not taken.
4 if (maxBytes < 0 || maxBytes > MaxAttachmentBytes ||
163
2/2
✓ Branch 10 → 11 taken 2 times.
✓ Branch 10 → 12 taken 2 times.
4 onDisk < 0 || onDisk > maxBytes)
164 2 return false;
165
1/1
✓ Branch 12 → 13 taken 2 times.
2 QFile file(filePath);
166 // T-622/FUNC-07: Return false if file cannot be opened
167
2/3
✓ Branch 14 → 15 taken 2 times.
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 17 taken 2 times.
2 if (!file.open(QIODevice::ReadOnly))
168 ✗ return false;
169
170 2 Attachment att;
171
2/2
✓ Branch 17 → 18 taken 2 times.
✓ Branch 18 → 19 taken 2 times.
2 att.filename = QFileInfo(filePath).fileName();
172 // The file can grow after the stat. Bound the read itself and reject any
173 // changed-length snapshot before encoding or retaining attachment bytes.
174
1/1
✓ Branch 22 → 23 taken 2 times.
2 att.data = file.read(onDisk + 1);
175
3/5
✓ Branch 25 → 26 taken 2 times.
✓ Branch 28 → 29 taken 2 times.
✗ Branch 28 → 35 not taken.
✓ Branch 30 → 31 taken 2 times.
✗ Branch 30 → 35 not taken.
6 if (file.error() != QFile::NoError || att.data.size() != onDisk ||
176
6/10
✓ Branch 26 → 27 taken 2 times.
✗ Branch 26 → 35 not taken.
✓ Branch 31 → 32 taken 2 times.
✓ Branch 32 → 33 taken 2 times.
✗ Branch 32 → 35 not taken.
✓ Branch 33 → 34 taken 2 times.
✗ Branch 34 → 35 not taken.
✓ Branch 34 → 36 taken 2 times.
✗ Branch 37 → 38 not taken.
✓ Branch 37 → 39 taken 2 times.
6 att.data.size() > maxBytes || file.size() != onDisk || !file.atEnd())
177 ✗ return false;
178
2/2
✓ Branch 39 → 40 taken 2 times.
✓ Branch 40 → 41 taken 2 times.
2 att.mimeType = mimeTypeForFile(filePath, att.data.left(4096));
179 2 const qint64 actualBytes = att.data.size();
180
1/1
✓ Branch 46 → 47 taken 2 times.
2 m_attachments.append(std::move(att));
181
1/2
✗ Branch 47 → 48 not taken.
✓ Branch 47 → 49 taken 2 times.
2 if (loadedBytes) *loadedBytes = actualBytes;
182 2 return true;
183 2 }
184
185 85 QByteArray Rfc2822Builder::mimeTypeForFile(const QString &filePath, const QByteArray &head) {
186
4/7
✓ Branch 2 → 3 taken 8 times.
✓ Branch 2 → 8 taken 77 times.
✓ Branch 4 → 5 taken 8 times.
✗ Branch 4 → 8 not taken.
✓ Branch 5 → 6 taken 8 times.
✗ Branch 59 → 60 not taken.
✗ Branch 59 → 61 not taken.
85 static const QMimeDatabase database;
187
1/1
✓ Branch 8 → 9 taken 85 times.
85 const QMimeType byName = database.mimeTypeForFile(filePath, QMimeDatabase::MatchExtension);
188
4/4
✓ Branch 10 → 11 taken 43 times.
✓ Branch 10 → 12 taken 42 times.
✓ Branch 11 → 13 taken 43 times.
✓ Branch 12 → 13 taken 42 times.
85 const QMimeType type = head.isEmpty() ? byName : database.mimeTypeForFileNameAndData(filePath, head);
189
5/9
✓ Branch 13 → 14 taken 85 times.
✓ Branch 14 → 15 taken 85 times.
✗ Branch 14 → 17 not taken.
✓ Branch 15 → 16 taken 85 times.
✓ Branch 16 → 18 taken 85 times.
✓ Branch 18 → 19 taken 85 times.
✗ Branch 18 → 20 not taken.
✗ Branch 62 → 63 not taken.
✗ Branch 62 → 64 not taken.
85 QByteArray name = type.isValid() ? type.name().toLatin1() : QByteArray();
190 // Only a plain type/subtype token reaches the header.
191
4/7
✓ Branch 20 → 21 taken 8 times.
✓ Branch 20 → 33 taken 77 times.
✓ Branch 22 → 23 taken 8 times.
✗ Branch 22 → 33 not taken.
✓ Branch 27 → 28 taken 8 times.
✗ Branch 73 → 74 not taken.
✗ Branch 73 → 75 not taken.
93 static const QRegularExpression token(QStringLiteral("^[a-z0-9][a-z0-9!#$&^_.+-]*/[a-z0-9][a-z0-9!#$&^_.+-]*$"));
192
9/19
✓ Branch 34 → 35 taken 85 times.
✗ Branch 34 → 42 not taken.
✓ Branch 36 → 37 taken 85 times.
✗ Branch 36 → 42 not taken.
✓ Branch 38 → 39 taken 85 times.
✓ Branch 39 → 40 taken 85 times.
✓ Branch 40 → 41 taken 85 times.
✗ Branch 41 → 42 not taken.
✓ Branch 41 → 43 taken 85 times.
✓ Branch 44 → 45 taken 85 times.
✗ Branch 44 → 46 not taken.
✓ Branch 46 → 47 taken 85 times.
✗ Branch 46 → 48 not taken.
✗ Branch 48 → 49 not taken.
✓ Branch 48 → 53 taken 85 times.
✗ Branch 76 → 77 not taken.
✗ Branch 76 → 78 not taken.
✗ Branch 80 → 81 not taken.
✗ Branch 80 → 82 not taken.
170 if (name.isEmpty() || name == "application/x-zerosize" || !token.match(QString::fromLatin1(name)).hasMatch())
193 ✗ return QByteArrayLiteral("application/octet-stream");
194 85 return name;
195 85 }
196
197 12 void Rfc2822Builder::setMailJdHeader(const QString &name, const QString &value) {
198
4/7
✓ Branch 2 → 3 taken 4 times.
✓ Branch 2 → 15 taken 8 times.
✓ Branch 4 → 5 taken 4 times.
✗ Branch 4 → 15 not taken.
✓ Branch 9 → 10 taken 4 times.
✗ Branch 35 → 36 not taken.
✗ Branch 35 → 37 not taken.
16 static const QRegularExpression valid(QStringLiteral("^X-MailJD-[A-Za-z0-9-]{1,40}$"));
199
4/4
✓ Branch 16 → 17 taken 12 times.
✓ Branch 17 → 18 taken 12 times.
✓ Branch 19 → 20 taken 1 time.
✓ Branch 19 → 21 taken 11 times.
12 if (!valid.match(name).hasMatch()) return;
200
2/2
✓ Branch 21 → 22 taken 11 times.
✓ Branch 23 → 24 taken 11 times.
11 m_mailJdHeaders.append({name.toLatin1(), value});
201 }
202
203 // ═══════════════════════════════════════════════════════
204 // RFC 2047: Header encoding for non-ASCII
205 // ═══════════════════════════════════════════════════════
206
207 1220 QByteArray Rfc2822Builder::encodeHeader(const QString &value) {
208
1/1
✓ Branch 2 → 3 taken 1220 times.
1220 QByteArray utf8 = value.toUtf8();
209
210 // T-400/Bug 4: Always strip CR/LF to prevent header injection; S106-030:
211 // and NUL, which RFC 5322 §2.2 forbids in header fields.
212
1/1
✓ Branch 4 → 5 taken 1220 times.
1220 utf8.replace('\r', "");
213
1/1
✓ Branch 6 → 7 taken 1220 times.
1220 utf8.replace('\n', "");
214
1/1
✓ Branch 8 → 9 taken 1220 times.
1220 utf8.replace('\0', "");
215
216 // Check if encoding is needed: any non-ASCII, or a token folding cannot
217 // break (folding happens only at whitespace; a longer run made a header
218 // line above RFC 5322's 998 bytes — encoded words split anywhere).
219 1220 bool needsEncoding = false;
220 1220 qsizetype run = 0;
221
4/4
✓ Branch 9 → 10 taken 1220 times.
✓ Branch 10 → 11 taken 1220 times.
✓ Branch 21 → 12 taken 20120 times.
✓ Branch 21 → 22 taken 1113 times.
21233 for (char c : utf8) {
222
2/2
✓ Branch 12 → 13 taken 106 times.
✓ Branch 12 → 14 taken 20014 times.
20120 if (static_cast<unsigned char>(c) > 127) {
223 106 needsEncoding = true;
224 106 break;
225 }
226
4/4
✓ Branch 14 → 15 taken 18352 times.
✓ Branch 14 → 17 taken 1662 times.
✓ Branch 15 → 16 taken 18350 times.
✓ Branch 15 → 17 taken 2 times.
20014 run = (c == ' ' || c == '\t') ? 0 : run + 1;
227
2/2
✓ Branch 18 → 19 taken 1 time.
✓ Branch 18 → 20 taken 20013 times.
20014 if (run > kMaxUnfoldableToken) {
228 1 needsEncoding = true;
229 1 break;
230 }
231 }
232
233
2/2
✓ Branch 22 → 23 taken 1113 times.
✓ Branch 22 → 25 taken 107 times.
1220 if (!needsEncoding)
234 1113 return utf8;
235
236 // RFC 2047 Base64 encoded-word: =?UTF-8?B?<base64>?=
237 // Split into chunks if needed (max 75 chars per encoded-word)
238 107 QByteArray encoded;
239
1/1
✓ Branch 25 → 26 taken 107 times.
107 const QByteArray prefix = "=?UTF-8?B?";
240
1/1
✓ Branch 26 → 27 taken 107 times.
107 const QByteArray suffix = "?=";
241 // Max base64 payload to fit in 75 chars: 75 - prefix.size() - suffix.size()
242 107 const int maxPayloadBytes = ((75 - prefix.size() - suffix.size()) / 4) * 3;
243
244 107 int pos = 0;
245
2/2
✓ Branch 61 → 30 taken 149 times.
✓ Branch 61 → 62 taken 107 times.
256 while (pos < utf8.size()) {
246 // Find a chunk boundary that doesn't split a UTF-8 multibyte sequence
247 149 int chunkEnd = qMin(pos + maxPayloadBytes, utf8.size());
248 // Don't split in the middle of a UTF-8 multibyte char
249
5/6
✓ Branch 35 → 36 taken 45 times.
✓ Branch 35 → 40 taken 107 times.
✓ Branch 36 → 37 taken 45 times.
✗ Branch 36 → 40 not taken.
✓ Branch 41 → 33 taken 3 times.
✓ Branch 41 → 42 taken 149 times.
197 while (chunkEnd < utf8.size() && chunkEnd > pos &&
250
3/3
✓ Branch 37 → 38 taken 45 times.
✓ Branch 38 → 39 taken 3 times.
✓ Branch 38 → 40 taken 42 times.
45 (static_cast<unsigned char>(utf8[chunkEnd]) & 0xC0) == 0x80) {
251 3 --chunkEnd;
252 }
253
1/2
✗ Branch 42 → 43 not taken.
✓ Branch 42 → 46 taken 149 times.
149 if (chunkEnd <= pos)
254 ✗ chunkEnd = qMin(pos + maxPayloadBytes, utf8.size());
255
256
1/1
✓ Branch 46 → 47 taken 149 times.
149 QByteArray chunk = utf8.mid(pos, chunkEnd - pos);
257
258
2/2
✓ Branch 48 → 49 taken 42 times.
✓ Branch 48 → 50 taken 107 times.
149 if (!encoded.isEmpty())
259
1/1
✓ Branch 49 → 50 taken 42 times.
42 encoded += "\r\n "; // Folding whitespace between encoded-words
260
261
4/4
✓ Branch 51 → 52 taken 149 times.
✓ Branch 52 → 53 taken 149 times.
✓ Branch 53 → 54 taken 149 times.
✓ Branch 54 → 55 taken 149 times.
149 encoded += prefix + chunk.toBase64() + suffix;
262 149 pos = chunkEnd;
263 149 }
264
265 107 return encoded;
266 1220 }
267
268 795 QByteArray Rfc2822Builder::encodeAddress(const QString &name,
269 const QString &email) {
270 // T-400/Bug 4: Sanitize CRLF (and NUL, S106-030) from email to prevent
271 // header injection
272
2/2
✓ Branch 2 → 3 taken 795 times.
✓ Branch 3 → 4 taken 795 times.
795 const QByteArray safeEmail = headerSafe(email).left(254); // RFC 5321 path limit (fuzz_rfc2822_builder)
273
274
2/2
✓ Branch 6 → 7 taken 17 times.
✓ Branch 6 → 9 taken 778 times.
795 if (name.isEmpty())
275 17 return safeEmail;
276
277
1/1
✓ Branch 9 → 10 taken 778 times.
778 auto encodedName = encodeHeader(name);
278 // ASCII phrase punctuation needs a quoted display name. Encoded words for
279 // international names already carry their own RFC 2047 representation.
280 // F-14 (SPRINT-104): ':' is group syntax in an unquoted phrase
281 // ("Support: Team <s@x.org>" parses as a group named "Support"), so it
282 // joins the specials that force quoting.
283 // S106-029: whether encoding happened is decided by the name, not by the
284 // result's first bytes — an ASCII name starting with "=?" was emitted
285 // unquoted (and a literal "=?…?=" would even be decoded as an encoded
286 // word), so ASCII names containing "=?" are quoted too.
287
1/1
✓ Branch 10 → 11 taken 778 times.
778 const bool encodedWords = encodedName != name.toUtf8();
288
7/8
✓ Branch 13 → 14 taken 742 times.
✓ Branch 13 → 36 taken 36 times.
✓ Branch 14 → 15 taken 742 times.
✓ Branch 15 → 16 taken 735 times.
✓ Branch 15 → 35 taken 7 times.
✓ Branch 16 → 17 taken 735 times.
✓ Branch 17 → 18 taken 735 times.
✗ Branch 17 → 35 not taken.
1513 if (!encodedWords && (encodedName.contains(',') || encodedName.contains(';') ||
289
6/9
✓ Branch 18 → 19 taken 735 times.
✓ Branch 19 → 20 taken 735 times.
✗ Branch 19 → 35 not taken.
✓ Branch 20 → 21 taken 735 times.
✓ Branch 21 → 22 taken 735 times.
✗ Branch 21 → 35 not taken.
✓ Branch 22 → 23 taken 735 times.
✓ Branch 23 → 24 taken 735 times.
✗ Branch 23 → 35 not taken.
735 encodedName.contains('"') || encodedName.contains('\\') || encodedName.contains('<') ||
290
6/9
✓ Branch 24 → 25 taken 735 times.
✓ Branch 25 → 26 taken 735 times.
✗ Branch 25 → 35 not taken.
✓ Branch 26 → 27 taken 735 times.
✓ Branch 27 → 28 taken 735 times.
✗ Branch 27 → 35 not taken.
✓ Branch 28 → 29 taken 735 times.
✓ Branch 29 → 30 taken 735 times.
✗ Branch 29 → 35 not taken.
735 encodedName.contains('>') || encodedName.contains('(') || encodedName.contains(')') ||
291
6/7
✓ Branch 30 → 31 taken 735 times.
✓ Branch 31 → 32 taken 734 times.
✓ Branch 31 → 35 taken 1 time.
✗ Branch 34 → 35 not taken.
✓ Branch 34 → 36 taken 734 times.
✓ Branch 37 → 38 taken 8 times.
✓ Branch 37 → 50 taken 770 times.
1513 encodedName.contains(':') || encodedName.contains("=?"))) {
292
1/1
✓ Branch 40 → 41 taken 8 times.
8 encodedName.replace("\\", "\\\\");
293
1/1
✓ Branch 43 → 44 taken 8 times.
8 encodedName.replace("\"", "\\\"");
294
2/2
✓ Branch 44 → 45 taken 8 times.
✓ Branch 45 → 46 taken 8 times.
8 encodedName = "\"" + encodedName + "\"";
295 }
296
3/3
✓ Branch 50 → 51 taken 778 times.
✓ Branch 51 → 52 taken 778 times.
✓ Branch 52 → 53 taken 778 times.
778 return encodedName + " <" + safeEmail + ">";
297 795 }
298
299 461 QByteArray Rfc2822Builder::encodeAddressList(const QStringList &addresses) {
300 // Each address can be "name <email>" or just "email"
301 // Parse and re-encode each
302 461 QByteArray result;
303
2/2
✓ Branch 56 → 4 taken 515 times.
✓ Branch 56 → 57 taken 461 times.
976 for (const QString &addr : addresses) {
304
2/2
✓ Branch 6 → 7 taken 55 times.
✓ Branch 6 → 8 taken 460 times.
515 if (!result.isEmpty())
305
1/1
✓ Branch 7 → 8 taken 55 times.
55 result += ",\r\n ";
306
307
1/1
✓ Branch 8 → 9 taken 515 times.
515 QString trimmed = addr.trimmed();
308 // F-14 (SPRINT-104): a quoted display name may itself contain '<'
309 // ("Doe, John <CEO>" <j@x.org>) — take the LAST '<' as the address
310 // opener, and strip exactly one enclosing quote pair from the name.
311 515 int ltIdx = trimmed.lastIndexOf('<');
312 515 int gtIdx = trimmed.indexOf('>', ltIdx);
313
314
3/4
✓ Branch 13 → 14 taken 367 times.
✓ Branch 13 → 47 taken 148 times.
✓ Branch 14 → 15 taken 367 times.
✗ Branch 14 → 47 not taken.
515 if (ltIdx > 0 && gtIdx > ltIdx) {
315 // "Display Name <email>"
316
2/2
✓ Branch 15 → 16 taken 367 times.
✓ Branch 16 → 17 taken 367 times.
367 QString name = trimmed.left(ltIdx).trimmed();
317 // Remove surrounding quotes if present
318
8/10
✓ Branch 19 → 20 taken 367 times.
✗ Branch 19 → 27 not taken.
✓ Branch 21 → 22 taken 367 times.
✓ Branch 22 → 23 taken 5 times.
✓ Branch 22 → 27 taken 362 times.
✓ Branch 24 → 25 taken 5 times.
✓ Branch 25 → 26 taken 5 times.
✗ Branch 25 → 27 not taken.
✓ Branch 28 → 29 taken 5 times.
✓ Branch 28 → 34 taken 362 times.
367 if (name.size() >= 2 && name.startsWith('"') && name.endsWith('"'))
319
1/1
✓ Branch 30 → 31 taken 5 times.
5 name = name.mid(1, name.size() - 2);
320
2/2
✓ Branch 34 → 35 taken 367 times.
✓ Branch 35 → 36 taken 367 times.
367 QString email = trimmed.mid(ltIdx + 1, gtIdx - ltIdx - 1).trimmed();
321 // S106-031: one address with a long ASCII name is folded at its
322 // spaces (FWS is allowed inside a quoted display name).
323
4/4
✓ Branch 37 → 38 taken 367 times.
✓ Branch 38 → 39 taken 367 times.
✓ Branch 39 → 40 taken 367 times.
✓ Branch 40 → 41 taken 367 times.
367 result += foldHeader("To:", encodeAddress(name, email));
324 367 } else {
325 // Plain email address — T-509: sanitize CRLF (and NUL) to prevent
326 // header injection. Sprint 108 (fuzz_rfc2822_builder): an address
327 // has at most 254 bytes (RFC 5321 §4.5.3.1.3); anything longer is not
328 // one and must not make a header line exceed 998 bytes.
329
3/3
✓ Branch 47 → 48 taken 148 times.
✓ Branch 48 → 49 taken 148 times.
✓ Branch 49 → 50 taken 148 times.
148 result += headerSafe(trimmed).left(254);
330 }
331 515 }
332 461 return result;
333 ✗ }
334
335 // ═══════════════════════════════════════════════════════
336 // Quoted-Printable encoding (RFC 2045)
337 // ═══════════════════════════════════════════════════════
338
339 461 QByteArray Rfc2822Builder::encodeQuotedPrintable(const QByteArray &data) {
340 461 QByteArray result;
341 461 int lineLen = 0;
342
343
2/2
✓ Branch 70 → 3 taken 47076 times.
✓ Branch 70 → 71 taken 461 times.
47537 for (int i = 0; i < data.size(); ++i) {
344 47076 unsigned char c = static_cast<unsigned char>(data[i]);
345
346 // Handle CRLF pass-through
347
6/8
✓ Branch 4 → 5 taken 4 times.
✓ Branch 4 → 10 taken 47072 times.
✓ Branch 6 → 7 taken 4 times.
✗ Branch 6 → 10 not taken.
✓ Branch 8 → 9 taken 4 times.
✗ Branch 8 → 10 not taken.
✓ Branch 11 → 12 taken 4 times.
✓ Branch 11 → 14 taken 47072 times.
47076 if (c == '\r' && i + 1 < data.size() && data[i + 1] == '\n') {
348
1/1
✓ Branch 12 → 13 taken 4 times.
4 result += "\r\n";
349 4 lineLen = 0;
350 4 ++i; // Skip the \n
351 696 continue;
352 }
353
354 // Handle bare LF → CRLF
355
2/2
✓ Branch 14 → 15 taken 692 times.
✓ Branch 14 → 17 taken 46380 times.
47072 if (c == '\n') {
356
1/1
✓ Branch 15 → 16 taken 692 times.
692 result += "\r\n";
357 692 lineLen = 0;
358 692 continue;
359 }
360
361 46380 QByteArray encoded;
362 // Printable ASCII (33-126) except '=' can be literal
363
10/10
✓ Branch 17 → 18 taken 41346 times.
✓ Branch 17 → 20 taken 5034 times.
✓ Branch 18 → 19 taken 40706 times.
✓ Branch 18 → 20 taken 640 times.
✓ Branch 19 → 20 taken 340 times.
✓ Branch 19 → 22 taken 40366 times.
✓ Branch 20 → 21 taken 6009 times.
✓ Branch 20 → 22 taken 5 times.
✓ Branch 21 → 22 taken 5029 times.
✓ Branch 21 → 49 taken 980 times.
46380 if ((c >= 33 && c <= 126 && c != '=') || c == '\t' || c == ' ') {
364 // Space/tab at end of line must be encoded
365 bool atEol =
366
1/2
✓ Branch 25 → 26 taken 45013 times.
✗ Branch 25 → 28 not taken.
90413 (i + 1 >= data.size()) ||
367
5/6
✓ Branch 23 → 24 taken 45013 times.
✓ Branch 23 → 32 taken 387 times.
✓ Branch 27 → 28 taken 45009 times.
✓ Branch 27 → 32 taken 4 times.
✓ Branch 29 → 30 taken 45009 times.
✗ Branch 29 → 33 not taken.
180431 (i + 1 < data.size() && data[i + 1] == '\r') ||
368
2/2
✓ Branch 31 → 32 taken 426 times.
✓ Branch 31 → 33 taken 44583 times.
90018 (i + 1 < data.size() && data[i + 1] == '\n');
369
6/6
✓ Branch 34 → 35 taken 40371 times.
✓ Branch 34 → 36 taken 5029 times.
✓ Branch 35 → 36 taken 5 times.
✓ Branch 35 → 47 taken 40366 times.
✓ Branch 36 → 37 taken 28 times.
✓ Branch 36 → 47 taken 5006 times.
45400 if ((c == ' ' || c == '\t') && atEol) {
370
4/4
✓ Branch 37 → 38 taken 28 times.
✓ Branch 38 → 39 taken 28 times.
✓ Branch 39 → 40 taken 28 times.
✓ Branch 40 → 41 taken 28 times.
28 encoded = "=" + QByteArray::number(c, 16).toUpper().rightJustified(2, '0');
371 } else {
372
1/1
✓ Branch 47 → 48 taken 45372 times.
45372 encoded += static_cast<char>(c);
373 }
374 45400 } else {
375
4/4
✓ Branch 49 → 50 taken 980 times.
✓ Branch 50 → 51 taken 980 times.
✓ Branch 51 → 52 taken 980 times.
✓ Branch 52 → 53 taken 980 times.
980 encoded = "=" + QByteArray::number(c, 16).toUpper().rightJustified(2, '0');
376 }
377
378 // Soft line break if line would exceed 76 chars
379
2/2
✓ Branch 60 → 61 taken 364 times.
✓ Branch 60 → 63 taken 46016 times.
46380 if (lineLen + encoded.size() > 75) {
380
1/1
✓ Branch 61 → 62 taken 364 times.
364 result += "=\r\n";
381 364 lineLen = 0;
382 }
383
384
1/1
✓ Branch 63 → 64 taken 46380 times.
46380 result += encoded;
385 46380 lineLen += encoded.size();
386 46380 }
387
388 461 return result;
389 ✗ }
390
391 // ═══════════════════════════════════════════════════════
392 // Message-ID and boundary generation
393 // ═══════════════════════════════════════════════════════
394
395 115 QByteArray Rfc2822Builder::generateMessageId(const QString &senderAddress) {
396
4/4
✓ Branch 2 → 3 taken 115 times.
✓ Branch 3 → 4 taken 115 times.
✓ Branch 5 → 6 taken 115 times.
✓ Branch 6 → 7 taken 115 times.
115 const QByteArray uuid = QUuid::createUuid().toString(QUuid::WithoutBraces).remove('-').toLatin1();
397 115 const int at = senderAddress.lastIndexOf(QLatin1Char('@'));
398
9/13
✓ Branch 11 → 12 taken 114 times.
✓ Branch 11 → 16 taken 1 time.
✓ Branch 13 → 14 taken 114 times.
✓ Branch 14 → 15 taken 114 times.
✓ Branch 15 → 17 taken 114 times.
✓ Branch 17 → 18 taken 114 times.
✓ Branch 17 → 19 taken 1 time.
✓ Branch 19 → 20 taken 114 times.
✓ Branch 19 → 21 taken 1 time.
✗ Branch 69 → 70 not taken.
✗ Branch 69 → 71 not taken.
✗ Branch 73 → 74 not taken.
✗ Branch 73 → 75 not taken.
115 QByteArray domain = at > 0 ? QUrl::toAce(senderAddress.mid(at + 1)).toLower() : QByteArray();
399
2/4
✓ Branch 21 → 22 taken 115 times.
✗ Branch 22 → 23 not taken.
✓ Branch 22 → 24 taken 115 times.
✗ Branch 23 → 24 not taken.
115 if (domain.endsWith('.')) domain.chop(1);
400 34 static const QRegularExpression validDomain(QStringLiteral(
401
4/7
✓ Branch 24 → 25 taken 17 times.
✓ Branch 24 → 37 taken 98 times.
✓ Branch 26 → 27 taken 17 times.
✗ Branch 26 → 37 not taken.
✓ Branch 31 → 32 taken 17 times.
✗ Branch 85 → 86 not taken.
✗ Branch 85 → 87 not taken.
149 "^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$"));
402
10/17
✓ Branch 38 → 39 taken 115 times.
✗ Branch 38 → 44 not taken.
✓ Branch 40 → 41 taken 115 times.
✓ Branch 41 → 42 taken 115 times.
✓ Branch 42 → 43 taken 115 times.
✓ Branch 43 → 44 taken 4 times.
✓ Branch 43 → 45 taken 111 times.
✓ Branch 46 → 47 taken 115 times.
✗ Branch 46 → 48 not taken.
✓ Branch 48 → 49 taken 115 times.
✗ Branch 48 → 50 not taken.
✓ Branch 50 → 51 taken 4 times.
✓ Branch 50 → 52 taken 111 times.
✗ Branch 88 → 89 not taken.
✗ Branch 88 → 90 not taken.
✗ Branch 92 → 93 not taken.
✗ Branch 92 → 94 not taken.
230 if (domain.size() > 253 || !validDomain.match(QString::fromLatin1(domain)).hasMatch())
403
1/1
✓ Branch 51 → 52 taken 4 times.
4 domain = "mailjd.invalid";
404
4/4
✓ Branch 52 → 53 taken 115 times.
✓ Branch 53 → 54 taken 115 times.
✓ Branch 54 → 55 taken 115 times.
✓ Branch 55 → 56 taken 115 times.
230 return '<' + uuid + '@' + domain + '>';
405 115 }
406
407 109 QByteArray Rfc2822Builder::generateBoundary() const {
408
2/2
✓ Branch 3 → 4 taken 20 times.
✓ Branch 3 → 13 taken 89 times.
109 if (!m_boundarySeed.isEmpty())
409
4/4
✓ Branch 4 → 5 taken 20 times.
✓ Branch 5 → 6 taken 20 times.
✓ Branch 6 → 7 taken 20 times.
✓ Branch 7 → 8 taken 20 times.
20 return "----=_Part_" + m_boundarySeed + '_' + QByteArray::number(++m_boundaryCounter);
410 return "----=_Part_" +
411
1/1
✓ Branch 13 → 14 taken 89 times.
89 QUuid::createUuid()
412
1/1
✓ Branch 14 → 15 taken 89 times.
89 .toString(QUuid::WithoutBraces)
413
1/1
✓ Branch 16 → 17 taken 89 times.
178 .remove('-')
414
2/2
✓ Branch 17 → 18 taken 89 times.
✓ Branch 18 → 19 taken 89 times.
178 .toLatin1();
415 }
416
417 // ═══════════════════════════════════════════════════════
418 // Build the complete RFC-2822 message
419 // ═══════════════════════════════════════════════════════
420
421 25 void Rfc2822Builder::setCalendarPart(const QByteArray &calendar, const QString &method) {
422 25 m_calendar = calendar;
423 // A header parameter: only the iTIP method token survives.
424 25 m_calendarMethod.clear();
425
5/5
✓ Branch 4 → 5 taken 25 times.
✓ Branch 5 → 6 taken 25 times.
✓ Branch 6 → 7 taken 25 times.
✓ Branch 28 → 8 taken 171 times.
✓ Branch 28 → 29 taken 25 times.
196 for (const QChar c : method.toUpper())
426
8/9
✓ Branch 12 → 13 taken 165 times.
✓ Branch 12 → 18 taken 6 times.
✗ Branch 17 → 18 not taken.
✓ Branch 17 → 22 taken 165 times.
✓ Branch 21 → 22 taken 1 time.
✓ Branch 21 → 23 taken 5 times.
✓ Branch 24 → 25 taken 166 times.
✓ Branch 24 → 27 taken 5 times.
✓ Branch 26 → 27 taken 166 times.
196 if ((c >= QLatin1Char('A') && c <= QLatin1Char('Z')) || c == QLatin1Char('-')) m_calendarMethod += char(c.unicode());
427 25 }
428
429 32 QString Rfc2822Builder::messageIdOf(const QByteArray &message) {
430
1/1
✓ Branch 4 → 5 taken 32 times.
32 const QByteArray headers = message.left(message.indexOf("\r\n\r\n"));
431
5/5
✓ Branch 5 → 6 taken 32 times.
✓ Branch 6 → 7 taken 32 times.
✓ Branch 7 → 8 taken 32 times.
✓ Branch 24 → 9 taken 150 times.
✓ Branch 24 → 25 taken 1 time.
151 for (const QByteArray &line : headers.split('\n'))
432
8/8
✓ Branch 10 → 11 taken 150 times.
✓ Branch 14 → 15 taken 31 times.
✓ Branch 14 → 22 taken 119 times.
✓ Branch 15 → 16 taken 31 times.
✓ Branch 16 → 17 taken 31 times.
✓ Branch 17 → 18 taken 31 times.
✓ Branch 27 → 28 taken 1 time.
✓ Branch 27 → 30 taken 31 times.
151 if (line.toLower().startsWith("message-id:")) return QString::fromLatin1(line.mid(11).trimmed());
433 1 return {};
434 32 }
435
436 namespace {
437 // F-15 (SPRINT-104): fold an unstructured header value onto continuation
438 // lines at word boundaries. The first line keeps the "Name:" prefix
439 // within 78 characters where possible; every emitted line stays below
440 // the 998-byte hard limit. Values without spaces long enough to matter
441 // (encoded words without whitespace are one token) are left untouched
442 // rather than split mid-token.
443 1347 QByteArray foldHeader(const QByteArray &name, const QByteArray &value) {
444
1/1
✓ Branch 2 → 3 taken 1347 times.
1347 const QByteArray prefix = name + " ";
445
2/2
✓ Branch 5 → 6 taken 1309 times.
✓ Branch 5 → 7 taken 38 times.
1347 if (prefix.size() + value.size() <= 78)
446 1309 return value;
447 // S106-026: encodeHeader already folds its encoded words ("\r\n " between
448 // words of at most 75 characters). Folding that again at the join's space
449 // emitted "\r\n" twice in a row — an empty line that ended the header
450 // block, so Message-ID, References and Content-Type became body text.
451
2/2
✓ Branch 9 → 10 taken 9 times.
✓ Branch 9 → 11 taken 29 times.
38 if (value.contains("\r\n"))
452 9 return value;
453 29 QByteArray result;
454 29 qsizetype lineStart = 0; // offset into value where the current line begins
455 29 qsizetype search = 0;
456 29 qsizetype lastSpace = -1;
457 29 const qsizetype firstBudget = 78 - prefix.size();
458 29 qsizetype budget = firstBudget;
459
2/2
✓ Branch 31 → 13 taken 13772 times.
✓ Branch 31 → 32 taken 29 times.
13801 for (qsizetype i = 0; i < value.size(); ++i) {
460 // Both WSP characters are fold points, as encodeHeader assumes when it
461 // leaves a value unencoded (a tab-separated run once stayed one line
462 // of 1 210 bytes — fuzz_rfc2822_builder, pipeline #7214).
463
6/6
✓ Branch 14 → 15 taken 13598 times.
✓ Branch 14 → 17 taken 174 times.
✓ Branch 16 → 17 taken 2 times.
✓ Branch 16 → 18 taken 13596 times.
✓ Branch 19 → 20 taken 176 times.
✓ Branch 19 → 21 taken 13596 times.
13772 if (value.at(i) == ' ' || value.at(i) == '\t')
464 176 lastSpace = i;
465
4/4
✓ Branch 21 → 22 taken 2763 times.
✓ Branch 21 → 29 taken 11009 times.
✓ Branch 22 → 23 taken 128 times.
✓ Branch 22 → 29 taken 2635 times.
13772 if (i - lineStart >= budget && lastSpace > lineStart) {
466 // The whitespace starts the continuation line: unfolding (removing
467 // the CRLF) gives the value back unchanged.
468
3/3
✓ Branch 23 → 24 taken 128 times.
✓ Branch 24 → 25 taken 128 times.
✓ Branch 25 → 26 taken 128 times.
128 result += value.mid(lineStart, lastSpace - lineStart) + "\r\n";
469 128 lineStart = lastSpace;
470 128 i = lineStart;
471 128 lastSpace = -1;
472 128 budget = 78; // the continuation line's leading whitespace included
473 }
474 }
475
2/2
✓ Branch 32 → 33 taken 29 times.
✓ Branch 33 → 34 taken 29 times.
29 result += value.mid(lineStart);
476 // A single token longer than the budget stays on its own line; values
477 // from encodeHeader never carry one above kMaxUnfoldableToken.
478 29 return result;
479 1347 }
480 } // namespace
481
482 423 QByteArray Rfc2822Builder::build(BccHeader bccHeader) const {
483 423 QByteArray msg;
484 423 m_boundaryCounter = 0;
485
486 // Date header — required
487
4/4
✓ Branch 2 → 3 taken 423 times.
✓ Branch 3 → 4 taken 298 times.
✓ Branch 3 → 5 taken 125 times.
✓ Branch 5 → 6 taken 125 times.
423 QDateTime date = m_date.isValid() ? m_date : QDateTime::currentDateTimeUtc();
488 // T-400/Bug 15: Use C locale for RFC 5322 compliant English date names
489 423 msg += "Date: " +
490
5/5
✓ Branch 6 → 7 taken 423 times.
✓ Branch 10 → 11 taken 423 times.
✓ Branch 11 → 12 taken 423 times.
✓ Branch 12 → 13 taken 423 times.
✓ Branch 13 → 14 taken 423 times.
1269 QLocale(QLocale::C).toString(date, QStringLiteral("ddd, dd MMM yyyy HH:mm:ss")).toUtf8();
491
1/1
✓ Branch 20 → 21 taken 423 times.
423 int utcOffset = date.offsetFromUtc();
492
2/2
✓ Branch 21 → 22 taken 393 times.
✓ Branch 21 → 23 taken 30 times.
423 if (utcOffset == 0) {
493
1/1
✓ Branch 22 → 40 taken 393 times.
393 msg += " +0000";
494 } else {
495 30 int hours = qAbs(utcOffset) / 3600;
496 30 int mins = (qAbs(utcOffset) % 3600) / 60;
497
3/3
✓ Branch 25 → 26 taken 29 times.
✓ Branch 25 → 27 taken 1 time.
✓ Branch 28 → 29 taken 30 times.
30 msg += (utcOffset > 0 ? " +" : " -");
498
3/3
✓ Branch 29 → 30 taken 30 times.
✓ Branch 30 → 31 taken 30 times.
✓ Branch 31 → 32 taken 30 times.
30 msg += QByteArray::number(hours).rightJustified(2, '0');
499
3/3
✓ Branch 34 → 35 taken 30 times.
✓ Branch 35 → 36 taken 30 times.
✓ Branch 36 → 37 taken 30 times.
30 msg += QByteArray::number(mins).rightJustified(2, '0');
500 }
501
1/1
✓ Branch 40 → 41 taken 423 times.
423 msg += "\r\n";
502
503 // From
504
1/2
✓ Branch 42 → 43 taken 423 times.
✗ Branch 42 → 55 not taken.
423 if (!m_fromEmail.isEmpty()) {
505
6/6
✓ Branch 43 → 44 taken 423 times.
✓ Branch 44 → 45 taken 423 times.
✓ Branch 45 → 46 taken 423 times.
✓ Branch 46 → 47 taken 423 times.
✓ Branch 47 → 48 taken 423 times.
✓ Branch 48 → 49 taken 423 times.
423 msg += "From: " + foldHeader("From:", encodeAddress(m_fromName, m_fromEmail)) + "\r\n";
506 }
507
508 // To
509
2/2
✓ Branch 56 → 57 taken 407 times.
✓ Branch 56 → 65 taken 16 times.
423 if (!m_to.isEmpty()) {
510
4/4
✓ Branch 57 → 58 taken 407 times.
✓ Branch 58 → 59 taken 407 times.
✓ Branch 59 → 60 taken 407 times.
✓ Branch 60 → 61 taken 407 times.
407 msg += "To: " + encodeAddressList(m_to) + "\r\n";
511 }
512
513 // Cc
514
2/2
✓ Branch 66 → 67 taken 24 times.
✓ Branch 66 → 75 taken 399 times.
423 if (!m_cc.isEmpty()) {
515
4/4
✓ Branch 67 → 68 taken 24 times.
✓ Branch 68 → 69 taken 24 times.
✓ Branch 69 → 70 taken 24 times.
✓ Branch 70 → 71 taken 24 times.
24 msg += "Cc: " + encodeAddressList(m_cc) + "\r\n";
516 }
517
518 // Bcc is a required per-build choice: private IMAP copies include it, SMTP omits it.
519
6/6
✓ Branch 76 → 77 taken 25 times.
✓ Branch 76 → 79 taken 398 times.
✓ Branch 77 → 78 taken 12 times.
✓ Branch 77 → 79 taken 13 times.
✓ Branch 80 → 81 taken 12 times.
✓ Branch 80 → 89 taken 411 times.
423 if (!m_bcc.isEmpty() && bccHeader == BccHeader::Include) {
520
4/4
✓ Branch 81 → 82 taken 12 times.
✓ Branch 82 → 83 taken 12 times.
✓ Branch 83 → 84 taken 12 times.
✓ Branch 84 → 85 taken 12 times.
12 msg += "Bcc: " + encodeAddressList(m_bcc) + "\r\n";
521 }
522
523 // Subject — RFC 2047 encoded if non-ASCII
524 // F-15 (SPRINT-104): fold at whitespace so no header line exceeds the
525 // 998-byte transport limit (RFC 5322 §2.1.1). Encoded words carry no
526 // spaces and are never split; a 1200-character ASCII subject used to
527 // leave as one 1210-byte line.
528
6/6
✓ Branch 89 → 90 taken 423 times.
✓ Branch 90 → 91 taken 423 times.
✓ Branch 91 → 92 taken 423 times.
✓ Branch 92 → 93 taken 423 times.
✓ Branch 93 → 94 taken 423 times.
✓ Branch 94 → 95 taken 423 times.
423 msg += "Subject: " + foldHeader("Subject:", encodeHeader(m_subject)) + "\r\n";
529
530 // Message-ID — T-509: sanitize CRLF to prevent header injection
531
2/2
✓ Branch 101 → 102 taken 115 times.
✓ Branch 101 → 108 taken 308 times.
423 if (m_messageId.isEmpty())
532
2/2
✓ Branch 102 → 103 taken 115 times.
✓ Branch 103 → 104 taken 115 times.
115 m_messageId = QString::fromUtf8(generateMessageId(m_fromEmail));
533
4/4
✓ Branch 108 → 109 taken 423 times.
✓ Branch 109 → 110 taken 423 times.
✓ Branch 110 → 111 taken 423 times.
✓ Branch 111 → 112 taken 423 times.
423 msg += "Message-ID: " + headerSafe(m_messageId) + "\r\n";
534
535 // In-Reply-To — T-400/Bug 4: sanitized; S106-031: folded like Subject.
536
3/3
✓ Branch 115 → 116 taken 423 times.
✓ Branch 117 → 118 taken 62 times.
✓ Branch 117 → 130 taken 361 times.
423 if (const QByteArrayList ids = messageIdsSafe(m_inReplyTo); !ids.isEmpty())
537
6/6
✓ Branch 118 → 119 taken 62 times.
✓ Branch 119 → 120 taken 62 times.
✓ Branch 120 → 121 taken 62 times.
✓ Branch 121 → 122 taken 62 times.
✓ Branch 122 → 123 taken 62 times.
✓ Branch 123 → 124 taken 62 times.
423 msg += "In-Reply-To: " + foldHeader("In-Reply-To:", ids.join(' ')) + "\r\n";
538
539 // References — S106-031: a long thread's ids (~21 already pass the
540 // 998-byte line limit of RFC 5322 §2.1.1) fold between the ids.
541
2/2
✓ Branch 132 → 133 taken 60 times.
✓ Branch 132 → 158 taken 363 times.
423 if (!m_references.isEmpty()) {
542 60 QByteArrayList safeRefs;
543
4/4
✓ Branch 136 → 137 taken 199 times.
✓ Branch 137 → 138 taken 199 times.
✓ Branch 141 → 135 taken 199 times.
✓ Branch 141 → 142 taken 60 times.
259 for (const QString &ref : m_references) safeRefs.append(messageIdsSafe(ref));
544
7/8
✓ Branch 143 → 144 taken 60 times.
✗ Branch 143 → 156 not taken.
✓ Branch 144 → 145 taken 60 times.
✓ Branch 145 → 146 taken 60 times.
✓ Branch 146 → 147 taken 60 times.
✓ Branch 147 → 148 taken 60 times.
✓ Branch 148 → 149 taken 60 times.
✓ Branch 149 → 150 taken 60 times.
60 if (!safeRefs.isEmpty()) msg += "References: " + foldHeader("References:", safeRefs.join(' ')) + "\r\n";
545 60 }
546
547 // Sprint 108 optional headers (absent unless set: plain builds unchanged).
548
2/2
✓ Branch 159 → 160 taken 6 times.
✓ Branch 159 → 168 taken 417 times.
423 if (!m_replyTo.isEmpty())
549
4/4
✓ Branch 160 → 161 taken 6 times.
✓ Branch 161 → 162 taken 6 times.
✓ Branch 162 → 163 taken 6 times.
✓ Branch 163 → 164 taken 6 times.
6 msg += "Reply-To: " + encodeAddressList(m_replyTo) + "\r\n";
550
3/4
✓ Branch 168 → 169 taken 418 times.
✓ Branch 168 → 170 taken 5 times.
✗ Branch 169 → 170 not taken.
✓ Branch 169 → 172 taken 418 times.
423 if (m_priority == 1 || m_priority == 2)
551
1/1
✓ Branch 170 → 171 taken 5 times.
5 msg += "Importance: high\r\nX-Priority: 1 (Highest)\r\n";
552
3/4
✓ Branch 172 → 173 taken 418 times.
✗ Branch 172 → 174 not taken.
✓ Branch 173 → 174 taken 1 time.
✓ Branch 173 → 175 taken 417 times.
418 else if (m_priority == 4 || m_priority == 5)
553
1/1
✓ Branch 174 → 175 taken 1 time.
1 msg += "Importance: low\r\nX-Priority: 5 (Lowest)\r\n";
554
2/2
✓ Branch 176 → 177 taken 6 times.
✓ Branch 176 → 191 taken 417 times.
423 if (!m_dispositionNotificationTo.isEmpty())
555
6/8
✓ Branch 179 → 180 taken 6 times.
✓ Branch 180 → 181 taken 6 times.
✓ Branch 181 → 182 taken 6 times.
✓ Branch 182 → 183 taken 6 times.
✓ Branch 188 → 189 taken 6 times.
✓ Branch 188 → 190 taken 6 times.
✗ Branch 533 → 534 not taken.
✗ Branch 533 → 535 not taken.
12 msg += "Disposition-Notification-To: " + encodeAddressList({m_dispositionNotificationTo}) + "\r\n";
556
2/2
✓ Branch 209 → 193 taken 12 times.
✓ Branch 209 → 210 taken 423 times.
435 for (const auto &header : m_mailJdHeaders)
557
7/7
✓ Branch 194 → 195 taken 12 times.
✓ Branch 195 → 196 taken 12 times.
✓ Branch 196 → 197 taken 12 times.
✓ Branch 197 → 198 taken 12 times.
✓ Branch 198 → 199 taken 12 times.
✓ Branch 199 → 200 taken 12 times.
✓ Branch 200 → 201 taken 12 times.
12 msg += header.first + ": " + foldHeader(header.first + ':', encodeHeader(header.second)) + "\r\n";
558
559 // MIME-Version
560
1/1
✓ Branch 210 → 211 taken 423 times.
423 msg += "MIME-Version: 1.0\r\n";
561
562
2/2
✓ Branch 212 → 213 taken 31 times.
✓ Branch 212 → 215 taken 392 times.
423 if (!m_bodyHtml.isNull()) {
563
1/1
✓ Branch 213 → 214 taken 31 times.
31 appendFormattedBody(msg);
564 31 return msg;
565 }
566
567 // Body + optional attachments
568
6/6
✓ Branch 216 → 217 taken 351 times.
✓ Branch 216 → 220 taken 41 times.
✓ Branch 218 → 219 taken 23 times.
✓ Branch 218 → 220 taken 328 times.
✓ Branch 221 → 222 taken 23 times.
✓ Branch 221 → 272 taken 369 times.
392 if (m_attachments.isEmpty() && !m_calendar.isEmpty()) {
569
1/1
✓ Branch 222 → 223 taken 23 times.
23 const QByteArray boundary = generateBoundary();
570
3/3
✓ Branch 223 → 224 taken 23 times.
✓ Branch 224 → 225 taken 23 times.
✓ Branch 225 → 226 taken 23 times.
23 msg += "Content-Type: multipart/alternative;\r\n boundary=\"" + boundary + "\"\r\n\r\n";
571
3/3
✓ Branch 228 → 229 taken 23 times.
✓ Branch 229 → 230 taken 23 times.
✓ Branch 230 → 231 taken 23 times.
23 msg += "--" + boundary + "\r\n";
572
1/1
✓ Branch 233 → 234 taken 23 times.
23 msg += "Content-Type: text/plain; charset=UTF-8\r\n";
573
1/1
✓ Branch 234 → 235 taken 23 times.
23 msg += "Content-Transfer-Encoding: quoted-printable\r\n\r\n";
574
4/4
✓ Branch 235 → 236 taken 23 times.
✓ Branch 236 → 237 taken 23 times.
✓ Branch 237 → 238 taken 23 times.
✓ Branch 238 → 239 taken 23 times.
23 msg += encodeQuotedPrintable(m_bodyText.toUtf8()) + "\r\n";
575
3/3
✓ Branch 242 → 243 taken 23 times.
✓ Branch 243 → 244 taken 23 times.
✓ Branch 244 → 245 taken 23 times.
23 msg += "--" + boundary + "\r\n";
576
3/3
✓ Branch 247 → 248 taken 23 times.
✓ Branch 248 → 249 taken 23 times.
✓ Branch 249 → 250 taken 23 times.
23 msg += "Content-Type: text/calendar; method=" + m_calendarMethod + "; charset=UTF-8\r\n";
577
1/1
✓ Branch 252 → 253 taken 23 times.
23 msg += "Content-Transfer-Encoding: base64\r\n\r\n";
578
1/1
✓ Branch 254 → 255 taken 23 times.
23 const QByteArray encoded = m_calendar.toBase64();
579
2/2
✓ Branch 263 → 256 taken 188 times.
✓ Branch 263 → 264 taken 23 times.
211 for (qsizetype offset = 0; offset < encoded.size(); offset += 76)
580
3/3
✓ Branch 256 → 257 taken 188 times.
✓ Branch 257 → 258 taken 188 times.
✓ Branch 258 → 259 taken 188 times.
188 msg += encoded.mid(offset, 76) + "\r\n";
581
3/3
✓ Branch 264 → 265 taken 23 times.
✓ Branch 265 → 266 taken 23 times.
✓ Branch 266 → 267 taken 23 times.
23 msg += "--" + boundary + "--\r\n";
582
2/2
✓ Branch 273 → 274 taken 328 times.
✓ Branch 273 → 283 taken 41 times.
392 } else if (m_attachments.isEmpty()) {
583 // Simple text message
584
1/1
✓ Branch 274 → 275 taken 328 times.
328 msg += "Content-Type: text/plain; charset=UTF-8\r\n";
585
1/1
✓ Branch 275 → 276 taken 328 times.
328 msg += "Content-Transfer-Encoding: quoted-printable\r\n";
586
1/1
✓ Branch 276 → 277 taken 328 times.
328 msg += "\r\n";
587
3/3
✓ Branch 277 → 278 taken 328 times.
✓ Branch 278 → 279 taken 328 times.
✓ Branch 279 → 280 taken 328 times.
328 msg += encodeQuotedPrintable(m_bodyText.toUtf8());
588 } else {
589 // Multipart/mixed
590
1/1
✓ Branch 283 → 284 taken 41 times.
41 QByteArray boundary = generateBoundary();
591
1/1
✓ Branch 284 → 285 taken 41 times.
82 msg += "Content-Type: multipart/mixed;\r\n boundary=\"" + boundary +
592
2/2
✓ Branch 285 → 286 taken 41 times.
✓ Branch 286 → 287 taken 41 times.
41 "\"\r\n";
593
1/1
✓ Branch 289 → 290 taken 41 times.
41 msg += "\r\n";
594
595 // F-17 (SPRINT-104): with a calendar part, the text body becomes a
596 // multipart/alternative of text and text/calendar (RFC 2045 §5.1.4
597 // nesting) inside the mixed container — latent before: no caller
598 // combined both, but the builder silently dropped the invitation
599 // part when one did.
600
2/2
✓ Branch 291 → 292 taken 3 times.
✓ Branch 291 → 347 taken 38 times.
41 if (!m_calendar.isEmpty()) {
601
1/1
✓ Branch 292 → 293 taken 3 times.
3 const QByteArray inner = generateBoundary();
602
3/3
✓ Branch 293 → 294 taken 3 times.
✓ Branch 294 → 295 taken 3 times.
✓ Branch 295 → 296 taken 3 times.
3 msg += "--" + boundary + "\r\n";
603
3/3
✓ Branch 298 → 299 taken 3 times.
✓ Branch 299 → 300 taken 3 times.
✓ Branch 300 → 301 taken 3 times.
3 msg += "Content-Type: multipart/alternative;\r\n boundary=\"" + inner + "\"\r\n\r\n";
604
3/3
✓ Branch 303 → 304 taken 3 times.
✓ Branch 304 → 305 taken 3 times.
✓ Branch 305 → 306 taken 3 times.
3 msg += "--" + inner + "\r\n";
605
1/1
✓ Branch 308 → 309 taken 3 times.
3 msg += "Content-Type: text/plain; charset=UTF-8\r\n";
606
1/1
✓ Branch 309 → 310 taken 3 times.
3 msg += "Content-Transfer-Encoding: quoted-printable\r\n\r\n";
607
4/4
✓ Branch 310 → 311 taken 3 times.
✓ Branch 311 → 312 taken 3 times.
✓ Branch 312 → 313 taken 3 times.
✓ Branch 313 → 314 taken 3 times.
3 msg += encodeQuotedPrintable(m_bodyText.toUtf8()) + "\r\n";
608
3/3
✓ Branch 317 → 318 taken 3 times.
✓ Branch 318 → 319 taken 3 times.
✓ Branch 319 → 320 taken 3 times.
3 msg += "--" + inner + "\r\n";
609
3/3
✓ Branch 322 → 323 taken 3 times.
✓ Branch 323 → 324 taken 3 times.
✓ Branch 324 → 325 taken 3 times.
3 msg += "Content-Type: text/calendar; method=" + m_calendarMethod + "; charset=UTF-8\r\n";
610
1/1
✓ Branch 327 → 328 taken 3 times.
3 msg += "Content-Transfer-Encoding: base64\r\n\r\n";
611
1/1
✓ Branch 329 → 330 taken 3 times.
3 const QByteArray encoded = m_calendar.toBase64();
612
2/2
✓ Branch 338 → 331 taken 13 times.
✓ Branch 338 → 339 taken 3 times.
16 for (qsizetype offset = 0; offset < encoded.size(); offset += 76)
613
3/3
✓ Branch 331 → 332 taken 13 times.
✓ Branch 332 → 333 taken 13 times.
✓ Branch 333 → 334 taken 13 times.
13 msg += encoded.mid(offset, 76) + "\r\n";
614
3/3
✓ Branch 339 → 340 taken 3 times.
✓ Branch 340 → 341 taken 3 times.
✓ Branch 341 → 342 taken 3 times.
3 msg += "--" + inner + "--\r\n";
615 3 } else {
616 // Text body part
617
3/3
✓ Branch 347 → 348 taken 38 times.
✓ Branch 348 → 349 taken 38 times.
✓ Branch 349 → 350 taken 38 times.
38 msg += "--" + boundary + "\r\n";
618
1/1
✓ Branch 352 → 353 taken 38 times.
38 msg += "Content-Type: text/plain; charset=UTF-8\r\n";
619
1/1
✓ Branch 353 → 354 taken 38 times.
38 msg += "Content-Transfer-Encoding: quoted-printable\r\n";
620
1/1
✓ Branch 354 → 355 taken 38 times.
38 msg += "\r\n";
621
4/4
✓ Branch 355 → 356 taken 38 times.
✓ Branch 356 → 357 taken 38 times.
✓ Branch 357 → 358 taken 38 times.
✓ Branch 358 → 359 taken 38 times.
38 msg += encodeQuotedPrintable(m_bodyText.toUtf8()) + "\r\n";
622 }
623
624 // Attachment parts
625
1/1
✓ Branch 363 → 364 taken 41 times.
41 appendAttachmentParts(msg, boundary);
626
627
3/3
✓ Branch 364 → 365 taken 41 times.
✓ Branch 365 → 366 taken 41 times.
✓ Branch 366 → 367 taken 41 times.
41 msg += "--" + boundary + "--\r\n";
628 41 }
629
630 392 return msg;
631 429 }
632
633 45 void Rfc2822Builder::appendAttachmentParts(QByteArray &msg, const QByteArray &boundary) const {
634
2/2
✓ Branch 50 → 4 taken 59 times.
✓ Branch 50 → 51 taken 45 times.
104 for (const auto &att : m_attachments) {
635
3/3
✓ Branch 5 → 6 taken 59 times.
✓ Branch 6 → 7 taken 59 times.
✓ Branch 7 → 8 taken 59 times.
59 msg += "--" + boundary + "\r\n";
636 // T-614/SEC-13: Sanitize mimeType to prevent CRLF header injection
637
1/1
✓ Branch 10 → 11 taken 59 times.
59 const QByteArray mime = leafMimeType(att.mimeType);
638
2/2
✓ Branch 13 → 14 taken 59 times.
✓ Branch 14 → 15 taken 59 times.
118 msg += "Content-Type: " + mime + ";\r\n" +
639
5/5
✓ Branch 11 → 12 taken 59 times.
✓ Branch 12 → 13 taken 59 times.
✓ Branch 15 → 16 taken 59 times.
✓ Branch 16 → 17 taken 59 times.
✓ Branch 17 → 18 taken 59 times.
177 encodeMimeFilenameParameters("name", att.filename) + "\r\n";
640
1/1
✓ Branch 24 → 25 taken 59 times.
59 msg += "Content-Transfer-Encoding: base64\r\n";
641 59 msg += "Content-Disposition: attachment;\r\n" +
642
3/3
✓ Branch 25 → 26 taken 59 times.
✓ Branch 26 → 27 taken 59 times.
✓ Branch 27 → 28 taken 59 times.
177 encodeMimeFilenameParameters("filename", att.filename) +
643
2/2
✓ Branch 28 → 29 taken 59 times.
✓ Branch 29 → 30 taken 59 times.
59 "\r\n";
644
1/1
✓ Branch 34 → 35 taken 59 times.
59 msg += "\r\n";
645
646 // Base64 with 76-char line wrapping
647
1/1
✓ Branch 36 → 37 taken 59 times.
59 QByteArray b64 = att.data.toBase64();
648
2/2
✓ Branch 45 → 38 taken 170755 times.
✓ Branch 45 → 46 taken 59 times.
170814 for (int i = 0; i < b64.size(); i += 76) {
649
3/3
✓ Branch 38 → 39 taken 170755 times.
✓ Branch 39 → 40 taken 170755 times.
✓ Branch 40 → 41 taken 170755 times.
170755 msg += b64.mid(i, 76) + "\r\n";
650 }
651 59 }
652 45 }
653
654 namespace {
655 9 QByteArray base64Lines(const QByteArray &data) {
656 9 QByteArray out;
657
1/1
✓ Branch 3 → 4 taken 9 times.
9 const QByteArray encoded = data.toBase64();
658
2/2
✓ Branch 12 → 5 taken 26 times.
✓ Branch 12 → 13 taken 9 times.
35 for (qsizetype offset = 0; offset < encoded.size(); offset += 76)
659
3/3
✓ Branch 5 → 6 taken 26 times.
✓ Branch 6 → 7 taken 26 times.
✓ Branch 7 → 8 taken 26 times.
26 out += encoded.mid(offset, 76) + "\r\n";
660 9 return out;
661 9 }
662
663 // A token for a Content-ID: what the composer generates, never header syntax.
664 7 QByteArray safeContentId(const QString &contentId) {
665 7 QByteArray out;
666
2/2
✓ Branch 30 → 4 taken 148 times.
✓ Branch 30 → 31 taken 7 times.
155 for (const QChar c : contentId)
667
3/6
✓ Branch 7 → 8 taken 148 times.
✗ Branch 7 → 25 not taken.
✓ Branch 11 → 12 taken 148 times.
✗ Branch 11 → 25 not taken.
✓ Branch 15 → 16 taken 148 times.
✗ Branch 15 → 25 not taken.
296 if (c.unicode() > 32 && c.unicode() < 127 && c != QLatin1Char('<') && c != QLatin1Char('>') &&
668
4/8
✓ Branch 5 → 6 taken 148 times.
✗ Branch 5 → 25 not taken.
✓ Branch 19 → 20 taken 148 times.
✗ Branch 19 → 25 not taken.
✓ Branch 23 → 24 taken 148 times.
✗ Branch 23 → 25 not taken.
✓ Branch 26 → 27 taken 148 times.
✗ Branch 26 → 29 not taken.
296 c != QLatin1Char('"') && c != QLatin1Char('\\'))
669
1/1
✓ Branch 28 → 29 taken 148 times.
148 out += char(c.unicode());
670
1/1
✓ Branch 31 → 32 taken 7 times.
14 return out.left(200);
671 7 }
672 } // namespace
673
674 // Sprint 108 S108.2.4: the formatted body. Tree (F-17 order kept):
675 // mixed → alternative [text, related(html, images) | html, calendar] → files
676 // where each container appears only when needed.
677 31 void Rfc2822Builder::appendFormattedBody(QByteArray &msg) const {
678
3/3
✓ Branch 3 → 4 taken 27 times.
✓ Branch 3 → 5 taken 4 times.
✓ Branch 5 → 6 taken 4 times.
31 const QByteArray mixed = m_attachments.isEmpty() ? QByteArray() : generateBoundary();
679
1/1
✓ Branch 6 → 7 taken 31 times.
31 const QByteArray alternative = generateBoundary();
680
3/3
✓ Branch 8 → 9 taken 24 times.
✓ Branch 8 → 10 taken 7 times.
✓ Branch 10 → 11 taken 7 times.
31 const QByteArray related = m_inlineImages.isEmpty() ? QByteArray() : generateBoundary();
681
2/2
✓ Branch 12 → 13 taken 4 times.
✓ Branch 12 → 24 taken 27 times.
31 if (!mixed.isEmpty()) {
682
3/3
✓ Branch 13 → 14 taken 4 times.
✓ Branch 14 → 15 taken 4 times.
✓ Branch 15 → 16 taken 4 times.
4 msg += "Content-Type: multipart/mixed;\r\n boundary=\"" + mixed + "\"\r\n\r\n";
683
3/3
✓ Branch 18 → 19 taken 4 times.
✓ Branch 19 → 20 taken 4 times.
✓ Branch 20 → 21 taken 4 times.
4 msg += "--" + mixed + "\r\n";
684 }
685
3/3
✓ Branch 24 → 25 taken 31 times.
✓ Branch 25 → 26 taken 31 times.
✓ Branch 26 → 27 taken 31 times.
31 msg += "Content-Type: multipart/alternative;\r\n boundary=\"" + alternative + "\"\r\n\r\n";
686
3/3
✓ Branch 29 → 30 taken 31 times.
✓ Branch 30 → 31 taken 31 times.
✓ Branch 31 → 32 taken 31 times.
31 msg += "--" + alternative + "\r\n";
687
1/1
✓ Branch 34 → 35 taken 31 times.
31 msg += "Content-Type: text/plain; charset=UTF-8\r\n";
688
1/1
✓ Branch 35 → 36 taken 31 times.
31 msg += "Content-Transfer-Encoding: quoted-printable\r\n\r\n";
689
4/4
✓ Branch 36 → 37 taken 31 times.
✓ Branch 37 → 38 taken 31 times.
✓ Branch 38 → 39 taken 31 times.
✓ Branch 39 → 40 taken 31 times.
31 msg += encodeQuotedPrintable(m_bodyText.toUtf8()) + "\r\n";
690
3/3
✓ Branch 43 → 44 taken 31 times.
✓ Branch 44 → 45 taken 31 times.
✓ Branch 45 → 46 taken 31 times.
31 msg += "--" + alternative + "\r\n";
691
2/2
✓ Branch 49 → 50 taken 7 times.
✓ Branch 49 → 61 taken 24 times.
31 if (!related.isEmpty()) {
692
3/3
✓ Branch 50 → 51 taken 7 times.
✓ Branch 51 → 52 taken 7 times.
✓ Branch 52 → 53 taken 7 times.
7 msg += "Content-Type: multipart/related;\r\n boundary=\"" + related + "\"; type=\"text/html\"\r\n\r\n";
693
3/3
✓ Branch 55 → 56 taken 7 times.
✓ Branch 56 → 57 taken 7 times.
✓ Branch 57 → 58 taken 7 times.
7 msg += "--" + related + "\r\n";
694 }
695
1/1
✓ Branch 61 → 62 taken 31 times.
31 msg += "Content-Type: text/html; charset=UTF-8\r\n";
696
1/1
✓ Branch 62 → 63 taken 31 times.
31 msg += "Content-Transfer-Encoding: quoted-printable\r\n\r\n";
697
4/4
✓ Branch 63 → 64 taken 31 times.
✓ Branch 64 → 65 taken 31 times.
✓ Branch 65 → 66 taken 31 times.
✓ Branch 66 → 67 taken 31 times.
31 msg += encodeQuotedPrintable(m_bodyHtml.toUtf8()) + "\r\n";
698
2/2
✓ Branch 71 → 72 taken 7 times.
✓ Branch 71 → 124 taken 24 times.
31 if (!related.isEmpty()) {
699
2/2
✓ Branch 117 → 74 taken 7 times.
✓ Branch 117 → 118 taken 7 times.
14 for (const InlineImage &image : m_inlineImages) {
700
1/1
✓ Branch 75 → 76 taken 7 times.
7 const QByteArray mime = leafMimeType(image.mimeType);
701
3/3
✓ Branch 76 → 77 taken 7 times.
✓ Branch 77 → 78 taken 7 times.
✓ Branch 78 → 79 taken 7 times.
7 msg += "--" + related + "\r\n";
702
7/7
✓ Branch 81 → 82 taken 7 times.
✓ Branch 82 → 83 taken 7 times.
✓ Branch 83 → 84 taken 7 times.
✓ Branch 84 → 85 taken 7 times.
✓ Branch 85 → 86 taken 7 times.
✓ Branch 86 → 87 taken 7 times.
✓ Branch 87 → 88 taken 7 times.
7 msg += "Content-Type: " + mime + ";\r\n" + encodeMimeFilenameParameters("name", image.filename) + "\r\n";
703
1/1
✓ Branch 94 → 95 taken 7 times.
7 msg += "Content-Transfer-Encoding: base64\r\n";
704
4/4
✓ Branch 95 → 96 taken 7 times.
✓ Branch 96 → 97 taken 7 times.
✓ Branch 97 → 98 taken 7 times.
✓ Branch 98 → 99 taken 7 times.
7 msg += "Content-ID: <" + safeContentId(image.contentId) + ">\r\n";
705
3/3
✓ Branch 102 → 103 taken 7 times.
✓ Branch 103 → 104 taken 7 times.
✓ Branch 104 → 105 taken 7 times.
14 msg += "Content-Disposition: inline;\r\n" + encodeMimeFilenameParameters("filename", image.filename) +
706
2/2
✓ Branch 105 → 106 taken 7 times.
✓ Branch 106 → 107 taken 7 times.
7 "\r\n\r\n";
707
2/2
✓ Branch 111 → 112 taken 7 times.
✓ Branch 112 → 113 taken 7 times.
7 msg += base64Lines(image.data);
708 7 }
709
3/3
✓ Branch 118 → 119 taken 7 times.
✓ Branch 119 → 120 taken 7 times.
✓ Branch 120 → 121 taken 7 times.
7 msg += "--" + related + "--\r\n";
710 }
711
2/2
✓ Branch 125 → 126 taken 2 times.
✓ Branch 125 → 141 taken 29 times.
31 if (!m_calendar.isEmpty()) {
712
3/3
✓ Branch 126 → 127 taken 2 times.
✓ Branch 127 → 128 taken 2 times.
✓ Branch 128 → 129 taken 2 times.
2 msg += "--" + alternative + "\r\n";
713
3/3
✓ Branch 131 → 132 taken 2 times.
✓ Branch 132 → 133 taken 2 times.
✓ Branch 133 → 134 taken 2 times.
2 msg += "Content-Type: text/calendar; method=" + m_calendarMethod + "; charset=UTF-8\r\n";
714
1/1
✓ Branch 136 → 137 taken 2 times.
2 msg += "Content-Transfer-Encoding: base64\r\n\r\n";
715
2/2
✓ Branch 137 → 138 taken 2 times.
✓ Branch 138 → 139 taken 2 times.
2 msg += base64Lines(m_calendar);
716 }
717
3/3
✓ Branch 141 → 142 taken 31 times.
✓ Branch 142 → 143 taken 31 times.
✓ Branch 143 → 144 taken 31 times.
31 msg += "--" + alternative + "--\r\n";
718
2/2
✓ Branch 147 → 148 taken 4 times.
✓ Branch 147 → 155 taken 27 times.
31 if (!mixed.isEmpty()) {
719
1/1
✓ Branch 148 → 149 taken 4 times.
4 appendAttachmentParts(msg, mixed);
720
3/3
✓ Branch 149 → 150 taken 4 times.
✓ Branch 150 → 151 taken 4 times.
✓ Branch 151 → 152 taken 4 times.
4 msg += "--" + mixed + "--\r\n";
721 }
722 31 }
723