Michael Zanetti 19e84d83da MailNotification: Fix double mail ending
While apparently the plugin was working with most mail servers,
the double \r\n.\r\n at the end violates the spec and newer
OpenSMTP servers are rejecting this.
2023-12-17 22:31:09 +01:00

467 lines
14 KiB
C++

/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2020, nymea GmbH
* Contact: contact@nymea.io
*
* This file is part of nymea.
* This project including source code and documentation is protected by
* copyright law, and remains the property of nymea GmbH. All rights, including
* reproduction, publication, editing and translation, are reserved. The use of
* this project is subject to the terms of a license agreement to be concluded
* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
* under https://nymea.io/license
*
* GNU Lesser General Public License Usage
* Alternatively, this project may be redistributed and/or modified under the
* terms of the GNU Lesser General Public License as published by the Free
* Software Foundation; version 3. This project 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this project. If not, see <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include "smtpclient.h"
#include "extern-plugininfo.h"
#include <QDateTime>
SmtpClient::SmtpClient(QObject *parent):
QObject(parent)
{
m_socket = new QSslSocket(this);
connect(m_socket, &QSslSocket::connected, this, &SmtpClient::connected);
connect(m_socket, &QSslSocket::readyRead, this, &SmtpClient::readData);
connect(m_socket, &QSslSocket::disconnected, this, &SmtpClient::disconnected);
connect(m_socket, &QSslSocket::encrypted, this, &SmtpClient::onEncrypted);
connect(m_socket, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(onSocketError(QAbstractSocket::SocketError)));
}
void SmtpClient::connectToHost()
{
switch (m_encryptionType) {
case EncryptionTypeNone:
case EncryptionTypeTLS:
// Note: the handshake will be done later, not from the beginning
m_socket->connectToHost(m_host, m_port);
break;
case EncryptionTypeSSL:
m_socket->connectToHostEncrypted(m_host, m_port);
break;
}
}
void SmtpClient::testLogin()
{
qCDebug(dcMailNotification()) << "Starting test login";
m_testLogin = true;
setState(StateInitialize);
m_socket->close();
connectToHost();
}
void SmtpClient::connected()
{
qCDebug(dcMailNotification()) << "Connected";
}
void SmtpClient::disconnected()
{
qCDebug(dcMailNotification()) << "Disconnected";
setState(StateIdle);
sendNextMail();
}
void SmtpClient::onEncrypted()
{
qCDebug(dcMailNotification()) << "Socket encrypted";
send("EHLO localhost");
setState(StateAuthentification);
}
void SmtpClient::readData()
{
while(m_socket->canReadLine()) {
QString responseLine;
responseLine = m_socket->readLine();
qCDebug(dcMailNotification()) << "<--" << responseLine;
bool responseCodeParseSuccess = false;
int responseCode = responseLine.left(3).toInt(&responseCodeParseSuccess);
if (!responseCodeParseSuccess) {
qCWarning(dcMailNotification()) << "Could not convert status code to a valid integer" << responseLine;
if (m_state != StateIdle) {
handleSmtpFailure();
continue;
}
} else {
processServerResponse(responseCode, responseLine);
}
}
}
int SmtpClient::sendMail(const QString &subject, const QString &body)
{
static int ids = 0;
Message message;
message.subject = subject;
message.body = body;
message.id = ids++;
m_messageQueue.enqueue(message);
sendNextMail();
return message.id;
}
void SmtpClient::setHost(const QString &host)
{
m_host = host;
}
void SmtpClient::setPort(const quint16 &port)
{
m_port = port;
}
void SmtpClient::setEncryptionType(const SmtpClient::EncryptionType &encryptionType)
{
m_encryptionType = encryptionType;
}
void SmtpClient::setAuthenticationMethod(const SmtpClient::AuthenticationMethod &authenticationMethod)
{
m_authenticationMethod = authenticationMethod;
}
void SmtpClient::setUser(const QString &user)
{
m_user = user;
}
void SmtpClient::setPassword(const QString &password)
{
m_password = password;
}
void SmtpClient::setSender(const QString &sender)
{
m_sender = sender;
}
void SmtpClient::setRecipients(const QStringList &recipients)
{
m_recipients = recipients;
}
QString SmtpClient::createDateString()
{
return QDateTime::currentDateTime().toString(Qt::RFC2822Date);
}
void SmtpClient::setState(SmtpClient::State state)
{
if (m_state == state)
return;
qCDebug(dcMailNotification()) << state;
m_state = state;
}
void SmtpClient::processServerResponse(int responseCode, const QString &response)
{
qCDebug(dcMailNotification()) << "Server response:" << responseCode << response;
switch (m_state) {
case StateIdle:
// Check if we have to send an other email, otherwise we are done and remain in idle
sendNextMail();
break;
case StateInitialize:
if (responseCode == 220) {
send("EHLO localhost");
if (m_encryptionType == EncryptionTypeNone) {
setState(StateAuthentification);
break;
}
if (m_encryptionType == EncryptionTypeSSL) {
setState(StateHandShake);
break;
}
if (m_encryptionType == EncryptionTypeTLS) {
setState(StateStartTls);
break;
}
}
break;
case StateHandShake:
// Ignore server information messages
if (responseCode == 250) {
break;
}
// We need a 220 befor continue
if (responseCode == 220) {
if (!m_socket->isEncrypted() && m_encryptionType != EncryptionTypeNone) {
qCDebug(dcMailNotification()) << "Start client encryption...";
m_socket->startClientEncryption();
}
} else {
handleUnexpectedSmtpCode(responseCode, response);
}
break;
case StateStartTls:
// Ignore server information messages until we get a '250 ...' instead of '250-....'
if (responseCode == 250 && response.at(3) != ' ') {
break;
}
if (responseCode == 250) {
send("STARTTLS");
setState(StateHandShake);
} else {
handleUnexpectedSmtpCode(responseCode, response);
}
break;
case StateAuthentification:
// Ignore server information messages of '250-....' (with dash), we need a clear "250 ..."
if (responseCode == 250 && response.at(3) != ' ') {
break;
}
if (responseCode == 250 || responseCode == 220) {
if (m_authenticationMethod == AuthenticationMethodLogin) {
send("AUTH LOGIN");
setState(StateUser);
break;
}
if (m_authenticationMethod == AuthenticationMethodPlain) {
send("AUTH PLAIN " + QByteArray().append(static_cast<char>(0))
.append(m_user)
.append(static_cast<char>(0))
.append(m_password)
.toBase64());
// If we just want to test the Login, we are almost done here
if (!m_testLogin) {
setState(StateMail);
} else {
setState(StateTestLoginFinished);
}
break;
}
} else {
handleUnexpectedSmtpCode(responseCode, response);
}
break;
case StateUser:
if (responseCode == 334) {
send(QByteArray().append(m_user).toBase64());
setState(StatePassword);
} else {
handleUnexpectedSmtpCode(responseCode, response);
}
break;
case StatePassword:
if (responseCode == 334) {
send(QByteArray().append(m_password).toBase64());
// if we just want to test the Login, we are almost done here
if (!m_testLogin) {
setState(StateMail);
} else {
setState(StateTestLoginFinished);
}
break;
} else {
handleUnexpectedSmtpCode(responseCode, response);
}
break;
case StateTestLoginFinished:
// Ignore server information messages
if (responseCode == 250) {
break;
}
if (responseCode == 235) {
emit testLoginFinished(true);
} else {
emit testLoginFinished(false);
}
m_socket->close();
m_testLogin = false;
break;
case StateMail:
// Ignore server information messages
if (responseCode == 250) {
break;
}
if (responseCode == 235) {
send("MAIL FROM:<" + m_sender + ">");
// Prepare queue for recipients
m_recipientsQueue.clear();
qCDebug(dcMailNotification()) << "Prepare recipients list" << m_recipients;
foreach (const QString &recipient, m_recipients) {
m_recipientsQueue.enqueue(recipient.trimmed());
}
setState(StateRcpt);
} else {
handleUnexpectedSmtpCode(responseCode, response);
}
break;
case StateRcpt:
// Ignore server information messages until we get a '250 ...' instead of '250-....'
if (responseCode == 250 && response.at(3) != ' ') {
break;
}
if (responseCode == 250) {
send("RCPT TO:<" + m_recipientsQueue.dequeue() + ">");
if (m_recipientsQueue.isEmpty()) {
setState(StateData);
}
} else {
handleUnexpectedSmtpCode(responseCode, response);
}
break;
case StateData:
// Ignore server information messages until we get a '250 ...' instead of '250-....'
if (responseCode == 250 && response.at(3) != ' ') {
break;
}
if (responseCode == 250) {
send("DATA");
setState(StateBody);
} else {
handleUnexpectedSmtpCode(responseCode, response);
}
break;
case StateBody:
if (responseCode == 354) {
send(m_messageData);
setState(StateQuit);
} else {
handleUnexpectedSmtpCode(responseCode, response);
}
break;
case StateQuit:
// Ignore server information messages until we get a '250 ...' instead of '250-....'
if (responseCode == 250 && response.at(3) != ' ') {
break;
}
if (responseCode == 250) {
emit sendMailFinished(true, m_message.id);
send("QUIT");
setState(StateClose);
} else {
handleUnexpectedSmtpCode(responseCode, response);
}
break;
case StateClose:
if (responseCode == 221) {
m_socket->close();
} else {
qCDebug(dcMailNotification()) << "The server does not handle the QUIT command. This is ok, we close the socket either way.";
}
// some mail server does not recognize the QUIT command...so close the connection either way
m_socket->close();
break;
}
}
void SmtpClient::sendNextMail()
{
// Check if there is a mail left to send
if (m_messageQueue.isEmpty())
return;
// Check if busy
if (m_state != StateIdle)
return;
sendEmailInternally(m_messageQueue.dequeue());
}
void SmtpClient::sendEmailInternally(const Message &message)
{
qCDebug(dcMailNotification()) << "Start sending message" << message.subject << message.body;
// Initialize data for sending
m_message = message;
m_messageData.clear();
// Create plain message content
m_messageData = "To: " + m_recipients.join(",") + "\r\n";
m_messageData.append("From: " + m_sender + "\r\n");
m_messageData.append("Subject: " + message.subject + "\r\n");
m_messageData.append("Date: " + createDateString() + "\r\n");
m_messageData.append("Content-Type: text/plain; charset=\"UTF-8\"\r\n");
m_messageData.append("Content-Transfer-Encoding: quoted-printable\r\n");
m_messageData.append("MIME-Version: 1.0\r\n");
m_messageData.append("X-Mailer: nymea;\r\n");
m_messageData.append("\r\n");
m_messageData.append(message.body);
m_messageData.append("\r\n.\r\n");
setState(StateInitialize);
// Make sure the connection starts from the beginning
m_socket->close();
connectToHost();
}
void SmtpClient::handleSmtpFailure()
{
if (m_testLogin) {
emit testLoginFinished(false);
} else {
emit sendMailFinished(false, m_message.id);
}
// Clean up
m_socket->close();
m_messageData.clear();
m_testLogin = false;
// Set idle state (handles the queue)
setState(StateIdle);
}
void SmtpClient::handleUnexpectedSmtpCode(int responseCode, const QString &serverMessage)
{
qCWarning(dcMailNotification()) << "Received unexpected error code from smtp server" << responseCode << serverMessage;
handleSmtpFailure();
}
void SmtpClient::onSocketError(QAbstractSocket::SocketError error)
{
qCWarning(dcMailNotification()) << "Mail socket error" << error << m_socket->errorString();
if (m_state != StateIdle) {
handleSmtpFailure();
}
}
void SmtpClient::send(const QString &data)
{
qCDebug(dcMailNotification()) << "-->" << data;
m_socket->write(data.toUtf8() + "\r\n");
m_socket->flush();
}