Enable customizing the save statistics time interval
[qBittorrent.git] / src / base / net / smtp.cpp
blob94f8338100ee4934e675fd88b962066c29409cf8
1 /*
2 * Bittorrent Client using Qt and libtorrent.
3 * Copyright (C) 2011 Christophe Dumez <chris@qbittorrent.org>
5 * This program is free software; you can redistribute it and/or
6 * modify it under the terms of the GNU General Public License
7 * as published by the Free Software Foundation; either version 2
8 * of the License, or (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License
16 * along with this program; if not, write to the Free Software
17 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 * In addition, as a special exception, the copyright holders give permission to
20 * link this program with the OpenSSL project's "OpenSSL" library (or with
21 * modified versions of it that use the same license as the "OpenSSL" library),
22 * and distribute the linked executables. You must obey the GNU General Public
23 * License in all respects for all of the code used other than "OpenSSL". If you
24 * modify file(s), you may extend this exception to your version of the file(s),
25 * but you are not obligated to do so. If you do not wish to do so, delete this
26 * exception statement from your version.
30 * This code is based on QxtSmtp from libqxt (http://libqxt.org)
33 #include "smtp.h"
35 #include <QCryptographicHash>
36 #include <QDateTime>
37 #include <QDebug>
38 #include <QHostInfo>
39 #include <QStringList>
41 #ifndef QT_NO_OPENSSL
42 #include <QSslSocket>
43 #else
44 #include <QTcpSocket>
45 #endif
47 #include "base/global.h"
48 #include "base/logger.h"
49 #include "base/preferences.h"
50 #include "base/utils/string.h"
52 namespace
54 const short DEFAULT_PORT = 25;
55 #ifndef QT_NO_OPENSSL
56 const short DEFAULT_PORT_SSL = 465;
57 #endif
59 QByteArray hmacMD5(QByteArray key, const QByteArray &msg)
61 const int blockSize = 64; // HMAC-MD5 block size
62 if (key.length() > blockSize) // if key is longer than block size (64), reduce key length with MD5 compression
63 key = QCryptographicHash::hash(key, QCryptographicHash::Md5);
65 QByteArray innerPadding(blockSize, char(0x36)); // initialize inner padding with char "6"
66 QByteArray outerPadding(blockSize, char(0x5c)); // initialize outer padding with char "\"
67 // ascii characters 0x36 ("6") and 0x5c ("\") are selected because they have large
68 // Hamming distance (http://en.wikipedia.org/wiki/Hamming_distance)
70 for (int i = 0; i < key.length(); ++i)
72 innerPadding[i] = innerPadding[i] ^ key.at(i); // XOR operation between every byte in key and innerpadding, of key length
73 outerPadding[i] = outerPadding[i] ^ key.at(i); // XOR operation between every byte in key and outerpadding, of key length
76 // result = hash ( outerPadding CONCAT hash ( innerPadding CONCAT baseString ) ).toBase64
77 QByteArray total = outerPadding;
78 QByteArray part = innerPadding;
79 part.append(msg);
80 total.append(QCryptographicHash::hash(part, QCryptographicHash::Md5));
81 return QCryptographicHash::hash(total, QCryptographicHash::Md5);
84 QByteArray determineFQDN()
86 QString hostname = QHostInfo::localHostName();
87 if (hostname.isEmpty())
88 hostname = u"localhost"_s;
90 return hostname.toLocal8Bit();
93 bool canEncodeAsLatin1(const QStringView string)
95 return std::none_of(string.cbegin(), string.cend(), [](const QChar &ch)
97 return ch > QChar(0xff);
98 });
100 } // namespace
102 using namespace Net;
104 Smtp::Smtp(QObject *parent)
105 : QObject(parent)
107 static bool needToRegisterMetaType = true;
109 if (needToRegisterMetaType)
111 qRegisterMetaType<QAbstractSocket::SocketError>();
112 needToRegisterMetaType = false;
115 #ifndef QT_NO_OPENSSL
116 m_socket = new QSslSocket(this);
117 #else
118 m_socket = new QTcpSocket(this);
119 #endif
121 connect(m_socket, &QIODevice::readyRead, this, &Smtp::readyRead);
122 connect(m_socket, &QAbstractSocket::disconnected, this, &QObject::deleteLater);
123 connect(m_socket, &QAbstractSocket::errorOccurred, this, &Smtp::error);
125 // Test hmacMD5 function (http://www.faqs.org/rfcs/rfc2202.html)
126 Q_ASSERT(hmacMD5("Jefe", "what do ya want for nothing?").toHex()
127 == "750c783e6ab0b503eaa86e310a5db738");
128 Q_ASSERT(hmacMD5(QByteArray::fromHex("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b"), "Hi There").toHex()
129 == "9294727a3638bb1c13f48ef8158bfc9d");
132 Smtp::~Smtp()
134 qDebug() << Q_FUNC_INFO;
137 void Smtp::sendMail(const QString &from, const QString &to, const QString &subject, const QString &body)
139 const Preferences *const pref = Preferences::instance();
140 m_message = "Date: " + getCurrentDateTime().toLatin1() + "\r\n"
141 + encodeMimeHeader(u"From"_s, u"qBittorrent <%1>"_s.arg(from))
142 + encodeMimeHeader(u"Subject"_s, subject)
143 + encodeMimeHeader(u"To"_s, to)
144 + "MIME-Version: 1.0\r\n"
145 + "Content-Type: text/plain; charset=UTF-8\r\n"
146 + "Content-Transfer-Encoding: base64\r\n"
147 + "\r\n";
148 // Encode the body in base64
149 QString crlfBody = body;
150 const QByteArray b = crlfBody.replace(u"\n"_s, u"\r\n"_s).toUtf8().toBase64();
151 const int ct = b.length();
152 for (int i = 0; i < ct; i += 78)
153 m_message += b.mid(i, 78);
154 m_from = from;
155 m_rcpt = to;
156 // Authentication
157 if (pref->getMailNotificationSMTPAuth())
159 m_username = pref->getMailNotificationSMTPUsername();
160 m_password = pref->getMailNotificationSMTPPassword();
163 // Connect to SMTP server
164 const QStringList serverEndpoint = pref->getMailNotificationSMTP().split(u':');
165 const QString &serverAddress = serverEndpoint[0];
166 const std::optional<int> serverPort = Utils::String::parseInt(serverEndpoint.value(1));
168 #ifndef QT_NO_OPENSSL
169 if (pref->getMailNotificationSMTPSSL())
171 m_socket->connectToHostEncrypted(serverAddress, serverPort.value_or(DEFAULT_PORT_SSL));
172 m_useSsl = true;
174 else
176 #endif
177 m_socket->connectToHost(serverAddress, serverPort.value_or(DEFAULT_PORT));
178 m_useSsl = false;
179 #ifndef QT_NO_OPENSSL
181 #endif
184 void Smtp::readyRead()
186 qDebug() << Q_FUNC_INFO;
187 // SMTP is line-oriented
188 m_buffer += m_socket->readAll();
189 while (true)
191 const int pos = m_buffer.indexOf("\r\n");
192 if (pos < 0) return; // Loop exit condition
193 const QByteArray line = m_buffer.left(pos);
194 m_buffer.remove(0, (pos + 2));
195 qDebug() << "Response line:" << line;
196 // Extract response code
197 const QByteArray code = line.left(3);
199 switch (m_state)
201 case Init:
202 if (code[0] == '2')
204 // The server may send a multiline greeting/INIT/220 response.
205 // We wait until it finishes.
206 if (line[3] != ' ')
207 break;
208 // Connection was successful
209 ehlo();
211 else
213 logError(tr("Connection failed, unrecognized reply: %1").arg(QString::fromUtf8(line)));
214 m_state = Close;
216 break;
217 case EhloSent:
218 case HeloSent:
219 case EhloGreetReceived:
220 parseEhloResponse(code, (line[3] != ' '), QString::fromUtf8(line.mid(4)));
221 break;
222 #ifndef QT_NO_OPENSSL
223 case StartTLSSent:
224 if (code == "220")
226 m_socket->startClientEncryption();
227 ehlo();
229 else
231 authenticate();
233 break;
234 #endif
235 case AuthRequestSent:
236 case AuthUsernameSent:
237 if (m_authType == AuthPlain) authPlain();
238 else if (m_authType == AuthLogin) authLogin();
239 else authCramMD5(line.mid(4));
240 break;
241 case AuthSent:
242 case Authenticated:
243 if (code[0] == '2')
245 qDebug() << "Sending <mail from>...";
246 m_socket->write("mail from:<" + m_from.toLatin1() + ">\r\n");
247 m_socket->flush();
248 m_state = Rcpt;
250 else
252 // Authentication failed!
253 logError(tr("Authentication failed, msg: %1").arg(QString::fromUtf8(line)));
254 m_state = Close;
256 break;
257 case Rcpt:
258 if (code[0] == '2')
260 m_socket->write("rcpt to:<" + m_rcpt.toLatin1() + ">\r\n");
261 m_socket->flush();
262 m_state = Data;
264 else
266 logError(tr("<mail from> was rejected by server, msg: %1").arg(QString::fromUtf8(line)));
267 m_state = Close;
269 break;
270 case Data:
271 if (code[0] == '2')
273 m_socket->write("data\r\n");
274 m_socket->flush();
275 m_state = Body;
277 else
279 logError(tr("<Rcpt to> was rejected by server, msg: %1").arg(QString::fromUtf8(line)));
280 m_state = Close;
282 break;
283 case Body:
284 if (code[0] == '3')
286 m_socket->write(m_message + "\r\n.\r\n");
287 m_socket->flush();
288 m_state = Quit;
290 else
292 logError(tr("<data> was rejected by server, msg: %1").arg(QString::fromUtf8(line)));
293 m_state = Close;
295 break;
296 case Quit:
297 if (code[0] == '2')
299 m_socket->write("QUIT\r\n");
300 m_socket->flush();
301 // here, we just close.
302 m_state = Close;
304 else
306 logError(tr("Message was rejected by the server, error: %1").arg(QString::fromUtf8(line)));
307 m_state = Close;
309 break;
310 default:
311 qDebug() << "Disconnecting from host";
312 m_socket->disconnectFromHost();
313 return;
318 QByteArray Smtp::encodeMimeHeader(const QString &key, const QString &value, const QByteArray &prefix)
320 QByteArray rv = "";
321 QByteArray line = key.toLatin1() + ": ";
322 if (!prefix.isEmpty()) line += prefix;
323 if (!value.contains(u"=?") && canEncodeAsLatin1(value))
325 bool firstWord = true;
326 for (const QByteArray &word : asConst(value.toLatin1().split(' ')))
328 if (line.size() > 78)
330 rv = rv + line + "\r\n";
331 line.clear();
333 if (firstWord)
334 line += word;
335 else
336 line += ' ' + word;
337 firstWord = false;
340 else
342 // The text cannot be losslessly encoded as Latin-1. Therefore, we
343 // must use base64 encoding.
344 const QByteArray utf8 = value.toUtf8();
345 // Use base64 encoding
346 const QByteArray base64 = utf8.toBase64();
347 const int ct = base64.length();
348 line += "=?utf-8?b?";
349 for (int i = 0; i < ct; i += 4)
351 /*if (line.length() > 72)
353 rv += line + "?\n\r";
354 line = " =?utf-8?b?";
356 line = line + base64.mid(i, 4);
358 line += "?="; // end encoded-word atom
360 return rv + line + "\r\n";
363 void Smtp::ehlo()
365 const QByteArray address = determineFQDN();
366 m_socket->write("ehlo " + address + "\r\n");
367 m_socket->flush();
368 m_state = EhloSent;
371 void Smtp::helo()
373 const QByteArray address = determineFQDN();
374 m_socket->write("helo " + address + "\r\n");
375 m_socket->flush();
376 m_state = HeloSent;
379 void Smtp::parseEhloResponse(const QByteArray &code, const bool continued, const QString &line)
381 if (code != "250")
383 // Error
384 if (m_state == EhloSent)
386 // try to send HELO instead of EHLO
387 qDebug() << "EHLO failed, trying HELO instead...";
388 helo();
390 else
392 // Both EHLO and HELO failed, chances are this is NOT
393 // a SMTP server
394 logError(tr("Both EHLO and HELO failed, msg: %1").arg(line));
395 m_state = Close;
397 return;
400 if (m_state != EhloGreetReceived)
402 if (!continued)
404 // greeting only, no extensions
405 qDebug() << "No extension";
406 m_state = EhloDone;
408 else
410 // greeting followed by extensions
411 m_state = EhloGreetReceived;
412 qDebug() << "EHLO greet received";
413 return;
416 else
418 qDebug() << Q_FUNC_INFO << "Supported extension: " << line.section(u' ', 0, 0).toUpper()
419 << line.section(u' ', 1);
420 m_extensions[line.section(u' ', 0, 0).toUpper()] = line.section(u' ', 1);
421 if (!continued)
422 m_state = EhloDone;
425 if (m_state != EhloDone) return;
427 if (m_extensions.contains(u"STARTTLS"_s) && m_useSsl)
429 qDebug() << "STARTTLS";
430 startTLS();
432 else
434 authenticate();
438 void Smtp::authenticate()
440 qDebug() << Q_FUNC_INFO;
441 if (!m_extensions.contains(u"AUTH"_s) ||
442 m_username.isEmpty() || m_password.isEmpty())
444 // Skip authentication
445 qDebug() << "Skipping authentication...";
446 m_state = Authenticated;
447 // At this point the server will not send any response
448 // So fill the buffer with a fake one to pass the tests
449 // in readyRead()
450 m_buffer.push_front("250 QBT FAKE RESPONSE\r\n");
451 return;
453 // AUTH extension is supported, check which
454 // authentication modes are supported by
455 // the server
456 const QStringList auth = m_extensions[u"AUTH"_s].toUpper().split(u' ', Qt::SkipEmptyParts);
457 if (auth.contains(u"CRAM-MD5"))
459 qDebug() << "Using CRAM-MD5 authentication...";
460 authCramMD5();
462 else if (auth.contains(u"PLAIN"))
464 qDebug() << "Using PLAIN authentication...";
465 authPlain();
467 else if (auth.contains(u"LOGIN"))
469 qDebug() << "Using LOGIN authentication...";
470 authLogin();
472 else
474 // Skip authentication
475 logError(tr("The SMTP server does not seem to support any of the authentications modes "
476 "we support [CRAM-MD5|PLAIN|LOGIN], skipping authentication, "
477 "knowing it is likely to fail... Server Auth Modes: %1").arg(auth.join(u'|')));
478 m_state = Authenticated;
479 // At this point the server will not send any response
480 // So fill the buffer with a fake one to pass the tests
481 // in readyRead()
482 m_buffer.push_front("250 QBT FAKE RESPONSE\r\n");
486 void Smtp::startTLS()
488 qDebug() << Q_FUNC_INFO;
489 #ifndef QT_NO_OPENSSL
490 m_socket->write("starttls\r\n");
491 m_socket->flush();
492 m_state = StartTLSSent;
493 #else
494 authenticate();
495 #endif
498 void Smtp::authCramMD5(const QByteArray &challenge)
500 if (m_state != AuthRequestSent)
502 m_socket->write("auth cram-md5\r\n");
503 m_socket->flush();
504 m_authType = AuthCramMD5;
505 m_state = AuthRequestSent;
507 else
509 const QByteArray response = m_username.toLatin1() + ' '
510 + hmacMD5(m_password.toLatin1(), QByteArray::fromBase64(challenge)).toHex();
511 m_socket->write(response.toBase64() + "\r\n");
512 m_socket->flush();
513 m_state = AuthSent;
517 void Smtp::authPlain()
519 if (m_state != AuthRequestSent)
521 m_authType = AuthPlain;
522 // Prepare Auth string
523 QByteArray auth;
524 auth += '\0';
525 auth += m_username.toLatin1();
526 qDebug() << "username: " << m_username.toLatin1();
527 auth += '\0';
528 auth += m_password.toLatin1();
529 qDebug() << "password: " << m_password.toLatin1();
530 // Send it
531 m_socket->write("auth plain " + auth.toBase64() + "\r\n");
532 m_socket->flush();
533 m_state = AuthSent;
537 void Smtp::authLogin()
539 if ((m_state != AuthRequestSent) && (m_state != AuthUsernameSent))
541 m_socket->write("auth login\r\n");
542 m_socket->flush();
543 m_authType = AuthLogin;
544 m_state = AuthRequestSent;
546 else if (m_state == AuthRequestSent)
548 m_socket->write(m_username.toLatin1().toBase64() + "\r\n");
549 m_socket->flush();
550 m_state = AuthUsernameSent;
552 else
554 m_socket->write(m_password.toLatin1().toBase64() + "\r\n");
555 m_socket->flush();
556 m_state = AuthSent;
560 void Smtp::logError(const QString &msg)
562 qDebug() << "Email Notification Error:" << msg;
563 LogMsg(tr("Email Notification Error: %1").arg(msg), Log::WARNING);
566 QString Smtp::getCurrentDateTime() const
568 // return date & time in the format specified in RFC 2822, section 3.3
569 const QDateTime nowDateTime = QDateTime::currentDateTime();
570 const QDate nowDate = nowDateTime.date();
571 const QLocale eng(QLocale::English);
573 const QString timeStr = nowDateTime.time().toString(u"HH:mm:ss");
574 const QString weekDayStr = eng.dayName(nowDate.dayOfWeek(), QLocale::ShortFormat);
575 const QString dayStr = QString::number(nowDate.day());
576 const QString monthStr = eng.monthName(nowDate.month(), QLocale::ShortFormat);
577 const QString yearStr = QString::number(nowDate.year());
579 QDateTime tmp = nowDateTime;
580 tmp.setTimeSpec(Qt::UTC);
581 const int timeOffsetHour = nowDateTime.secsTo(tmp) / 3600;
582 const int timeOffsetMin = nowDateTime.secsTo(tmp) / 60 - (60 * timeOffsetHour);
583 const int timeOffset = timeOffsetHour * 100 + timeOffsetMin;
584 // buf size = 11 to avoid format truncation warnings from snprintf
585 char buf[11] = {0};
586 std::snprintf(buf, sizeof(buf), "%+05d", timeOffset);
587 const auto timeOffsetStr = QString::fromUtf8(buf);
589 const QString ret = weekDayStr + u", " + dayStr + u' ' + monthStr + u' ' + yearStr + u' ' + timeStr + u' ' + timeOffsetStr;
590 return ret;
593 void Smtp::error(QAbstractSocket::SocketError socketError)
595 // Getting a remote host closed error is apparently normal, even when successfully sending
596 // an email
597 if (socketError != QAbstractSocket::RemoteHostClosedError)
598 logError(m_socket->errorString());