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: 90.5 % 325 294
Test Date: 2026-06-21 21:10:19 Functions: 95.0 % 20 19
Legend: Lines:     hit not hit
Branches: + taken - not taken # not executed
Branches: 55.7 % 548 305

             Branch data     Line data    Source code
       1                 :             : #include "SmtpService.h"
       2                 :             : #include "util/SecureUtil.h"
       3                 :             : 
       4                 :             : #include <QHostInfo>
       5                 :             : #include <QLoggingCategory>
       6                 :             : #include <QTimer>
       7                 :             : 
       8                 :             : #include "data/AccountConfig.h"
       9                 :             : 
      10   [ +  +  +  -  :          89 : Q_LOGGING_CATEGORY(lcSmtp, "mailjd.smtp")
             +  -  -  - ]
      11                 :             : 
      12         [ +  - ]:         100 : SmtpService::SmtpService(QObject *parent) : QObject(parent) {
      13   [ +  -  +  -  :         100 :   m_socket = new QSslSocket(this);
             -  +  -  - ]
      14         [ +  - ]:         100 :   m_socket->setReadBufferSize(MaxResponseBufferSize);
      15         [ +  - ]:         100 :   connect(m_socket, &QSslSocket::connected, this, &SmtpService::onConnected);
      16         [ +  - ]:         100 :   connect(m_socket, &QSslSocket::encrypted, this, &SmtpService::onEncrypted);
      17         [ +  - ]:         100 :   connect(m_socket, &QSslSocket::readyRead, this, &SmtpService::onReadyRead);
      18         [ +  - ]:         100 :   connect(m_socket, &QSslSocket::errorOccurred, this, &SmtpService::onError);
      19                 :             : 
      20                 :             :   // T-506: Handle SSL certificate errors (were previously silently ignored)
      21                 :         100 :   connect(m_socket, &QSslSocket::sslErrors, this,
      22         [ +  - ]:         100 :           [this](const QList<QSslError> &errors) {
      23         [ +  + ]:           2 :     for (const auto &err : errors)
      24   [ +  -  +  -  :           2 :       qCWarning(lcSmtp) << "SSL error:" << err.errorString();
          +  -  +  -  +  
                -  +  + ]
      25         [ +  - ]:           2 :     emit sendFailed(QStringLiteral("SSL certificate error"));
      26                 :           1 :   });
      27                 :             : 
      28                 :             :   // T-612/SEC-11: Timeout timer to prevent hanging on slow/malicious servers
      29   [ +  -  +  -  :         100 :   m_timeoutTimer = new QTimer(this);
             -  +  -  - ]
      30         [ +  - ]:         100 :   m_timeoutTimer->setSingleShot(true);
      31         [ +  - ]:         100 :   connect(m_timeoutTimer, &QTimer::timeout, this, [this]() {
      32   [ +  -  +  -  :           2 :     qCWarning(lcSmtp) << "SMTP timeout in state" << static_cast<int>(m_state);
          +  -  +  -  +  
                      + ]
      33                 :           1 :     m_socket->abort();
      34                 :           1 :     m_state = State::Disconnected;
      35         [ +  - ]:           2 :     emit sendFailed(QStringLiteral("SMTP timeout"));
      36                 :           1 :   });
      37                 :         100 : }
      38                 :             : 
      39                 :         188 : SmtpService::~SmtpService() = default;
      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                 :           8 : void SmtpService::resetSessionState() {
     107         [ +  - ]:           8 :   if (m_timeoutTimer)
     108                 :           8 :     m_timeoutTimer->stop();
     109                 :           8 :   m_state = State::Disconnected;
     110                 :           8 :   m_from.clear();
     111                 :           8 :   m_recipients.clear();
     112                 :           8 :   m_message.clear();
     113                 :           8 :   m_rcptIndex = 0;
     114                 :           8 :   m_responseBuffer.clear();
     115                 :           8 :   m_ehloCapabilities = {};
     116                 :           8 : }
     117                 :             : 
     118                 :           8 : void SmtpService::sendMail(const SmtpConfig &config, const QString &from,
     119                 :             :                            const QStringList &recipients,
     120                 :             :                            const QByteArray &message) {
     121                 :             :   // Bug 35: Reset socket state from any previous send
     122   [ +  -  -  + ]:          16 :   if (m_state != State::Disconnected ||
     123         [ -  + ]:           8 :       m_socket->state() != QAbstractSocket::UnconnectedState) {
     124                 :           0 :     m_socket->abort();
     125                 :             :   }
     126                 :           8 :   resetSessionState();
     127                 :             : 
     128                 :           8 :   m_config = config;
     129                 :           8 :   m_from = from;
     130                 :           8 :   m_recipients = recipients;
     131                 :           8 :   m_message = message;
     132                 :             : 
     133   [ +  -  +  - ]:           8 :   emit statusMessage(tr("Connecting to SMTP server..."));
     134                 :             : 
     135                 :             :   // T-612/SEC-11: Start connection timeout (30 seconds)
     136                 :           8 :   m_timeoutTimer->start(30 * 1000);
     137                 :             : 
     138         [ +  + ]:           8 :   if (config.security == QLatin1String("ssl")) {
     139                 :           2 :     m_state = State::Connecting;
     140         [ +  - ]:           2 :     m_socket->connectToHostEncrypted(config.host, config.port);
     141                 :             :   } else {
     142                 :           6 :     m_state = State::WaitGreeting;
     143         [ +  - ]:           6 :     m_socket->connectToHost(config.host, config.port);
     144                 :             :   }
     145                 :           8 : }
     146                 :             : 
     147                 :           5 : void SmtpService::onConnected() {
     148   [ +  -  +  -  :          10 :   qCInfo(lcSmtp) << "Connected to" << m_config.host << ":" << m_config.port;
          +  -  +  -  +  
             -  +  -  +  
                      + ]
     149                 :             :   // For plain/STARTTLS, wait for the server greeting
     150                 :           5 : }
     151                 :             : 
     152                 :           0 : void SmtpService::onEncrypted() {
     153   [ #  #  #  #  :           0 :   qCInfo(lcSmtp) << "TLS encrypted";
             #  #  #  # ]
     154         [ #  # ]:           0 :   if (m_state == State::Connecting) {
     155                 :             :     // Direct SSL: wait for greeting after encryption
     156                 :           0 :     m_state = State::WaitGreeting;
     157         [ #  # ]:           0 :   } else if (m_state == State::WaitStartTls) {
     158                 :             :     // STARTTLS upgrade done, send EHLO again
     159                 :           0 :     m_state = State::WaitEhloAfterTls;
     160                 :           0 :     m_ehloCapabilities = {};
     161   [ #  #  #  #  :           0 :     sendCommand(QStringLiteral("EHLO ") + QHostInfo::localHostName());
                   #  # ]
     162                 :             :   }
     163                 :           0 : }
     164                 :             : 
     165                 :          34 : void SmtpService::onReadyRead() {
     166   [ -  +  -  + ]:          34 :   if (!m_socket->canReadLine() &&
     167         [ #  # ]:           0 :       m_socket->bytesAvailable() >= MaxResponseBufferSize) {
     168                 :           0 :     failResponseTooLarge();
     169                 :           0 :     return;
     170                 :             :   }
     171                 :             : 
     172         [ +  + ]:          77 :   while (m_socket->canReadLine()) {
     173   [ +  -  +  -  :          43 :     QString line = QString::fromUtf8(m_socket->readLine()).trimmed();
                   +  - ]
     174   [ +  -  +  -  :          86 :     qCDebug(lcSmtp) << "S:" << line;
          +  -  +  -  +  
                      + ]
     175                 :             : 
     176                 :             :     // Multi-line responses: 250-... continues, 250 ... is final
     177   [ +  -  +  -  :          43 :     if (line.length() >= 4 && line[3] == '-') {
             +  +  +  + ]
     178   [ +  -  +  -  :           9 :       if (!appendResponseText(line + QLatin1Char('\n')))
                   -  + ]
     179                 :           0 :         return;
     180                 :           9 :       continue;
     181                 :             :     }
     182   [ +  -  -  + ]:          34 :     if (!appendResponseText(line))
     183                 :           0 :       return;
     184                 :          34 :     const QString response = m_responseBuffer;
     185                 :          34 :     m_responseBuffer.clear();
     186         [ +  - ]:          34 :     processResponse(response);
     187      [ +  -  + ]:          43 :   }
     188                 :             : }
     189                 :             : 
     190                 :          44 : bool SmtpService::appendResponseText(const QString &text) {
     191         [ +  + ]:          44 :   if (m_responseBuffer.size() + text.size() > MaxResponseBufferSize) {
     192                 :           1 :     failResponseTooLarge();
     193                 :           1 :     return false;
     194                 :             :   }
     195                 :          43 :   m_responseBuffer += text;
     196                 :          43 :   return true;
     197                 :             : }
     198                 :             : 
     199                 :           1 : void SmtpService::failResponseTooLarge() {
     200   [ +  -  +  -  :           2 :   qCWarning(lcSmtp) << "SMTP response exceeded buffer limit";
             +  -  +  + ]
     201                 :           1 :   m_responseBuffer.clear();
     202                 :           1 :   m_timeoutTimer->stop();
     203                 :           1 :   m_socket->abort();
     204                 :           1 :   m_state = State::Disconnected;
     205         [ +  - ]:           1 :   emit sendFailed(QStringLiteral("SMTP response too large"));
     206                 :           1 : }
     207                 :             : 
     208                 :           3 : void SmtpService::onError(QAbstractSocket::SocketError error) {
     209                 :             :   Q_UNUSED(error)
     210         [ +  - ]:           3 :   m_timeoutTimer->stop(); // T-612: Cancel timeout on socket error
     211         [ +  - ]:           3 :   QString errMsg = m_socket->errorString();
     212   [ +  -  +  -  :           6 :   qCWarning(lcSmtp) << "Socket error:" << errMsg;
          +  -  +  -  +  
                      + ]
     213                 :           3 :   m_state = State::Disconnected;
     214         [ +  - ]:           3 :   emit sendFailed(errMsg);
     215                 :           3 : }
     216                 :             : 
     217                 :          51 : void SmtpService::processResponse(const QString &line) {
     218                 :             :   // T-612/SEC-11: Restart timeout on each server response.
     219                 :             :   // Use longer timeout for DATA phase (uploading message body).
     220         [ +  + ]:          51 :   int timeoutMs = (m_state == State::WaitDataContent) ? 120 * 1000 : 60 * 1000;
     221                 :          51 :   m_timeoutTimer->start(timeoutMs);
     222                 :             : 
     223   [ +  -  +  - ]:          51 :   int code = line.left(3).toInt();
     224                 :             : 
     225   [ +  +  +  +  :          51 :   switch (m_state) {
          +  +  +  +  +  
             +  +  +  + ]
     226                 :           5 :   case State::WaitGreeting:
     227         [ +  + ]:           5 :     if (code == 220) {
     228                 :           4 :       m_state = State::WaitEhlo;
     229                 :             :       // T-516/L1: Use actual hostname instead of generic 'localhost'
     230   [ +  -  +  -  :           8 :       sendCommand(QStringLiteral("EHLO ") + QHostInfo::localHostName());
                   +  - ]
     231                 :             :     } else {
     232   [ +  -  +  - ]:           1 :       emit sendFailed(QStringLiteral("Unexpected greeting: ") + line);
     233                 :           1 :       m_socket->close();
     234                 :           1 :       m_state = State::Disconnected;
     235                 :             :     }
     236                 :           5 :     break;
     237                 :             : 
     238                 :           6 :   case State::WaitEhlo:
     239         [ +  + ]:           6 :     if (code == 250) {
     240         [ +  - ]:           5 :       m_ehloCapabilities = parseEhloCapabilities(line);
     241         [ +  + ]:           5 :       if (m_config.security == QLatin1String("starttls")) {
     242         [ +  - ]:           1 :         if (!m_ehloCapabilities.supportsStartTls) {
     243         [ +  - ]:           1 :           emit sendFailed(QStringLiteral("Server does not support STARTTLS"));
     244                 :           1 :           m_socket->close();
     245                 :           1 :           m_state = State::Disconnected;
     246                 :           1 :           return;
     247                 :             :         }
     248                 :           0 :         m_state = State::WaitStartTls;
     249         [ #  # ]:           0 :         sendCommand(QStringLiteral("STARTTLS"));
     250                 :             :       } else {
     251                 :           4 :         beginAuthLogin();
     252                 :             :       }
     253                 :             :     } else {
     254   [ +  -  +  - ]:           1 :       emit sendFailed(QStringLiteral("EHLO failed: ") + line);
     255                 :           1 :       m_socket->close();
     256                 :           1 :       m_state = State::Disconnected;
     257                 :             :     }
     258                 :           5 :     break;
     259                 :             : 
     260                 :           2 :   case State::WaitStartTls:
     261         [ +  + ]:           2 :     if (code == 220) {
     262   [ -  +  -  -  :           1 :       if (!m_responseBuffer.isEmpty() || m_socket->bytesAvailable() > 0) {
                   +  - ]
     263   [ +  -  +  -  :           2 :         qCWarning(lcSmtp) << "Unexpected data before TLS handshake";
             +  -  +  + ]
     264         [ +  - ]:           1 :         emit sendFailed(
     265                 :           2 :             QStringLiteral("Unexpected data before TLS handshake"));
     266                 :           1 :         m_socket->abort();
     267                 :           1 :         m_state = State::Disconnected;
     268                 :           1 :         return;
     269                 :             :       }
     270                 :             :       // Server accepted STARTTLS, start TLS handshake
     271                 :           0 :       m_socket->startClientEncryption();
     272                 :             :       // onEncrypted() will fire
     273                 :             :     } else {
     274   [ +  -  +  - ]:           1 :       emit sendFailed(QStringLiteral("STARTTLS failed: ") + line);
     275                 :           1 :       m_socket->close();
     276                 :           1 :       m_state = State::Disconnected;
     277                 :             :     }
     278                 :           1 :     break;
     279                 :             : 
     280                 :           1 :   case State::WaitEhloAfterTls:
     281         [ +  - ]:           1 :     if (code == 250) {
     282         [ +  - ]:           1 :       m_ehloCapabilities = parseEhloCapabilities(line);
     283                 :             :       // T-506: Verify encryption after STARTTLS before AUTH
     284         [ +  - ]:           1 :       if (!m_socket->isEncrypted()) {
     285   [ +  -  +  -  :           2 :         qCWarning(lcSmtp) << "STARTTLS succeeded but connection not encrypted";
             +  -  +  + ]
     286         [ +  - ]:           1 :         emit sendFailed(QStringLiteral("TLS negotiation failed"));
     287                 :           1 :         m_socket->close();
     288                 :           1 :         m_state = State::Disconnected;
     289                 :           1 :         return;
     290                 :             :       }
     291                 :           0 :       beginAuthLogin();
     292                 :             :     } else {
     293   [ #  #  #  # ]:           0 :       emit sendFailed(QStringLiteral("EHLO after TLS failed: ") + line);
     294                 :           0 :       m_socket->close();
     295                 :           0 :       m_state = State::Disconnected;
     296                 :             :     }
     297                 :           0 :     break;
     298                 :             : 
     299                 :           3 :   case State::WaitAuth:
     300         [ +  - ]:           3 :     if (code == 334) {
     301                 :             :       // Server requests username (Base64 encoded "Username:")
     302                 :           3 :       m_state = State::WaitAuthUser;
     303   [ +  -  +  -  :           6 :       qCDebug(lcSmtp) << "C: <base64-username>";
             +  -  +  + ]
     304         [ +  - ]:           6 :       m_socket->write(
     305   [ +  -  +  -  :           9 :           (QString::fromUtf8(m_config.username.toUtf8().toBase64()) +
                   +  - ]
     306         [ +  - ]:           9 :            QStringLiteral("\r\n"))
     307         [ +  - ]:           9 :               .toUtf8());
     308                 :             :     } else {
     309   [ #  #  #  # ]:           0 :       emit sendFailed(QStringLiteral("AUTH LOGIN failed: ") + line);
     310                 :           0 :       m_socket->close();
     311                 :           0 :       m_state = State::Disconnected;
     312                 :             :     }
     313                 :           3 :     break;
     314                 :             : 
     315                 :           4 :   case State::WaitAuthUser:
     316         [ +  - ]:           4 :     if (code == 334) {
     317                 :             :       // Server requests password (Base64 encoded "Password:")
     318                 :           4 :       m_state = State::WaitAuthPass;
     319   [ +  -  +  -  :           8 :       qCDebug(lcSmtp) << "C: <base64-password>";
             +  -  +  + ]
     320         [ +  - ]:           8 :       m_socket->write(
     321   [ +  -  +  - ]:           8 :           (QString::fromUtf8(m_config.password.toBase64()) +
     322         [ +  - ]:          12 :            QStringLiteral("\r\n"))
     323         [ +  - ]:          12 :               .toUtf8());
     324                 :             :     } else {
     325   [ #  #  #  # ]:           0 :       emit sendFailed(QStringLiteral("AUTH username rejected: ") + line);
     326                 :           0 :       m_socket->close();
     327                 :           0 :       m_state = State::Disconnected;
     328                 :             :     }
     329                 :           4 :     break;
     330                 :             : 
     331                 :           4 :   case State::WaitAuthPass:
     332                 :             :     // SEC-22: Zero password from memory after AUTH (success or failure)
     333                 :           4 :     SecureUtil::zeroMemory(m_config.password);
     334         [ +  + ]:           4 :     if (code == 235) {
     335                 :             :       // Auth successful, start MAIL FROM
     336   [ +  -  +  - ]:           3 :       emit statusMessage(tr("Authenticated, sending mail..."));
     337                 :           3 :       m_state = State::WaitMailFrom;
     338                 :             :       // T-400/Bug 26: Sanitize address to prevent SMTP command injection
     339                 :           3 :       QString safeFrom = m_from;
     340         [ +  - ]:           3 :       safeFrom.remove('\r');
     341         [ +  - ]:           3 :       safeFrom.remove('\n');
     342         [ +  - ]:           3 :       sendCommand(
     343         [ +  - ]:           9 :           QStringLiteral("MAIL FROM:<%1>").arg(safeFrom));
     344                 :           3 :     } else {
     345         [ +  - ]:           2 :       emit sendFailed(QStringLiteral("Authentifizierung fehlgeschlagen: ") +
     346         [ +  - ]:           1 :                        line);
     347                 :           1 :       m_socket->close();
     348                 :           1 :       m_state = State::Disconnected;
     349                 :             :     }
     350                 :           4 :     break;
     351                 :             : 
     352                 :           4 :   case State::WaitMailFrom:
     353         [ +  + ]:           4 :     if (code == 250) {
     354                 :           3 :       m_rcptIndex = 0;
     355                 :           3 :       nextRcptTo();
     356                 :             :     } else {
     357   [ +  -  +  - ]:           1 :       emit sendFailed(QStringLiteral("MAIL FROM rejected: ") + line);
     358                 :           1 :       m_socket->close();
     359                 :           1 :       m_state = State::Disconnected;
     360                 :             :     }
     361                 :           4 :     break;
     362                 :             : 
     363                 :           8 :   case State::WaitRcptTo:
     364         [ +  + ]:           8 :     if (code == 250) {
     365                 :           7 :       m_rcptIndex++;
     366                 :           7 :       nextRcptTo();
     367                 :             :     } else {
     368   [ +  -  +  - ]:           1 :       emit sendFailed(QStringLiteral("RCPT TO rejected: ") + line);
     369                 :           1 :       m_socket->close();
     370                 :           1 :       m_state = State::Disconnected;
     371                 :             :     }
     372                 :           8 :     break;
     373                 :             : 
     374                 :           4 :   case State::WaitData:
     375         [ +  + ]:           4 :     if (code == 354) {
     376                 :             :       // T-400/Bug 3: RFC 5321 §4.5.2 dot-stuffing
     377                 :             :       // Lines starting with '.' must be prefixed with an extra '.'
     378         [ +  - ]:           3 :       QList<QByteArray> lines = m_message.split('\n');
     379   [ +  -  +  -  :          21 :       for (const QByteArray &line : lines) {
                   +  + ]
     380                 :          18 :         QByteArray trimmed = line;
     381                 :             :         // Remove trailing \r if present (we re-add CRLF)
     382   [ +  -  +  + ]:          18 :         if (trimmed.endsWith('\r'))
     383         [ +  - ]:          15 :           trimmed.chop(1);
     384   [ +  -  -  + ]:          18 :         if (trimmed.startsWith('.')) {
     385   [ #  #  #  #  :           0 :           m_socket->write("." + trimmed + "\r\n");
                   #  # ]
     386                 :             :         } else {
     387   [ +  -  +  - ]:          18 :           m_socket->write(trimmed + "\r\n");
     388                 :             :         }
     389                 :          18 :       }
     390         [ +  - ]:           3 :       m_socket->write(".\r\n");
     391                 :           3 :       m_state = State::WaitDataContent;
     392                 :           3 :     } else {
     393   [ +  -  +  - ]:           1 :       emit sendFailed(QStringLiteral("DATA rejected: ") + line);
     394                 :           1 :       m_socket->close();
     395                 :           1 :       m_state = State::Disconnected;
     396                 :             :     }
     397                 :           4 :     break;
     398                 :             : 
     399                 :           5 :   case State::WaitDataContent:
     400         [ +  + ]:           5 :     if (code == 250) {
     401   [ +  -  +  - ]:           4 :       emit statusMessage(tr("Mail sent successfully!"));
     402                 :           4 :       m_state = State::WaitQuit;
     403         [ +  - ]:           4 :       sendCommand(QStringLiteral("QUIT"));
     404                 :             :     } else {
     405   [ +  -  +  - ]:           1 :       emit sendFailed(QStringLiteral("Message rejected: ") + line);
     406                 :           1 :       m_socket->close();
     407                 :           1 :       m_state = State::Disconnected;
     408                 :             :     }
     409                 :           5 :     break;
     410                 :             : 
     411                 :           4 :   case State::WaitQuit:
     412                 :             :     // 221 or whatever, we're done
     413                 :           4 :     m_timeoutTimer->stop(); // T-612: Stop timeout on success
     414                 :           4 :     m_socket->close();
     415                 :           4 :     m_state = State::Disconnected;
     416                 :           4 :     emit sendSuccess();
     417                 :           4 :     break;
     418                 :             : 
     419                 :           1 :   default:
     420                 :           1 :     break;
     421                 :             :   }
     422                 :             : }
     423                 :             : 
     424                 :           6 : bool SmtpService::beginAuthLogin() {
     425         [ +  + ]:          12 :   if (!m_ehloCapabilities.authMechanisms.contains(QStringLiteral("LOGIN"))) {
     426         [ +  - ]:           1 :     emit sendFailed(QStringLiteral("Server does not support AUTH LOGIN"));
     427                 :           1 :     m_socket->close();
     428                 :           1 :     m_state = State::Disconnected;
     429                 :           1 :     return false;
     430                 :             :   }
     431                 :             : 
     432                 :             :   // T-506: Verify encryption before sending credentials
     433                 :             :   // (skip check for security=none — plaintext AUTH is intentional)
     434   [ +  +  +  -  :           5 :   if (m_config.security != QLatin1String("none") && !m_socket->isEncrypted()) {
             +  -  +  + ]
     435   [ +  -  +  -  :           2 :     qCWarning(lcSmtp) << "Refusing AUTH LOGIN over unencrypted connection";
             +  -  +  + ]
     436         [ +  - ]:           1 :     emit sendFailed(QStringLiteral("Connection is not encrypted"));
     437                 :           1 :     m_socket->close();
     438                 :           1 :     m_state = State::Disconnected;
     439                 :           1 :     return false;
     440                 :             :   }
     441                 :             : 
     442                 :           4 :   m_state = State::WaitAuth;
     443         [ +  - ]:           4 :   sendCommand(QStringLiteral("AUTH LOGIN"));
     444                 :           4 :   return true;
     445                 :             : }
     446                 :             : 
     447                 :          25 : void SmtpService::sendCommand(const QString &cmd) {
     448   [ +  -  +  -  :          50 :   qCDebug(lcSmtp) << "C:" << (cmd.startsWith("AUTH") ? "AUTH ***" : cmd);
          +  -  +  -  +  
          -  +  +  +  -  
             +  -  +  + ]
     449   [ +  -  +  -  :          25 :   m_socket->write((cmd + "\r\n").toUtf8());
                   +  - ]
     450                 :          25 : }
     451                 :             : 
     452                 :          10 : void SmtpService::nextRcptTo() {
     453         [ +  + ]:          10 :   if (m_rcptIndex < m_recipients.size()) {
     454                 :           6 :     m_state = State::WaitRcptTo;
     455                 :             :     // T-400/Bug 26: Sanitize recipient to prevent SMTP command injection
     456         [ +  - ]:           6 :     QString safeRcpt = m_recipients[m_rcptIndex];
     457         [ +  - ]:           6 :     safeRcpt.remove('\r');
     458         [ +  - ]:           6 :     safeRcpt.remove('\n');
     459         [ +  - ]:           6 :     sendCommand(
     460         [ +  - ]:          18 :         QStringLiteral("RCPT TO:<%1>").arg(safeRcpt));
     461                 :           6 :   } else {
     462                 :             :     // All recipients sent, start DATA
     463                 :           4 :     m_state = State::WaitData;
     464         [ +  - ]:           4 :     sendCommand(QStringLiteral("DATA"));
     465                 :             :   }
     466                 :          10 : }
        

Generated by: LCOV version 2.0-1