qBittorrent
smtp.cpp
Go to the documentation of this file.
1 /*
2  * Bittorrent Client using Qt and libtorrent.
3  * Copyright (C) 2011 Christophe Dumez <chris@qbittorrent.org>
4  *
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.
9  *
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.
14  *
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.
18  *
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.
27  */
28 
29 /*
30  * This code is based on QxtSmtp from libqxt (http://libqxt.org)
31  */
32 
33 #include "smtp.h"
34 
35 #include <QCryptographicHash>
36 #include <QDateTime>
37 #include <QDebug>
38 #include <QHostInfo>
39 #include <QStringList>
40 
41 #ifndef QT_NO_OPENSSL
42 #include <QSslSocket>
43 #else
44 #include <QTcpSocket>
45 #endif
46 
47 #include "base/global.h"
48 #include "base/logger.h"
49 #include "base/preferences.h"
50 
51 namespace
52 {
53  const short DEFAULT_PORT = 25;
54 #ifndef QT_NO_OPENSSL
55  const short DEFAULT_PORT_SSL = 465;
56 #endif
57 
58  QByteArray hmacMD5(QByteArray key, const QByteArray &msg)
59  {
60  const int blockSize = 64; // HMAC-MD5 block size
61  if (key.length() > blockSize) // if key is longer than block size (64), reduce key length with MD5 compression
62  key = QCryptographicHash::hash(key, QCryptographicHash::Md5);
63 
64  QByteArray innerPadding(blockSize, char(0x36)); // initialize inner padding with char "6"
65  QByteArray outerPadding(blockSize, char(0x5c)); // initialize outer padding with char "\"
66  // ascii characters 0x36 ("6") and 0x5c ("\") are selected because they have large
67  // Hamming distance (http://en.wikipedia.org/wiki/Hamming_distance)
68 
69  for (int i = 0; i < key.length(); ++i)
70  {
71  innerPadding[i] = innerPadding[i] ^ key.at(i); // XOR operation between every byte in key and innerpadding, of key length
72  outerPadding[i] = outerPadding[i] ^ key.at(i); // XOR operation between every byte in key and outerpadding, of key length
73  }
74 
75  // result = hash ( outerPadding CONCAT hash ( innerPadding CONCAT baseString ) ).toBase64
76  QByteArray total = outerPadding;
77  QByteArray part = innerPadding;
78  part.append(msg);
79  total.append(QCryptographicHash::hash(part, QCryptographicHash::Md5));
80  return QCryptographicHash::hash(total, QCryptographicHash::Md5);
81  }
82 
83  QByteArray determineFQDN()
84  {
85  QString hostname = QHostInfo::localHostName();
86  if (hostname.isEmpty())
87  hostname = "localhost";
88 
89  return hostname.toLocal8Bit();
90  }
91 
92  bool canEncodeAsLatin1(const QStringView string)
93  {
94  return std::none_of(string.cbegin(), string.cend(), [](const QChar &ch)
95  {
96  return ch > QChar(0xff);
97  });
98  }
99 } // namespace
100 
101 using namespace Net;
102 
103 Smtp::Smtp(QObject *parent)
104  : QObject(parent)
105  , m_state(Init)
106  , m_useSsl(false)
107  , m_authType(AuthPlain)
108 {
109  static bool needToRegisterMetaType = true;
110 
111  if (needToRegisterMetaType)
112  {
113  qRegisterMetaType<QAbstractSocket::SocketError>();
114  needToRegisterMetaType = false;
115  }
116 
117 #ifndef QT_NO_OPENSSL
118  m_socket = new QSslSocket(this);
119 #else
120  m_socket = new QTcpSocket(this);
121 #endif
122 
123  connect(m_socket, &QIODevice::readyRead, this, &Smtp::readyRead);
124  connect(m_socket, &QAbstractSocket::disconnected, this, &QObject::deleteLater);
125  connect(m_socket, &QAbstractSocket::errorOccurred, this, &Smtp::error);
126 
127  // Test hmacMD5 function (http://www.faqs.org/rfcs/rfc2202.html)
128  Q_ASSERT(hmacMD5("Jefe", "what do ya want for nothing?").toHex()
129  == "750c783e6ab0b503eaa86e310a5db738");
130  Q_ASSERT(hmacMD5(QByteArray::fromHex("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b"), "Hi There").toHex()
131  == "9294727a3638bb1c13f48ef8158bfc9d");
132 }
133 
135 {
136  qDebug() << Q_FUNC_INFO;
137 }
138 
139 void Smtp::sendMail(const QString &from, const QString &to, const QString &subject, const QString &body)
140 {
141  const Preferences *const pref = Preferences::instance();
142  m_message = "Date: " + getCurrentDateTime().toLatin1() + "\r\n"
143  + encodeMimeHeader("From", from)
144  + encodeMimeHeader("Subject", subject)
145  + encodeMimeHeader("To", to)
146  + "MIME-Version: 1.0\r\n"
147  + "Content-Type: text/plain; charset=UTF-8\r\n"
148  + "Content-Transfer-Encoding: base64\r\n"
149  + "\r\n";
150  // Encode the body in base64
151  QString crlfBody = body;
152  const QByteArray b = crlfBody.replace("\n", "\r\n").toUtf8().toBase64();
153  const int ct = b.length();
154  for (int i = 0; i < ct; i += 78)
155  m_message += b.mid(i, 78);
156  m_from = from;
157  m_rcpt = to;
158  // Authentication
159  if (pref->getMailNotificationSMTPAuth())
160  {
163  }
164 
165  // Connect to SMTP server
166 #ifndef QT_NO_OPENSSL
167  if (pref->getMailNotificationSMTPSSL())
168  {
169  m_socket->connectToHostEncrypted(pref->getMailNotificationSMTP(), DEFAULT_PORT_SSL);
170  m_useSsl = true;
171  }
172  else
173  {
174 #endif
175  m_socket->connectToHost(pref->getMailNotificationSMTP(), DEFAULT_PORT);
176  m_useSsl = false;
177 #ifndef QT_NO_OPENSSL
178  }
179 #endif
180 }
181 
183 {
184  qDebug() << Q_FUNC_INFO;
185  // SMTP is line-oriented
186  m_buffer += m_socket->readAll();
187  while (true)
188  {
189  const int pos = m_buffer.indexOf("\r\n");
190  if (pos < 0) return; // Loop exit condition
191  const QByteArray line = m_buffer.left(pos);
192  m_buffer = m_buffer.mid(pos + 2);
193  qDebug() << "Response line:" << line;
194  // Extract response code
195  const QByteArray code = line.left(3);
196 
197  switch (m_state)
198  {
199  case Init:
200  if (code[0] == '2')
201  {
202  // The server may send a multiline greeting/INIT/220 response.
203  // We wait until it finishes.
204  if (line[3] != ' ')
205  break;
206  // Connection was successful
207  ehlo();
208  }
209  else
210  {
211  logError(QLatin1String("Connection failed, unrecognized reply: ") + line);
212  m_state = Close;
213  }
214  break;
215  case EhloSent:
216  case HeloSent:
217  case EhloGreetReceived:
218  parseEhloResponse(code, line[3] != ' ', line.mid(4));
219  break;
220 #ifndef QT_NO_OPENSSL
221  case StartTLSSent:
222  if (code == "220")
223  {
224  m_socket->startClientEncryption();
225  ehlo();
226  }
227  else
228  {
229  authenticate();
230  }
231  break;
232 #endif
233  case AuthRequestSent:
234  case AuthUsernameSent:
235  if (m_authType == AuthPlain) authPlain();
236  else if (m_authType == AuthLogin) authLogin();
237  else authCramMD5(line.mid(4));
238  break;
239  case AuthSent:
240  case Authenticated:
241  if (code[0] == '2')
242  {
243  qDebug() << "Sending <mail from>...";
244  m_socket->write("mail from:<" + m_from.toLatin1() + ">\r\n");
245  m_socket->flush();
246  m_state = Rcpt;
247  }
248  else
249  {
250  // Authentication failed!
251  logError(QLatin1String("Authentication failed, msg: ") + line);
252  m_state = Close;
253  }
254  break;
255  case Rcpt:
256  if (code[0] == '2')
257  {
258  m_socket->write("rcpt to:<" + m_rcpt.toLatin1() + ">\r\n");
259  m_socket->flush();
260  m_state = Data;
261  }
262  else
263  {
264  logError(QLatin1String("<mail from> was rejected by server, msg: ") + line);
265  m_state = Close;
266  }
267  break;
268  case Data:
269  if (code[0] == '2')
270  {
271  m_socket->write("data\r\n");
272  m_socket->flush();
273  m_state = Body;
274  }
275  else
276  {
277  logError(QLatin1String("<Rcpt to> was rejected by server, msg: ") + line);
278  m_state = Close;
279  }
280  break;
281  case Body:
282  if (code[0] == '3')
283  {
284  m_socket->write(m_message + "\r\n.\r\n");
285  m_socket->flush();
286  m_state = Quit;
287  }
288  else
289  {
290  logError(QLatin1String("<data> was rejected by server, msg: ") + line);
291  m_state = Close;
292  }
293  break;
294  case Quit:
295  if (code[0] == '2')
296  {
297  m_socket->write("QUIT\r\n");
298  m_socket->flush();
299  // here, we just close.
300  m_state = Close;
301  }
302  else
303  {
304  logError(QLatin1String("Message was rejected by the server, error: ") + line);
305  m_state = Close;
306  }
307  break;
308  default:
309  qDebug() << "Disconnecting from host";
310  m_socket->disconnectFromHost();
311  return;
312  }
313  }
314 }
315 
316 QByteArray Smtp::encodeMimeHeader(const QString &key, const QString &value, const QByteArray &prefix)
317 {
318  QByteArray rv = "";
319  QByteArray line = key.toLatin1() + ": ";
320  if (!prefix.isEmpty()) line += prefix;
321  if (!value.contains("=?") && canEncodeAsLatin1(value))
322  {
323  bool firstWord = true;
324  for (const QByteArray &word : asConst(value.toLatin1().split(' ')))
325  {
326  if (line.size() > 78)
327  {
328  rv = rv + line + "\r\n";
329  line.clear();
330  }
331  if (firstWord)
332  line += word;
333  else
334  line += ' ' + word;
335  firstWord = false;
336  }
337  }
338  else
339  {
340  // The text cannot be losslessly encoded as Latin-1. Therefore, we
341  // must use base64 encoding.
342  const QByteArray utf8 = value.toUtf8();
343  // Use base64 encoding
344  const QByteArray base64 = utf8.toBase64();
345  const int ct = base64.length();
346  line += "=?utf-8?b?";
347  for (int i = 0; i < ct; i += 4)
348  {
349  /*if (line.length() > 72)
350  {
351  rv += line + "?\n\r";
352  line = " =?utf-8?b?";
353  }*/
354  line = line + base64.mid(i, 4);
355  }
356  line += "?="; // end encoded-word atom
357  }
358  return rv + line + "\r\n";
359 }
360 
362 {
363  const QByteArray address = determineFQDN();
364  m_socket->write("ehlo " + address + "\r\n");
365  m_socket->flush();
366  m_state = EhloSent;
367 }
368 
370 {
371  const QByteArray address = determineFQDN();
372  m_socket->write("helo " + address + "\r\n");
373  m_socket->flush();
374  m_state = HeloSent;
375 }
376 
377 void Smtp::parseEhloResponse(const QByteArray &code, const bool continued, const QString &line)
378 {
379  if (code != "250")
380  {
381  // Error
382  if (m_state == EhloSent)
383  {
384  // try to send HELO instead of EHLO
385  qDebug() << "EHLO failed, trying HELO instead...";
386  helo();
387  }
388  else
389  {
390  // Both EHLO and HELO failed, chances are this is NOT
391  // a SMTP server
392  logError("Both EHLO and HELO failed, msg: " + line);
393  m_state = Close;
394  }
395  return;
396  }
397 
398  if (m_state != EhloGreetReceived)
399  {
400  if (!continued)
401  {
402  // greeting only, no extensions
403  qDebug() << "No extension";
404  m_state = EhloDone;
405  }
406  else
407  {
408  // greeting followed by extensions
410  qDebug() << "EHLO greet received";
411  return;
412  }
413  }
414  else
415  {
416  qDebug() << Q_FUNC_INFO << "Supported extension: " << line.section(' ', 0, 0).toUpper()
417  << line.section(' ', 1);
418  m_extensions[line.section(' ', 0, 0).toUpper()] = line.section(' ', 1);
419  if (!continued)
420  m_state = EhloDone;
421  }
422 
423  if (m_state != EhloDone) return;
424 
425  if (m_extensions.contains("STARTTLS") && m_useSsl)
426  {
427  qDebug() << "STARTTLS";
428  startTLS();
429  }
430  else
431  {
432  authenticate();
433  }
434 }
435 
437 {
438  qDebug() << Q_FUNC_INFO;
439  if (!m_extensions.contains("AUTH") ||
440  m_username.isEmpty() || m_password.isEmpty())
441  {
442  // Skip authentication
443  qDebug() << "Skipping authentication...";
445  // At this point the server will not send any response
446  // So fill the buffer with a fake one to pass the tests
447  // in readyRead()
448  m_buffer.push_front("250 QBT FAKE RESPONSE\r\n");
449  return;
450  }
451  // AUTH extension is supported, check which
452  // authentication modes are supported by
453  // the server
454  const QStringList auth = m_extensions["AUTH"].toUpper().split(' ', Qt::SkipEmptyParts);
455  if (auth.contains("CRAM-MD5"))
456  {
457  qDebug() << "Using CRAM-MD5 authentication...";
458  authCramMD5();
459  }
460  else if (auth.contains("PLAIN"))
461  {
462  qDebug() << "Using PLAIN authentication...";
463  authPlain();
464  }
465  else if (auth.contains("LOGIN"))
466  {
467  qDebug() << "Using LOGIN authentication...";
468  authLogin();
469  }
470  else
471  {
472  // Skip authentication
473  logError("The SMTP server does not seem to support any of the authentications modes "
474  "we support [CRAM-MD5|PLAIN|LOGIN], skipping authentication, "
475  "knowing it is likely to fail... Server Auth Modes: " + auth.join('|'));
477  // At this point the server will not send any response
478  // So fill the buffer with a fake one to pass the tests
479  // in readyRead()
480  m_buffer.push_front("250 QBT FAKE RESPONSE\r\n");
481  }
482 }
483 
485 {
486  qDebug() << Q_FUNC_INFO;
487 #ifndef QT_NO_OPENSSL
488  m_socket->write("starttls\r\n");
489  m_socket->flush();
491 #else
492  authenticate();
493 #endif
494 }
495 
496 void Smtp::authCramMD5(const QByteArray &challenge)
497 {
498  if (m_state != AuthRequestSent)
499  {
500  m_socket->write("auth cram-md5\r\n");
501  m_socket->flush();
504  }
505  else
506  {
507  const QByteArray response = m_username.toLatin1() + ' '
508  + hmacMD5(m_password.toLatin1(), QByteArray::fromBase64(challenge)).toHex();
509  m_socket->write(response.toBase64() + "\r\n");
510  m_socket->flush();
511  m_state = AuthSent;
512  }
513 }
514 
516 {
517  if (m_state != AuthRequestSent)
518  {
520  // Prepare Auth string
521  QByteArray auth;
522  auth += '\0';
523  auth += m_username.toLatin1();
524  qDebug() << "username: " << m_username.toLatin1();
525  auth += '\0';
526  auth += m_password.toLatin1();
527  qDebug() << "password: " << m_password.toLatin1();
528  // Send it
529  m_socket->write("auth plain " + auth.toBase64() + "\r\n");
530  m_socket->flush();
531  m_state = AuthSent;
532  }
533 }
534 
536 {
538  {
539  m_socket->write("auth login\r\n");
540  m_socket->flush();
543  }
544  else if (m_state == AuthRequestSent)
545  {
546  m_socket->write(m_username.toLatin1().toBase64() + "\r\n");
547  m_socket->flush();
549  }
550  else
551  {
552  m_socket->write(m_password.toLatin1().toBase64() + "\r\n");
553  m_socket->flush();
554  m_state = AuthSent;
555  }
556 }
557 
558 void Smtp::logError(const QString &msg)
559 {
560  qDebug() << "Email Notification Error:" << msg;
561  Logger::instance()->addMessage(tr("Email Notification Error:") + ' ' + msg, Log::CRITICAL);
562 }
563 
565 {
566  // return date & time in the format specified in RFC 2822, section 3.3
567  const QDateTime nowDateTime = QDateTime::currentDateTime();
568  const QDate nowDate = nowDateTime.date();
569  const QLocale eng(QLocale::English);
570 
571  const QString timeStr = nowDateTime.time().toString("HH:mm:ss");
572  const QString weekDayStr = eng.dayName(nowDate.dayOfWeek(), QLocale::ShortFormat);
573  const QString dayStr = QString::number(nowDate.day());
574  const QString monthStr = eng.monthName(nowDate.month(), QLocale::ShortFormat);
575  const QString yearStr = QString::number(nowDate.year());
576 
577  QDateTime tmp = nowDateTime;
578  tmp.setTimeSpec(Qt::UTC);
579  const int timeOffsetHour = nowDateTime.secsTo(tmp) / 3600;
580  const int timeOffsetMin = nowDateTime.secsTo(tmp) / 60 - (60 * timeOffsetHour);
581  const int timeOffset = timeOffsetHour * 100 + timeOffsetMin;
582  // buf size = 11 to avoid format truncation warnings from snprintf
583  char buf[11] = {0};
584  std::snprintf(buf, sizeof(buf), "%+05d", timeOffset);
585  const QString timeOffsetStr = buf;
586 
587  const QString ret = weekDayStr + ", " + dayStr + ' ' + monthStr + ' ' + yearStr + ' ' + timeStr + ' ' + timeOffsetStr;
588  return ret;
589 }
590 
591 void Smtp::error(QAbstractSocket::SocketError socketError)
592 {
593  // Getting a remote host closed error is apparently normal, even when successfully sending
594  // an email
595  if (socketError != QAbstractSocket::RemoteHostClosedError)
596  logError(m_socket->errorString());
597 }
void addMessage(const QString &message, const Log::MsgType &type=Log::NORMAL)
Definition: logger.cpp:73
static Logger * instance()
Definition: logger.cpp:56
@ EhloSent
Definition: smtp.h:68
@ Init
Definition: smtp.h:78
@ AuthUsernameSent
Definition: smtp.h:74
@ Body
Definition: smtp.h:79
@ AuthSent
Definition: smtp.h:73
@ HeloSent
Definition: smtp.h:69
@ EhloGreetReceived
Definition: smtp.h:71
@ StartTLSSent
Definition: smtp.h:76
@ Rcpt
Definition: smtp.h:67
@ AuthRequestSent
Definition: smtp.h:72
@ Close
Definition: smtp.h:81
@ Data
Definition: smtp.h:77
@ Quit
Definition: smtp.h:80
@ EhloDone
Definition: smtp.h:70
@ Authenticated
Definition: smtp.h:75
void authPlain()
Definition: smtp.cpp:515
@ AuthPlain
Definition: smtp.h:86
@ AuthLogin
Definition: smtp.h:87
@ AuthCramMD5
Definition: smtp.h:88
Smtp(QObject *parent=nullptr)
Definition: smtp.cpp:103
void authLogin()
Definition: smtp.cpp:535
void ehlo()
Definition: smtp.cpp:361
QByteArray m_message
Definition: smtp.h:103
QHash< QString, QString > m_extensions
Definition: smtp.h:113
void logError(const QString &msg)
Definition: smtp.cpp:558
void startTLS()
Definition: smtp.cpp:484
QString m_username
Definition: smtp.h:117
QString getCurrentDateTime() const
Definition: smtp.cpp:564
void authCramMD5(const QByteArray &challenge={})
Definition: smtp.cpp:496
QString m_password
Definition: smtp.h:118
AuthType m_authType
Definition: smtp.h:116
void parseEhloResponse(const QByteArray &code, bool continued, const QString &line)
Definition: smtp.cpp:377
void error(QAbstractSocket::SocketError socketError)
Definition: smtp.cpp:591
QByteArray m_buffer
Definition: smtp.h:114
void authenticate()
Definition: smtp.cpp:436
void readyRead()
Definition: smtp.cpp:182
~Smtp()
Definition: smtp.cpp:134
void helo()
Definition: smtp.cpp:369
QString m_from
Definition: smtp.h:109
int m_state
Definition: smtp.h:112
QByteArray encodeMimeHeader(const QString &key, const QString &value, const QByteArray &prefix={})
Definition: smtp.cpp:316
bool m_useSsl
Definition: smtp.h:115
QSslSocket * m_socket
Definition: smtp.h:105
QString m_rcpt
Definition: smtp.h:110
void sendMail(const QString &from, const QString &to, const QString &subject, const QString &body)
Definition: smtp.cpp:139
QString getMailNotificationSMTPUsername() const
QString getMailNotificationSMTPPassword() const
static Preferences * instance()
bool getMailNotificationSMTPAuth() const
bool getMailNotificationSMTPSSL() const
QString getMailNotificationSMTP() const
constexpr std::add_const_t< T > & asConst(T &t) noexcept
Definition: global.h:42
@ CRITICAL
Definition: logger.h:48
Definition: session.h:86
T value(const QString &key, const T &defaultValue={})
Definition: preferences.cpp:64
QByteArray hmacMD5(QByteArray key, const QByteArray &msg)
Definition: smtp.cpp:58
bool canEncodeAsLatin1(const QStringView string)
Definition: smtp.cpp:92
const short DEFAULT_PORT_SSL
Definition: smtp.cpp:55
QByteArray determineFQDN()
Definition: smtp.cpp:83
const short DEFAULT_PORT
Definition: smtp.cpp:53