mirror of
https://github.com/qbittorrent/qBittorrent
synced 2025-07-31 03:50:20 -07:00
FEATURE: Added support for secure SMTP connection (SSL)
FEATURE: Added support for SMTP authentication
This commit is contained in:
parent
0a6f591cf5
commit
58bfa6f1bb
9 changed files with 599 additions and 173 deletions
476
src/smtp.cpp
476
src/smtp.cpp
|
@ -1,26 +1,105 @@
|
|||
/****************************************************************************
|
||||
** $Id: qt/smtp.h 3.3.6 edited Aug 31 2005 $
|
||||
**
|
||||
** Copyright (C) 1992-2005 Trolltech AS. All rights reserved.
|
||||
**
|
||||
** This file is part of an example program for Qt. This example
|
||||
** program may be used, distributed and modified without limitation.
|
||||
**
|
||||
*****************************************************************************/
|
||||
/*
|
||||
* Bittorrent Client using Qt4 and libtorrent.
|
||||
* Copyright (C) 2011 Christophe Dumez
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 2
|
||||
* of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
*
|
||||
* In addition, as a special exception, the copyright holders give permission to
|
||||
* link this program with the OpenSSL project's "OpenSSL" library (or with
|
||||
* modified versions of it that use the same license as the "OpenSSL" library),
|
||||
* and distribute the linked executables. You must obey the GNU General Public
|
||||
* License in all respects for all of the code used other than "OpenSSL". If you
|
||||
* modify file(s), you may extend this exception to your version of the file(s),
|
||||
* but you are not obligated to do so. If you do not wish to do so, delete this
|
||||
* exception statement from your version.
|
||||
*
|
||||
* Contact : chris@qbittorrent.org
|
||||
*/
|
||||
|
||||
/*
|
||||
* This code is based on QxtSmtp from libqxt (http://libqxt.org)
|
||||
*/
|
||||
|
||||
#include "smtp.h"
|
||||
#include "preferences.h"
|
||||
|
||||
#include <QTextStream>
|
||||
#ifndef QT_NO_OPENSSL
|
||||
#include <QSslSocket>
|
||||
#else
|
||||
#include <QTcpSocket>
|
||||
#endif
|
||||
#include <QTextCodec>
|
||||
#include <QDebug>
|
||||
#include <QHostAddress>
|
||||
#include <QNetworkInterface>
|
||||
#include <QCryptographicHash>
|
||||
|
||||
Smtp::Smtp(const QString &from, const QString &to, const QString &subject, const QString &body) {
|
||||
const short DEFAULT_PORT = 25;
|
||||
const short DEFAULT_PORT_SSL = 465;
|
||||
|
||||
QByteArray hmacMD5(QByteArray key, const QByteArray &msg)
|
||||
{
|
||||
const int blockSize = 64; // HMAC-MD5 block size
|
||||
if (key.length() > blockSize) { // if key is longer than block size (64), reduce key length with MD5 compression
|
||||
key = QCryptographicHash::hash(key, QCryptographicHash::Md5);
|
||||
}
|
||||
|
||||
QByteArray innerPadding(blockSize, char(0x36)); // initialize inner padding with char "6"
|
||||
QByteArray outerPadding(blockSize, char(0x5c)); // initialize outer padding with char "\"
|
||||
// ascii characters 0x36 ("6") and 0x5c ("\") are selected because they have large
|
||||
// Hamming distance (http://en.wikipedia.org/wiki/Hamming_distance)
|
||||
|
||||
for (int i = 0; i < key.length(); i++) {
|
||||
innerPadding[i] = innerPadding[i] ^ key.at(i); // XOR operation between every byte in key and innerpadding, of key length
|
||||
outerPadding[i] = outerPadding[i] ^ key.at(i); // XOR operation between every byte in key and outerpadding, of key length
|
||||
}
|
||||
|
||||
// result = hash ( outerPadding CONCAT hash ( innerPadding CONCAT baseString ) ).toBase64
|
||||
QByteArray total = outerPadding;
|
||||
QByteArray part = innerPadding;
|
||||
part.append(msg);
|
||||
total.append(QCryptographicHash::hash(part, QCryptographicHash::Md5));
|
||||
return QCryptographicHash::hash(total, QCryptographicHash::Md5);
|
||||
}
|
||||
|
||||
Smtp::Smtp(QObject *parent): QObject(parent),
|
||||
state(Init), use_ssl(false) {
|
||||
#ifndef QT_NO_OPENSSL
|
||||
socket = new QSslSocket(this);
|
||||
#else
|
||||
socket = new QTcpSocket(this);
|
||||
#endif
|
||||
|
||||
connect( socket, SIGNAL( readyRead() ), this, SLOT( readyRead() ) );
|
||||
connect(socket, SIGNAL(readyRead()), SLOT(readyRead()));
|
||||
connect(socket, SIGNAL(disconnected()), SLOT(deleteLater()));
|
||||
|
||||
// Test hmacMD5 function (http://www.faqs.org/rfcs/rfc2202.html)
|
||||
Q_ASSERT(hmacMD5("Jefe", "what do ya want for nothing?").toHex()
|
||||
== "750c783e6ab0b503eaa86e310a5db738");
|
||||
Q_ASSERT(hmacMD5(QByteArray::fromHex("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b"),
|
||||
"Hi There").toHex()
|
||||
== "9294727a3638bb1c13f48ef8158bfc9d");
|
||||
}
|
||||
|
||||
Smtp::~Smtp() {
|
||||
qDebug() << Q_FUNC_INFO;
|
||||
}
|
||||
|
||||
void Smtp::sendMail(const QString &from, const QString &to, const QString &subject, const QString &body) {
|
||||
Preferences pref;
|
||||
QTextCodec* latin1 = QTextCodec::codecForName("latin1");
|
||||
message = "";
|
||||
message += encode_mime_header("Date", QDateTime::currentDateTime().toUTC().toString("ddd, d MMM yyyy hh:mm:ss UT"), latin1);
|
||||
|
@ -41,15 +120,135 @@ Smtp::Smtp(const QString &from, const QString &to, const QString &subject, const
|
|||
}
|
||||
this->from = from;
|
||||
rcpt = to;
|
||||
state = Init;
|
||||
socket->connectToHost(Preferences().getMailNotificationSMTP(), 25);
|
||||
if(socket->waitForConnected ( 30000 )) {
|
||||
qDebug("connected");
|
||||
} else {
|
||||
t = 0;
|
||||
deleteLater();
|
||||
// Authentication
|
||||
if(pref.getMailNotificationSMTPAuth()) {
|
||||
username = pref.getMailNotificationSMTPUsername();
|
||||
password = pref.getMailNotificationSMTPPassword();
|
||||
}
|
||||
|
||||
// Connect to SMTP server
|
||||
#ifndef QT_NO_OPENSSL
|
||||
if(pref.getMailNotificationSMTPSSL()) {
|
||||
socket->connectToHostEncrypted(pref.getMailNotificationSMTP(), DEFAULT_PORT_SSL);
|
||||
use_ssl = true;
|
||||
} else {
|
||||
#endif
|
||||
socket->connectToHost(pref.getMailNotificationSMTP(), DEFAULT_PORT);
|
||||
use_ssl = false;
|
||||
#ifndef QT_NO_OPENSSL
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void Smtp::readyRead()
|
||||
{
|
||||
qDebug() << Q_FUNC_INFO;
|
||||
// SMTP is line-oriented
|
||||
buffer += socket->readAll();
|
||||
while (true)
|
||||
{
|
||||
int pos = buffer.indexOf("\r\n");
|
||||
if (pos < 0) return; // Loop exit condition
|
||||
QByteArray line = buffer.left(pos);
|
||||
buffer = buffer.mid(pos + 2);
|
||||
qDebug() << "Response line:" << line;
|
||||
// Extract reponse code
|
||||
QByteArray code = line.left(3);
|
||||
|
||||
switch(state) {
|
||||
case Init: {
|
||||
if(code[0] == '2') {
|
||||
// Connection was successful
|
||||
ehlo();
|
||||
} else {
|
||||
// TODO: Log something
|
||||
qDebug() << "Connection failed, unrecognized reply:" << line;
|
||||
state = Close;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case EhloSent:
|
||||
case HeloSent:
|
||||
case EhloGreetReceived:
|
||||
parseEhloResponse(code, line[3] != ' ', line.mid(4));
|
||||
break;
|
||||
#ifndef QT_NO_OPENSSL
|
||||
case StartTLSSent:
|
||||
if (code == "220") {
|
||||
socket->startClientEncryption();
|
||||
ehlo();
|
||||
} else {
|
||||
authenticate();
|
||||
}
|
||||
break;
|
||||
#endif
|
||||
case AuthRequestSent:
|
||||
case AuthUsernameSent:
|
||||
if (authType == AuthPlain) authPlain();
|
||||
else if (authType == AuthLogin) authLogin();
|
||||
else authCramMD5(line.mid(4));
|
||||
break;
|
||||
case AuthSent:
|
||||
case Authenticated:
|
||||
if (code[0] == '2') {
|
||||
qDebug() << "Login was OK, send <mail from>...";
|
||||
socket->write("mail from:<" + from.toAscii() + ">\r\n");
|
||||
socket->flush();
|
||||
state = Rcpt;
|
||||
} else {
|
||||
// Authentication failed!
|
||||
// TODO: Log something
|
||||
qDebug() << "Authentication not sent properly, aborting";
|
||||
state = Close;
|
||||
}
|
||||
break;
|
||||
case Rcpt:
|
||||
if (code[0] == '2') {
|
||||
socket->write("rcpt to:<" + rcpt.toAscii() + ">\r\n");
|
||||
socket->flush();
|
||||
state = Data;
|
||||
} else {
|
||||
qDebug() << "<Mail from> not sent properly, aborting";
|
||||
state = Close;
|
||||
}
|
||||
break;
|
||||
case Data:
|
||||
if (code[0] == '2') {
|
||||
socket->write("data\r\n");
|
||||
socket->flush();
|
||||
state = Body;
|
||||
} else {
|
||||
qDebug() << "<Rcpt to> not sent properly, aborting";
|
||||
state = Close;
|
||||
}
|
||||
break;
|
||||
case Body:
|
||||
if (code[0] == '3') {
|
||||
socket->write(message + "\r\n.\r\n");
|
||||
socket->flush();
|
||||
state = Quit;
|
||||
} else {
|
||||
qDebug() << "data not sent properly, aborting";
|
||||
state = Close;
|
||||
}
|
||||
break;
|
||||
case Quit:
|
||||
if (code[0] == '2') {
|
||||
socket->write("QUIT\r\n");
|
||||
socket->flush();
|
||||
// here, we just close.
|
||||
state = Close;
|
||||
} else {
|
||||
qDebug() << "Message not sent properly, aborting";
|
||||
state = Close;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
qDebug() << "Disconnecting from host";
|
||||
socket->disconnectFromHost();
|
||||
return;
|
||||
}
|
||||
}
|
||||
t = new QTextStream(socket);
|
||||
}
|
||||
|
||||
QByteArray Smtp::encode_mime_header(const QString& key, const QString& value, QTextCodec* latin1, const QByteArray& prefix)
|
||||
|
@ -91,102 +290,163 @@ QByteArray Smtp::encode_mime_header(const QString& key, const QString& value, QT
|
|||
return rv + line + "\r\n";
|
||||
}
|
||||
|
||||
Smtp::~Smtp()
|
||||
void Smtp::ehlo()
|
||||
{
|
||||
if(t)
|
||||
delete t;
|
||||
delete socket;
|
||||
QByteArray address = "127.0.0.1";
|
||||
foreach(const QHostAddress& addr, QNetworkInterface::allAddresses())
|
||||
{
|
||||
if (addr == QHostAddress::LocalHost || addr == QHostAddress::LocalHostIPv6)
|
||||
continue;
|
||||
address = addr.toString().toAscii();
|
||||
break;
|
||||
}
|
||||
// Send EHLO
|
||||
socket->write("ehlo "+ address + "\r\n");
|
||||
socket->flush();
|
||||
state = EhloSent;
|
||||
}
|
||||
|
||||
void Smtp::readyRead()
|
||||
void Smtp::parseEhloResponse(const QByteArray& code, bool continued, const QString& line)
|
||||
{
|
||||
|
||||
qDebug() << "readyRead";
|
||||
// SMTP is line-oriented
|
||||
|
||||
QString responseLine;
|
||||
do
|
||||
{
|
||||
responseLine = socket->readLine();
|
||||
response += responseLine;
|
||||
}
|
||||
while ( socket->canReadLine() && responseLine[3] != ' ' );
|
||||
|
||||
qDebug("Response line: %s", qPrintable(response));
|
||||
|
||||
responseLine.truncate( 3 );
|
||||
|
||||
|
||||
if ( state == Init && responseLine[0] == '2' )
|
||||
{
|
||||
// banner was okay, let's go on
|
||||
QByteArray address = "127.0.0.1";
|
||||
foreach(const QHostAddress& addr, QNetworkInterface::allAddresses())
|
||||
{
|
||||
if (addr == QHostAddress::LocalHost || addr == QHostAddress::LocalHostIPv6)
|
||||
continue;
|
||||
address = addr.toString().toAscii();
|
||||
break;
|
||||
}
|
||||
*t << "ehlo "+ address + "\r\n";
|
||||
t->flush();
|
||||
|
||||
state = Mail;
|
||||
}
|
||||
else if ( state == Mail || state == Mail2 )
|
||||
{
|
||||
if(responseLine[0] == '2') {
|
||||
// EHLO response was okay (well, it has to be)
|
||||
*t << "mail from:<" << from << ">\r\n";
|
||||
t->flush();
|
||||
state = Rcpt;
|
||||
if (code != "250") {
|
||||
// Error
|
||||
if(state == EhloSent) {
|
||||
// try to send HELO instead of EHLO
|
||||
qDebug() << "EHLO failed, trying HELO instead...";
|
||||
socket->write("helo\r\n");
|
||||
socket->flush();
|
||||
state = HeloSent;
|
||||
} else {
|
||||
if(state == Mail) {
|
||||
// ehlo did not work, try helo instead
|
||||
*t << "helo\r\n";
|
||||
t->flush();
|
||||
state = Mail2;
|
||||
}
|
||||
// Both EHLO and HELO failed, chances are this is NOT
|
||||
// a SMTP server
|
||||
// TODO: log something
|
||||
qDebug() << "Both EHLO and HELO failed, aborting.";
|
||||
state = Close;
|
||||
}
|
||||
}
|
||||
else if ( state == Rcpt && responseLine[0] == '2' )
|
||||
{
|
||||
|
||||
*t << "rcpt to:<" << rcpt << ">\r\n"; //r
|
||||
t->flush();
|
||||
state = Data;
|
||||
}
|
||||
else if ( state == Data && responseLine[0] == '2' )
|
||||
{
|
||||
|
||||
*t << "data\r\n";
|
||||
t->flush();
|
||||
state = Body;
|
||||
}
|
||||
else if ( state == Body && responseLine[0] == '3' )
|
||||
{
|
||||
|
||||
*t << message << "\r\n.\r\n";
|
||||
t->flush();
|
||||
state = Quit;
|
||||
}
|
||||
else if(state == Quit && responseLine[0] == '2')
|
||||
{
|
||||
|
||||
*t << "QUIT\r\n";
|
||||
t->flush();
|
||||
// here, we just close.
|
||||
state = Close;
|
||||
}
|
||||
else if ( state == Close )
|
||||
{
|
||||
deleteLater();
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
// something broke.
|
||||
state = Close;
|
||||
if (state != EhloGreetReceived) {
|
||||
if (!continued) {
|
||||
// greeting only, no extensions
|
||||
qDebug() << "No extension";
|
||||
state = EhloDone;
|
||||
} else {
|
||||
// greeting followed by extensions
|
||||
state = EhloGreetReceived;
|
||||
qDebug () << "EHLO greet received";
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
qDebug() << Q_FUNC_INFO << "Supported extension: " << line.section(' ', 0, 0).toUpper()
|
||||
<< line.section(' ', 1);
|
||||
extensions[line.section(' ', 0, 0).toUpper()] = line.section(' ', 1);
|
||||
if (!continued)
|
||||
state = EhloDone;
|
||||
}
|
||||
if (state != EhloDone) return;
|
||||
if (extensions.contains("STARTTLS") && use_ssl) {
|
||||
qDebug() << "STARTTLS";
|
||||
startTLS();
|
||||
} else {
|
||||
authenticate();
|
||||
}
|
||||
}
|
||||
|
||||
void Smtp::authenticate()
|
||||
{
|
||||
qDebug() << Q_FUNC_INFO;
|
||||
if (!extensions.contains("AUTH") ||
|
||||
username.isEmpty() || password.isEmpty()) {
|
||||
// Skip authentication
|
||||
qDebug() << "Skipping authentication...";
|
||||
state = Authenticated;
|
||||
return;
|
||||
}
|
||||
// AUTH extension is supported, check which
|
||||
// authentication modes are supported by
|
||||
// the server
|
||||
QStringList auth = extensions["AUTH"].toUpper().split(' ', QString::SkipEmptyParts);
|
||||
if (auth.contains("CRAM-MD5")) {
|
||||
qDebug() << "Using CRAM-MD5 authentication...";
|
||||
authCramMD5();
|
||||
}
|
||||
else if (auth.contains("PLAIN")) {
|
||||
qDebug() << "Using PLAIN authentication...";
|
||||
authPlain();
|
||||
}
|
||||
else if (auth.contains("LOGIN")) {
|
||||
qDebug() << "Using LOGIN authentication...";
|
||||
authLogin();
|
||||
} else {
|
||||
// Skip authentication
|
||||
qDebug() << "Server does not support any of our AUTH modes, skip authentication...";
|
||||
state = Authenticated;
|
||||
}
|
||||
}
|
||||
|
||||
void Smtp::startTLS()
|
||||
{
|
||||
qDebug() << Q_FUNC_INFO;
|
||||
#ifndef QT_NO_OPENSSL
|
||||
socket->write("starttls\r\n");
|
||||
socket->flush();
|
||||
state = StartTLSSent;
|
||||
#else
|
||||
authenticate();
|
||||
#endif
|
||||
}
|
||||
|
||||
void Smtp::authCramMD5(const QByteArray& challenge)
|
||||
{
|
||||
if (state != AuthRequestSent) {
|
||||
socket->write("auth cram-md5\r\n");
|
||||
socket->flush();
|
||||
authType = AuthCramMD5;
|
||||
state = AuthRequestSent;
|
||||
} else {
|
||||
QByteArray response = username.toAscii() + ' '
|
||||
+ hmacMD5(password.toAscii(), QByteArray::fromBase64(challenge)).toHex();
|
||||
socket->write(response.toBase64() + "\r\n");
|
||||
socket->flush();
|
||||
state = AuthSent;
|
||||
}
|
||||
}
|
||||
|
||||
void Smtp::authPlain()
|
||||
{
|
||||
if (state != AuthRequestSent) {
|
||||
authType = AuthPlain;
|
||||
// Prepare Auth string
|
||||
QByteArray auth;
|
||||
auth += '\0';
|
||||
auth += username.toAscii();
|
||||
qDebug() << "username: " << username.toAscii();
|
||||
auth += '\0';
|
||||
auth += password.toAscii();
|
||||
qDebug() << "password: " << password.toAscii();
|
||||
// Send it
|
||||
socket->write("auth plain "+ auth.toBase64() + "\r\n");
|
||||
socket->flush();
|
||||
state = AuthSent;
|
||||
}
|
||||
}
|
||||
|
||||
void Smtp::authLogin()
|
||||
{
|
||||
if (state != AuthRequestSent && state != AuthUsernameSent) {
|
||||
socket->write("auth login\r\n");
|
||||
socket->flush();
|
||||
authType = AuthLogin;
|
||||
state = AuthRequestSent;
|
||||
}
|
||||
else if (state == AuthRequestSent) {
|
||||
socket->write(username.toAscii().toBase64() + "\r\n");
|
||||
socket->flush();
|
||||
state = AuthUsernameSent;
|
||||
}
|
||||
else {
|
||||
socket->write(password.toAscii().toBase64() + "\r\n");
|
||||
socket->flush();
|
||||
state = AuthSent;
|
||||
}
|
||||
response = "";
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue