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)
35 #include <QCryptographicHash>
39 #include <QStringList>
47 #include "base/global.h"
48 #include "base/logger.h"
49 #include "base/preferences.h"
50 #include "base/utils/string.h"
54 const short DEFAULT_PORT
= 25;
56 const short DEFAULT_PORT_SSL
= 465;
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
;
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);
104 Smtp::Smtp(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);
118 m_socket
= new QTcpSocket(this);
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");
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"
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);
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
));
177 m_socket
->connectToHost(serverAddress
, serverPort
.value_or(DEFAULT_PORT
));
179 #ifndef QT_NO_OPENSSL
184 void Smtp::readyRead()
186 qDebug() << Q_FUNC_INFO
;
187 // SMTP is line-oriented
188 m_buffer
+= m_socket
->readAll();
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);
204 // The server may send a multiline greeting/INIT/220 response.
205 // We wait until it finishes.
208 // Connection was successful
213 logError(tr("Connection failed, unrecognized reply: %1").arg(QString::fromUtf8(line
)));
219 case EhloGreetReceived
:
220 parseEhloResponse(code
, (line
[3] != ' '), QString::fromUtf8(line
.mid(4)));
222 #ifndef QT_NO_OPENSSL
226 m_socket
->startClientEncryption();
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));
245 qDebug() << "Sending <mail from>...";
246 m_socket
->write("mail from:<" + m_from
.toLatin1() + ">\r\n");
252 // Authentication failed!
253 logError(tr("Authentication failed, msg: %1").arg(QString::fromUtf8(line
)));
260 m_socket
->write("rcpt to:<" + m_rcpt
.toLatin1() + ">\r\n");
266 logError(tr("<mail from> was rejected by server, msg: %1").arg(QString::fromUtf8(line
)));
273 m_socket
->write("data\r\n");
279 logError(tr("<Rcpt to> was rejected by server, msg: %1").arg(QString::fromUtf8(line
)));
286 m_socket
->write(m_message
+ "\r\n.\r\n");
292 logError(tr("<data> was rejected by server, msg: %1").arg(QString::fromUtf8(line
)));
299 m_socket
->write("QUIT\r\n");
301 // here, we just close.
306 logError(tr("Message was rejected by the server, error: %1").arg(QString::fromUtf8(line
)));
311 qDebug() << "Disconnecting from host";
312 m_socket
->disconnectFromHost();
318 QByteArray
Smtp::encodeMimeHeader(const QString
&key
, const QString
&value
, const QByteArray
&prefix
)
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";
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";
365 const QByteArray address
= determineFQDN();
366 m_socket
->write("ehlo " + address
+ "\r\n");
373 const QByteArray address
= determineFQDN();
374 m_socket
->write("helo " + address
+ "\r\n");
379 void Smtp::parseEhloResponse(const QByteArray
&code
, const bool continued
, const QString
&line
)
384 if (m_state
== EhloSent
)
386 // try to send HELO instead of EHLO
387 qDebug() << "EHLO failed, trying HELO instead...";
392 // Both EHLO and HELO failed, chances are this is NOT
394 logError(tr("Both EHLO and HELO failed, msg: %1").arg(line
));
400 if (m_state
!= EhloGreetReceived
)
404 // greeting only, no extensions
405 qDebug() << "No extension";
410 // greeting followed by extensions
411 m_state
= EhloGreetReceived
;
412 qDebug() << "EHLO greet received";
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);
425 if (m_state
!= EhloDone
) return;
427 if (m_extensions
.contains(u
"STARTTLS"_s
) && m_useSsl
)
429 qDebug() << "STARTTLS";
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
450 m_buffer
.push_front("250 QBT FAKE RESPONSE\r\n");
453 // AUTH extension is supported, check which
454 // authentication modes are supported by
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...";
462 else if (auth
.contains(u
"PLAIN"))
464 qDebug() << "Using PLAIN authentication...";
467 else if (auth
.contains(u
"LOGIN"))
469 qDebug() << "Using LOGIN authentication...";
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
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");
492 m_state
= StartTLSSent
;
498 void Smtp::authCramMD5(const QByteArray
&challenge
)
500 if (m_state
!= AuthRequestSent
)
502 m_socket
->write("auth cram-md5\r\n");
504 m_authType
= AuthCramMD5
;
505 m_state
= AuthRequestSent
;
509 const QByteArray response
= m_username
.toLatin1() + ' '
510 + hmacMD5(m_password
.toLatin1(), QByteArray::fromBase64(challenge
)).toHex();
511 m_socket
->write(response
.toBase64() + "\r\n");
517 void Smtp::authPlain()
519 if (m_state
!= AuthRequestSent
)
521 m_authType
= AuthPlain
;
522 // Prepare Auth string
525 auth
+= m_username
.toLatin1();
526 qDebug() << "username: " << m_username
.toLatin1();
528 auth
+= m_password
.toLatin1();
529 qDebug() << "password: " << m_password
.toLatin1();
531 m_socket
->write("auth plain " + auth
.toBase64() + "\r\n");
537 void Smtp::authLogin()
539 if ((m_state
!= AuthRequestSent
) && (m_state
!= AuthUsernameSent
))
541 m_socket
->write("auth login\r\n");
543 m_authType
= AuthLogin
;
544 m_state
= AuthRequestSent
;
546 else if (m_state
== AuthRequestSent
)
548 m_socket
->write(m_username
.toLatin1().toBase64() + "\r\n");
550 m_state
= AuthUsernameSent
;
554 m_socket
->write(m_password
.toLatin1().toBase64() + "\r\n");
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
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
;
593 void Smtp::error(QAbstractSocket::SocketError socketError
)
595 // Getting a remote host closed error is apparently normal, even when successfully sending
597 if (socketError
!= QAbstractSocket::RemoteHostClosedError
)
598 logError(m_socket
->errorString());