Branch data Line data Source code
1 : : #include "SmtpService.h"
2 : : #include "util/SecureUtil.h"
3 : :
4 : : #include <QLoggingCategory>
5 : : #include <QTimer>
6 : :
7 : : #include "data/AccountConfig.h"
8 : :
9 [ + + + - : 113 : Q_LOGGING_CATEGORY(lcSmtp, "mailjd.smtp")
+ - - - ]
10 : :
11 [ + - ]: 112 : SmtpService::SmtpService(QObject *parent) : QObject(parent) {
12 [ + - + - : 112 : m_socket = new QSslSocket(this);
- + - - ]
13 [ + - ]: 112 : m_socket->setReadBufferSize(MaxResponseBufferSize);
14 [ + - ]: 112 : connect(m_socket, &QSslSocket::connected, this, &SmtpService::onConnected);
15 [ + - ]: 112 : connect(m_socket, &QSslSocket::encrypted, this, &SmtpService::onEncrypted);
16 [ + - ]: 112 : connect(m_socket, &QSslSocket::readyRead, this, &SmtpService::onReadyRead);
17 [ + - ]: 112 : connect(m_socket, &QSslSocket::errorOccurred, this, &SmtpService::onError);
18 : 112 : connect(m_socket, &QAbstractSocket::disconnected, this,
19 [ + - ]: 112 : &SmtpService::clearCredentials);
20 : :
21 : : // T-506: Handle SSL certificate errors (were previously silently ignored)
22 : 112 : connect(m_socket, &QSslSocket::sslErrors, this,
23 [ + - ]: 112 : [this](const QList<QSslError> &errors) {
24 [ + + ]: 2 : for (const auto &err : errors)
25 [ + - + - : 2 : qCWarning(lcSmtp) << "SSL error:" << err.errorString();
+ - + - +
- + + ]
26 : : // ignoreSslErrors() is intentionally NOT called — TLS stays fail-closed.
27 [ + - ]: 2 : failSession(QStringLiteral("SSL certificate error"));
28 : 1 : });
29 : :
30 : : // T-612/SEC-11: Timeout timer to prevent hanging on slow/malicious servers
31 [ + - + - : 112 : m_timeoutTimer = new QTimer(this);
- + - - ]
32 [ + - ]: 112 : m_timeoutTimer->setSingleShot(true);
33 [ + - ]: 112 : connect(m_timeoutTimer, &QTimer::timeout, this, [this]() {
34 [ + - + - : 2 : qCWarning(lcSmtp) << "SMTP timeout in state" << static_cast<int>(m_state);
+ - + - +
+ ]
35 [ + - ]: 2 : failSession(QStringLiteral("SMTP timeout"));
36 : 1 : });
37 : 112 : }
38 : :
39 : 210 : SmtpService::~SmtpService() { clearCredentials(); }
40 : :
41 : : #ifdef MAILJD_UNIT_TEST
42 : 6 : bool SmtpService::ehloResponseSupportsAuthLoginForTest(const QString &response) {
43 [ + - ]: 18 : return parseEhloCapabilities(response).authMechanisms.contains(
44 : 18 : QStringLiteral("LOGIN"));
45 : : }
46 : :
47 : 3 : bool SmtpService::ehloResponseSupportsStartTlsForTest(const QString &response) {
48 [ + - ]: 3 : return parseEhloCapabilities(response).supportsStartTls;
49 : : }
50 : : #endif
51 : :
52 : : SmtpService::EhloCapabilities
53 : 15 : SmtpService::parseEhloCapabilities(const QString &response) {
54 : 15 : EhloCapabilities capabilities;
55 [ + - ]: 15 : const auto lines = response.split('\n', Qt::SkipEmptyParts);
56 : :
57 [ + + ]: 47 : for (QString line : lines) {
58 [ + - ]: 32 : line = line.trimmed();
59 [ + - ]: 32 : if (line.length() >= 4) {
60 : 32 : bool hasCode = false;
61 [ + - + - ]: 32 : line.left(3).toInt(&hasCode);
62 [ + - + - : 42 : if (hasCode && (line[3] == QLatin1Char('-') ||
+ + + - ]
63 [ + - + - ]: 42 : line[3] == QLatin1Char(' '))) {
64 [ + - + - ]: 32 : line = line.mid(4).trimmed();
65 : : }
66 : : }
67 : :
68 [ - + ]: 32 : if (line.isEmpty())
69 : 0 : continue;
70 : :
71 : 32 : const int spaceIndex = line.indexOf(QLatin1Char(' '));
72 : 32 : const int equalsIndex = line.indexOf(QLatin1Char('='));
73 : 32 : int delimiterIndex = -1;
74 [ + + + + ]: 32 : if (spaceIndex >= 0 && equalsIndex >= 0) {
75 : 1 : delimiterIndex = qMin(spaceIndex, equalsIndex);
76 : : } else {
77 : 31 : delimiterIndex = qMax(spaceIndex, equalsIndex);
78 : : }
79 : :
80 [ + + + - ]: 64 : const QString key = (delimiterIndex >= 0 ? line.left(delimiterIndex) : line)
81 [ + - ]: 64 : .trimmed()
82 [ + - ]: 32 : .toUpper();
83 : : const QString value =
84 [ + + + - ]: 64 : (delimiterIndex >= 0 ? line.mid(delimiterIndex + 1) : QString())
85 [ + - ]: 64 : .trimmed()
86 [ + - ]: 64 : .toUpper()
87 [ + - ]: 32 : .simplified();
88 : :
89 [ + + ]: 32 : if (key == QLatin1String("STARTTLS")) {
90 : 2 : capabilities.supportsStartTls = true;
91 : 2 : continue;
92 : : }
93 : :
94 [ + + ]: 30 : if (key == QLatin1String("AUTH")) {
95 [ + - ]: 9 : const auto mechanisms = value.split(QLatin1Char(' '), Qt::SkipEmptyParts);
96 [ + + ]: 25 : for (const QString &mechanism : mechanisms) {
97 [ + - ]: 16 : if (!capabilities.authMechanisms.contains(mechanism))
98 [ + - ]: 16 : capabilities.authMechanisms.append(mechanism);
99 : : }
100 : 9 : }
101 [ + + + + : 36 : }
+ + ]
102 : :
103 : 15 : return capabilities;
104 : 15 : }
105 : :
106 : 34 : void SmtpService::resetSessionState() {
107 [ + - ]: 34 : if (m_timeoutTimer)
108 : 34 : m_timeoutTimer->stop();
109 : 34 : m_state = State::Disconnected;
110 : 34 : m_from.clear();
111 : 34 : m_recipients.clear();
112 : 34 : m_message.clear();
113 : 34 : m_rcptIndex = 0;
114 : 34 : m_responseBuffer.clear();
115 : 34 : m_ehloCapabilities = {};
116 : 34 : clearCredentials();
117 : 34 : }
118 : :
119 : 160 : void SmtpService::clearCredentials() {
120 : 160 : SecureUtil::zeroMemory(m_config.password);
121 : 160 : m_config.username.clear();
122 : 160 : }
123 : :
124 : 21 : void SmtpService::failSession(const QString &error) {
125 [ + + ]: 21 : if (m_operationFinished)
126 : 2 : return;
127 : :
128 : : // abort() can synchronously or asynchronously trigger socket errors. Set
129 : : // the terminal guard first so every race still emits exactly one failure.
130 : 19 : m_operationFinished = true;
131 [ + - + - : 38 : qCWarning(lcSmtp) << "SMTP send failed:" << error;
+ - + - +
+ ]
132 : 19 : m_socket->abort();
133 : 19 : resetSessionState();
134 : 19 : emit sendFailed(error);
135 : : }
136 : :
137 : 5 : void SmtpService::completeSession() {
138 [ - + ]: 5 : if (m_operationFinished)
139 : 0 : return;
140 : :
141 : 5 : m_operationFinished = true;
142 : 5 : m_socket->close();
143 : 5 : resetSessionState();
144 : 5 : emit sendSuccess();
145 : : }
146 : :
147 : 10 : void SmtpService::sendMail(const SmtpConfig &config, const QString &from,
148 : : const QStringList &recipients,
149 : : const QByteArray &message) {
150 : : // Bug 35: Reset socket state from any previous send
151 [ + - - + ]: 20 : if (m_state != State::Disconnected ||
152 [ - + ]: 10 : m_socket->state() != QAbstractSocket::UnconnectedState) {
153 [ # # ]: 0 : failSession(QStringLiteral("SMTP send replaced by a new request"));
154 : : }
155 : 10 : resetSessionState();
156 : 10 : m_operationFinished = false;
157 : :
158 : 10 : m_config = config;
159 : 10 : m_from = from;
160 : 10 : m_recipients = recipients;
161 : 10 : m_message = message;
162 : :
163 [ + + ]: 10 : if (!isValidSmtpAddress(m_from)) {
164 [ + - ]: 1 : failSession(QStringLiteral("Invalid SMTP envelope sender"));
165 : 1 : return;
166 : : }
167 [ - + ]: 9 : if (m_recipients.isEmpty()) {
168 [ # # ]: 0 : failSession(QStringLiteral("SMTP envelope has no recipients"));
169 : 0 : return;
170 : : }
171 [ + + ]: 20 : for (const QString &recipient : std::as_const(m_recipients)) {
172 [ + - - + ]: 11 : if (!isValidSmtpAddress(recipient)) {
173 [ # # ]: 0 : failSession(QStringLiteral("Invalid SMTP envelope recipient"));
174 : 0 : return;
175 : : }
176 : : }
177 : :
178 [ + - + - ]: 9 : emit statusMessage(tr("Connecting to SMTP server..."));
179 : :
180 : : // T-612/SEC-11: Start connection timeout (30 seconds)
181 : 9 : m_timeoutTimer->start(30 * 1000);
182 : :
183 [ + + ]: 9 : if (config.security == QLatin1String("ssl")) {
184 : 2 : m_state = State::Connecting;
185 [ + - ]: 2 : m_socket->connectToHostEncrypted(config.host, config.port);
186 : : } else {
187 : 7 : m_state = State::WaitGreeting;
188 [ + - ]: 7 : m_socket->connectToHost(config.host, config.port);
189 : : }
190 : : }
191 : :
192 : 6 : void SmtpService::onConnected() {
193 [ + - + - : 12 : qCInfo(lcSmtp) << "Connected to" << m_config.host << ":" << m_config.port;
+ - + - +
- + - +
+ ]
194 : : // For plain/STARTTLS, wait for the server greeting
195 : 6 : }
196 : :
197 : 0 : void SmtpService::onEncrypted() {
198 [ # # # # : 0 : qCInfo(lcSmtp) << "TLS encrypted";
# # # # ]
199 [ # # ]: 0 : if (m_state == State::Connecting) {
200 : : // Direct SSL: wait for greeting after encryption
201 : 0 : m_state = State::WaitGreeting;
202 [ # # ]: 0 : } else if (m_state == State::WaitStartTls) {
203 : : // STARTTLS upgrade done, send EHLO again
204 : 0 : m_state = State::WaitEhloAfterTls;
205 : 0 : m_ehloCapabilities = {};
206 [ # # # # : 0 : sendCommand(QStringLiteral("EHLO ") + QLatin1String(EhloClientName));
# # ]
207 : : }
208 : 0 : }
209 : :
210 : 35 : void SmtpService::onReadyRead() {
211 [ - + ]: 35 : if (m_operationFinished)
212 : 0 : return;
213 [ - + - + ]: 35 : if (!m_socket->canReadLine() &&
214 [ # # ]: 0 : m_socket->bytesAvailable() >= MaxResponseBufferSize) {
215 : 0 : failResponseTooLarge();
216 : 0 : return;
217 : : }
218 : :
219 [ + + ]: 75 : while (m_socket->canReadLine()) {
220 [ + - + - : 44 : QString line = QString::fromUtf8(m_socket->readLine()).trimmed();
+ - ]
221 [ + - + - : 88 : qCDebug(lcSmtp) << "S:" << line;
+ - + - +
+ ]
222 : :
223 : : // Multi-line responses: 250-... continues, 250 ... is final
224 [ + - + - : 44 : if (line.length() >= 4 && line[3] == '-') {
+ + + + ]
225 [ + - + - : 9 : if (!appendResponseText(line + QLatin1Char('\n')))
- + ]
226 : 0 : return;
227 : 9 : continue;
228 : : }
229 [ + - - + ]: 35 : if (!appendResponseText(line))
230 : 0 : return;
231 : 35 : const QString response = m_responseBuffer;
232 : 35 : m_responseBuffer.clear();
233 [ + - ]: 35 : processResponse(response);
234 [ + + ]: 35 : if (m_operationFinished)
235 : 4 : return;
236 [ + + + + : 48 : }
+ ]
237 : : }
238 : :
239 : 45 : bool SmtpService::appendResponseText(const QString &text) {
240 [ + + ]: 45 : if (m_responseBuffer.size() + text.size() > MaxResponseBufferSize) {
241 : 1 : failResponseTooLarge();
242 : 1 : return false;
243 : : }
244 : 44 : m_responseBuffer += text;
245 : 44 : return true;
246 : : }
247 : :
248 : 1 : void SmtpService::failResponseTooLarge() {
249 [ + - + - : 2 : qCWarning(lcSmtp) << "SMTP response exceeded buffer limit";
+ - + + ]
250 : 1 : m_responseBuffer.clear();
251 [ + - ]: 1 : failSession(QStringLiteral("SMTP response too large"));
252 : 1 : }
253 : :
254 : 4 : void SmtpService::onError(QAbstractSocket::SocketError error) {
255 : : Q_UNUSED(error)
256 [ + - ]: 4 : m_timeoutTimer->stop(); // T-612: Cancel timeout on socket error
257 [ + - ]: 4 : QString errMsg = m_socket->errorString();
258 [ + - + - : 8 : qCWarning(lcSmtp) << "Socket error:" << errMsg;
+ - + - +
+ ]
259 [ + - ]: 4 : failSession(errMsg);
260 : 4 : }
261 : :
262 : 54 : void SmtpService::processResponse(const QString &line) {
263 [ + + ]: 54 : if (m_operationFinished)
264 : 1 : return;
265 : :
266 : : // T-612/SEC-11: Restart timeout on each server response.
267 : : // Use longer timeout for DATA phase (uploading message body).
268 [ + + ]: 53 : int timeoutMs = (m_state == State::WaitDataContent) ? 120 * 1000 : 60 * 1000;
269 : 53 : m_timeoutTimer->start(timeoutMs);
270 : :
271 [ + - + - ]: 53 : int code = line.left(3).toInt();
272 : :
273 [ + + + + : 53 : switch (m_state) {
+ + + + +
+ + + - ]
274 : 6 : case State::WaitGreeting:
275 [ + + ]: 6 : if (code == 220) {
276 : 5 : m_state = State::WaitEhlo;
277 [ + - + - : 10 : sendCommand(QStringLiteral("EHLO ") + QLatin1String(EhloClientName));
+ - ]
278 : : } else {
279 [ + - + - ]: 1 : failSession(QStringLiteral("Unexpected greeting: ") + line);
280 : : }
281 : 6 : break;
282 : :
283 : 6 : case State::WaitEhlo:
284 [ + + ]: 6 : if (code == 250) {
285 [ + - ]: 5 : m_ehloCapabilities = parseEhloCapabilities(line);
286 [ + + ]: 5 : if (m_config.security == QLatin1String("starttls")) {
287 [ + - ]: 1 : if (!m_ehloCapabilities.supportsStartTls) {
288 [ + - ]: 1 : failSession(QStringLiteral("Server does not support STARTTLS"));
289 : 1 : return;
290 : : }
291 : 0 : m_state = State::WaitStartTls;
292 [ # # ]: 0 : sendCommand(QStringLiteral("STARTTLS"));
293 : : } else {
294 : 4 : beginAuthLogin();
295 : : }
296 : : } else {
297 [ + - + - ]: 1 : failSession(QStringLiteral("EHLO failed: ") + line);
298 : : }
299 : 5 : break;
300 : :
301 : 2 : case State::WaitStartTls:
302 [ + + ]: 2 : if (code == 220) {
303 [ - + - - : 1 : if (!m_responseBuffer.isEmpty() || m_socket->bytesAvailable() > 0) {
+ - ]
304 [ + - + - : 2 : qCWarning(lcSmtp) << "Unexpected data before TLS handshake";
+ - + + ]
305 [ + - ]: 1 : failSession(QStringLiteral("Unexpected data before TLS handshake"));
306 : 1 : return;
307 : : }
308 : : // Server accepted STARTTLS, start TLS handshake
309 : 0 : m_socket->startClientEncryption();
310 : : // onEncrypted() will fire
311 : : } else {
312 [ + - + - ]: 1 : failSession(QStringLiteral("STARTTLS failed: ") + line);
313 : : }
314 : 1 : break;
315 : :
316 : 1 : case State::WaitEhloAfterTls:
317 [ + - ]: 1 : if (code == 250) {
318 [ + - ]: 1 : m_ehloCapabilities = parseEhloCapabilities(line);
319 : : // T-506: Verify encryption after STARTTLS before AUTH
320 [ + - ]: 1 : if (!m_socket->isEncrypted()) {
321 [ + - + - : 2 : qCWarning(lcSmtp) << "STARTTLS succeeded but connection not encrypted";
+ - + + ]
322 [ + - ]: 1 : failSession(QStringLiteral("TLS negotiation failed"));
323 : 1 : return;
324 : : }
325 : 0 : beginAuthLogin();
326 : : } else {
327 [ # # # # ]: 0 : failSession(QStringLiteral("EHLO after TLS failed: ") + line);
328 : : }
329 : 0 : break;
330 : :
331 : 3 : case State::WaitAuth:
332 [ + - ]: 3 : if (code == 334) {
333 : : // Server requests username (Base64 encoded "Username:")
334 : 3 : m_state = State::WaitAuthUser;
335 [ + - + - : 6 : qCDebug(lcSmtp) << "C: <base64-username>";
+ - + + ]
336 [ + - + - : 6 : QByteArray encodedUsername = m_config.username.toUtf8().toBase64() + "\r\n";
+ - ]
337 [ + - ]: 3 : m_socket->write(encodedUsername);
338 [ + - ]: 3 : SecureUtil::zeroMemory(encodedUsername);
339 : 3 : } else {
340 [ # # # # ]: 0 : failSession(QStringLiteral("AUTH LOGIN failed: ") + line);
341 : : }
342 : 3 : break;
343 : :
344 : 5 : case State::WaitAuthUser:
345 [ + - ]: 5 : if (code == 334) {
346 : : // Server requests password (Base64 encoded "Password:")
347 : 5 : m_state = State::WaitAuthPass;
348 [ + - + - : 10 : qCDebug(lcSmtp) << "C: <base64-password>";
+ - + + ]
349 [ + - + - ]: 5 : QByteArray encodedPassword = m_config.password.toBase64() + "\r\n";
350 [ + - ]: 5 : m_socket->write(encodedPassword);
351 [ + - ]: 5 : SecureUtil::zeroMemory(encodedPassword);
352 [ + - ]: 5 : clearCredentials();
353 : 5 : } else {
354 [ # # # # ]: 0 : failSession(QStringLiteral("AUTH username rejected: ") + line);
355 : : }
356 : 5 : break;
357 : :
358 : 4 : case State::WaitAuthPass:
359 : 4 : clearCredentials();
360 [ + + ]: 4 : if (code == 235) {
361 : : // Auth successful, start MAIL FROM
362 [ + - + - ]: 3 : emit statusMessage(tr("Authenticated, sending mail..."));
363 : 3 : m_state = State::WaitMailFrom;
364 [ + - + - ]: 6 : sendCommand(QStringLiteral("MAIL FROM:<%1>").arg(m_from));
365 : : } else {
366 [ + - + - ]: 1 : failSession(QStringLiteral("Authentifizierung fehlgeschlagen: ") + line);
367 : : }
368 : 4 : break;
369 : :
370 : 4 : case State::WaitMailFrom:
371 [ + + ]: 4 : if (code == 250) {
372 : 3 : m_rcptIndex = 0;
373 : 3 : nextRcptTo();
374 : : } else {
375 [ + - + - ]: 1 : failSession(QStringLiteral("MAIL FROM rejected: ") + line);
376 : : }
377 : 4 : break;
378 : :
379 : 8 : case State::WaitRcptTo:
380 [ + + ]: 8 : if (code == 250) {
381 : 7 : m_rcptIndex++;
382 : 7 : nextRcptTo();
383 : : } else {
384 [ + - + - ]: 1 : failSession(QStringLiteral("RCPT TO rejected: ") + line);
385 : : }
386 : 8 : break;
387 : :
388 : 4 : case State::WaitData:
389 [ + + ]: 4 : if (code == 354) {
390 : : // T-400/Bug 3: RFC 5321 §4.5.2 dot-stuffing
391 : : // Lines starting with '.' must be prefixed with an extra '.'
392 [ + - ]: 3 : QList<QByteArray> lines = m_message.split('\n');
393 [ + - + - : 21 : for (const QByteArray &line : lines) {
+ + ]
394 : 18 : QByteArray trimmed = line;
395 : : // Remove trailing \r if present (we re-add CRLF)
396 [ + - + + ]: 18 : if (trimmed.endsWith('\r'))
397 [ + - ]: 15 : trimmed.chop(1);
398 [ + - - + ]: 18 : if (trimmed.startsWith('.')) {
399 [ # # # # : 0 : m_socket->write("." + trimmed + "\r\n");
# # ]
400 : : } else {
401 [ + - + - ]: 18 : m_socket->write(trimmed + "\r\n");
402 : : }
403 : 18 : }
404 [ + - ]: 3 : m_socket->write(".\r\n");
405 : 3 : m_state = State::WaitDataContent;
406 : 3 : } else {
407 [ + - + - ]: 1 : failSession(QStringLiteral("DATA rejected: ") + line);
408 : : }
409 : 4 : break;
410 : :
411 : 5 : case State::WaitDataContent:
412 [ + + ]: 5 : if (code == 250) {
413 [ + - + - ]: 4 : emit statusMessage(tr("Mail sent successfully!"));
414 : 4 : m_state = State::WaitQuit;
415 [ + - ]: 4 : sendCommand(QStringLiteral("QUIT"));
416 : : } else {
417 [ + - + - ]: 1 : failSession(QStringLiteral("Message rejected: ") + line);
418 : : }
419 : 5 : break;
420 : :
421 : 5 : case State::WaitQuit:
422 : : // 221 or whatever, we're done
423 : 5 : completeSession();
424 : 5 : break;
425 : :
426 : 0 : default:
427 : 0 : break;
428 : : }
429 : : }
430 : :
431 : 6 : bool SmtpService::beginAuthLogin() {
432 [ + + ]: 12 : if (!m_ehloCapabilities.authMechanisms.contains(QStringLiteral("LOGIN"))) {
433 [ + - ]: 1 : failSession(QStringLiteral("Server does not support AUTH LOGIN"));
434 : 1 : return false;
435 : : }
436 : :
437 : : // T-506: Verify encryption before sending credentials
438 : : // (skip check for security=none — plaintext AUTH is intentional)
439 [ + + + - : 5 : if (m_config.security != QLatin1String("none") && !m_socket->isEncrypted()) {
+ - + + ]
440 [ + - + - : 2 : qCWarning(lcSmtp) << "Refusing AUTH LOGIN over unencrypted connection";
+ - + + ]
441 [ + - ]: 1 : failSession(QStringLiteral("Connection is not encrypted"));
442 : 1 : return false;
443 : : }
444 : :
445 : 4 : m_state = State::WaitAuth;
446 [ + - ]: 4 : sendCommand(QStringLiteral("AUTH LOGIN"));
447 : 4 : return true;
448 : : }
449 : :
450 : 26 : void SmtpService::sendCommand(const QString &cmd) {
451 [ + - + - : 52 : qCDebug(lcSmtp) << "C:" << (cmd.startsWith("AUTH") ? "AUTH ***" : cmd);
+ - + - +
- + + + -
+ - + + ]
452 [ + - + - : 26 : m_socket->write((cmd + "\r\n").toUtf8());
+ - ]
453 : 26 : }
454 : :
455 : 10 : void SmtpService::nextRcptTo() {
456 [ + + ]: 10 : if (m_rcptIndex < m_recipients.size()) {
457 : 6 : m_state = State::WaitRcptTo;
458 : 6 : const QString &recipient = m_recipients[m_rcptIndex];
459 [ - + ]: 6 : if (!isValidSmtpAddress(recipient)) {
460 [ # # ]: 0 : failSession(QStringLiteral("Invalid SMTP envelope recipient"));
461 : 0 : return;
462 : : }
463 [ + - + - ]: 12 : sendCommand(QStringLiteral("RCPT TO:<%1>").arg(recipient));
464 : : } else {
465 : : // All recipients sent, start DATA
466 : 4 : m_state = State::WaitData;
467 [ + - ]: 4 : sendCommand(QStringLiteral("DATA"));
468 : : }
469 : : }
470 : :
471 : 35 : bool SmtpService::isValidSmtpAddress(const QString &address) {
472 [ + - - + : 35 : if (address.isEmpty() || address.size() > 254) {
- + ]
473 : 0 : return false;
474 : : }
475 : :
476 : 35 : qsizetype at = -1;
477 : 35 : bool quoted = false;
478 : 35 : bool escaped = false;
479 [ + + ]: 528 : for (qsizetype i = 0; i < address.size(); ++i) {
480 : 496 : const QChar c = address.at(i);
481 : 496 : const ushort value = c.unicode();
482 [ + - + - : 992 : if (value < 0x20 || value == 0x7F || c == QLatin1Char('<') ||
+ - + + ]
483 [ + + ]: 992 : c == QLatin1Char('>')) {
484 : 3 : return false;
485 : : }
486 [ + + ]: 494 : if (at >= 0) {
487 [ + + + - : 253 : if (c == QLatin1Char('@') || c == QLatin1Char('"') || c.isSpace())
- + + + ]
488 : 1 : return false;
489 : 289 : continue;
490 : : }
491 [ - + ]: 241 : if (escaped) {
492 : 0 : escaped = false;
493 : 0 : continue;
494 : : }
495 [ + + - + : 241 : if (quoted && c == QLatin1Char('\\')) {
- + ]
496 : 0 : escaped = true;
497 : 0 : continue;
498 : : }
499 [ + + ]: 241 : if (c == QLatin1Char('"')) {
500 : 3 : quoted = !quoted;
501 : 3 : continue;
502 : : }
503 [ + + + + : 238 : if (!quoted && c == QLatin1Char('@')) {
+ + ]
504 : 34 : at = i;
505 : 34 : continue;
506 : : }
507 [ + + - + : 204 : if (!quoted && c.isSpace())
- + ]
508 : 0 : return false;
509 : : }
510 [ + + - + ]: 32 : if (quoted || escaped)
511 : 1 : return false;
512 [ + - - + : 31 : if (at <= 0 || at == address.size() - 1)
- + ]
513 : 0 : return false;
514 : :
515 [ + - ]: 31 : const QString local = address.left(at);
516 [ + - ]: 31 : const QString domain = address.mid(at + 1);
517 [ + - ]: 31 : const bool quotedLocal = local.startsWith(QLatin1Char('"'));
518 [ + - ]: 31 : if (quotedLocal != local.endsWith(QLatin1Char('"')) ||
519 [ + + + - : 31 : (!quotedLocal && local.contains(QLatin1Char('"'))) || local.size() > 64 ||
+ - + - ]
520 [ + + + - : 31 : (!quotedLocal && local.startsWith(QLatin1Char('.'))) ||
+ - ]
521 [ + + + - : 31 : (!quotedLocal && local.endsWith(QLatin1Char('.'))) ||
+ - ]
522 [ + + + - : 92 : (!quotedLocal && local.contains(QStringLiteral(".."))) ||
+ - + + -
+ - - -
- ]
523 [ + - + - : 124 : domain.startsWith(QLatin1Char('.')) || domain.endsWith(QLatin1Char('.')) ||
+ - + - +
- ]
524 [ + - - + : 62 : domain.contains(QStringLiteral(".."))) {
+ - + - +
+ - - -
- ]
525 : 0 : return false;
526 : : }
527 : :
528 [ + + ]: 31 : if (!quotedLocal) {
529 : : static const QString localPunctuation =
530 [ + + + - ]: 34 : QStringLiteral("!#$%&'*+-/=?^_`{|}~.");
531 [ + + ]: 195 : for (const QChar &c : local) {
532 : 165 : const ushort u = c.unicode();
533 [ + + + - : 167 : if (!((u >= 'A' && u <= 'Z') || (u >= 'a' && u <= 'z') ||
+ + - + -
+ - + ]
534 [ - - + - : 2 : (u >= '0' && u <= '9') || localPunctuation.contains(c))) {
- + ]
535 : 0 : return false;
536 : : }
537 : : }
538 : : }
539 : :
540 [ + - + + ]: 32 : if (domain.startsWith(QLatin1Char('[')) &&
541 [ + - + - : 32 : domain.endsWith(QLatin1Char(']'))) {
+ + ]
542 [ + - ]: 1 : const QString literal = domain.mid(1, domain.size() - 2);
543 [ - + ]: 1 : if (literal.isEmpty())
544 : 0 : return false;
545 [ + + ]: 10 : for (const QChar &c : literal) {
546 [ + + + - ]: 12 : if (!(c.isLetterOrNumber() || c == QLatin1Char(':') ||
547 [ - + - - : 12 : c == QLatin1Char('.') || c == QLatin1Char('-'))) {
- + ]
548 : 0 : return false;
549 : : }
550 : : }
551 : 1 : return true;
552 : 1 : }
553 : :
554 [ + - ]: 30 : const QStringList labels = domain.split(QLatin1Char('.'));
555 [ + + ]: 84 : for (const QString &label : labels) {
556 [ + - ]: 110 : if (label.isEmpty() || label.size() > 63 ||
557 [ + - + - : 110 : label.startsWith(QLatin1Char('-')) || label.endsWith(QLatin1Char('-')))
+ + + - -
+ + + ]
558 : 1 : return false;
559 [ + + ]: 256 : for (const QChar &c : label) {
560 : 202 : const ushort u = c.unicode();
561 [ + - + - : 202 : if (!((u >= 'A' && u <= 'Z') || (u >= 'a' && u <= 'z') ||
+ - - + -
- - - ]
562 [ # # ]: 0 : (u >= '0' && u <= '9') || u == '-')) {
563 : 0 : return false;
564 : : }
565 : : }
566 : : }
567 : 29 : return true;
568 : 31 : }
|