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 ¶meter, | |
| 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 |