/* * Bittorrent Client using Qt 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 "base/preferences.h" #include "base/logger.h" #include #ifndef QT_NO_OPENSSL #include #else #include #endif #include #include #include #include #include #include #include namespace { const short DEFAULT_PORT = 25; #ifndef QT_NO_OPENSSL const short DEFAULT_PORT_SSL = 465; #endif 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); } QByteArray determineFQDN() { QString hostname = QHostInfo::localHostName(); if (hostname.isEmpty()) hostname = "localhost"; return hostname.toLocal8Bit(); } } // namespace using namespace Net; Smtp::Smtp(QObject *parent) : QObject(parent) , m_state(Init) , m_useSsl(false) , m_authType(AuthPlain) { static bool needToRegisterMetaType = true; if (needToRegisterMetaType) { qRegisterMetaType(); needToRegisterMetaType = false; } #ifndef QT_NO_OPENSSL m_socket = new QSslSocket(this); #else m_socket = new QTcpSocket(this); #endif connect(m_socket, SIGNAL(readyRead()), SLOT(readyRead())); connect(m_socket, SIGNAL(disconnected()), SLOT(deleteLater())); connect(m_socket, SIGNAL(error(QAbstractSocket::SocketError)), SLOT(error(QAbstractSocket::SocketError))); // 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) { const Preferences* const pref = Preferences::instance(); QTextCodec* latin1 = QTextCodec::codecForName("latin1"); m_message = "Date: " + getCurrentDateTime().toLatin1() + "\r\n" + encodeMimeHeader("From", from, latin1) + encodeMimeHeader("Subject", subject, latin1) + encodeMimeHeader("To", to, latin1) + "MIME-Version: 1.0\r\n" + "Content-Type: text/plain; charset=UTF-8\r\n" + "Content-Transfer-Encoding: base64\r\n" + "\r\n"; // Encode the body in base64 QString crlf_body = body; QByteArray b = crlf_body.replace("\n", "\r\n").toUtf8().toBase64(); int ct = b.length(); for (int i = 0; i < ct; i += 78) m_message += b.mid(i, 78); m_from = from; m_rcpt = to; // Authentication if (pref->getMailNotificationSMTPAuth()) { m_username = pref->getMailNotificationSMTPUsername(); m_password = pref->getMailNotificationSMTPPassword(); } // Connect to SMTP server #ifndef QT_NO_OPENSSL if (pref->getMailNotificationSMTPSSL()) { m_socket->connectToHostEncrypted(pref->getMailNotificationSMTP(), DEFAULT_PORT_SSL); m_useSsl = true; } else { #endif m_socket->connectToHost(pref->getMailNotificationSMTP(), DEFAULT_PORT); m_useSsl = false; #ifndef QT_NO_OPENSSL } #endif } void Smtp::readyRead() { qDebug() << Q_FUNC_INFO; // SMTP is line-oriented m_buffer += m_socket->readAll(); while (true) { int pos = m_buffer.indexOf("\r\n"); if (pos < 0) return; // Loop exit condition QByteArray line = m_buffer.left(pos); m_buffer = m_buffer.mid(pos + 2); qDebug() << "Response line:" << line; // Extract response code QByteArray code = line.left(3); switch (m_state) { case Init: { if (code[0] == '2') { // The server may send a multiline greeting/INIT/220 response. // We wait until it finishes. if (line[3] != ' ') break; // Connection was successful ehlo(); } else { logError("Connection failed, unrecognized reply: " + line); m_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") { m_socket->startClientEncryption(); ehlo(); } else { authenticate(); } break; #endif case AuthRequestSent: case AuthUsernameSent: if (m_authType == AuthPlain) authPlain(); else if (m_authType == AuthLogin) authLogin(); else authCramMD5(line.mid(4)); break; case AuthSent: case Authenticated: if (code[0] == '2') { qDebug() << "Sending ..."; m_socket->write("mail from:<" + m_from.toLatin1() + ">\r\n"); m_socket->flush(); m_state = Rcpt; } else { // Authentication failed! logError("Authentication failed, msg: " + line); m_state = Close; } break; case Rcpt: if (code[0] == '2') { m_socket->write("rcpt to:<" + m_rcpt.toLatin1() + ">\r\n"); m_socket->flush(); m_state = Data; } else { logError(" was rejected by server, msg: " + line); m_state = Close; } break; case Data: if (code[0] == '2') { m_socket->write("data\r\n"); m_socket->flush(); m_state = Body; } else { logError(" was rejected by server, msg: " + line); m_state = Close; } break; case Body: if (code[0] == '3') { m_socket->write(m_message + "\r\n.\r\n"); m_socket->flush(); m_state = Quit; } else { logError(" was rejected by server, msg: " + line); m_state = Close; } break; case Quit: if (code[0] == '2') { m_socket->write("QUIT\r\n"); m_socket->flush(); // here, we just close. m_state = Close; } else { logError("Message was rejected by the server, error: " + line); m_state = Close; } break; default: qDebug() << "Disconnecting from host"; m_socket->disconnectFromHost(); return; } } } QByteArray Smtp::encodeMimeHeader(const QString &key, const QString &value, QTextCodec *latin1, const QByteArray &prefix) { QByteArray rv = ""; QByteArray line = key.toLatin1() + ": "; if (!prefix.isEmpty()) line += prefix; if (!value.contains("=?") && latin1->canEncode(value)) { bool firstWord = true; foreach (const QByteArray& word, value.toLatin1().split(' ')) { if (line.size() > 78) { rv = rv + line + "\r\n"; line.clear(); } if (firstWord) line += word; else line += " " + word; firstWord = false; } } else { // The text cannot be losslessly encoded as Latin-1. Therefore, we // must use base64 encoding. QByteArray utf8 = value.toUtf8(); // Use base64 encoding QByteArray base64 = utf8.toBase64(); int ct = base64.length(); line += "=?utf-8?b?"; for (int i = 0; i < ct; i += 4) { /*if (line.length() > 72) { rv += line + "?\n\r"; line = " =?utf-8?b?"; }*/ line = line + base64.mid(i, 4); } line += "?="; // end encoded-word atom } return rv + line + "\r\n"; } void Smtp::ehlo() { QByteArray address = determineFQDN(); m_socket->write("ehlo " + address + "\r\n"); m_socket->flush(); m_state = EhloSent; } void Smtp::helo() { QByteArray address = determineFQDN(); m_socket->write("helo " + address + "\r\n"); m_socket->flush(); m_state = HeloSent; } void Smtp::parseEhloResponse(const QByteArray &code, bool continued, const QString &line) { if (code != "250") { // Error if (m_state == EhloSent) { // try to send HELO instead of EHLO qDebug() << "EHLO failed, trying HELO instead..."; helo(); } else { // Both EHLO and HELO failed, chances are this is NOT // a SMTP server logError("Both EHLO and HELO failed, msg: " + line); m_state = Close; } return; } if (m_state != EhloGreetReceived) { if (!continued) { // greeting only, no extensions qDebug() << "No extension"; m_state = EhloDone; } else { // greeting followed by extensions m_state = EhloGreetReceived; qDebug() << "EHLO greet received"; return; } } else { qDebug() << Q_FUNC_INFO << "Supported extension: " << line.section(' ', 0, 0).toUpper() << line.section(' ', 1); m_extensions[line.section(' ', 0, 0).toUpper()] = line.section(' ', 1); if (!continued) m_state = EhloDone; } if (m_state != EhloDone) return; if (m_extensions.contains("STARTTLS") && m_useSsl) { qDebug() << "STARTTLS"; startTLS(); } else { authenticate(); } } void Smtp::authenticate() { qDebug() << Q_FUNC_INFO; if (!m_extensions.contains("AUTH") || m_username.isEmpty() || m_password.isEmpty()) { // Skip authentication qDebug() << "Skipping authentication..."; m_state = Authenticated; // At this point the server will not send any response // So fill the buffer with a fake one to pass the tests // in readyRead() m_buffer.push_front("250 QBT FAKE RESPONSE\r\n"); return; } // AUTH extension is supported, check which // authentication modes are supported by // the server QStringList auth = m_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 logError("The SMTP server does not seem to support any of the authentications modes " "we support [CRAM-MD5|PLAIN|LOGIN], skipping authentication, " "knowing it is likely to fail... Server Auth Modes: " + auth.join("|")); m_state = Authenticated; // At this point the server will not send any response // So fill the buffer with a fake one to pass the tests // in readyRead() m_buffer.push_front("250 QBT FAKE RESPONSE\r\n"); } } void Smtp::startTLS() { qDebug() << Q_FUNC_INFO; #ifndef QT_NO_OPENSSL m_socket->write("starttls\r\n"); m_socket->flush(); m_state = StartTLSSent; #else authenticate(); #endif } void Smtp::authCramMD5(const QByteArray& challenge) { if (m_state != AuthRequestSent) { m_socket->write("auth cram-md5\r\n"); m_socket->flush(); m_authType = AuthCramMD5; m_state = AuthRequestSent; } else { QByteArray response = m_username.toLatin1() + ' ' + hmacMD5(m_password.toLatin1(), QByteArray::fromBase64(challenge)).toHex(); m_socket->write(response.toBase64() + "\r\n"); m_socket->flush(); m_state = AuthSent; } } void Smtp::authPlain() { if (m_state != AuthRequestSent) { m_authType = AuthPlain; // Prepare Auth string QByteArray auth; auth += '\0'; auth += m_username.toLatin1(); qDebug() << "username: " << m_username.toLatin1(); auth += '\0'; auth += m_password.toLatin1(); qDebug() << "password: " << m_password.toLatin1(); // Send it m_socket->write("auth plain " + auth.toBase64() + "\r\n"); m_socket->flush(); m_state = AuthSent; } } void Smtp::authLogin() { if ((m_state != AuthRequestSent) && (m_state != AuthUsernameSent)) { m_socket->write("auth login\r\n"); m_socket->flush(); m_authType = AuthLogin; m_state = AuthRequestSent; } else if (m_state == AuthRequestSent) { m_socket->write(m_username.toLatin1().toBase64() + "\r\n"); m_socket->flush(); m_state = AuthUsernameSent; } else { m_socket->write(m_password.toLatin1().toBase64() + "\r\n"); m_socket->flush(); m_state = AuthSent; } } void Smtp::logError(const QString &msg) { qDebug() << "Email Notification Error:" << msg; Logger::instance()->addMessage(tr("Email Notification Error:") + " " + msg, Log::CRITICAL); } QString Smtp::getCurrentDateTime() const { // return date & time in the format specified in RFC 2822, section 3.3 const QDateTime nowDateTime = QDateTime::currentDateTime(); const QDate nowDate = nowDateTime.date(); const QLocale eng(QLocale::English); QString timeStr = nowDateTime.time().toString("HH:mm:ss"); QString weekDayStr = eng.dayName(nowDate.dayOfWeek(), QLocale::ShortFormat); QString dayStr = QString::number(nowDate.day()); QString monthStr = eng.monthName(nowDate.month(), QLocale::ShortFormat); QString yearStr = QString::number(nowDate.year()); QDateTime tmp = nowDateTime; tmp.setTimeSpec(Qt::UTC); int timeOffsetHour = nowDateTime.secsTo(tmp) / 3600; int timeOffsetMin = nowDateTime.secsTo(tmp) / 60 - (60 * timeOffsetHour); int timeOffset = timeOffsetHour * 100 + timeOffsetMin; char buf[6] = {0}; std::snprintf(buf, sizeof(buf), "%+05d", timeOffset); QString timeOffsetStr = buf; QString ret = weekDayStr + ", " + dayStr + " " + monthStr + " " + yearStr + " " + timeStr + " " + timeOffsetStr; return ret; } void Smtp::error(QAbstractSocket::SocketError socketError) { // Getting a remote host closed error is apparently normal, even when successfully sending // an email if (socketError != QAbstractSocket::RemoteHostClosedError) logError(m_socket->errorString()); }