MailJDTourDownloadDocsQualityTestsCoverage

Which source lines and branches the tests execute. Lines 91.5 %, branches 75.0 % (minimum 85 % and 72 %). main @ 399ffe3d · 21 October 2026 · pipeline 7238.

GCC Code Coverage Report


Directory: src/
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 77.8% 7 / 0 / 9
Functions: 71.4% 5 / 0 / 7
Branches: -% 0 / 0 / 0

service/SmtpService.h
Line Branch Exec Source
1 #pragma once
2
3 #include "SmtpReplies.h"
4
5 #include <QObject>
6 #include <QSslSocket>
7 #include <QStringList>
8 #include <QTimer>
9
10 #include "data/AccountConfig.h"
11
12 // SmtpService handles sending emails via SMTP with STARTTLS or SSL.
13 // Asynchronous: emits sendSuccess/sendFailed signals.
14 class SmtpService : public QObject {
15 9 Q_OBJECT
16 #ifdef MAILJD_UNIT_TEST
17 friend class TestSmtpService;
18 friend class QuickComposeSafetyTest;
19 #endif
20
21 public:
22 // The DATA bytes for `message`: dot-stuffed lines ending in CRLF and the
23 // terminator line (RFC 5321 §4.5.2).
24 static QByteArray dataPayload(const QByteArray &message);
25 explicit SmtpService(QObject *parent = nullptr);
26 ~SmtpService() override;
27
28 // Shared structural validation for presentation and transport admission.
29 static bool isValidSmtpAddress(const QString &address);
30
31 // Send an email. The message should be a complete RFC 5322 message.
32 // from: envelope sender, recipients: all envelope recipients
33 void sendMail(const SmtpConfig &config, const QString &from,
34 const QStringList &recipients, const QByteArray &message);
35
36 // Sprint 108 S108.7.3 (D-20): with partial recipients, a permanently
37 // refused RCPT (5xx) is recorded and the message still goes to the
38 // accepted ones; only when every recipient is refused does the send fail.
39 // The outbox reads the refused addresses after sendSuccess().
40 9 void setPartialRecipients(bool on) { m_partialRecipients = on; }
41 ✗ QStringList refusedRecipients() const { return m_refusedRecipients; }
42 ✗ QString refusalReply() const { return m_refusalReply; }
43 // The reply code of the server line that ended the last attempt (0 for
44 // local failures: connection, TLS, timeout).
45 9 int lastReplyCode() const { return m_lastReplyCode; }
46
47 #ifdef MAILJD_E2E_TESTING
48 quint16 localPortForE2e() const {
49 return m_socket ? m_socket->localPort() : 0;
50 }
51 #endif
52
53 #ifdef MAILJD_UNIT_TEST
54 static bool ehloResponseSupportsAuthLoginForTest(const QString &response);
55 static bool ehloResponseSupportsStartTlsForTest(const QString &response);
56 #endif
57
58 signals:
59 void sendSuccess();
60 void sendFailed(const QString &error);
61 void statusMessage(const QString &message);
62 // Sprint 108 S108.2.5 (D-18): the server's RFC 1870 SIZE limit from the
63 // EHLO that precedes authentication (0 = none announced).
64 void sizeLimitAnnounced(const QString &host, qint64 maximumMessageBytes);
65 #ifdef MAILJD_E2E_TESTING
66 // Metadata-only boundary hook. It exposes neither SMTP commands,
67 // addresses, credentials, nor RFC message bytes.
68 void e2eTransportPhase(const QString &phase, quint64 attemptToken);
69 #endif
70
71 private slots:
72 void onConnected();
73 void onEncrypted();
74 void onReadyRead();
75 void onError(QAbstractSocket::SocketError error);
76
77 private:
78 enum class State {
79 Disconnected,
80 Connecting,
81 WaitGreeting,
82 WaitEhlo,
83 WaitStartTls,
84 WaitEhloAfterTls,
85 WaitAuth,
86 WaitAuthUser,
87 WaitAuthPass,
88 WaitAuthPlain,
89 WaitMailFrom,
90 WaitRcptTo,
91 WaitData,
92 WaitDataContent,
93 WaitQuit,
94 };
95
96 using EhloCapabilities = SmtpReplies::EhloCapabilities;
97 using AuthMechanism = SmtpReplies::AuthMechanism;
98
99 22 static EhloCapabilities parseEhloCapabilities(const QString &response) {
100 22 return SmtpReplies::parseEhlo(response);
101 }
102 7 static AuthMechanism selectAuthMechanism(const EhloCapabilities &capabilities) {
103 7 return SmtpReplies::selectAuthMechanism(capabilities);
104 }
105 void createSocketForAttempt();
106 void resetSessionState();
107 void clearCredentials();
108 void failSession(const QString &error);
109 void completeSession();
110 void failResponseTooLarge();
111 bool appendResponseText(const QString &text);
112 void processResponse(const QString &line);
113 bool beginAuthentication();
114 bool beginAuthLogin();
115 bool beginAuthPlain();
116 void sendAuthPlainPayload(bool initialResponse);
117 void authSucceeded();
118 void sendCommand(const QString &cmd);
119 #ifdef MAILJD_E2E_TESTING
120 void reportE2eTransportPhase(const QString &phase);
121 #endif
122 void nextRcptTo();
123
124 QSslSocket *m_socket = nullptr;
125 State m_state = State::Disconnected;
126 SmtpConfig m_config;
127 QString m_from;
128 QStringList m_recipients;
129 QByteArray m_message;
130 int m_rcptIndex = 0;
131 bool m_partialRecipients = false;
132 QStringList m_refusedRecipients;
133 QString m_refusalReply;
134 int m_lastReplyCode = 0;
135 QString m_responseBuffer;
136 EhloCapabilities m_ehloCapabilities;
137 bool m_attemptActive = false;
138 bool m_operationFinished = true;
139 bool m_plainChallengeAnswered = false;
140 quint64 m_attemptGeneration = 0;
141 QTimer *m_timeoutTimer = nullptr; // T-612/SEC-11: Timeout for each SMTP phase
142 static constexpr qint64 MaxResponseBufferSize = 64 * 1024;
143 static constexpr auto EhloClientName = "mailjd.invalid";
144 // F-01: watchdog intervals of the DATA upload. UploadInactivityTimeoutMs
145 // is restarted by every flushed chunk (bytesWritten) while payload is
146 // still buffered; UploadFinalAcceptTimeoutMs covers the server's own
147 // processing (virus/spam scan) after the terminating "." left the wire.
148 static constexpr int ResponseTimeoutMs = 60 * 1000;
149 static constexpr int UploadInactivityTimeoutMs = 60 * 1000;
150 static constexpr int UploadFinalAcceptTimeoutMs = 300 * 1000;
151 };
152