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: 88.0% 453 / 0 / 515
Functions: 97.2% 35 / 0 / 36
Branches: 79.6% 618 / 0 / 776

service/SmtpService.cpp
Line Branch Exec Source
1 #include "SmtpService.h"
2 #include "util/SecureUtil.h"
3
4 #include <utility>
5 #include <QLoggingCategory>
6 #include <QTimer>
7 #include <QPointer>
8
9 #include "data/AccountConfig.h"
10
11
4/7
✓ Branch 2 → 3 taken 5 times.
✓ Branch 2 → 8 taken 110 times.
✓ Branch 4 → 5 taken 5 times.
✗ Branch 4 → 8 not taken.
✓ Branch 5 → 6 taken 5 times.
✗ Branch 10 → 11 not taken.
✗ Branch 10 → 12 not taken.
115 Q_LOGGING_CATEGORY(lcSmtp, "mailjd.smtp")
12
13
1/1
✓ Branch 3 → 4 taken 881 times.
881 SmtpService::SmtpService(QObject *parent) : QObject(parent) {
14
1/1
✓ Branch 11 → 12 taken 881 times.
881 createSocketForAttempt();
15
16 // T-612/SEC-11: Timeout timer to prevent hanging on slow/malicious servers
17
3/6
✓ Branch 12 → 13 taken 881 times.
✓ Branch 13 → 14 taken 881 times.
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 16 taken 881 times.
✗ Branch 20 → 21 not taken.
✗ Branch 20 → 22 not taken.
881 m_timeoutTimer = new QTimer(this);
18
1/1
✓ Branch 16 → 17 taken 881 times.
881 m_timeoutTimer->setSingleShot(true);
19
1/1
✓ Branch 17 → 18 taken 881 times.
881 connect(m_timeoutTimer, &QTimer::timeout, this, [this]() {
20
6/6
✓ Branch 2 → 3 taken 2 times.
✓ Branch 6 → 7 taken 2 times.
✓ Branch 7 → 8 taken 2 times.
✓ Branch 8 → 9 taken 2 times.
✓ Branch 12 → 4 taken 2 times.
✓ Branch 12 → 13 taken 2 times.
4 qCWarning(lcSmtp) << "SMTP timeout in state" << static_cast<int>(m_state);
21
1/1
✓ Branch 16 → 17 taken 2 times.
4 failSession(QStringLiteral("SMTP timeout"));
22 2 });
23 881 }
24
25 // T-400/Bug 3: RFC 5321 §4.5.2 dot-stuffing; every line ends in CRLF and the
26 // payload ends with the "." terminator line.
27 6 QByteArray SmtpService::dataPayload(const QByteArray &message) {
28
1/1
✓ Branch 2 → 3 taken 6 times.
6 QList<QByteArray> lines = message.split('\n');
29 // S106-032: a message ending in a line break leaves an empty last
30 // element — it is the end of the final line, not one more line (it used
31 // to add a CRLF that the built and Sent-copy bytes do not have).
32
7/7
✓ Branch 4 → 5 taken 5 times.
✓ Branch 4 → 9 taken 1 time.
✓ Branch 5 → 6 taken 5 times.
✓ Branch 7 → 8 taken 4 times.
✓ Branch 7 → 9 taken 1 time.
✓ Branch 10 → 11 taken 4 times.
✓ Branch 10 → 12 taken 2 times.
6 if (lines.size() > 1 && lines.last().isEmpty())
33 4 lines.removeLast();
34 6 QByteArray payload;
35
1/1
✓ Branch 14 → 15 taken 6 times.
6 payload.reserve(message.size() + message.size() / 40 + 8);
36
2/2
✓ Branch 32 → 18 taken 19 times.
✓ Branch 32 → 33 taken 6 times.
25 for (QByteArray line : std::as_const(lines)) {
37 // Remove trailing \r if present (we re-add CRLF)
38
3/3
✓ Branch 20 → 21 taken 19 times.
✓ Branch 21 → 22 taken 11 times.
✓ Branch 21 → 23 taken 8 times.
19 if (line.endsWith('\r'))
39
1/1
✓ Branch 22 → 23 taken 11 times.
11 line.chop(1);
40
3/3
✓ Branch 23 → 24 taken 19 times.
✓ Branch 24 → 25 taken 6 times.
✓ Branch 24 → 26 taken 13 times.
19 if (line.startsWith('.'))
41
1/1
✓ Branch 25 → 26 taken 6 times.
6 payload += '.';
42
2/2
✓ Branch 26 → 27 taken 19 times.
✓ Branch 27 → 28 taken 19 times.
19 payload += line + "\r\n";
43 19 }
44
1/1
✓ Branch 33 → 34 taken 6 times.
6 payload += ".\r\n";
45 6 return payload;
46 6 }
47
48 899 void SmtpService::createSocketForAttempt() {
49
2/2
✓ Branch 2 → 3 taken 18 times.
✓ Branch 2 → 6 taken 881 times.
899 if (m_socket) {
50 18 m_socket->disconnect(this);
51 18 m_socket->abort();
52 18 m_socket->deleteLater();
53 }
54
55
2/5
✓ Branch 7 → 8 taken 899 times.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 899 times.
✗ Branch 26 → 27 not taken.
✗ Branch 26 → 28 not taken.
899 m_socket = new QSslSocket(this);
56 899 m_socket->setReadBufferSize(MaxResponseBufferSize);
57 899 const quint64 generation = m_attemptGeneration;
58
1/1
✓ Branch 11 → 12 taken 899 times.
899 connect(m_socket, &QSslSocket::connected, this, [this, generation]() {
59
2/2
✓ Branch 2 → 3 taken 7 times.
✓ Branch 2 → 4 taken 1 time.
8 if (generation == m_attemptGeneration)
60 7 onConnected();
61 8 });
62
1/1
✓ Branch 13 → 14 taken 899 times.
899 connect(m_socket, &QSslSocket::encrypted, this, [this, generation]() {
63
2/2
✓ Branch 2 → 3 taken 3 times.
✓ Branch 2 → 4 taken 1 time.
4 if (generation == m_attemptGeneration)
64 3 onEncrypted();
65 4 });
66
1/1
✓ Branch 15 → 16 taken 899 times.
899 connect(m_socket, &QSslSocket::readyRead, this, [this, generation]() {
67
2/2
✓ Branch 2 → 3 taken 7 times.
✓ Branch 2 → 4 taken 1 time.
8 if (generation == m_attemptGeneration)
68 7 onReadyRead();
69 8 });
70 899 connect(m_socket, &QSslSocket::errorOccurred, this,
71
1/1
✓ Branch 17 → 18 taken 899 times.
899 [this, generation](QAbstractSocket::SocketError error) {
72
2/2
✓ Branch 2 → 3 taken 2 times.
✓ Branch 2 → 4 taken 1 time.
3 if (generation == m_attemptGeneration)
73 2 onError(error);
74 else
75
5/5
✓ Branch 4 → 5 taken 1 time.
✓ Branch 8 → 9 taken 1 time.
✓ Branch 9 → 10 taken 1 time.
✓ Branch 14 → 6 taken 1 time.
✓ Branch 14 → 15 taken 1 time.
2 qCDebug(lcSmtp) << "Ignoring socket error from SMTP attempt"
76
1/1
✓ Branch 10 → 11 taken 1 time.
1 << generation;
77 3 });
78 // F-01: while the DATA payload is on the wire, every flushed chunk
79 // restarts the upload watchdog — a slow uplink that keeps making
80 // progress must never hit a fixed 60 s deadline. The base interval is
81 // an inactivity timeout; once the buffer drained (the terminating "."
82 // included), the longer final window covers the server's own
83 // processing of the accepted message.
84 899 connect(m_socket, &QSslSocket::bytesWritten, this,
85
1/1
✓ Branch 19 → 20 taken 899 times.
899 [this, generation](qint64) {
86
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 7 times.
7 if (generation != m_attemptGeneration) return;
87
3/4
✓ Branch 4 → 5 taken 2 times.
✓ Branch 4 → 6 taken 5 times.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 2 times.
7 if (m_state != State::WaitDataContent || m_operationFinished) return;
88
1/2
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 2 times.
2 m_timeoutTimer->start(m_socket->bytesToWrite() > 0
89 ? UploadInactivityTimeoutMs
90 : UploadFinalAcceptTimeoutMs);
91 });
92 899 connect(m_socket, &QAbstractSocket::disconnected, this,
93
1/1
✓ Branch 21 → 22 taken 899 times.
899 [this, generation]() {
94
2/2
✓ Branch 2 → 3 taken 6 times.
✓ Branch 2 → 4 taken 1 time.
7 if (generation == m_attemptGeneration)
95 6 clearCredentials();
96 7 });
97
98 // T-506: Handle SSL certificate errors (were previously silently ignored)
99 899 connect(m_socket, &QSslSocket::sslErrors, this,
100
1/1
✓ Branch 23 → 24 taken 899 times.
899 [this, generation](const QList<QSslError> &errors) {
101
2/2
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 15 taken 1 time.
2 if (generation != m_attemptGeneration) {
102
6/6
✓ Branch 3 → 4 taken 1 time.
✓ Branch 7 → 8 taken 1 time.
✓ Branch 8 → 9 taken 1 time.
✓ Branch 9 → 10 taken 1 time.
✓ Branch 13 → 5 taken 1 time.
✓ Branch 13 → 14 taken 1 time.
2 qCDebug(lcSmtp) << "Ignoring TLS errors from SMTP attempt" << generation;
103 1 return;
104 }
105
2/2
✓ Branch 33 → 17 taken 1 time.
✓ Branch 33 → 34 taken 1 time.
2 for (const auto &err : errors)
106
7/7
✓ Branch 18 → 19 taken 1 time.
✓ Branch 22 → 23 taken 1 time.
✓ Branch 23 → 24 taken 1 time.
✓ Branch 24 → 25 taken 1 time.
✓ Branch 25 → 26 taken 1 time.
✓ Branch 30 → 20 taken 1 time.
✓ Branch 30 → 31 taken 1 time.
2 qCWarning(lcSmtp) << "SSL error:" << err.errorString();
107 #ifdef MAILJD_E2E_TESTING
108 ✗ qCWarning(lcSmtp) << "Ignoring SSL errors (E2E test build)";
109 ✗ m_socket->ignoreSslErrors();
110 #else
111 // ignoreSslErrors() is intentionally NOT called — TLS stays fail-closed.
112
1/1
✓ Branch 37 → 38 taken 1 time.
2 failSession(QStringLiteral("SSL certificate error"));
113 #endif
114 });
115 899 }
116
117 1749 SmtpService::~SmtpService() {
118
3/4
✓ Branch 2 → 3 taken 15 times.
✓ Branch 2 → 11 taken 866 times.
✓ Branch 3 → 4 taken 15 times.
✗ Branch 3 → 11 not taken.
881 if (m_attemptActive && !m_operationFinished)
119 15 failSession(QStringLiteral("SMTP send cancelled"));
120 881 clearCredentials();
121 1749 }
122
123 #ifdef MAILJD_UNIT_TEST
124 2 bool SmtpService::ehloResponseSupportsAuthLoginForTest(const QString &response) {
125
1/1
✓ Branch 2 → 3 taken 2 times.
6 return parseEhloCapabilities(response).authMechanisms.contains(
126 6 QStringLiteral("LOGIN"));
127 }
128
129 2 bool SmtpService::ehloResponseSupportsStartTlsForTest(const QString &response) {
130
1/1
✓ Branch 2 → 3 taken 2 times.
2 return parseEhloCapabilities(response).supportsStartTls;
131 }
132 #endif
133
134 77 void SmtpService::resetSessionState() {
135
1/2
✓ Branch 2 → 3 taken 77 times.
✗ Branch 2 → 4 not taken.
77 if (m_timeoutTimer)
136 77 m_timeoutTimer->stop();
137 77 m_state = State::Disconnected;
138 77 m_from.clear();
139 77 m_recipients.clear();
140 77 m_message.clear();
141 77 m_rcptIndex = 0;
142 77 m_responseBuffer.clear();
143 77 m_ehloCapabilities = {};
144 77 m_plainChallengeAnswered = false;
145 77 clearCredentials();
146 77 }
147
148 972 void SmtpService::clearCredentials() {
149 972 SecureUtil::zeroMemory(m_config.password);
150 972 m_config.username.clear();
151 972 }
152
153 59 void SmtpService::failSession(const QString &error) {
154
3/4
✓ Branch 2 → 3 taken 55 times.
✓ Branch 2 → 4 taken 4 times.
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 16 taken 55 times.
59 if (!m_attemptActive || m_operationFinished) {
155
6/6
✓ Branch 4 → 5 taken 4 times.
✓ Branch 8 → 9 taken 4 times.
✓ Branch 9 → 10 taken 4 times.
✓ Branch 10 → 11 taken 4 times.
✓ Branch 14 → 6 taken 4 times.
✓ Branch 14 → 15 taken 4 times.
8 qCDebug(lcSmtp) << "Ignoring secondary SMTP failure:" << error;
156 4 return;
157 }
158
159 // abort() can synchronously or asynchronously trigger socket errors. Set
160 // the terminal guard first so every race still emits exactly one failure.
161 55 m_operationFinished = true;
162 55 m_attemptActive = false;
163
6/6
✓ Branch 16 → 17 taken 55 times.
✓ Branch 20 → 21 taken 55 times.
✓ Branch 21 → 22 taken 55 times.
✓ Branch 22 → 23 taken 55 times.
✓ Branch 26 → 18 taken 55 times.
✓ Branch 26 → 27 taken 55 times.
110 qCWarning(lcSmtp) << "SMTP send failed:" << error;
164 55 m_socket->abort();
165 55 resetSessionState();
166 55 emit sendFailed(error);
167 }
168
169 4 void SmtpService::completeSession() {
170
2/4
✓ Branch 2 → 3 taken 4 times.
✗ Branch 2 → 4 not taken.
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 4 times.
4 if (!m_attemptActive || m_operationFinished)
171 ✗ return;
172
173 4 m_operationFinished = true;
174 4 m_attemptActive = false;
175 // The DATA acceptance is terminal. QUIT is only best-effort cleanup;
176 // socket errors during or after it cannot turn acceptance into failure.
177
2/2
✓ Branch 5 → 6 taken 3 times.
✓ Branch 5 → 13 taken 1 time.
4 if (m_state == State::WaitDataContent)
178
1/1
✓ Branch 9 → 10 taken 3 times.
3 sendCommand(QStringLiteral("QUIT"));
179
1/1
✓ Branch 13 → 14 taken 4 times.
4 m_socket->close();
180
1/1
✓ Branch 14 → 15 taken 4 times.
4 resetSessionState();
181 4 const auto generation = m_attemptGeneration;
182
1/1
✓ Branch 15 → 16 taken 4 times.
4 QPointer<SmtpService> guard(this);
183
1/1
✓ Branch 16 → 17 taken 4 times.
4 emit sendSuccess();
184 // A subscriber may start another attempt or destroy the transport.
185
5/6
✓ Branch 18 → 19 taken 3 times.
✓ Branch 18 → 22 taken 1 time.
✓ Branch 20 → 21 taken 3 times.
✗ Branch 20 → 22 not taken.
✓ Branch 23 → 24 taken 3 times.
✓ Branch 23 → 29 taken 1 time.
4 if (guard && guard->m_attemptGeneration == generation)
186
2/2
✓ Branch 25 → 26 taken 3 times.
✓ Branch 26 → 27 taken 3 times.
3 emit guard->statusMessage(tr("Mail sent successfully!"));
187 4 }
188
189 18 void SmtpService::sendMail(const SmtpConfig &config, const QString &from,
190 const QStringList &recipients,
191 const QByteArray &message) {
192 // Bug 35: Reset socket state from any previous send
193 53 if (m_state != State::Disconnected ||
194
5/6
✓ Branch 2 → 3 taken 17 times.
✓ Branch 2 → 6 taken 1 time.
✓ Branch 4 → 5 taken 17 times.
✗ Branch 4 → 6 not taken.
✓ Branch 8 → 9 taken 1 time.
✓ Branch 8 → 16 taken 17 times.
35 m_socket->state() != QAbstractSocket::UnconnectedState ||
195
1/2
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 17 times.
17 m_attemptActive) {
196
1/1
✓ Branch 12 → 13 taken 1 time.
1 failSession(QStringLiteral("SMTP send replaced by a new request"));
197 }
198 18 ++m_attemptGeneration;
199 18 m_refusedRecipients.clear();
200 18 m_refusalReply.clear();
201 18 m_lastReplyCode = 0;
202 18 createSocketForAttempt();
203 18 resetSessionState();
204 18 m_operationFinished = false;
205 18 m_attemptActive = true;
206
207 18 m_config = config;
208 18 m_from = from;
209 18 m_recipients = recipients;
210 18 m_message = message;
211
212
6/6
✓ Branch 25 → 26 taken 16 times.
✓ Branch 25 → 28 taken 2 times.
✓ Branch 27 → 28 taken 8 times.
✓ Branch 27 → 29 taken 8 times.
✓ Branch 30 → 31 taken 10 times.
✓ Branch 30 → 38 taken 8 times.
18 if (m_config.username.isEmpty() || m_config.password.isEmpty()) {
213
1/1
✓ Branch 34 → 35 taken 10 times.
10 failSession(QStringLiteral("SMTP username and password are required"));
214 10 return;
215 }
216
5/5
✓ Branch 39 → 40 taken 8 times.
✓ Branch 40 → 41 taken 7 times.
✓ Branch 40 → 43 taken 1 time.
✓ Branch 45 → 46 taken 2 times.
✓ Branch 45 → 53 taken 6 times.
15 if (m_config.username.contains(QChar::Null) ||
217
3/3
✓ Branch 41 → 42 taken 7 times.
✓ Branch 42 → 43 taken 1 time.
✓ Branch 42 → 44 taken 6 times.
7 m_config.password.contains('\0')) {
218
1/1
✓ Branch 49 → 50 taken 2 times.
2 failSession(QStringLiteral("SMTP credentials contain an invalid NUL byte"));
219 2 return;
220 }
221
222
2/2
✓ Branch 54 → 55 taken 2 times.
✓ Branch 54 → 62 taken 4 times.
6 if (!isValidSmtpAddress(m_from)) {
223
1/1
✓ Branch 58 → 59 taken 2 times.
2 failSession(QStringLiteral("Invalid SMTP envelope sender"));
224 2 return;
225 }
226
1/2
✗ Branch 63 → 64 not taken.
✓ Branch 63 → 71 taken 4 times.
4 if (m_recipients.isEmpty()) {
227 ✗ failSession(QStringLiteral("SMTP envelope has no recipients"));
228 ✗ return;
229 }
230
2/2
✓ Branch 86 → 74 taken 6 times.
✓ Branch 86 → 87 taken 4 times.
10 for (const QString &recipient : std::as_const(m_recipients)) {
231
2/3
✓ Branch 75 → 76 taken 6 times.
✗ Branch 76 → 77 not taken.
✓ Branch 76 → 84 taken 6 times.
6 if (!isValidSmtpAddress(recipient)) {
232 ✗ failSession(QStringLiteral("Invalid SMTP envelope recipient"));
233 ✗ return;
234 }
235 }
236
237
2/2
✓ Branch 87 → 88 taken 4 times.
✓ Branch 88 → 89 taken 4 times.
4 emit statusMessage(tr("Connecting to SMTP server..."));
238
239 // T-612/SEC-11: Start connection timeout (30 seconds)
240 4 m_timeoutTimer->start(30 * 1000);
241
242 #ifdef MAILJD_E2E_TESTING
243 ✗ reportE2eTransportPhase(QStringLiteral("tcp-connect"));
244 #endif
245
246
1/4
✗ Branch 93 → 94 not taken.
✓ Branch 93 → 97 taken 4 times.
✗ Branch 99 → 100 not taken.
✗ Branch 99 → 103 not taken.
4 if (config.security == QLatin1String("ssl")) {
247 ✗ m_state = State::Connecting;
248 ✗ m_socket->connectToHostEncrypted(config.host, config.port);
249 } else {
250 4 m_state = State::WaitGreeting;
251
1/2
✓ Branch 98 → 99 taken 4 times.
✗ Branch 104 → 105 not taken.
4 m_socket->connectToHost(config.host, config.port);
252 }
253 }
254
255 7 void SmtpService::onConnected() {
256
8/8
✓ Branch 2 → 3 taken 7 times.
✓ Branch 6 → 7 taken 7 times.
✓ Branch 7 → 8 taken 7 times.
✓ Branch 8 → 9 taken 7 times.
✓ Branch 9 → 10 taken 7 times.
✓ Branch 10 → 11 taken 7 times.
✓ Branch 14 → 4 taken 7 times.
✓ Branch 14 → 15 taken 7 times.
14 qCInfo(lcSmtp) << "Connected to" << m_config.host << ":" << m_config.port;
257 // For plain/STARTTLS, wait for the server greeting
258 7 }
259
260 5 void SmtpService::onEncrypted() {
261
5/5
✓ Branch 2 → 3 taken 5 times.
✓ Branch 6 → 7 taken 5 times.
✓ Branch 7 → 8 taken 5 times.
✓ Branch 11 → 4 taken 5 times.
✓ Branch 11 → 12 taken 5 times.
10 qCInfo(lcSmtp) << "TLS encrypted";
262
2/2
✓ Branch 12 → 13 taken 2 times.
✓ Branch 12 → 14 taken 3 times.
5 if (m_state == State::Connecting) {
263 // Direct SSL: wait for greeting after encryption
264 2 m_state = State::WaitGreeting;
265
2/2
✓ Branch 14 → 15 taken 2 times.
✓ Branch 14 → 29 taken 1 time.
3 } else if (m_state == State::WaitStartTls) {
266 // STARTTLS upgrade done, send EHLO again
267 2 m_state = State::WaitEhloAfterTls;
268 2 m_ehloCapabilities = {};
269
3/3
✓ Branch 18 → 19 taken 2 times.
✓ Branch 22 → 23 taken 2 times.
✓ Branch 23 → 24 taken 2 times.
4 sendCommand(QStringLiteral("EHLO ") + QLatin1String(EhloClientName));
270 }
271 5 }
272
273 7 void SmtpService::onReadyRead() {
274
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 7 times.
7 if (m_operationFinished)
275 ✗ return;
276
2/4
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 9 taken 7 times.
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 13 taken 7 times.
7 if (!m_socket->canReadLine() &&
277 ✗ m_socket->bytesAvailable() >= MaxResponseBufferSize) {
278 ✗ failResponseTooLarge();
279 ✗ return;
280 }
281
282
2/2
✓ Branch 80 → 14 taken 7 times.
✓ Branch 80 → 81 taken 3 times.
10 while (m_socket->canReadLine()) {
283
3/3
✓ Branch 14 → 15 taken 7 times.
✓ Branch 15 → 16 taken 7 times.
✓ Branch 16 → 17 taken 7 times.
7 QString line = QString::fromUtf8(m_socket->readLine()).trimmed();
284
6/6
✓ Branch 19 → 20 taken 7 times.
✓ Branch 23 → 24 taken 7 times.
✓ Branch 24 → 25 taken 7 times.
✓ Branch 25 → 26 taken 7 times.
✓ Branch 29 → 21 taken 7 times.
✓ Branch 29 → 30 taken 7 times.
14 qCDebug(lcSmtp) << "S:" << line;
285
286 // Multi-line responses: 250-... continues, 250 ... is final
287
4/7
✓ Branch 31 → 32 taken 7 times.
✗ Branch 31 → 37 not taken.
✓ Branch 33 → 34 taken 7 times.
✗ Branch 35 → 36 not taken.
✓ Branch 35 → 37 taken 7 times.
✗ Branch 38 → 39 not taken.
✓ Branch 38 → 47 taken 7 times.
7 if (line.length() >= 4 && line[3] == '-') {
288 ✗ if (!appendResponseText(line + QLatin1Char('\n')))
289 ✗ return;
290 ✗ continue;
291 }
292
2/3
✓ Branch 47 → 48 taken 7 times.
✗ Branch 48 → 49 not taken.
✓ Branch 48 → 50 taken 7 times.
7 if (!appendResponseText(line))
293 ✗ return;
294 7 const QString response = m_responseBuffer;
295 7 m_responseBuffer.clear();
296
1/1
✓ Branch 52 → 53 taken 7 times.
7 QPointer<SmtpService> guard(this);
297 7 const auto generation = m_attemptGeneration;
298
1/1
✓ Branch 53 → 54 taken 7 times.
7 processResponse(response);
299
7/8
✓ Branch 55 → 56 taken 6 times.
✓ Branch 55 → 60 taken 1 time.
✓ Branch 57 → 58 taken 3 times.
✓ Branch 57 → 60 taken 3 times.
✗ Branch 59 → 60 not taken.
✓ Branch 59 → 61 taken 3 times.
✓ Branch 62 → 63 taken 4 times.
✓ Branch 62 → 64 taken 3 times.
7 if (!guard || guard->m_operationFinished || generation != guard->m_attemptGeneration)
300 4 return;
301 15 }
302 }
303
304 8 bool SmtpService::appendResponseText(const QString &text) {
305
2/2
✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 7 taken 7 times.
8 if (m_responseBuffer.size() + text.size() > MaxResponseBufferSize) {
306 1 failResponseTooLarge();
307 1 return false;
308 }
309 7 m_responseBuffer += text;
310 7 return true;
311 }
312
313 1 void SmtpService::failResponseTooLarge() {
314
5/5
✓ Branch 2 → 3 taken 1 time.
✓ Branch 6 → 7 taken 1 time.
✓ Branch 7 → 8 taken 1 time.
✓ Branch 11 → 4 taken 1 time.
✓ Branch 11 → 12 taken 1 time.
2 qCWarning(lcSmtp) << "SMTP response exceeded buffer limit";
315 1 m_responseBuffer.clear();
316
1/1
✓ Branch 16 → 17 taken 1 time.
1 failSession(QStringLiteral("SMTP response too large"));
317 1 }
318
319 3 void SmtpService::onError(QAbstractSocket::SocketError error) {
320 Q_UNUSED(error)
321
1/1
✓ Branch 2 → 3 taken 3 times.
3 m_timeoutTimer->stop(); // T-612: Cancel timeout on socket error
322
1/1
✓ Branch 3 → 4 taken 3 times.
3 QString errMsg = m_socket->errorString();
323
6/6
✓ Branch 4 → 5 taken 3 times.
✓ Branch 8 → 9 taken 3 times.
✓ Branch 9 → 10 taken 3 times.
✓ Branch 10 → 11 taken 3 times.
✓ Branch 14 → 6 taken 3 times.
✓ Branch 14 → 15 taken 3 times.
6 qCWarning(lcSmtp) << "Socket error:" << errMsg;
324
1/1
✓ Branch 15 → 16 taken 3 times.
3 failSession(errMsg);
325 3 }
326
327 44 void SmtpService::processResponse(const QString &line) {
328
2/2
✓ Branch 2 → 3 taken 2 times.
✓ Branch 2 → 4 taken 42 times.
44 if (m_operationFinished)
329 6 return;
330
331 // completeSession()/failSession() emit to subscribers that may destroy the
332 // transport or start the next attempt before the switch below returns.
333
1/1
✓ Branch 4 → 5 taken 42 times.
42 QPointer<SmtpService> guard(this);
334 42 const auto generation = m_attemptGeneration;
335
2/2
✓ Branch 5 → 6 taken 42 times.
✓ Branch 6 → 7 taken 42 times.
42 int code = line.left(3).toInt();
336 42 m_lastReplyCode = code;
337
338
14/17
✓ Branch 8 → 9 taken 5 times.
✓ Branch 8 → 32 taken 5 times.
✓ Branch 8 → 66 taken 2 times.
✓ Branch 8 → 102 taken 1 time.
✓ Branch 8 → 137 taken 1 time.
✓ Branch 8 → 168 taken 3 times.
✓ Branch 8 → 198 taken 2 times.
✓ Branch 8 → 211 taken 4 times.
✓ Branch 8 → 230 taken 3 times.
✓ Branch 8 → 242 taken 6 times.
✓ Branch 8 → 293 taken 4 times.
✓ Branch 8 → 308 taken 4 times.
✗ Branch 8 → 314 not taken.
✓ Branch 8 → 320 taken 1 time.
✓ Branch 8 → 322 taken 1 time.
✗ Branch 8 → 326 not taken.
✗ Branch 8 → 328 not taken.
42 switch (m_state) {
339 5 case State::WaitGreeting:
340
2/2
✓ Branch 9 → 10 taken 4 times.
✓ Branch 9 → 22 taken 1 time.
5 if (code == 220) {
341 4 m_state = State::WaitEhlo;
342
3/3
✓ Branch 11 → 12 taken 4 times.
✓ Branch 15 → 16 taken 4 times.
✓ Branch 16 → 17 taken 4 times.
8 sendCommand(QStringLiteral("EHLO ") + QLatin1String(EhloClientName));
343 } else {
344
2/2
✓ Branch 25 → 26 taken 1 time.
✓ Branch 26 → 27 taken 1 time.
1 failSession(QStringLiteral("Unexpected greeting: ") + line);
345 }
346 5 break;
347
348 5 case State::WaitEhlo:
349
2/2
✓ Branch 32 → 33 taken 2 times.
✓ Branch 32 → 56 taken 3 times.
5 if (code == 250) {
350
1/1
✓ Branch 33 → 34 taken 2 times.
2 m_ehloCapabilities = parseEhloCapabilities(line);
351
2/2
✓ Branch 38 → 39 taken 1 time.
✓ Branch 38 → 54 taken 1 time.
2 if (m_config.security == QLatin1String("starttls")) {
352
1/2
✓ Branch 39 → 40 taken 1 time.
✗ Branch 39 → 47 not taken.
1 if (!m_ehloCapabilities.supportsStartTls) {
353
1/1
✓ Branch 43 → 44 taken 1 time.
1 failSession(QStringLiteral("Server does not support STARTTLS"));
354 1 return;
355 }
356 ✗ m_state = State::WaitStartTls;
357 ✗ sendCommand(QStringLiteral("STARTTLS"));
358 } else {
359
1/1
✓ Branch 54 → 55 taken 1 time.
1 emit sizeLimitAnnounced(m_config.host, m_ehloCapabilities.maximumMessageBytes);
360
1/1
✓ Branch 55 → 65 taken 1 time.
1 beginAuthentication();
361 }
362 } else {
363
2/2
✓ Branch 59 → 60 taken 3 times.
✓ Branch 60 → 61 taken 3 times.
3 failSession(QStringLiteral("EHLO failed: ") + line);
364 }
365 4 break;
366
367 2 case State::WaitStartTls:
368
2/2
✓ Branch 66 → 67 taken 1 time.
✓ Branch 66 → 92 taken 1 time.
2 if (code == 220) {
369
2/7
✗ Branch 68 → 69 not taken.
✓ Branch 68 → 71 taken 1 time.
✗ Branch 69 → 70 not taken.
✗ Branch 70 → 71 not taken.
✗ Branch 70 → 72 not taken.
✓ Branch 73 → 74 taken 1 time.
✗ Branch 73 → 91 not taken.
1 if (!m_responseBuffer.isEmpty() || m_socket->bytesAvailable() > 0) {
370
5/5
✓ Branch 74 → 75 taken 1 time.
✓ Branch 78 → 79 taken 1 time.
✓ Branch 79 → 80 taken 1 time.
✓ Branch 83 → 76 taken 1 time.
✓ Branch 83 → 84 taken 1 time.
2 qCWarning(lcSmtp) << "Unexpected data before TLS handshake";
371
1/1
✓ Branch 87 → 88 taken 1 time.
1 failSession(QStringLiteral("Unexpected data before TLS handshake"));
372 1 return;
373 }
374 // Server accepted STARTTLS, start TLS handshake
375 ✗ m_socket->startClientEncryption();
376 // onEncrypted() will fire
377 } else {
378
2/2
✓ Branch 95 → 96 taken 1 time.
✓ Branch 96 → 97 taken 1 time.
1 failSession(QStringLiteral("STARTTLS failed: ") + line);
379 }
380 1 break;
381
382 1 case State::WaitEhloAfterTls:
383
1/2
✓ Branch 102 → 103 taken 1 time.
✗ Branch 102 → 127 not taken.
1 if (code == 250) {
384
1/1
✓ Branch 103 → 104 taken 1 time.
1 m_ehloCapabilities = parseEhloCapabilities(line);
385 // T-506: Verify encryption after STARTTLS before AUTH
386
2/3
✓ Branch 106 → 107 taken 1 time.
✓ Branch 107 → 108 taken 1 time.
✗ Branch 107 → 125 not taken.
1 if (!m_socket->isEncrypted()) {
387
5/5
✓ Branch 108 → 109 taken 1 time.
✓ Branch 112 → 113 taken 1 time.
✓ Branch 113 → 114 taken 1 time.
✓ Branch 117 → 110 taken 1 time.
✓ Branch 117 → 118 taken 1 time.
2 qCWarning(lcSmtp) << "STARTTLS succeeded but connection not encrypted";
388
1/1
✓ Branch 121 → 122 taken 1 time.
1 failSession(QStringLiteral("TLS negotiation failed"));
389 1 return;
390 }
391 ✗ emit sizeLimitAnnounced(m_config.host, m_ehloCapabilities.maximumMessageBytes);
392 ✗ beginAuthentication();
393 } else {
394 ✗ failSession(QStringLiteral("EHLO after TLS failed: ") + line);
395 }
396 ✗ break;
397
398 1 case State::WaitAuth:
399
1/2
✓ Branch 137 → 138 taken 1 time.
✗ Branch 137 → 158 not taken.
1 if (code == 334) {
400 // Server requests username (Base64 encoded "Username:")
401 1 m_state = State::WaitAuthUser;
402
5/5
✓ Branch 138 → 139 taken 1 time.
✓ Branch 142 → 143 taken 1 time.
✓ Branch 143 → 144 taken 1 time.
✓ Branch 147 → 140 taken 1 time.
✓ Branch 147 → 148 taken 1 time.
2 qCDebug(lcSmtp) << "C: <base64-username>";
403
3/3
✓ Branch 148 → 149 taken 1 time.
✓ Branch 150 → 151 taken 1 time.
✓ Branch 151 → 152 taken 1 time.
2 QByteArray encodedUsername = m_config.username.toUtf8().toBase64() + "\r\n";
404
1/1
✓ Branch 154 → 155 taken 1 time.
1 m_socket->write(encodedUsername);
405
1/1
✓ Branch 155 → 156 taken 1 time.
1 SecureUtil::zeroMemory(encodedUsername);
406 1 } else {
407 ✗ failSession(QStringLiteral("AUTH LOGIN failed: ") + line);
408 }
409 1 break;
410
411 3 case State::WaitAuthUser:
412
2/2
✓ Branch 168 → 169 taken 2 times.
✓ Branch 168 → 188 taken 1 time.
3 if (code == 334) {
413 // Server requests password (Base64 encoded "Password:")
414 2 m_state = State::WaitAuthPass;
415
5/5
✓ Branch 169 → 170 taken 2 times.
✓ Branch 173 → 174 taken 2 times.
✓ Branch 174 → 175 taken 2 times.
✓ Branch 178 → 171 taken 2 times.
✓ Branch 178 → 179 taken 2 times.
4 qCDebug(lcSmtp) << "C: <base64-password>";
416
2/2
✓ Branch 180 → 181 taken 2 times.
✓ Branch 181 → 182 taken 2 times.
2 QByteArray encodedPassword = m_config.password.toBase64() + "\r\n";
417
1/1
✓ Branch 183 → 184 taken 2 times.
2 m_socket->write(encodedPassword);
418
1/1
✓ Branch 184 → 185 taken 2 times.
2 SecureUtil::zeroMemory(encodedPassword);
419
1/1
✓ Branch 185 → 186 taken 2 times.
2 clearCredentials();
420 2 } else {
421
2/2
✓ Branch 191 → 192 taken 1 time.
✓ Branch 192 → 193 taken 1 time.
1 failSession(QStringLiteral("AUTH username rejected: ") + line);
422 }
423 3 break;
424
425 2 case State::WaitAuthPass:
426
1/1
✓ Branch 198 → 199 taken 2 times.
2 clearCredentials();
427
2/2
✓ Branch 199 → 200 taken 1 time.
✓ Branch 199 → 201 taken 1 time.
2 if (code == 235) {
428
1/1
✓ Branch 200 → 210 taken 1 time.
1 authSucceeded();
429 } else {
430
2/2
✓ Branch 204 → 205 taken 1 time.
✓ Branch 205 → 206 taken 1 time.
1 failSession(QStringLiteral("Authentication failed: ") + line);
431 }
432 2 break;
433
434 4 case State::WaitAuthPlain:
435
2/2
✓ Branch 211 → 212 taken 1 time.
✓ Branch 211 → 214 taken 3 times.
4 if (code == 235) {
436
1/1
✓ Branch 212 → 213 taken 1 time.
1 clearCredentials();
437
1/1
✓ Branch 213 → 229 taken 1 time.
1 authSucceeded();
438
4/4
✓ Branch 214 → 215 taken 2 times.
✓ Branch 214 → 219 taken 1 time.
✓ Branch 215 → 216 taken 1 time.
✓ Branch 215 → 219 taken 1 time.
3 } else if (code == 334 && !m_plainChallengeAnswered) {
439 1 m_plainChallengeAnswered = true;
440
1/1
✓ Branch 216 → 217 taken 1 time.
1 sendAuthPlainPayload(false);
441
1/1
✓ Branch 217 → 218 taken 1 time.
1 clearCredentials();
442 } else {
443
1/1
✓ Branch 219 → 220 taken 2 times.
2 clearCredentials();
444
2/2
✓ Branch 223 → 224 taken 2 times.
✓ Branch 224 → 225 taken 2 times.
2 failSession(QStringLiteral("AUTH PLAIN failed: ") + line);
445 }
446 4 break;
447
448 3 case State::WaitMailFrom:
449
2/2
✓ Branch 230 → 231 taken 2 times.
✓ Branch 230 → 232 taken 1 time.
3 if (code == 250) {
450 2 m_rcptIndex = 0;
451
1/1
✓ Branch 231 → 241 taken 2 times.
2 nextRcptTo();
452 } else {
453
2/2
✓ Branch 235 → 236 taken 1 time.
✓ Branch 236 → 237 taken 1 time.
1 failSession(QStringLiteral("MAIL FROM rejected: ") + line);
454 }
455 3 break;
456
457 6 case State::WaitRcptTo:
458 // F-21 (SPRINT-104): 251 ("user not local; will forward") is a
459 // positive answer — RFC 5321 §3.4 — not a rejected recipient.
460
4/4
✓ Branch 242 → 243 taken 3 times.
✓ Branch 242 → 244 taken 3 times.
✓ Branch 243 → 244 taken 1 time.
✓ Branch 243 → 246 taken 2 times.
6 if (code == 250 || code == 251) {
461 4 m_rcptIndex++;
462
1/1
✓ Branch 244 → 245 taken 4 times.
4 nextRcptTo();
463 } else {
464 // F-21: name the recipient that failed (a 551's forwarding hint is
465 // part of the server's line) instead of a bare protocol error.
466
2/2
✓ Branch 248 → 249 taken 1 time.
✓ Branch 248 → 251 taken 1 time.
2 const QString recipient = m_rcptIndex >= 0 && m_rcptIndex < m_recipients.size()
467
1/2
✓ Branch 246 → 247 taken 2 times.
✗ Branch 246 → 251 not taken.
3 ? m_recipients.at(m_rcptIndex)
468 2 : QString();
469 // D-20: a permanent refusal of one recipient need not stop the others.
470
1/6
✗ Branch 252 → 253 not taken.
✓ Branch 252 → 278 taken 2 times.
✗ Branch 253 → 254 not taken.
✗ Branch 253 → 278 not taken.
✗ Branch 254 → 255 not taken.
✗ Branch 254 → 278 not taken.
2 if (m_partialRecipients && code >= 500 && code < 600) {
471 ✗ m_refusedRecipients.append(recipient);
472 ✗ if (m_refusalReply.isEmpty()) m_refusalReply = line;
473 ✗ m_rcptIndex++;
474 ✗ if (m_rcptIndex >= m_recipients.size() && m_refusedRecipients.size() == m_recipients.size()) {
475 ✗ failSession(QStringLiteral("Recipient %1 rejected: %2").arg(recipient, line));
476 ✗ break;
477 }
478 ✗ nextRcptTo();
479 ✗ break;
480 }
481
2/2
✓ Branch 281 → 282 taken 2 times.
✓ Branch 282 → 283 taken 2 times.
2 failSession(QStringLiteral("Recipient %1 rejected: %2").arg(recipient, line));
482 2 }
483 6 break;
484
485 4 case State::WaitData:
486
2/3
✓ Branch 293 → 294 taken 3 times.
✓ Branch 293 → 298 taken 1 time.
✗ Branch 293 → 304 not taken.
4 if (code == 354) {
487 // T-400/Bug 3: RFC 5321 §4.5.2 dot-stuffing
488 // Lines starting with '.' must be prefixed with an extra '.'
489 #ifdef MAILJD_E2E_TESTING
490 ✗ reportE2eTransportPhase(QStringLiteral("data-body-write"));
491 #endif
492
2/4
✓ Branch 294 → 295 taken 3 times.
✓ Branch 295 → 296 taken 3 times.
✗ Branch 300 → 301 not taken.
✗ Branch 301 → 302 not taken.
3 m_socket->write(dataPayload(m_message));
493 3 m_state = State::WaitDataContent;
494 } else {
495
2/4
✓ Branch 301 → 302 taken 1 time.
✓ Branch 302 → 303 taken 1 time.
✗ Branch 307 → 308 not taken.
✗ Branch 308 → 309 not taken.
1 failSession(QStringLiteral("DATA rejected: ") + line);
496 }
497 4 break;
498
499 4 case State::WaitDataContent:
500
2/4
✓ Branch 308 → 309 taken 3 times.
✓ Branch 308 → 310 taken 1 time.
✗ Branch 314 → 315 not taken.
✗ Branch 314 → 316 not taken.
4 if (code == 250) {
501
1/2
✓ Branch 309 → 319 taken 3 times.
✗ Branch 315 → 325 not taken.
3 completeSession();
502 } else {
503
2/4
✓ Branch 313 → 314 taken 1 time.
✓ Branch 314 → 315 taken 1 time.
✗ Branch 319 → 320 not taken.
✗ Branch 320 → 321 not taken.
1 failSession(QStringLiteral("Message rejected: ") + line);
504 }
505 4 break;
506
507 1 case State::WaitQuit:
508 // 221 or whatever, we're done
509
1/2
✓ Branch 320 → 321 taken 1 time.
✗ Branch 326 → 327 not taken.
1 completeSession();
510 1 break;
511
512 1 default:
513 1 break;
514 }
515
516
5/12
✓ Branch 324 → 325 taken 38 times.
✓ Branch 324 → 326 taken 1 time.
✗ Branch 325 → 326 not taken.
✓ Branch 325 → 327 taken 38 times.
✓ Branch 328 → 329 taken 1 time.
✓ Branch 328 → 330 taken 38 times.
✗ Branch 330 → 331 not taken.
✗ Branch 330 → 332 not taken.
✗ Branch 331 → 332 not taken.
✗ Branch 331 → 333 not taken.
✗ Branch 334 → 335 not taken.
✗ Branch 334 → 336 not taken.
39 if (!guard || generation != m_attemptGeneration)
517 1 return;
518
519 // T-612/SEC-11: restart the watchdog per server response, sized for the
520 // state the response handler entered — F-01: the 354 handler switches to
521 // WaitDataContent and queues the payload, whose watchdog is then kept
522 // alive by bytesWritten; the interval here only covers the gap until the
523 // first chunk is flushed.
524
3/8
✓ Branch 330 → 331 taken 20 times.
✓ Branch 330 → 336 taken 18 times.
✓ Branch 331 → 332 taken 20 times.
✗ Branch 331 → 336 not taken.
✗ Branch 336 → 337 not taken.
✗ Branch 336 → 342 not taken.
✗ Branch 337 → 338 not taken.
✗ Branch 337 → 342 not taken.
38 if (!m_operationFinished && m_attemptActive) {
525
3/6
✓ Branch 332 → 333 taken 3 times.
✓ Branch 332 → 334 taken 17 times.
✓ Branch 335 → 336 taken 20 times.
✗ Branch 338 → 339 not taken.
✗ Branch 338 → 340 not taken.
✗ Branch 341 → 342 not taken.
20 m_timeoutTimer->start(m_state == State::WaitDataContent
526 ? UploadInactivityTimeoutMs
527 : ResponseTimeoutMs);
528 }
529 42 }
530
531 1 bool SmtpService::beginAuthentication() {
532
1/2
✓ Branch 3 → 4 taken 1 time.
✗ Branch 3 → 21 not taken.
1 if (!m_socket->isEncrypted()) {
533
5/5
✓ Branch 4 → 5 taken 1 time.
✓ Branch 8 → 9 taken 1 time.
✓ Branch 9 → 10 taken 1 time.
✓ Branch 13 → 6 taken 1 time.
✓ Branch 13 → 14 taken 1 time.
2 qCWarning(lcSmtp) << "Refusing SMTP AUTH over unencrypted connection";
534
1/1
✓ Branch 17 → 18 taken 1 time.
1 failSession(QStringLiteral("Connection is not encrypted"));
535 1 return false;
536 }
537
538 ✗ const AuthMechanism mechanism = selectAuthMechanism(m_ehloCapabilities);
539 ✗ if (mechanism == AuthMechanism::Login)
540 ✗ return beginAuthLogin();
541 ✗ if (mechanism == AuthMechanism::Plain)
542 ✗ return beginAuthPlain();
543
544 ✗ failSession(QStringLiteral("Server offers no supported SMTP AUTH mechanism"));
545 ✗ return false;
546 }
547
548 2 bool SmtpService::beginAuthLogin() {
549
1/2
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 16 taken 2 times.
4 if (!m_ehloCapabilities.authMechanisms.contains(QStringLiteral("LOGIN"))) {
550 ✗ failSession(QStringLiteral("Server does not support AUTH LOGIN"));
551 ✗ return false;
552 }
553
554
1/2
✓ Branch 17 → 18 taken 2 times.
✗ Branch 17 → 35 not taken.
2 if (!m_socket->isEncrypted()) {
555
5/5
✓ Branch 18 → 19 taken 2 times.
✓ Branch 22 → 23 taken 2 times.
✓ Branch 23 → 24 taken 2 times.
✓ Branch 27 → 20 taken 2 times.
✓ Branch 27 → 28 taken 2 times.
4 qCWarning(lcSmtp) << "Refusing AUTH LOGIN over unencrypted connection";
556
1/1
✓ Branch 31 → 32 taken 2 times.
2 failSession(QStringLiteral("Connection is not encrypted"));
557 2 return false;
558 }
559
560 ✗ m_state = State::WaitAuth;
561 ✗ sendCommand(QStringLiteral("AUTH LOGIN"));
562 ✗ return true;
563 }
564
565 2 bool SmtpService::beginAuthPlain() {
566
2/2
✓ Branch 8 → 9 taken 1 time.
✓ Branch 8 → 16 taken 1 time.
4 if (!m_ehloCapabilities.authMechanisms.contains(QStringLiteral("PLAIN"))) {
567
1/1
✓ Branch 12 → 13 taken 1 time.
1 failSession(QStringLiteral("Server does not support AUTH PLAIN"));
568 1 return false;
569 }
570
1/2
✓ Branch 17 → 18 taken 1 time.
✗ Branch 17 → 35 not taken.
1 if (!m_socket->isEncrypted()) {
571
5/5
✓ Branch 18 → 19 taken 1 time.
✓ Branch 22 → 23 taken 1 time.
✓ Branch 23 → 24 taken 1 time.
✓ Branch 27 → 20 taken 1 time.
✓ Branch 27 → 28 taken 1 time.
2 qCWarning(lcSmtp) << "Refusing AUTH PLAIN over unencrypted connection";
572
1/1
✓ Branch 31 → 32 taken 1 time.
1 failSession(QStringLiteral("Connection is not encrypted"));
573 1 return false;
574 }
575
576 ✗ m_state = State::WaitAuthPlain;
577 ✗ m_plainChallengeAnswered = false;
578 ✗ sendAuthPlainPayload(true);
579 ✗ return true;
580 }
581
582 2 void SmtpService::sendAuthPlainPayload(bool initialResponse) {
583 2 QByteArray raw;
584
1/1
✓ Branch 2 → 3 taken 2 times.
2 QByteArray username = m_config.username.toUtf8();
585
1/1
✓ Branch 5 → 6 taken 2 times.
2 raw.reserve(2 + username.size() + m_config.password.size());
586
1/1
✓ Branch 6 → 7 taken 2 times.
2 raw.append('\0');
587
1/1
✓ Branch 7 → 8 taken 2 times.
2 raw.append(username);
588
1/1
✓ Branch 8 → 9 taken 2 times.
2 raw.append('\0');
589
1/1
✓ Branch 9 → 10 taken 2 times.
2 raw.append(m_config.password);
590
1/1
✓ Branch 10 → 11 taken 2 times.
2 SecureUtil::zeroMemory(username);
591
592
1/1
✓ Branch 12 → 13 taken 2 times.
2 QByteArray encoded = raw.toBase64();
593
1/1
✓ Branch 13 → 14 taken 2 times.
2 SecureUtil::zeroMemory(raw);
594
595 2 QByteArray command;
596
2/2
✓ Branch 14 → 15 taken 1 time.
✓ Branch 14 → 16 taken 1 time.
2 if (initialResponse)
597
1/1
✓ Branch 15 → 16 taken 1 time.
1 command.append("AUTH PLAIN ");
598
1/1
✓ Branch 16 → 17 taken 2 times.
2 command.append(encoded);
599
1/1
✓ Branch 17 → 18 taken 2 times.
2 command.append("\r\n");
600
4/4
✓ Branch 18 → 19 taken 2 times.
✓ Branch 22 → 23 taken 2 times.
✓ Branch 30 → 20 taken 2 times.
✓ Branch 30 → 31 taken 2 times.
4 qCDebug(lcSmtp) << (initialResponse ? "C: AUTH PLAIN ***"
601
3/3
✓ Branch 23 → 24 taken 1 time.
✓ Branch 23 → 25 taken 1 time.
✓ Branch 26 → 27 taken 2 times.
2 : "C: <AUTH PLAIN response>");
602
1/1
✓ Branch 31 → 32 taken 2 times.
2 m_socket->write(command);
603
1/1
✓ Branch 32 → 33 taken 2 times.
2 SecureUtil::zeroMemory(command);
604
1/1
✓ Branch 33 → 34 taken 2 times.
2 SecureUtil::zeroMemory(encoded);
605 2 }
606
607 2 void SmtpService::authSucceeded() {
608
2/2
✓ Branch 2 → 3 taken 2 times.
✓ Branch 3 → 4 taken 2 times.
2 emit statusMessage(tr("Authenticated, sending mail..."));
609 2 m_state = State::WaitMailFrom;
610
2/2
✓ Branch 9 → 10 taken 2 times.
✓ Branch 10 → 11 taken 2 times.
4 sendCommand(QStringLiteral("MAIL FROM:<%1>").arg(m_from));
611 2 }
612
613 17 void SmtpService::sendCommand(const QString &cmd) {
614
9/11
✓ Branch 2 → 3 taken 17 times.
✓ Branch 6 → 7 taken 17 times.
✓ Branch 7 → 8 taken 17 times.
✓ Branch 8 → 9 taken 17 times.
✓ Branch 9 → 10 taken 17 times.
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 17 times.
✗ Branch 11 → 13 not taken.
✓ Branch 13 → 14 taken 17 times.
✓ Branch 19 → 4 taken 17 times.
✓ Branch 19 → 20 taken 17 times.
34 qCDebug(lcSmtp) << "C:" << (cmd.startsWith("AUTH") ? "AUTH ***" : cmd);
615 #ifdef MAILJD_E2E_TESTING
616 ✗ QString commandType = cmd.section(QLatin1Char(' '), 0, 0).toUpper();
617 ✗ if (commandType == QLatin1String("MAIL"))
618 ✗ commandType = QStringLiteral("MAIL_FROM");
619 ✗ else if (commandType == QLatin1String("RCPT"))
620 ✗ commandType = QStringLiteral("RCPT_TO");
621 ✗ reportE2eTransportPhase(QStringLiteral("command-write:%1").arg(commandType));
622 #endif
623
3/6
✓ Branch 20 → 21 taken 17 times.
✓ Branch 21 → 22 taken 17 times.
✓ Branch 22 → 23 taken 17 times.
✗ Branch 55 → 56 not taken.
✗ Branch 56 → 57 not taken.
✗ Branch 57 → 58 not taken.
17 m_socket->write((cmd + "\r\n").toUtf8());
624 17 }
625
626 #ifdef MAILJD_E2E_TESTING
627 ✗ void SmtpService::reportE2eTransportPhase(const QString &phase) {
628 ✗ emit e2eTransportPhase(phase, m_attemptGeneration);
629 ✗ }
630 #endif
631
632 6 void SmtpService::nextRcptTo() {
633
2/2
✓ Branch 3 → 4 taken 4 times.
✓ Branch 3 → 24 taken 2 times.
6 if (m_rcptIndex < m_recipients.size()) {
634 4 m_state = State::WaitRcptTo;
635 4 const QString &recipient = m_recipients[m_rcptIndex];
636
1/2
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 14 taken 4 times.
4 if (!isValidSmtpAddress(recipient)) {
637 ✗ failSession(QStringLiteral("Invalid SMTP envelope recipient"));
638 ✗ return;
639 }
640
2/2
✓ Branch 18 → 19 taken 4 times.
✓ Branch 19 → 20 taken 4 times.
8 sendCommand(QStringLiteral("RCPT TO:<%1>").arg(recipient));
641 } else {
642 // All recipients sent, start DATA
643 2 m_state = State::WaitData;
644
1/1
✓ Branch 27 → 28 taken 2 times.
2 sendCommand(QStringLiteral("DATA"));
645 }
646 }
647
648 4642 bool SmtpService::isValidSmtpAddress(const QString &address) {
649
6/6
✓ Branch 3 → 4 taken 4641 times.
✓ Branch 3 → 6 taken 1 time.
✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 7 taken 4640 times.
✓ Branch 8 → 9 taken 2 times.
✓ Branch 8 → 10 taken 4640 times.
4642 if (address.isEmpty() || address.size() > 254) {
650 2 return false;
651 }
652
653 4640 qsizetype at = -1;
654 4640 bool quoted = false;
655 4640 bool escaped = false;
656
2/2
✓ Branch 80 → 11 taken 86220 times.
✓ Branch 80 → 81 taken 4625 times.
90845 for (qsizetype i = 0; i < address.size(); ++i) {
657 86220 const QChar c = address.at(i);
658 86220 const ushort value = c.unicode();
659
8/8
✓ Branch 13 → 14 taken 86219 times.
✓ Branch 13 → 23 taken 1 time.
✓ Branch 14 → 15 taken 86218 times.
✓ Branch 14 → 23 taken 1 time.
✓ Branch 18 → 19 taken 86217 times.
✓ Branch 18 → 23 taken 1 time.
✓ Branch 22 → 23 taken 3 times.
✓ Branch 22 → 24 taken 86214 times.
172437 if (value < 0x20 || value == 0x7F || c == QLatin1Char('<') ||
660
2/2
✓ Branch 25 → 26 taken 6 times.
✓ Branch 25 → 27 taken 86214 times.
172437 c == QLatin1Char('>')) {
661 15 return false;
662 }
663
2/2
✓ Branch 27 → 28 taken 56074 times.
✓ Branch 27 → 43 taken 30140 times.
86214 if (at >= 0) {
664
8/8
✓ Branch 31 → 32 taken 56071 times.
✓ Branch 31 → 38 taken 3 times.
✓ Branch 35 → 36 taken 56070 times.
✓ Branch 35 → 38 taken 1 time.
✓ Branch 37 → 38 taken 1 time.
✓ Branch 37 → 39 taken 56069 times.
✓ Branch 40 → 41 taken 5 times.
✓ Branch 40 → 42 taken 56069 times.
56074 if (c == QLatin1Char('@') || c == QLatin1Char('"') || c.isSpace())
665 5 return false;
666 60705 continue;
667 }
668
2/2
✓ Branch 43 → 44 taken 2 times.
✓ Branch 43 → 45 taken 30138 times.
30140 if (escaped) {
669 2 escaped = false;
670 2 continue;
671 }
672
6/6
✓ Branch 45 → 46 taken 91 times.
✓ Branch 45 → 51 taken 30047 times.
✓ Branch 49 → 50 taken 2 times.
✓ Branch 49 → 51 taken 89 times.
✓ Branch 52 → 53 taken 2 times.
✓ Branch 52 → 54 taken 30136 times.
30138 if (quoted && c == QLatin1Char('\\')) {
673 2 escaped = true;
674 2 continue;
675 }
676
2/2
✓ Branch 57 → 58 taken 8 times.
✓ Branch 57 → 59 taken 30128 times.
30136 if (c == QLatin1Char('"')) {
677 8 quoted = !quoted;
678 8 continue;
679 }
680
6/6
✓ Branch 59 → 60 taken 30041 times.
✓ Branch 59 → 65 taken 87 times.
✓ Branch 63 → 64 taken 4624 times.
✓ Branch 63 → 65 taken 25417 times.
✓ Branch 66 → 67 taken 4624 times.
✓ Branch 66 → 68 taken 25504 times.
30128 if (!quoted && c == QLatin1Char('@')) {
681 4624 at = i;
682 4624 continue;
683 }
684
6/6
✓ Branch 68 → 69 taken 25417 times.
✓ Branch 68 → 72 taken 87 times.
✓ Branch 70 → 71 taken 4 times.
✓ Branch 70 → 72 taken 25413 times.
✓ Branch 73 → 74 taken 4 times.
✓ Branch 73 → 75 taken 25500 times.
25504 if (!quoted && c.isSpace())
685 4 return false;
686 }
687
3/4
✓ Branch 81 → 82 taken 4621 times.
✓ Branch 81 → 83 taken 4 times.
✗ Branch 82 → 83 not taken.
✓ Branch 82 → 84 taken 4621 times.
4625 if (quoted || escaped)
688 4 return false;
689
6/6
✓ Branch 84 → 85 taken 4615 times.
✓ Branch 84 → 87 taken 6 times.
✓ Branch 86 → 87 taken 7 times.
✓ Branch 86 → 88 taken 4608 times.
✓ Branch 89 → 90 taken 13 times.
✓ Branch 89 → 91 taken 4608 times.
4621 if (at <= 0 || at == address.size() - 1)
690 13 return false;
691
692
1/1
✓ Branch 91 → 92 taken 4608 times.
4608 const QString local = address.left(at);
693
1/1
✓ Branch 92 → 93 taken 4608 times.
4608 const QString domain = address.mid(at + 1);
694
1/1
✓ Branch 95 → 96 taken 4608 times.
4608 const bool quotedLocal = local.startsWith(QLatin1Char('"'));
695
1/1
✓ Branch 98 → 99 taken 4608 times.
4608 if (quotedLocal != local.endsWith(QLatin1Char('"')) ||
696
6/7
✓ Branch 100 → 101 taken 4606 times.
✓ Branch 100 → 105 taken 2 times.
✓ Branch 103 → 104 taken 4606 times.
✓ Branch 104 → 105 taken 4606 times.
✗ Branch 104 → 136 not taken.
✓ Branch 106 → 107 taken 4607 times.
✓ Branch 106 → 136 taken 1 time.
4608 (!quotedLocal && local.contains(QLatin1Char('"'))) || local.size() > 64 ||
697
5/5
✓ Branch 107 → 108 taken 4605 times.
✓ Branch 107 → 112 taken 2 times.
✓ Branch 110 → 111 taken 4605 times.
✓ Branch 111 → 112 taken 4604 times.
✓ Branch 111 → 136 taken 1 time.
4607 (!quotedLocal && local.startsWith(QLatin1Char('.'))) ||
698
5/5
✓ Branch 112 → 113 taken 4604 times.
✓ Branch 112 → 117 taken 2 times.
✓ Branch 115 → 116 taken 4604 times.
✓ Branch 116 → 117 taken 4603 times.
✓ Branch 116 → 136 taken 1 time.
4606 (!quotedLocal && local.endsWith(QLatin1Char('.'))) ||
699
9/13
✓ Branch 117 → 118 taken 4603 times.
✓ Branch 117 → 123 taken 2 times.
✓ Branch 121 → 122 taken 4603 times.
✓ Branch 122 → 123 taken 4602 times.
✓ Branch 122 → 136 taken 1 time.
✓ Branch 144 → 145 taken 4603 times.
✓ Branch 144 → 146 taken 5 times.
✓ Branch 146 → 147 taken 7 times.
✓ Branch 146 → 148 taken 4601 times.
✗ Branch 277 → 278 not taken.
✗ Branch 277 → 279 not taken.
✗ Branch 281 → 282 not taken.
✗ Branch 281 → 283 not taken.
13816 (!quotedLocal && local.contains(QStringLiteral(".."))) ||
700
7/8
✓ Branch 99 → 100 taken 4608 times.
✗ Branch 99 → 136 not taken.
✓ Branch 125 → 126 taken 4604 times.
✓ Branch 126 → 127 taken 4603 times.
✓ Branch 126 → 136 taken 1 time.
✓ Branch 129 → 130 taken 4603 times.
✓ Branch 130 → 131 taken 4602 times.
✓ Branch 130 → 136 taken 1 time.
18426 domain.startsWith(QLatin1Char('.')) || domain.endsWith(QLatin1Char('.')) ||
701
9/13
✓ Branch 134 → 135 taken 4602 times.
✓ Branch 135 → 136 taken 1 time.
✓ Branch 135 → 137 taken 4601 times.
✓ Branch 138 → 139 taken 4602 times.
✓ Branch 138 → 140 taken 6 times.
✓ Branch 140 → 141 taken 4602 times.
✓ Branch 140 → 142 taken 6 times.
✓ Branch 142 → 143 taken 4603 times.
✓ Branch 142 → 144 taken 5 times.
✗ Branch 265 → 266 not taken.
✗ Branch 265 → 267 not taken.
✗ Branch 269 → 270 not taken.
✗ Branch 269 → 271 not taken.
9210 domain.contains(QStringLiteral(".."))) {
702 7 return false;
703 }
704
705
2/2
✓ Branch 148 → 149 taken 4599 times.
✓ Branch 148 → 176 taken 2 times.
4601 if (!quotedLocal) {
706 static const QString localPunctuation =
707
3/4
✓ Branch 149 → 150 taken 21 times.
✓ Branch 149 → 159 taken 4578 times.
✓ Branch 151 → 152 taken 21 times.
✗ Branch 151 → 159 not taken.
4620 QStringLiteral("!#$%&'*+-/=?^_`{|}~.");
708
2/2
✓ Branch 175 → 161 taken 25223 times.
✓ Branch 175 → 176 taken 4598 times.
29821 for (const QChar &c : local) {
709 25223 const ushort u = c.unicode();
710
12/12
✓ Branch 162 → 163 taken 25046 times.
✓ Branch 162 → 164 taken 177 times.
✓ Branch 163 → 164 taken 25034 times.
✓ Branch 163 → 171 taken 12 times.
✓ Branch 164 → 165 taken 25031 times.
✓ Branch 164 → 166 taken 180 times.
✓ Branch 165 → 166 taken 5 times.
✓ Branch 165 → 171 taken 25026 times.
✓ Branch 166 → 167 taken 34 times.
✓ Branch 166 → 168 taken 151 times.
✓ Branch 172 → 173 taken 1 time.
✓ Branch 172 → 174 taken 25222 times.
25408 if (!((u >= 'A' && u <= 'Z') || (u >= 'a' && u <= 'z') ||
711
5/5
✓ Branch 167 → 168 taken 10 times.
✓ Branch 167 → 171 taken 24 times.
✓ Branch 168 → 169 taken 161 times.
✓ Branch 169 → 170 taken 1 time.
✓ Branch 169 → 171 taken 160 times.
185 (u >= '0' && u <= '9') || localPunctuation.contains(c))) {
712 1 return false;
713 }
714 }
715 }
716
717
3/3
✓ Branch 178 → 179 taken 4600 times.
✓ Branch 179 → 180 taken 4 times.
✓ Branch 179 → 185 taken 4596 times.
4604 if (domain.startsWith(QLatin1Char('[')) &&
718
4/5
✓ Branch 182 → 183 taken 4 times.
✓ Branch 183 → 184 taken 4 times.
✗ Branch 183 → 185 not taken.
✓ Branch 186 → 187 taken 4 times.
✓ Branch 186 → 217 taken 4596 times.
4604 domain.endsWith(QLatin1Char(']'))) {
719
1/1
✓ Branch 188 → 189 taken 4 times.
4 const QString literal = domain.mid(1, domain.size() - 2);
720
2/2
✓ Branch 190 → 191 taken 1 time.
✓ Branch 190 → 192 taken 3 times.
4 if (literal.isEmpty())
721 1 return false;
722
2/2
✓ Branch 213 → 194 taken 35 times.
✓ Branch 213 → 214 taken 2 times.
37 for (const QChar &c : literal) {
723
4/4
✓ Branch 195 → 196 taken 11 times.
✓ Branch 195 → 209 taken 24 times.
✓ Branch 199 → 200 taken 7 times.
✓ Branch 199 → 209 taken 4 times.
42 if (!(c.isLetterOrNumber() || c == QLatin1Char(':') ||
724
5/6
✓ Branch 203 → 204 taken 1 time.
✓ Branch 203 → 209 taken 6 times.
✓ Branch 207 → 208 taken 1 time.
✗ Branch 207 → 209 not taken.
✓ Branch 210 → 211 taken 1 time.
✓ Branch 210 → 212 taken 34 times.
42 c == QLatin1Char('.') || c == QLatin1Char('-'))) {
725 1 return false;
726 }
727 }
728 2 return true;
729 4 }
730
731
1/1
✓ Branch 220 → 221 taken 4596 times.
4596 const QStringList labels = domain.split(QLatin1Char('.'));
732
2/2
✓ Branch 255 → 223 taken 9256 times.
✓ Branch 255 → 256 taken 4591 times.
13847 for (const QString &label : labels) {
733
2/2
✓ Branch 227 → 228 taken 9255 times.
✓ Branch 227 → 236 taken 1 time.
18512 if (label.isEmpty() || label.size() > 63 ||
734
9/10
✓ Branch 225 → 226 taken 9256 times.
✗ Branch 225 → 236 not taken.
✓ Branch 230 → 231 taken 9255 times.
✓ Branch 231 → 232 taken 9253 times.
✓ Branch 231 → 236 taken 2 times.
✓ Branch 234 → 235 taken 9253 times.
✓ Branch 235 → 236 taken 1 time.
✓ Branch 235 → 237 taken 9252 times.
✓ Branch 238 → 239 taken 4 times.
✓ Branch 238 → 240 taken 9252 times.
18512 label.startsWith(QLatin1Char('-')) || label.endsWith(QLatin1Char('-')))
735 5 return false;
736
2/2
✓ Branch 252 → 242 taken 51159 times.
✓ Branch 252 → 253 taken 9251 times.
60410 for (const QChar &c : label) {
737 51159 const ushort u = c.unicode();
738
11/12
✓ Branch 243 → 244 taken 51092 times.
✓ Branch 243 → 245 taken 67 times.
✓ Branch 244 → 245 taken 51078 times.
✓ Branch 244 → 251 taken 14 times.
✓ Branch 245 → 246 taken 51077 times.
✓ Branch 245 → 247 taken 68 times.
✗ Branch 246 → 247 not taken.
✓ Branch 246 → 251 taken 51077 times.
✓ Branch 247 → 248 taken 1 time.
✓ Branch 247 → 249 taken 67 times.
✓ Branch 249 → 250 taken 1 time.
✓ Branch 249 → 251 taken 67 times.
51160 if (!((u >= 'A' && u <= 'Z') || (u >= 'a' && u <= 'z') ||
739
1/2
✓ Branch 248 → 249 taken 1 time.
✗ Branch 248 → 251 not taken.
1 (u >= '0' && u <= '9') || u == '-')) {
740 1 return false;
741 }
742 }
743 }
744 4591 return true;
745 4608 }
746