diff --git a/mailnotification/devicepluginmailnotification.cpp b/mailnotification/devicepluginmailnotification.cpp index f526a274..15fcc3f3 100644 --- a/mailnotification/devicepluginmailnotification.cpp +++ b/mailnotification/devicepluginmailnotification.cpp @@ -36,11 +36,6 @@ \chapter Supported services - \section2 Google Mail - With the Google Mail Notification you can send a mail with your gmail address to a recipient. The - username is your mail address (e.g. "chuck.norris@gmail.com"). The recipient will receive the notification - from your gmail account. - \section2 Yahoo Mail The Yahoo Mail Notification you can send a mail with your yahoo address to a recipient. The username is your mail address (e.g. "chuck.norris@yahoo.com"). The recipient will receive the notification @@ -80,62 +75,20 @@ DevicePluginMailNotification::~DevicePluginMailNotification() DeviceManager::DeviceSetupStatus DevicePluginMailNotification::setupDevice(Device *device) { - // Google mail - if(device->deviceClassId() == googleMailDeviceClassId) { - SmtpClient *smtpClient = new SmtpClient(this); - smtpClient->setHost("smtp.gmail.com"); - smtpClient->setPort(465); - smtpClient->setUser(device->paramValue(googleMailDeviceUserParamTypeId).toString()); - // TODO: use cryptography to save password not as plain text - smtpClient->setPassword(device->paramValue(googleMailDevicePasswordParamTypeId).toString()); - smtpClient->setAuthMethod(SmtpClient::AuthMethodLogin); - smtpClient->setEncryptionType(SmtpClient::EncryptionTypeSSL); - smtpClient->setSender(device->paramValue(googleMailDeviceUserParamTypeId).toString()); - smtpClient->setRecipient(device->paramValue(googleMailDeviceRecipientParamTypeId).toString()); - - connect(smtpClient, &SmtpClient::testLoginFinished, this, &DevicePluginMailNotification::testLoginFinished); - connect(smtpClient, &SmtpClient::sendMailFinished, this, &DevicePluginMailNotification::sendMailFinished); - m_clients.insert(smtpClient,device); - - smtpClient->testLogin(); - - return DeviceManager::DeviceSetupStatusAsync; - } - // Yahoo mail - if(device->deviceClassId() == yahooMailDeviceClassId) { - SmtpClient *smtpClient = new SmtpClient(this); - smtpClient->setHost("smtp.mail.yahoo.com"); - smtpClient->setPort(465); - smtpClient->setUser(device->paramValue(yahooMailDeviceUserParamTypeId).toString()); - // TODO: use cryptography to save password not as plain text - smtpClient->setPassword(device->paramValue(yahooMailDevicePasswordParamTypeId).toString()); - smtpClient->setAuthMethod(SmtpClient::AuthMethodLogin); - smtpClient->setEncryptionType(SmtpClient::EncryptionTypeSSL); - smtpClient->setSender(device->paramValue(yahooMailDeviceUserParamTypeId).toString()); - smtpClient->setRecipient(device->paramValue(yahooMailDeviceRecipientParamTypeId).toString()); - - connect(smtpClient, &SmtpClient::testLoginFinished, this, &DevicePluginMailNotification::testLoginFinished); - connect(smtpClient, &SmtpClient::sendMailFinished, this, &DevicePluginMailNotification::sendMailFinished); - m_clients.insert(smtpClient,device); - - smtpClient->testLogin(); - - return DeviceManager::DeviceSetupStatusAsync; - } // Custom mail if(device->deviceClassId() == customMailDeviceClassId) { SmtpClient *smtpClient = new SmtpClient(this); smtpClient->setHost(device->paramValue(customMailDeviceSmtpParamTypeId).toString()); - smtpClient->setPort(device->paramValue(customMailDevicePortParamTypeId).toInt()); + smtpClient->setPort(static_cast(device->paramValue(customMailDevicePortParamTypeId).toUInt())); smtpClient->setUser(device->paramValue(customMailDeviceCustomUserParamTypeId).toString()); // TODO: use cryptography to save password not as plain text smtpClient->setPassword(device->paramValue(customMailDeviceCustomPasswordParamTypeId).toString()); if(device->paramValue(customMailDeviceAuthenticationParamTypeId).toString() == "PLAIN") { - smtpClient->setAuthMethod(SmtpClient::AuthMethodPlain); + smtpClient->setAuthenticationMethod(SmtpClient::AuthenticationMethodPlain); } else if(device->paramValue(customMailDeviceAuthenticationParamTypeId).toString() == "LOGIN") { - smtpClient->setAuthMethod(SmtpClient::AuthMethodLogin); + smtpClient->setAuthenticationMethod(SmtpClient::AuthenticationMethodLogin); } else { return DeviceManager::DeviceSetupStatusFailure; } @@ -150,7 +103,10 @@ DeviceManager::DeviceSetupStatus DevicePluginMailNotification::setupDevice(Devic return DeviceManager::DeviceSetupStatusFailure; } - smtpClient->setRecipient(device->paramValue(customMailDeviceCustomRecipientParamTypeId).toString()); + QString recipientsString = device->paramValue(customMailDeviceCustomRecipientParamTypeId).toString(); + QStringList recipients = recipientsString.split(","); + + smtpClient->setRecipients(recipients); smtpClient->setSender(device->paramValue(customMailDeviceCustomSenderParamTypeId).toString()); connect(smtpClient, &SmtpClient::testLoginFinished, this, &DevicePluginMailNotification::testLoginFinished); @@ -166,12 +122,22 @@ DeviceManager::DeviceSetupStatus DevicePluginMailNotification::setupDevice(Devic DeviceManager::DeviceError DevicePluginMailNotification::executeAction(Device *device, const Action &action) { - if(action.actionTypeId() == googleMailSendMailActionTypeId) { - SmtpClient *smtpClient = m_clients.key(device); - smtpClient->sendMail(action.param(googleMailSendMailActionSubjectParamTypeId).value().toString(), action.param(googleMailSendMailActionBodyParamTypeId).value().toString(), action.id()); - return DeviceManager::DeviceErrorAsync; + if (device->deviceClassId() == customMailDeviceClassId) { + if(action.actionTypeId() == customMailNotifyActionTypeId) { + SmtpClient *smtpClient = m_clients.key(device); + if (!smtpClient) { + qCWarning(dcMailNotification()) << "Could not find SMTP client for " << device; + return DeviceManager::DeviceErrorHardwareNotAvailable; + } + + smtpClient->sendMail(action.param(customMailNotifyActionTitleParamTypeId).value().toString(), + action.param(customMailNotifyActionBodyParamTypeId).value().toString(), + action.id()); + return DeviceManager::DeviceErrorAsync; + } + return DeviceManager::DeviceErrorActionTypeNotFound; } - return DeviceManager::DeviceErrorActionTypeNotFound; + return DeviceManager::DeviceErrorDeviceClassNotFound; } void DevicePluginMailNotification::deviceRemoved(Device *device) @@ -185,9 +151,11 @@ void DevicePluginMailNotification::testLoginFinished(const bool &success) { SmtpClient *smtpClient = static_cast(sender()); Device *device = m_clients.value(smtpClient); - if(success) { + if (success) { + qCDebug(dcMailNotification()) << "Email login test successfull"; emit deviceSetupFinished(device, DeviceManager::DeviceSetupStatusSuccess); } else { + qCWarning(dcMailNotification()) << "Email login test failed"; emit deviceSetupFinished(device, DeviceManager::DeviceSetupStatusFailure); if(m_clients.contains(smtpClient)) { m_clients.remove(smtpClient); @@ -198,9 +166,11 @@ void DevicePluginMailNotification::testLoginFinished(const bool &success) void DevicePluginMailNotification::sendMailFinished(const bool &success, const ActionId &actionId) { - if(success) { + if (success) { + qCDebug(dcMailNotification()) << "Email sent successfully"; emit actionExecutionFinished(actionId, DeviceManager::DeviceErrorNoError); } else { + qCWarning(dcMailNotification()) << "Email sending failed"; emit actionExecutionFinished(actionId, DeviceManager::DeviceErrorDeviceNotFound); } } diff --git a/mailnotification/devicepluginmailnotification.json b/mailnotification/devicepluginmailnotification.json index c18ed934..56ad4833 100644 --- a/mailnotification/devicepluginmailnotification.json +++ b/mailnotification/devicepluginmailnotification.json @@ -12,33 +12,34 @@ "id": "f4844c97-7ca6-4349-904e-ff9749a9fe74", "name": "customMail", "displayName": "Custom mail", - "createMethods": ["user"], + "createMethods": [ "user" ], + "interfaces": [ "notifications" ], "paramTypes": [ { "id": "af30ce7b-fb6b-42f0-889d-20b32f8b8fa4", "name": "customSender", - "displayName": "sender mail", + "displayName": "Sender mail", "type": "QString", "inputType": "Mail" }, { "id": "b91d0ecc-6903-4991-ae8d-f36757ce40a7", "name": "customUser", - "displayName": "user", + "displayName": "User", "type": "QString", "inputType": "TextLine" }, { "id": "ac29e643-1d18-4612-8d2f-65fb07a67182", "name": "customPassword", - "displayName": "password", + "displayName": "Password", "type": "QString", "inputType": "Password" }, { "id": "d657f002-9741-42e1-9fef-32eae96dacdb", "name": "customRecipient", - "displayName": "recipient", + "displayName": "Recipient", "type": "QString", "inputType": "Mail" }, @@ -52,9 +53,9 @@ { "id": "56ec204f-2e02-4a17-b9d1-e855e384b689", "name": "port", - "displayName": "port", + "displayName": "Port", "type": "int", - "defaultValue": "465" + "defaultValue": 25 }, { "id": "789b963b-4143-4e21-853c-2612707d726f", @@ -75,130 +76,12 @@ "actionTypes": [ { "id": "054613b0-3666-4dad-9252-e0ebca187edc", - "name": "send", + "name": "notify", "displayName": "send mail", "paramTypes": [ { "id": "2047e0f4-3d34-4214-bc8f-9ab741ae6006", - "name": "subject", - "displayName": "subject", - "type": "QString", - "inputType": "TextLine" - }, - { - "id": "aeb6e79e-9862-43e1-9873-cbdce549344a", - "name": "body", - "displayName": "body", - "type": "QString", - "inputType": "TextArea" - } - ] - } - ] - } - ] - }, - { - "name": "google", - "displayName": "Google", - "id": "d02399c5-8e79-40be-8cf6-e00b55bbfe7c", - "deviceClasses": [ - { - "id": "3869884a-1592-4b8f-84a7-994be18ff555", - "name": "googleMail", - "displayName": "Google mail", - "createMethods": ["user"], - "paramTypes": [ - { - "id": "0a4ea3ff-ea76-4a5f-adbb-b8f79faa2156", - "name": "user", - "displayName": "user", - "type": "QString", - "inputType": "Mail" - }, - { - "id": "f63f6486-5340-472a-b37c-be828111725c", - "name": "password", - "displayName": "password", - "type": "QString", - "inputType": "Password" - }, - { - "id": "76697fe2-3393-4ed0-a15d-9d041502cfd3", - "name": "recipient", - "displayName": "recipient", - "type": "QString", - "inputType": "Mail" - } - ], - "actionTypes": [ - { - "id": "054613b0-3666-4dad-9252-e0ebca187edc", - "name": "sendMail", - "displayName": "send mail", - "paramTypes": [ - { - "id": "2047e0f4-3d34-4214-bc8f-9ab741ae6006", - "name": "subject", - "displayName": "subject", - "type": "QString", - "inputType": "TextLine" - }, - { - "id": "aeb6e79e-9862-43e1-9873-cbdce549344a", - "name": "body", - "displayName": "body", - "type": "QString", - "inputType": "TextArea" - } - ] - } - ] - } - ] - }, - { - "name": "yahoo", - "displayName": "Yahoo", - "id": "5eb3e1e9-25a7-4740-806e-5cfc836e4bf5", - "deviceClasses": [ - { - "id": "59409e8f-0c83-414f-abd5-bbbf2758acba", - "name": "yahooMail", - "displayName": "Yahoo mail", - "createMethods": ["user"], - "paramTypes": [ - { - "id": "0a4ea3ff-ea76-4a5f-adbb-b8f79faa2156", - "name": "user", - "displayName": "user", - "type": "QString", - "inputType": "Mail" - }, - { - "id": "f63f6486-5340-472a-b37c-be828111725c", - "name": "password", - "displayName": "password", - "type": "QString", - "inputType": "Password" - }, - { - "id": "76697fe2-3393-4ed0-a15d-9d041502cfd3", - "name": "recipient", - "displayName": "recipient", - "type": "QString", - "inputType": "Mail" - } - ], - "actionTypes": [ - { - "id": "054613b0-3666-4dad-9252-e0ebca187edc", - "name": "send", - "displayName": "send mail", - "paramTypes": [ - { - "id": "2047e0f4-3d34-4214-bc8f-9ab741ae6006", - "name": "subject", + "name": "title", "displayName": "subject", "type": "QString", "inputType": "TextLine" diff --git a/mailnotification/smtpclient.cpp b/mailnotification/smtpclient.cpp index e6e9764f..4465f3b5 100644 --- a/mailnotification/smtpclient.cpp +++ b/mailnotification/smtpclient.cpp @@ -23,222 +23,94 @@ #include "smtpclient.h" #include "extern-plugininfo.h" +#include + +Q_LOGGING_CATEGORY(dcSmtpClient, "SmtpClient") + SmtpClient::SmtpClient(QObject *parent): QObject(parent) { m_socket = new QSslSocket(this); - m_state = InitState; - m_testLogin = false; - connect(m_socket, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(socketError(QAbstractSocket::SocketError))); - // error because QSslSocket has also a method called error...also QAbstractSocket or QTcpSocket don't work... - //connect(m_socket, &QSslSocket::error, this, &SmtpClient::socketError); 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; - case EncryptionTypeTLS: - m_socket->connectToHost(m_host,m_port); - break; - default: - break; } } void SmtpClient::testLogin() { - m_socket->close(); m_testLogin = true; + setState(StateInitialize); + m_socket->close(); connectToHost(); } void SmtpClient::connected() { + qCDebug(dcSmtpClient()) << "Connected"; } void SmtpClient::disconnected() { + qCDebug(dcSmtpClient()) << "Disconnected"; + setState(StateIdle); + sendNextMail(); +} + +void SmtpClient::onEncrypted() +{ + qCDebug(dcSmtpClient()) << "Socket encrypted"; + send("EHLO localhost"); + setState(StateAuthentification); } void SmtpClient::readData() { - QString response; - QString responseLine; - - while(m_socket->canReadLine() && responseLine[3] != ' ') { + while(m_socket->canReadLine()) { + QString responseLine; responseLine = m_socket->readLine(); - response.append(responseLine); - } - responseLine.truncate( 3 ); - switch (m_state) { - case InitState: - if(responseLine == "220") { - send("EHLO localhost"); - if(m_encryptionType == EncryptionTypeNone) { - m_state = AuthentificationState; - break; + qCDebug(dcSmtpClient()) << "<--" << responseLine; + + bool responseCodeParseSuccess = false; + int responseCode = responseLine.left(3).toInt(&responseCodeParseSuccess); + if (!responseCodeParseSuccess) { + qCWarning(dcSmtpClient()) << "Could not convert status code to a valid integer" << responseLine; + if (m_state != StateIdle) { + handleSmtpFailure(); + continue; } - if(m_encryptionType == EncryptionTypeSSL) { - m_state = HandShakeState; - break; - } - if(m_encryptionType == EncryptionTypeTLS) { - m_state = StartTlsState; - break; - } - } - break; - case HandShakeState: - if(responseLine == "250") { - if(!m_socket->isEncrypted() && m_encryptionType != EncryptionTypeNone) { - m_socket->startClientEncryption(); - } - send("EHLO localhost"); - m_state = AuthentificationState; - } - if(responseLine == "220") { - if(!m_socket->isEncrypted() && m_encryptionType != EncryptionTypeNone) { - m_socket->startClientEncryption(); - } - send("EHLO localhost"); - m_state = AuthentificationState; - } - break; - case StartTlsState: - if(responseLine == "250") { - send("STARTTLS"); - m_state = HandShakeState; - } - break; - case AuthentificationState: - if(responseLine == "250") { - if(m_authMethod == AuthMethodLogin) { - send("AUTH LOGIN"); - m_state = UserState; - break; - } - if(m_authMethod == AuthMethodPlain) { - send("AUTH PLAIN " + QByteArray().append((char) 0).append(m_user).append((char) 0).append(m_password).toBase64()); - // if we just want to test the Login, we are almost done here - if(!m_testLogin) { - m_state = MailState; - } else { - m_state = TestLoginFinishedState; - } - break; - } - } - break; - case UserState: - if(responseLine == "334") { - send(QByteArray().append(m_user).toBase64()); - m_state = PasswordState; - } - break; - case PasswordState: - if(responseLine == "334") { - send(QByteArray().append(m_password).toBase64()); - // if we just want to test the Login, we are almost done here - if(!m_testLogin) { - m_state = MailState; - } else { - m_state = TestLoginFinishedState; - } - break; - } - break; - case TestLoginFinishedState: - if(responseLine == "235") { - emit testLoginFinished(true); } else { - emit testLoginFinished(false); + processServerResponse(responseCode, responseLine); } - m_socket->close(); - m_testLogin = false; - break; - case MailState: - if(responseLine == "235") { - send("MAIL FROM:<" + m_sender + ">"); - m_state = RcptState; - } - break; - case RcptState: - if(responseLine == "250") { - send("RCPT TO:<" + m_rcpt + ">"); - m_state = DataState; - } - break; - case DataState: - if(responseLine == "250") { - send("DATA"); - m_state = BodyState; - } - break; - case BodyState: - if(responseLine == "354") { - send(m_message + "\r\n.\r\n"); - m_state = QuitState; - } - break; - case QuitState: - if(responseLine == "250") { - emit sendMailFinished(true, m_actionId); - send("QUIT"); - m_state = CloseState; - } - break; - case CloseState: - if(responseLine == "221") { - m_socket->close(); - } - // some mail server does not recognize the QUIT command...so close the connection either way - m_socket->close(); - break; - default: - // unexpecterd response code received... - if(m_testLogin) { - emit testLoginFinished(false); - m_testLogin = false; - m_socket->close(); - break; - } - emit sendMailFinished(false, m_actionId); - m_socket->close(); - break; } } -bool SmtpClient::sendMail(const QString &subject, const QString &body, const ActionId &actionId) +void SmtpClient::sendMail(const QString &subject, const QString &body, const ActionId &actionId) { - m_actionId = actionId; + Message message; + message.subject = subject; + message.body = body; + message.actionId = actionId; - // create mail content - m_state = InitState; - m_message.clear(); - - m_message = "To: " + m_rcpt + "\n"; - m_message.append("From: " + m_sender + "\n"); - m_message.append("Subject: [nymea notification] | " + subject + "\n"); - m_message.append(body); - m_message.replace( QString::fromLatin1( "\n" ), QString::fromLatin1( "\r\n" ) ); - m_message.replace( QString::fromLatin1( "\r\n.\r\n" ), QString::fromLatin1( "\r\n..\r\n" ) ); - m_message.append("\r\n.\r\n"); - m_socket->close(); - connectToHost(); - - return true; + m_messageQueue.enqueue(message); + sendNextMail(); } void SmtpClient::setHost(const QString &host) @@ -246,7 +118,7 @@ void SmtpClient::setHost(const QString &host) m_host = host; } -void SmtpClient::setPort(const int &port) +void SmtpClient::setPort(const quint16 &port) { m_port = port; } @@ -256,9 +128,9 @@ void SmtpClient::setEncryptionType(const SmtpClient::EncryptionType &encryptionT m_encryptionType = encryptionType; } -void SmtpClient::setAuthMethod(const SmtpClient::AuthMethod &authMethod) +void SmtpClient::setAuthenticationMethod(const SmtpClient::AuthenticationMethod &authenticationMethod) { - m_authMethod = authMethod; + m_authenticationMethod = authenticationMethod; } void SmtpClient::setUser(const QString &user) @@ -276,18 +148,297 @@ void SmtpClient::setSender(const QString &sender) m_sender = sender; } -void SmtpClient::setRecipient(const QString &rcpt) +void SmtpClient::setRecipients(const QStringList &recipients) { - m_rcpt = rcpt; + m_recipients = recipients; } -void SmtpClient::socketError(QAbstractSocket::SocketError error) +QString SmtpClient::createDateString() { - qCWarning(dcMailNotification) << "Mail socket -> " << error << m_socket->errorString(); + return QDateTime::currentDateTime().toString(Qt::RFC2822Date); +} + +void SmtpClient::setState(SmtpClient::State state) +{ + if (m_state == state) + return; + + qCDebug(dcSmtpClient()) << state; + m_state = state; +} + +void SmtpClient::processServerResponse(int responseCode, const QString &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(dcSmtpClient()) << "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 until we get a '250 ...' instead of '250-....' + if (responseCode == 250 && response.at(3) != ' ') { + break; + } + + if (responseCode == 250) { + if (m_authenticationMethod == AuthenticationMethodLogin) { + send("AUTH LOGIN"); + setState(StateUser); + break; + } + + if (m_authenticationMethod == AuthenticationMethodPlain) { + send("AUTH PLAIN " + QByteArray().append(static_cast(0)) + .append(m_user) + .append(static_cast(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: + if (responseCode == 235) { + emit testLoginFinished(true); + } else { + emit testLoginFinished(false); + } + m_socket->close(); + m_testLogin = false; + break; + case StateMail: + if (responseCode == 235) { + send("MAIL FROM:<" + m_sender + ">"); + + // Prepare queue for recipients + m_recipientsQueue.clear(); + qCDebug(dcSmtpClient()) << "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 + "\r\n.\r\n"); + 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.actionId); + send("QUIT"); + setState(StateClose); + } else { + handleUnexpectedSmtpCode(responseCode, response); + } + break; + case StateClose: + if (responseCode == 221) { + m_socket->close(); + } else { + qCDebug(dcSmtpClient()) << "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.actionId); + } + + // 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(dcSmtpClient()) << "-->" << data; m_socket->write(data.toUtf8() + "\r\n"); m_socket->flush(); } diff --git a/mailnotification/smtpclient.h b/mailnotification/smtpclient.h index 6f7e5ee1..21333f6e 100644 --- a/mailnotification/smtpclient.h +++ b/mailnotification/smtpclient.h @@ -23,87 +23,119 @@ #ifndef SMTPCLIENT_H #define SMTPCLIENT_H -#include +#include #include +#include #include #include +#include +#include #include "plugin/deviceplugin.h" +Q_DECLARE_LOGGING_CATEGORY(dcSmtpClient) + +struct Message { + QString subject; + QString body; + ActionId actionId; +}; + + class SmtpClient : public QObject { Q_OBJECT public: - enum AuthMethod{ - AuthMethodPlain, - AuthMethodLogin + enum AuthenticationMethod{ + AuthenticationMethodPlain, + AuthenticationMethodLogin }; + Q_ENUM(AuthenticationMethod) - enum SendState{ - InitState, - HandShakeState, - AuthentificationState, - StartTlsState, - UserState, - PasswordState, - TestLoginFinishedState, - MailState, - RcptState, - DataState, - BodyState, - QuitState, - CloseState + enum State{ + StateIdle, + StateInitialize, + StateHandShake, + StateAuthentification, + StateStartTls, + StateUser, + StatePassword, + StateTestLoginFinished, + StateMail, + StateRcpt, + StateData, + StateBody, + StateQuit, + StateClose }; + Q_ENUM(State) enum EncryptionType{ EncryptionTypeNone, EncryptionTypeSSL, EncryptionTypeTLS }; + Q_ENUM(EncryptionType) - explicit SmtpClient(QObject *parent = 0); + explicit SmtpClient(QObject *parent = nullptr); void connectToHost(); void testLogin(); - bool sendMail(const QString &subject, const QString &body, const ActionId &actionId); + void sendMail(const QString &subject, const QString &body, const ActionId &actionId); void setHost(const QString &host); - void setPort(const int &port); + void setPort(const quint16 &port); void setEncryptionType(const EncryptionType &encryptionType); - void setAuthMethod(const AuthMethod &authMethod); + void setAuthenticationMethod(const AuthenticationMethod &authenticationMethod); void setUser(const QString &user); void setPassword(const QString &password); void setSender(const QString &sender); - void setRecipient(const QString &rcpt); - + void setRecipients(const QStringList &recipients); private: - QSslSocket *m_socket; - SendState m_state; - QString m_host; - int m_port; + QSslSocket *m_socket = nullptr; + State m_state = StateIdle; + QString m_host = "127.0.0.1"; + quint16 m_port = 25; + QString m_user; QString m_password; QString m_sender; - AuthMethod m_authMethod; + AuthenticationMethod m_authenticationMethod; EncryptionType m_encryptionType; - QString m_rcpt; - QString m_subject; - QString m_boy; - QString m_message; - ActionId m_actionId; + QStringList m_recipients; + QQueue m_recipientsQueue; - bool m_testLogin; + // Created for each message + Message m_message; + QString m_messageData; + + QQueue m_messageQueue; + + bool m_testLogin = false; + + QString createDateString(); + void setState(State state); + + void processServerResponse(int responseCode, const QString &response); + + void sendNextMail(); + void sendEmailInternally(const Message &message); + + void handleSmtpFailure(); + void handleUnexpectedSmtpCode(int responseCode, const QString &serverMessage); signals: void sendMailFinished(const bool &success, const ActionId &actionId); void testLoginFinished(const bool &success); private slots: - void socketError(QAbstractSocket::SocketError error); + + void onSocketError(QAbstractSocket::SocketError error); void connected(); void disconnected(); + void onEncrypted(); void readData(); void send(const QString &data); };