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