KSMTP

session.cpp
1/*
2 SPDX-FileCopyrightText: 2010 BetterInbox <contact@betterinbox.com>
3 SPDX-FileContributor: Christophe Laveault <christophe@betterinbox.com>
4 SPDX-FileContributor: Gregory Schlomoff <gregory.schlomoff@gmail.com>
5
6 SPDX-License-Identifier: LGPL-2.1-or-later
7*/
8
9#include "session.h"
10#include "job.h"
11#include "ksmtp_debug.h"
12#include "loginjob.h"
13#include "sendjob.h"
14#include "serverresponse_p.h"
15#include "session_p.h"
16#include "sessionthread_p.h"
17
18#include <QHostAddress>
19#include <QHostInfo>
20#include <QPointer>
21#include <QUrl>
22
23using namespace KSmtp;
24
25Q_DECLARE_METATYPE(QSsl::SslProtocol)
26Q_DECLARE_METATYPE(KSslErrorUiData)
27
28SessionPrivate::SessionPrivate(Session *session)
29 : QObject(session)
30 , q(session)
31{
32 qRegisterMetaType<QSsl::SslProtocol>();
33 qRegisterMetaType<KSslErrorUiData>();
34}
35
36SessionPrivate::~SessionPrivate()
37{
38 m_thread->quit();
39 m_thread->wait(10000);
40 delete m_thread;
41}
42
43void SessionPrivate::handleSslError(const KSslErrorUiData &data)
44{
45 QPointer<SessionThread> _t = m_thread;
46 const bool ignore = m_uiProxy && m_uiProxy->ignoreSslError(data);
47 if (_t) {
48 _t->handleSslErrorResponse(ignore);
49 }
50}
51
52void SessionPrivate::setAuthenticationMethods(const QList<QByteArray> &authMethods)
53{
54 for (const QByteArray &method : authMethods) {
55 QString m = QString::fromLatin1(method);
56 if (!m_authModes.contains(m)) {
57 m_authModes.append(m);
58 }
59 }
60}
61
62void SessionPrivate::startHandshake()
63{
64 QString hostname = m_customHostname;
65
66 if (hostname.isEmpty()) {
67 // FIXME: QHostInfo::fromName can get a FQDN, but does a DNS lookup
69 if (hostname.isEmpty()) {
70 hostname = QStringLiteral("localhost.invalid");
71 } else if (!hostname.contains(QLatin1Char('.'))) {
72 hostname += QStringLiteral(".localnet");
73 }
74 }
75
76 QByteArray cmd;
77 if (!m_ehloRejected) {
78 cmd = "EHLO ";
79 } else {
80 cmd = "HELO ";
81 }
82 setState(Session::Handshake);
83 sendData(cmd + QUrl::toAce(hostname));
84}
85
86Session::Session(const QString &hostName, quint16 port, QObject *parent)
87 : QObject(parent)
88 , d(new SessionPrivate(this))
89{
90 qRegisterMetaType<KSmtp::ServerResponse>("KSmtp::ServerResponse");
91
92 QHostAddress ip;
93 QString saneHostName = hostName;
94 if (ip.setAddress(hostName)) {
95 // saneHostName = QStringLiteral("[%1]").arg(hostName);
96 }
97
98 d->m_thread = new SessionThread(saneHostName, port, this);
99 d->m_thread->start();
100
101 connect(d->m_thread, &SessionThread::sslError, d, &SessionPrivate::handleSslError);
102}
103
104Session::~Session() = default;
105
107{
108 d->m_thread->setUseNetworkProxy(useProxy);
109}
110
111void Session::setUiProxy(const SessionUiProxy::Ptr &uiProxy)
112{
113 d->m_uiProxy = uiProxy;
114}
115
116SessionUiProxy::Ptr Session::uiProxy() const
117{
118 return d->m_uiProxy;
119}
120
122{
123 return d->m_thread->hostName();
124}
125
126quint16 Session::port() const
127{
128 return d->m_thread->port();
129}
130
131Session::State Session::state() const
132{
133 return d->m_state;
134}
135
137{
138 return d->m_encryptionMode;
139}
140
142{
143 d->m_encryptionMode = mode;
144}
145
147{
148 return d->m_allowsTls;
149}
150
152{
153 return d->m_allowsDsn;
154}
155
157{
158 return d->m_authModes;
159}
160
162{
163 return d->m_size;
164}
165
166void Session::setSocketTimeout(int ms)
167{
168 bool timerActive = d->m_socketTimer.isActive();
169
170 if (timerActive) {
171 d->stopSocketTimer();
172 }
173
174 d->m_socketTimerInterval = ms;
175
176 if (timerActive) {
177 d->startSocketTimer();
178 }
179}
180
181int Session::socketTimeout() const
182{
183 return d->m_socketTimerInterval;
184}
185
187{
188 d->m_customHostname = hostname;
189}
190
191QString Session::customHostname() const
192{
193 return d->m_customHostname;
194}
195
197{
198 d->m_sslVersion = QSsl::UnknownProtocol;
199 d->m_thread->setConnectWithTls(d->m_encryptionMode == Session::TLS);
200 QTimer::singleShot(0, d->m_thread, &SessionThread::reconnect);
201 d->startSocketTimer();
202}
203
205{
206 if (d->m_state == Session::Disconnected) {
207 return;
208 }
209
210 d->setState(Quitting);
211 d->sendData("QUIT");
212}
213
214void SessionPrivate::setState(Session::State s)
215{
216 if (m_state == s) {
217 return;
218 }
219
220 m_state = s;
221 Q_EMIT q->stateChanged(m_state);
222}
223
224void SessionPrivate::sendData(const QByteArray &data)
225{
227 m_thread,
228 [this, data] {
229 m_thread->sendData(data);
230 },
232}
233
234void SessionPrivate::responseReceived(const ServerResponse &r)
235{
236 qCDebug(KSMTP_LOG) << "S:: [" << r.code() << "]" << (r.isMultiline() ? "-" : " ") << r.text();
237
238 if (m_state == Session::Quitting) {
239 m_thread->closeSocket();
240 return;
241 }
242
243 if (m_state == Session::Handshake) {
244 if (r.isCode(500) || r.isCode(502)) {
245 if (!m_ehloRejected) {
246 setState(Session::Ready);
247 m_ehloRejected = true;
248 } else {
249 qCWarning(KSMTP_LOG) << "KSmtp::Session: Handshake failed with both EHLO and HELO";
250 q->quit();
251 return;
252 }
253 } else if (r.isCode(25)) {
254 if (r.text().startsWith("SIZE ")) { // krazy:exclude=strings
255 m_size = r.text().remove(0, QByteArray("SIZE ").size()).toInt();
256 } else if (r.text() == "STARTTLS") {
257 m_allowsTls = true;
258 } else if (r.text().startsWith("AUTH ")) { // krazy:exclude=strings
259 setAuthenticationMethods(r.text().remove(0, QByteArray("AUTH ").size()).split(' '));
260 } else if (r.text() == "DSN") {
261 m_allowsDsn = true;
262 }
263
264 if (!r.isMultiline()) {
265 if (m_encryptionMode == Session::STARTTLS && m_sslVersion == QSsl::UnknownProtocol) {
266 if (m_allowsTls) {
267 m_starttlsSent = true;
268 sendData(QByteArrayLiteral("STARTTLS"));
269 } else {
270 qCWarning(KSMTP_LOG) << "STARTTLS not supported by the server!";
271 q->quit();
272 }
273 } else {
275 startNext();
276 }
277 }
278 } else if (r.isCode(220) && m_starttlsSent) { // STARTTLS accepted
279 m_starttlsSent = false;
280 startSsl();
281 }
282 }
283
284 if (m_state == Session::Ready) {
285 if (r.isCode(22) || m_ehloRejected) {
286 startHandshake();
287 return;
288 }
289 }
290
291 if (m_currentJob) {
292 m_currentJob->handleResponse(r);
293 }
294}
295
296void SessionPrivate::socketConnected()
297{
298 stopSocketTimer();
299 m_sslVersion = QSsl::UnknownProtocol;
300 setState(Session::Ready);
301}
302
303void SessionPrivate::socketDisconnected()
304{
305 qCDebug(KSMTP_LOG) << "Socket disconnected";
306 setState(Session::Disconnected);
307 m_thread->closeSocket();
308
309 if (m_currentJob) {
310 m_currentJob->connectionLost();
311 } else if (!m_queue.isEmpty()) {
312 m_currentJob = m_queue.takeFirst();
313 m_currentJob->connectionLost();
314 }
315
316 auto copy = m_queue;
317 qDeleteAll(copy);
318 m_queue.clear();
319}
320
321void SessionPrivate::startSsl()
322{
323 QMetaObject::invokeMethod(m_thread, &SessionThread::startSsl, Qt::QueuedConnection);
324}
325
326QSsl::SslProtocol SessionPrivate::negotiatedEncryption() const
327{
328 return m_sslVersion;
329}
330
331void SessionPrivate::encryptionNegotiationResult(bool encrypted, QSsl::SslProtocol version)
332{
333 if (encrypted) {
334 // Get the updated auth methods
335 startHandshake();
336 }
337
338 m_sslVersion = version;
339}
340
341void SessionPrivate::addJob(Job *job)
342{
343 m_queue.append(job);
344 // Q_EMIT q->jobQueueSizeChanged( q->jobQueueSize() );
345
346 connect(job, &KJob::result, this, &SessionPrivate::jobDone);
347 connect(job, &KJob::destroyed, this, &SessionPrivate::jobDestroyed);
348
349 if (m_state >= Session::NotAuthenticated) {
350 startNext();
351 } else {
352 m_thread->reconnect();
353 }
354}
355
356void SessionPrivate::startNext()
357{
358 QTimer::singleShot(0, this, [this]() {
359 doStartNext();
360 });
361}
362
363void SessionPrivate::doStartNext()
364{
365 if (m_queue.isEmpty() || m_jobRunning || m_state == Session::Disconnected) {
366 return;
367 }
368
369 startSocketTimer();
370 m_jobRunning = true;
371
372 m_currentJob = m_queue.dequeue();
373 m_currentJob->doStart();
374
375 // sending can take a while depending on bandwidth - don't fail with timeout
376 // if it takes longer
377 if (qobject_cast<const KSmtp::SendJob *>(m_currentJob)) {
378 stopSocketTimer();
379 }
380}
381
382void SessionPrivate::jobDone(KJob *job)
383{
384 Q_UNUSED(job)
385 Q_ASSERT(job == m_currentJob);
386
387 // If we're in disconnected state it's because we ended up
388 // here because the inactivity timer triggered, so no need to
389 // stop it (it is single shot)
390 if (m_state != Session::Disconnected) {
391 if (!qobject_cast<const KSmtp::SendJob *>(m_currentJob)) {
392 stopSocketTimer();
393 }
394 }
395
396 m_jobRunning = false;
397 m_currentJob = nullptr;
398 // Q_EMIT q->jobQueueSizeChanged( q->jobQueueSize() );
399 startNext();
400}
401
402void SessionPrivate::jobDestroyed(QObject *job)
403{
404 m_queue.removeAll(static_cast<Job *>(job));
405 if (m_currentJob == job) {
406 m_currentJob = nullptr;
407 }
408}
409
410void SessionPrivate::startSocketTimer()
411{
412 if (m_socketTimerInterval < 0) {
413 return;
414 }
415 Q_ASSERT(!m_socketTimer.isActive());
416
417 connect(&m_socketTimer, &QTimer::timeout, this, &SessionPrivate::onSocketTimeout);
418
419 m_socketTimer.setSingleShot(true);
420 m_socketTimer.start(m_socketTimerInterval);
421}
422
423void SessionPrivate::stopSocketTimer()
424{
425 if (m_socketTimerInterval < 0) {
426 return;
427 }
428 Q_ASSERT(m_socketTimer.isActive());
429
430 m_socketTimer.stop();
431 disconnect(&m_socketTimer, &QTimer::timeout, this, &SessionPrivate::onSocketTimeout);
432}
433
434void SessionPrivate::onSocketTimeout()
435{
436 socketDisconnected();
437}
438
439ServerResponse::ServerResponse(int code, const QByteArray &text, bool multiline)
440 : m_text(text)
441 , m_code(code)
442 , m_multiline(multiline)
443{
444}
445
446bool ServerResponse::isMultiline() const
447{
448 return m_multiline;
449}
450
451int ServerResponse::code() const
452{
453 return m_code;
454}
455
456QByteArray ServerResponse::text() const
457{
458 return m_text;
459}
460
461bool ServerResponse::isCode(int other) const
462{
463 int otherCpy = other;
464 int codeLength = 0;
465
466 if (other == 0) {
467 codeLength = 1;
468 } else {
469 while (otherCpy > 0) {
470 otherCpy /= 10;
471 codeLength++;
472 }
473 }
474
475 int div = 1;
476 for (int i = 0; i < 3 - codeLength; i++) {
477 div *= 10;
478 }
479
480 return m_code / div == other;
481}
482
483#include "moc_session_p.cpp"
484
485#include "moc_session.cpp"
void result(KJob *job)
The Job class.
Definition job.h:25
@ Disconnected
The Session is not connected to the server.
Definition session.h:29
@ Ready
(internal)
Definition session.h:30
@ Handshake
(internal)
Definition session.h:31
@ NotAuthenticated
The Session is ready for login.
Definition session.h:32
@ Quitting
(internal)
Definition session.h:34
void quit()
Requests the server to quit the connection.
Definition session.cpp:204
void setCustomHostname(const QString &hostname)
Custom hostname to send in EHLO/HELO command.
Definition session.cpp:186
void setEncryptionMode(EncryptionMode mode)
Sets the encryption mode for this session.
Definition session.cpp:141
bool allowsTls() const
Returns true if the SMTP server has indicated that it allows TLS connections, false otherwise.
Definition session.cpp:146
QString hostName() const
Returns the host name that has been provided in the Session's constructor.
Definition session.cpp:121
EncryptionMode encryptionMode() const
Returns the requested encryption mode for this session.
Definition session.cpp:136
void setUseNetworkProxy(bool useProxy)
Sets whether the SMTP network connection should use the system proxy settings.
Definition session.cpp:106
EncryptionMode
Transport encryption for a session.
Definition session.h:39
@ TLS
Use TLS encryption on the socket.
Definition session.h:41
@ STARTTLS
Use STARTTLS to upgrade an unencrypted connection to encrypted after the initial handshake.
Definition session.h:42
void open()
Opens the connection to the server.
Definition session.cpp:196
QStringList availableAuthModes() const
Definition session.cpp:156
int sizeLimit() const
Returns the maximum message size in bytes that the server accepts.
Definition session.cpp:161
quint16 port() const
Returns the port number that has been provided in the Session's constructor.
Definition session.cpp:126
bool allowsDsn() const
Returns true if the SMTP server has indicated that it allows Delivery Status Notification (DSN) suppo...
Definition session.cpp:151
KDB_EXPORT KDbVersionInfo version()
const QList< QKeySequence > & copy()
NETWORKMANAGERQT_EXPORT QString hostname()
void setAddress(quint32 ip4Addr)
QString localHostName()
bool invokeMethod(QObject *obj, const char *member, Qt::ConnectionType type, QGenericReturnArgument ret, QGenericArgument val0, QGenericArgument val1, QGenericArgument val2, QGenericArgument val3, QGenericArgument val4, QGenericArgument val5, QGenericArgument val6, QGenericArgument val7, QGenericArgument val8, QGenericArgument val9)
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
void destroyed(QObject *obj)
bool contains(QChar ch, Qt::CaseSensitivity cs) const const
QString fromLatin1(const char *str, int size)
bool isEmpty() const const
QueuedConnection
void timeout()
QByteArray toAce(const QString &domain)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Sun Feb 25 2024 18:45:08 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.