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 
hmacMD5(QByteArray key,const QByteArray & msg)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 
determineFQDN()83     QByteArray determineFQDN()
84     {
85         QString hostname = QHostInfo::localHostName();
86         if (hostname.isEmpty())
87             hostname = "localhost";
88 
89         return hostname.toLocal8Bit();
90     }
91 
canEncodeAsLatin1(const QStringView string)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 
Smtp(QObject * parent)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 #if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0))
126     connect(m_socket, &QAbstractSocket::errorOccurred, this, &Smtp::error);
127 #else
128     connect(m_socket, qOverload<QAbstractSocket::SocketError>(&QAbstractSocket::error)
129             , this, &Smtp::error);
130 #endif
131 
132     // Test hmacMD5 function (http://www.faqs.org/rfcs/rfc2202.html)
133     Q_ASSERT(hmacMD5("Jefe", "what do ya want for nothing?").toHex()
134              == "750c783e6ab0b503eaa86e310a5db738");
135     Q_ASSERT(hmacMD5(QByteArray::fromHex("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b"), "Hi There").toHex()
136              == "9294727a3638bb1c13f48ef8158bfc9d");
137 }
138 
~Smtp()139 Smtp::~Smtp()
140 {
141     qDebug() << Q_FUNC_INFO;
142 }
143 
sendMail(const QString & from,const QString & to,const QString & subject,const QString & body)144 void Smtp::sendMail(const QString &from, const QString &to, const QString &subject, const QString &body)
145 {
146     const Preferences *const pref = Preferences::instance();
147     m_message = "Date: " + getCurrentDateTime().toLatin1() + "\r\n"
148                 + encodeMimeHeader("From", from)
149                 + encodeMimeHeader("Subject", subject)
150                 + encodeMimeHeader("To", to)
151                 + "MIME-Version: 1.0\r\n"
152                 + "Content-Type: text/plain; charset=UTF-8\r\n"
153                 + "Content-Transfer-Encoding: base64\r\n"
154                 + "\r\n";
155     // Encode the body in base64
156     QString crlfBody = body;
157     const QByteArray b = crlfBody.replace("\n", "\r\n").toUtf8().toBase64();
158     const int ct = b.length();
159     for (int i = 0; i < ct; i += 78)
160         m_message += b.mid(i, 78);
161     m_from = from;
162     m_rcpt = to;
163     // Authentication
164     if (pref->getMailNotificationSMTPAuth())
165     {
166         m_username = pref->getMailNotificationSMTPUsername();
167         m_password = pref->getMailNotificationSMTPPassword();
168     }
169 
170     // Connect to SMTP server
171 #ifndef QT_NO_OPENSSL
172     if (pref->getMailNotificationSMTPSSL())
173     {
174         m_socket->connectToHostEncrypted(pref->getMailNotificationSMTP(), DEFAULT_PORT_SSL);
175         m_useSsl = true;
176     }
177     else
178     {
179 #endif
180     m_socket->connectToHost(pref->getMailNotificationSMTP(), DEFAULT_PORT);
181     m_useSsl = false;
182 #ifndef QT_NO_OPENSSL
183     }
184 #endif
185 }
186 
readyRead()187 void Smtp::readyRead()
188 {
189     qDebug() << Q_FUNC_INFO;
190     // SMTP is line-oriented
191     m_buffer += m_socket->readAll();
192     while (true)
193     {
194         const int pos = m_buffer.indexOf("\r\n");
195         if (pos < 0) return; // Loop exit condition
196         const QByteArray line = m_buffer.left(pos);
197         m_buffer = m_buffer.mid(pos + 2);
198         qDebug() << "Response line:" << line;
199         // Extract response code
200         const QByteArray code = line.left(3);
201 
202         switch (m_state)
203         {
204         case Init:
205             if (code[0] == '2')
206             {
207                 // The server may send a multiline greeting/INIT/220 response.
208                 // We wait until it finishes.
209                 if (line[3] != ' ')
210                     break;
211                 // Connection was successful
212                 ehlo();
213             }
214             else
215             {
216                 logError(QLatin1String("Connection failed, unrecognized reply: ") + line);
217                 m_state = Close;
218             }
219             break;
220         case EhloSent:
221         case HeloSent:
222         case EhloGreetReceived:
223             parseEhloResponse(code, line[3] != ' ', line.mid(4));
224             break;
225 #ifndef QT_NO_OPENSSL
226         case StartTLSSent:
227             if (code == "220")
228             {
229                 m_socket->startClientEncryption();
230                 ehlo();
231             }
232             else
233             {
234                 authenticate();
235             }
236             break;
237 #endif
238         case AuthRequestSent:
239         case AuthUsernameSent:
240             if (m_authType == AuthPlain) authPlain();
241             else if (m_authType == AuthLogin) authLogin();
242             else authCramMD5(line.mid(4));
243             break;
244         case AuthSent:
245         case Authenticated:
246             if (code[0] == '2')
247             {
248                 qDebug() << "Sending <mail from>...";
249                 m_socket->write("mail from:<" + m_from.toLatin1() + ">\r\n");
250                 m_socket->flush();
251                 m_state = Rcpt;
252             }
253             else
254             {
255                 // Authentication failed!
256                 logError(QLatin1String("Authentication failed, msg: ") + line);
257                 m_state = Close;
258             }
259             break;
260         case Rcpt:
261             if (code[0] == '2')
262             {
263                 m_socket->write("rcpt to:<" + m_rcpt.toLatin1() + ">\r\n");
264                 m_socket->flush();
265                 m_state = Data;
266             }
267             else
268             {
269                 logError(QLatin1String("<mail from> was rejected by server, msg: ") + line);
270                 m_state = Close;
271             }
272             break;
273         case Data:
274             if (code[0] == '2')
275             {
276                 m_socket->write("data\r\n");
277                 m_socket->flush();
278                 m_state = Body;
279             }
280             else
281             {
282                 logError(QLatin1String("<Rcpt to> was rejected by server, msg: ") + line);
283                 m_state = Close;
284             }
285             break;
286         case Body:
287             if (code[0] == '3')
288             {
289                 m_socket->write(m_message + "\r\n.\r\n");
290                 m_socket->flush();
291                 m_state = Quit;
292             }
293             else
294             {
295                 logError(QLatin1String("<data> was rejected by server, msg: ") + line);
296                 m_state = Close;
297             }
298             break;
299         case Quit:
300             if (code[0] == '2')
301             {
302                 m_socket->write("QUIT\r\n");
303                 m_socket->flush();
304                 // here, we just close.
305                 m_state = Close;
306             }
307             else
308             {
309                 logError(QLatin1String("Message was rejected by the server, error: ") + line);
310                 m_state = Close;
311             }
312             break;
313         default:
314             qDebug() << "Disconnecting from host";
315             m_socket->disconnectFromHost();
316             return;
317         }
318     }
319 }
320 
encodeMimeHeader(const QString & key,const QString & value,const QByteArray & prefix)321 QByteArray Smtp::encodeMimeHeader(const QString &key, const QString &value, const QByteArray &prefix)
322 {
323     QByteArray rv = "";
324     QByteArray line = key.toLatin1() + ": ";
325     if (!prefix.isEmpty()) line += prefix;
326     if (!value.contains("=?") && canEncodeAsLatin1(value))
327     {
328         bool firstWord = true;
329         for (const QByteArray &word : asConst(value.toLatin1().split(' ')))
330         {
331             if (line.size() > 78)
332             {
333                 rv = rv + line + "\r\n";
334                 line.clear();
335             }
336             if (firstWord)
337                 line += word;
338             else
339                 line += ' ' + word;
340             firstWord = false;
341         }
342     }
343     else
344     {
345         // The text cannot be losslessly encoded as Latin-1. Therefore, we
346         // must use base64 encoding.
347         const QByteArray utf8 = value.toUtf8();
348         // Use base64 encoding
349         const QByteArray base64 = utf8.toBase64();
350         const int ct = base64.length();
351         line += "=?utf-8?b?";
352         for (int i = 0; i < ct; i += 4)
353         {
354             /*if (line.length() > 72)
355             {
356                rv += line + "?\n\r";
357                line = " =?utf-8?b?";
358                }*/
359             line = line + base64.mid(i, 4);
360         }
361         line += "?="; // end encoded-word atom
362     }
363     return rv + line + "\r\n";
364 }
365 
ehlo()366 void Smtp::ehlo()
367 {
368     const QByteArray address = determineFQDN();
369     m_socket->write("ehlo " + address + "\r\n");
370     m_socket->flush();
371     m_state = EhloSent;
372 }
373 
helo()374 void Smtp::helo()
375 {
376     const QByteArray address = determineFQDN();
377     m_socket->write("helo " + address + "\r\n");
378     m_socket->flush();
379     m_state = HeloSent;
380 }
381 
parseEhloResponse(const QByteArray & code,const bool continued,const QString & line)382 void Smtp::parseEhloResponse(const QByteArray &code, const bool continued, const QString &line)
383 {
384     if (code != "250")
385     {
386         // Error
387         if (m_state == EhloSent)
388         {
389             // try to send HELO instead of EHLO
390             qDebug() << "EHLO failed, trying HELO instead...";
391             helo();
392         }
393         else
394         {
395             // Both EHLO and HELO failed, chances are this is NOT
396             // a SMTP server
397             logError("Both EHLO and HELO failed, msg: " + line);
398             m_state = Close;
399         }
400         return;
401     }
402 
403     if (m_state != EhloGreetReceived)
404     {
405         if (!continued)
406         {
407             // greeting only, no extensions
408             qDebug() << "No extension";
409             m_state = EhloDone;
410         }
411         else
412         {
413             // greeting followed by extensions
414             m_state = EhloGreetReceived;
415             qDebug() << "EHLO greet received";
416             return;
417         }
418     }
419     else
420     {
421         qDebug() << Q_FUNC_INFO << "Supported extension: " << line.section(' ', 0, 0).toUpper()
422                  << line.section(' ', 1);
423         m_extensions[line.section(' ', 0, 0).toUpper()] = line.section(' ', 1);
424         if (!continued)
425             m_state = EhloDone;
426     }
427 
428     if (m_state != EhloDone) return;
429 
430     if (m_extensions.contains("STARTTLS") && m_useSsl)
431     {
432         qDebug() << "STARTTLS";
433         startTLS();
434     }
435     else
436     {
437         authenticate();
438     }
439 }
440 
authenticate()441 void Smtp::authenticate()
442 {
443     qDebug() << Q_FUNC_INFO;
444     if (!m_extensions.contains("AUTH") ||
445         m_username.isEmpty() || m_password.isEmpty())
446         {
447         // Skip authentication
448         qDebug() << "Skipping authentication...";
449         m_state = Authenticated;
450         // At this point the server will not send any response
451         // So fill the buffer with a fake one to pass the tests
452         // in readyRead()
453         m_buffer.push_front("250 QBT FAKE RESPONSE\r\n");
454         return;
455     }
456     // AUTH extension is supported, check which
457     // authentication modes are supported by
458     // the server
459     const QStringList auth = m_extensions["AUTH"].toUpper().split(' ', QString::SkipEmptyParts);
460     if (auth.contains("CRAM-MD5"))
461     {
462         qDebug() << "Using CRAM-MD5 authentication...";
463         authCramMD5();
464     }
465     else if (auth.contains("PLAIN"))
466     {
467         qDebug() << "Using PLAIN authentication...";
468         authPlain();
469     }
470     else if (auth.contains("LOGIN"))
471     {
472         qDebug() << "Using LOGIN authentication...";
473         authLogin();
474     }
475     else
476     {
477         // Skip authentication
478         logError("The SMTP server does not seem to support any of the authentications modes "
479                  "we support [CRAM-MD5|PLAIN|LOGIN], skipping authentication, "
480                  "knowing it is likely to fail... Server Auth Modes: " + auth.join('|'));
481         m_state = Authenticated;
482         // At this point the server will not send any response
483         // So fill the buffer with a fake one to pass the tests
484         // in readyRead()
485         m_buffer.push_front("250 QBT FAKE RESPONSE\r\n");
486     }
487 }
488 
startTLS()489 void Smtp::startTLS()
490 {
491     qDebug() << Q_FUNC_INFO;
492 #ifndef QT_NO_OPENSSL
493     m_socket->write("starttls\r\n");
494     m_socket->flush();
495     m_state = StartTLSSent;
496 #else
497     authenticate();
498 #endif
499 }
500 
authCramMD5(const QByteArray & challenge)501 void Smtp::authCramMD5(const QByteArray &challenge)
502 {
503     if (m_state != AuthRequestSent)
504     {
505         m_socket->write("auth cram-md5\r\n");
506         m_socket->flush();
507         m_authType = AuthCramMD5;
508         m_state = AuthRequestSent;
509     }
510     else
511     {
512         const QByteArray response = m_username.toLatin1() + ' '
513                               + hmacMD5(m_password.toLatin1(), QByteArray::fromBase64(challenge)).toHex();
514         m_socket->write(response.toBase64() + "\r\n");
515         m_socket->flush();
516         m_state = AuthSent;
517     }
518 }
519 
authPlain()520 void Smtp::authPlain()
521 {
522     if (m_state != AuthRequestSent)
523     {
524         m_authType = AuthPlain;
525         // Prepare Auth string
526         QByteArray auth;
527         auth += '\0';
528         auth += m_username.toLatin1();
529         qDebug() << "username: " << m_username.toLatin1();
530         auth += '\0';
531         auth += m_password.toLatin1();
532         qDebug() << "password: " << m_password.toLatin1();
533         // Send it
534         m_socket->write("auth plain " + auth.toBase64() + "\r\n");
535         m_socket->flush();
536         m_state = AuthSent;
537     }
538 }
539 
authLogin()540 void Smtp::authLogin()
541 {
542     if ((m_state != AuthRequestSent) && (m_state != AuthUsernameSent))
543     {
544         m_socket->write("auth login\r\n");
545         m_socket->flush();
546         m_authType = AuthLogin;
547         m_state = AuthRequestSent;
548     }
549     else if (m_state == AuthRequestSent)
550     {
551         m_socket->write(m_username.toLatin1().toBase64() + "\r\n");
552         m_socket->flush();
553         m_state = AuthUsernameSent;
554     }
555     else
556     {
557         m_socket->write(m_password.toLatin1().toBase64() + "\r\n");
558         m_socket->flush();
559         m_state = AuthSent;
560     }
561 }
562 
logError(const QString & msg)563 void Smtp::logError(const QString &msg)
564 {
565     qDebug() << "Email Notification Error:" << msg;
566     Logger::instance()->addMessage(tr("Email Notification Error:") + ' ' + msg, Log::CRITICAL);
567 }
568 
getCurrentDateTime() const569 QString Smtp::getCurrentDateTime() const
570 {
571     // return date & time in the format specified in RFC 2822, section 3.3
572     const QDateTime nowDateTime = QDateTime::currentDateTime();
573     const QDate nowDate = nowDateTime.date();
574     const QLocale eng(QLocale::English);
575 
576     const QString timeStr = nowDateTime.time().toString("HH:mm:ss");
577     const QString weekDayStr = eng.dayName(nowDate.dayOfWeek(), QLocale::ShortFormat);
578     const QString dayStr = QString::number(nowDate.day());
579     const QString monthStr = eng.monthName(nowDate.month(), QLocale::ShortFormat);
580     const QString yearStr = QString::number(nowDate.year());
581 
582     QDateTime tmp = nowDateTime;
583     tmp.setTimeSpec(Qt::UTC);
584     const int timeOffsetHour = nowDateTime.secsTo(tmp) / 3600;
585     const int timeOffsetMin = nowDateTime.secsTo(tmp) / 60 - (60 * timeOffsetHour);
586     const int timeOffset = timeOffsetHour * 100 + timeOffsetMin;
587     // buf size = 11 to avoid format truncation warnings from snprintf
588     char buf[11] = {0};
589     std::snprintf(buf, sizeof(buf), "%+05d", timeOffset);
590     const QString timeOffsetStr = buf;
591 
592     const QString ret = weekDayStr + ", " + dayStr + ' ' + monthStr + ' ' + yearStr + ' ' + timeStr + ' ' + timeOffsetStr;
593     return ret;
594 }
595 
error(QAbstractSocket::SocketError socketError)596 void Smtp::error(QAbstractSocket::SocketError socketError)
597 {
598     // Getting a remote host closed error is apparently normal, even when successfully sending
599     // an email
600     if (socketError != QAbstractSocket::RemoteHostClosedError)
601         logError(m_socket->errorString());
602 }
603