/**************************************************************************** ** ** Copyright (C) Qxt Foundation. Some rights reserved. ** ** This file is part of the QxtWeb module of the Qxt library. ** ** This library is free software; you can redistribute it and/or modify it ** under the terms of the Common Public License, version 1.0, as published ** by IBM, and/or under the terms of the GNU Lesser General Public License, ** version 2.1, as published by the Free Software Foundation. ** ** This file is provided "AS IS", without WARRANTIES OR CONDITIONS OF ANY ** KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY ** WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR ** FITNESS FOR A PARTICULAR PURPOSE. ** ** You should have received a copy of the CPL and the LGPL along with this ** file. See the LICENSE file and the cpl1.0.txt/lgpl-2.1.txt files ** included with the source distribution for more information. ** If you did not receive a copy of the licenses, contact the Qxt Foundation. ** ** ** ****************************************************************************/ /*! * \class QxtSmtp * \inmodule QxtNetwork * \brief The QxtSmtp class implements the SMTP protocol for sending email */ #include "qxtsmtp.h" #include "qxthmac.h" #include "qxtsmtp_p.h" #include #include #include #include QxtSmtpPrivate::QxtSmtpPrivate() : QObject(0) { // empty ctor } QxtSmtp::QxtSmtp(QObject *parent) : QObject(parent) { QXT_INIT_PRIVATE(QxtSmtp); qxt_d().state = QxtSmtpPrivate::Disconnected; qxt_d().nextID = 0; qxt_d().socket = new QSslSocket(this); QObject::connect(socket(), SIGNAL(encrypted()), this, SIGNAL(encrypted())); // QObject::connect(socket(), SIGNAL(encrypted()), &qxt_d(), SLOT(ehlo())); QObject::connect(socket(), SIGNAL(connected()), this, SIGNAL(connected())); QObject::connect(socket(), SIGNAL(disconnected()), this, SIGNAL(disconnected())); #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) QObject::connect(socket(), SIGNAL(errorOccurred(QAbstractSocket::SocketError)), &qxt_d(), SLOT(socketError(QAbstractSocket::SocketError))); #else QObject::connect(socket(), SIGNAL(error(QAbstractSocket::SocketError)), &qxt_d(), SLOT(socketError(QAbstractSocket::SocketError))); #endif QObject::connect(this, SIGNAL(authenticated()), &qxt_d(), SLOT(sendNext())); QObject::connect(socket(), SIGNAL(readyRead()), &qxt_d(), SLOT(socketRead())); } QByteArray QxtSmtp::username() const { return qxt_d().username; } void QxtSmtp::setUsername(const QByteArray &username) { qxt_d().username = username; } QByteArray QxtSmtp::password() const { return qxt_d().password; } void QxtSmtp::setPassword(const QByteArray &password) { qxt_d().password = password; } int QxtSmtp::send(const QxtMailMessage &message) { int messageID = ++qxt_d().nextID; qxt_d().pending.append(qMakePair(messageID, message)); if (qxt_d().state == QxtSmtpPrivate::Waiting) qxt_d().sendNext(); return messageID; } int QxtSmtp::pendingMessages() const { return qxt_d().pending.count(); } QTcpSocket *QxtSmtp::socket() const { return qxt_d().socket; } void QxtSmtp::connectToHost(const QString &hostName, quint16 port) { qxt_d().useSecure = false; qxt_d().state = QxtSmtpPrivate::StartState; socket()->connectToHost(hostName, port); } void QxtSmtp::connectToHost(const QHostAddress &address, quint16 port) { connectToHost(address.toString(), port); } void QxtSmtp::disconnectFromHost() { socket()->disconnectFromHost(); } bool QxtSmtp::startTlsDisabled() const { return qxt_d().disableStartTLS; } void QxtSmtp::setStartTlsDisabled(bool disable) { qxt_d().disableStartTLS = disable; } QSslSocket *QxtSmtp::sslSocket() const { return qxt_d().socket; } void QxtSmtp::connectToSecureHost(const QString &hostName, quint16 port) { qxt_d().useSecure = true; qxt_d().state = QxtSmtpPrivate::StartState; sslSocket()->connectToHostEncrypted(hostName, port); } void QxtSmtp::connectToSecureHost(const QHostAddress &address, quint16 port) { connectToSecureHost(address.toString(), port); } bool QxtSmtp::hasExtension(const QString &extension) { return qxt_d().extensions.contains(extension); } QString QxtSmtp::extensionData(const QString &extension) { return qxt_d().extensions[extension]; } void QxtSmtpPrivate::socketError(QAbstractSocket::SocketError err) { if (err == QAbstractSocket::SslHandshakeFailedError) { emit qxt_p().encryptionFailed(); emit qxt_p().encryptionFailed(socket->errorString().toLatin1()); } else if (state == StartState) { emit qxt_p().connectionFailed(); emit qxt_p().connectionFailed(socket->errorString().toLatin1()); } } void QxtSmtpPrivate::socketRead() { buffer += socket->readAll(); while (true) { int pos = buffer.indexOf("\r\n"); if (pos < 0) return; QByteArray line = buffer.left(pos); buffer = buffer.mid(pos + 2); QByteArray code = line.left(3); switch (state) { case StartState: if (code[0] != '2') { socket->disconnectFromHost(); } else { ehlo(); } break; case HeloSent: case EhloSent: case EhloGreetReceived: parseEhlo(code, (line[3] != ' '), line.mid(4)); break; case StartTLSSent: if (code == "220") { socket->startClientEncryption(); ehlo(); } else { authenticate(); } break; case AuthRequestSent: case AuthUsernameSent: if (authType == AuthPlain) authPlain(); else if (authType == AuthLogin) authLogin(); else authCramMD5(line.mid(4)); break; case AuthSent: if (code[0] == '2') { state = Authenticated; emit qxt_p().authenticated(); } else { state = Disconnected; emit qxt_p().authenticationFailed(); emit qxt_p().authenticationFailed(line); emit socket->disconnectFromHost(); } break; case MailToSent: case RcptAckPending: if (code[0] != '2') { emit qxt_p().mailFailed(pending.first().first, code.toInt()); emit qxt_p().mailFailed(pending.first().first, code.toInt(), line); // pending.removeFirst(); // DO NOT remove it, the body sent state needs this message to assigned the next mail failed message // that will the sendNext a reset will be sent to clear things out sendNext(); state = BodySent; } else sendNextRcpt(code, line); break; case SendingBody: sendBody(code, line); break; case BodySent: if (pending.count()) { // if you removeFirst in RcpActpending/MailToSent on an error, and the queue is now empty, // you will get into this state and then crash because no check is done. CHeck added but shouldnt // be necessary since I commented out the removeFirst if (code[0] != '2') { emit qxt_p().mailFailed(pending.first().first, code.toInt()); emit qxt_p().mailFailed(pending.first().first, code.toInt(), line); } else emit qxt_p().mailSent(pending.first().first); pending.removeFirst(); } sendNext(); break; case Resetting: if (code[0] != '2') { emit qxt_p().connectionFailed(); emit qxt_p().connectionFailed(line); } else { state = Waiting; sendNext(); } break; case Disconnected: case EhloExtensionsReceived: case EhloDone: case Authenticated: case Waiting: // only to make compiler happy break; } } } void QxtSmtpPrivate::ehlo() { QByteArray address = "127.0.0.1"; for (const QHostAddress &addr : QNetworkInterface::allAddresses()) { if (addr == QHostAddress::LocalHost || addr == QHostAddress::LocalHostIPv6) continue; address = addr.toString().toLatin1(); break; } socket->write("ehlo " + address + "\r\n"); extensions.clear(); state = EhloSent; } void QxtSmtpPrivate::parseEhlo(const QByteArray &code, bool cont, const QString &line) { if (code != "250") { // error! if (state != HeloSent) { // maybe let's try HELO socket->write("helo\r\n"); state = HeloSent; } else { // nope socket->write("QUIT\r\n"); socket->flush(); socket->disconnectFromHost(); } return; } else if (state != EhloGreetReceived) { if (!cont) { // greeting only, no extensions state = EhloDone; } else { // greeting followed by extensions state = EhloGreetReceived; return; } } else { extensions[line.section(' ', 0, 0).toUpper()] = line.section(' ', 1); if (!cont) state = EhloDone; } if (state != EhloDone) return; if (extensions.contains("STARTTLS") && !disableStartTLS) { startTLS(); } else { authenticate(); } } void QxtSmtpPrivate::startTLS() { socket->write("starttls\r\n"); state = StartTLSSent; } void QxtSmtpPrivate::authenticate() { if (!extensions.contains("AUTH") || username.isEmpty() || password.isEmpty()) { state = Authenticated; emit qxt_p().authenticated(); } else { #if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) QStringList auth = extensions["AUTH"].toUpper().split(' ', Qt::SkipEmptyParts); #else QStringList auth = extensions["AUTH"].toUpper().split(' ', QString::SkipEmptyParts); #endif if (auth.contains("CRAM-MD5")) { authCramMD5(); } else if (auth.contains("PLAIN")) { authPlain(); } else if (auth.contains("LOGIN")) { authLogin(); } else { state = Authenticated; emit qxt_p().authenticated(); } } } void QxtSmtpPrivate::authCramMD5(const QByteArray &challenge) { if (state != AuthRequestSent) { socket->write("auth cram-md5\r\n"); authType = AuthCramMD5; state = AuthRequestSent; } else { QxtHmac hmac(QCryptographicHash::Md5); hmac.setKey(password); hmac.addData(QByteArray::fromBase64(challenge)); QByteArray response = username + ' ' + hmac.result().toHex(); socket->write(response.toBase64() + "\r\n"); state = AuthSent; } } void QxtSmtpPrivate::authPlain() { if (state != AuthRequestSent) { socket->write("auth plain\r\n"); authType = AuthPlain; state = AuthRequestSent; } else { QByteArray auth; auth += '\0'; auth += username; auth += '\0'; auth += password; socket->write(auth.toBase64() + "\r\n"); state = AuthSent; } } void QxtSmtpPrivate::authLogin() { if (state != AuthRequestSent && state != AuthUsernameSent) { socket->write("auth login\r\n"); authType = AuthLogin; state = AuthRequestSent; } else if (state == AuthRequestSent) { socket->write(username.toBase64() + "\r\n"); state = AuthUsernameSent; } else { socket->write(password.toBase64() + "\r\n"); state = AuthSent; } } static QByteArray qxt_extract_address(const QString &address) { int parenDepth = 0; int addrStart = -1; bool inQuote = false; int ct = address.length(); for (int i = 0; i < ct; i++) { QChar ch = address[i]; if (inQuote) { if (ch == '"') inQuote = false; } else if (addrStart != -1) { if (ch == '>') return address.mid(addrStart, (i - addrStart)).toLatin1(); } else if (ch == '(') { parenDepth++; } else if (ch == ')') { parenDepth--; if (parenDepth < 0) parenDepth = 0; } else if (ch == '"') { if (parenDepth == 0) inQuote = true; } else if (ch == '<') { if (!inQuote && parenDepth == 0) addrStart = i + 1; } } return address.toLatin1(); } void QxtSmtpPrivate::sendNext() { if (state == Disconnected) { // leave the mail in the queue if not ready to send return; } if (pending.isEmpty()) { // if there are no additional mails to send, finish up state = Waiting; emit qxt_p().finished(); return; } if (state != Waiting) { state = Resetting; socket->write("rset\r\n"); return; } const QxtMailMessage &msg = pending.first().second; rcptNumber = rcptAck = mailAck = 0; recipients = msg.recipients(QxtMailMessage::To) + msg.recipients(QxtMailMessage::Cc) + msg.recipients(QxtMailMessage::Bcc); if (recipients.count() == 0) { // can't send an e-mail with no recipients emit qxt_p().mailFailed(pending.first().first, QxtSmtp::NoRecipients); emit qxt_p().mailFailed(pending.first().first, QxtSmtp::NoRecipients, QByteArray("e-mail has no recipients")); pending.removeFirst(); sendNext(); return; } // We explicitly use lowercase keywords because for some reason gmail // interprets any string starting with an uppercase R as a request // to renegotiate the SSL connection. socket->write("mail from:<" + qxt_extract_address(msg.sender()) + ">\r\n"); if (extensions.contains("PIPELINING")) // almost all do nowadays { for (const QString &rcpt : recipients) { socket->write("rcpt to:<" + qxt_extract_address(rcpt) + ">\r\n"); } state = RcptAckPending; } else { state = MailToSent; } } void QxtSmtpPrivate::sendNextRcpt(const QByteArray &code, const QByteArray &line) { int messageID = pending.first().first; const QxtMailMessage &msg = pending.first().second; if (code[0] != '2') { // on failure, emit a warning signal if (!mailAck) { emit qxt_p().senderRejected(messageID, msg.sender()); emit qxt_p().senderRejected(messageID, msg.sender(), line); } else { emit qxt_p().recipientRejected(messageID, msg.sender()); emit qxt_p().recipientRejected(messageID, msg.sender(), line); } } else if (!mailAck) { mailAck = true; } else { rcptAck++; } if (rcptNumber == recipients.count()) { // all recipients have been sent if (rcptAck == 0) { // no recipients were considered valid emit qxt_p().mailFailed(messageID, code.toInt()); emit qxt_p().mailFailed(messageID, code.toInt(), line); pending.removeFirst(); sendNext(); } else { // at least one recipient was acknowledged, send mail body socket->write("data\r\n"); state = SendingBody; } } else if (state != RcptAckPending) { // send the next recipient unless we're only waiting on acks socket->write("rcpt to:<" + qxt_extract_address(recipients[rcptNumber]) + ">\r\n"); rcptNumber++; } else { // If we're only waiting on acks, just count them rcptNumber++; } } void QxtSmtpPrivate::sendBody(const QByteArray &code, const QByteArray &line) { int messageID = pending.first().first; const QxtMailMessage &msg = pending.first().second; if (code[0] != '3') { emit qxt_p().mailFailed(messageID, code.toInt()); emit qxt_p().mailFailed(messageID, code.toInt(), line); pending.removeFirst(); sendNext(); return; } socket->write(msg.rfc2822()); socket->write(".\r\n"); state = BodySent; }