MailJD nbsp;·nbsp; Test Dashboard nbsp;·nbsp; Coverage
LCOV - code coverage report
Current view: top level - service - SmtpService.cpp (source / functions) Coverage Total Hit
Test: MailJD Coverage (Unit + E2E) Lines: 87.7 % 389 341
Test Date: 2026-07-27 17:53:44 Functions: 95.8 % 24 23
Legend: Lines:     hit not hit
Branches: + taken - not taken # not executed
Branches: 57.2 % 794 454

             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 : }
        

Generated by: LCOV version 2.0-1