#include "ESPEasy_common.h" #ifdef USES_N001 //####################################################################################################### //########################### Notification Plugin 001: Email ############################################ //####################################################################################################### #define NPLUGIN_001 #define NPLUGIN_ID_001 1 #define NPLUGIN_NAME_001 "Email (SMTP)" #define NPLUGIN_001_TIMEOUT 5000 #include "src/DataStructs/ESPEasy_EventStruct.h" #include "src/DataStructs/NotificationSettingsStruct.h" #include "src/ESPEasyCore/ESPEasy_Log.h" #include "src/ESPEasyCore/ESPEasy_backgroundtasks.h" #include "src/Globals/NPlugins.h" #include "src/Globals/Settings.h" #include "src/Helpers/ESPEasy_Storage.h" #include "src/Helpers/ESPEasy_time_calc.h" #include "src/Helpers/Networking.h" #include "src/Helpers/StringParser.h" #include "src/Helpers/_CPlugin_Helper.h" // safeReadStringUntil #include "src/Helpers/_NPlugin_init.h" // Forward declaration bool NPlugin_001_send(const NotificationSettingsStruct& notificationsettings, const String& aSub, String& aMesg); bool NPlugin_001_Auth(WiFiClient& client, const String& user, const String& pass); bool NPlugin_001_MTA(WiFiClient& client, const String& aStr, const String &aWaitForPattern); bool getNextMailAddress(const String& data, String& address, int index); // The message body is included in event->String1 bool NPlugin_001(NPlugin::Function function, struct EventStruct *event, String& string) { bool success = false; switch (function) { case NPlugin::Function::NPLUGIN_PROTOCOL_ADD: { Notification[++notificationCount].Number = NPLUGIN_ID_001; Notification[notificationCount].usesMessaging = true; Notification[notificationCount].usesGPIO = 0; break; } case NPlugin::Function::NPLUGIN_GET_DEVICENAME: { string = F(NPLUGIN_NAME_001); break; } // Edwin: NPlugin::Function::NPLUGIN_WRITE seems to be not implemented/not used yet? Disabled because its confusing now. // case NPlugin::Function::NPLUGIN_WRITE: // { // String log; // String command = parseString(string, 1); // // if (command == F("email")) // { // MakeNotificationSettings(NotificationSettings); // LoadNotificationSettings(event->NotificationIndex, (uint8_t*)&NotificationSettings, sizeof(NotificationSettingsStruct)); // NPlugin_001_send(NotificationSettings.Domain, NotificationSettings.Receiver, NotificationSettings.Sender, NotificationSettings.Subject, NotificationSettings.Body, NotificationSettings.Server, NotificationSettings.Port); // success = true; // } // break; // } case NPlugin::Function::NPLUGIN_NOTIFY: { MakeNotificationSettings(NotificationSettings); LoadNotificationSettings(event->NotificationIndex, (uint8_t*)&NotificationSettings, sizeof(NotificationSettingsStruct)); NotificationSettings.validate(); String subject = NotificationSettings.Subject; String body; if (event->String1.length() > 0) body = event->String1; else body = NotificationSettings.Body; subject = parseTemplate(subject); body = parseTemplate(body); NPlugin_001_send(NotificationSettings, subject, body); success = true; break; } default: break; } return success; } bool NPlugin_001_send(const NotificationSettingsStruct& notificationsettings, const String& aSub, String& aMesg) { // String& aDomain , String aTo, String aFrom, String aSub, String aMesg, String aHost, int aPort) bool myStatus = false; // Use WiFiClient class to create TCP connections WiFiClient client; client.setTimeout(CONTROLLER_CLIENTTIMEOUT_DFLT); String aHost = notificationsettings.Server; if (loglevelActiveFor(LOG_LEVEL_DEBUG)) addLog(LOG_LEVEL_DEBUG, String(F("EMAIL: Connecting to ")) + aHost + notificationsettings.Port); if (!connectClient(client, aHost.c_str(), notificationsettings.Port, CONTROLLER_CLIENTTIMEOUT_DFLT)) { if (loglevelActiveFor(LOG_LEVEL_ERROR)) addLog(LOG_LEVEL_ERROR, String(F("EMAIL: Error connecting to ")) + aHost + notificationsettings.Port); myStatus = false; }else { String mailheader = F( "From: $nodename <$emailfrom>\r\n" "To: $ato\r\n" "Subject: $subject\r\n" "Reply-To: $nodename <$emailfrom>\r\n" "MIME-VERSION: 1.0\r\n" "Content-type: text/html; charset=UTF-8\r\n" "X-Mailer: EspEasy v$espeasyversion\r\n\r\n" ); String email_address = notificationsettings.Sender; int pos_less = email_address.indexOf('<'); if (pos_less == -1) { // No email address markup mailheader.replace(F("$nodename"), Settings.getHostname()); mailheader.replace(F("$emailfrom"), notificationsettings.Sender); } else { String senderName = email_address.substring(0, pos_less); senderName.replace(F("\""), EMPTY_STRING); // Remove quotes String address = email_address.substring(pos_less + 1); address.replace(F("<"), EMPTY_STRING); address.replace(F(">"), EMPTY_STRING); address.trim(); senderName.trim(); mailheader.replace(F("$nodename"), senderName); mailheader.replace(F("$emailfrom"), address); } mailheader.replace(F("$nodename"), Settings.getHostname()); mailheader.replace(F("$emailfrom"), notificationsettings.Sender); mailheader.replace(F("$ato"), notificationsettings.Receiver); mailheader.replace(F("$subject"), aSub); mailheader.replace(F("$espeasyversion"), String(BUILD)); aMesg.replace(F("\r"), F("
")); // re-write line breaks for Content-type: text/html // Wait for Client to Start Sending // The MTA Exchange while (true) { if (!NPlugin_001_MTA(client, EMPTY_STRING, F("220 "))) break; if (!NPlugin_001_MTA(client, String(F("EHLO ")) + notificationsettings.Domain, F("250 "))) break; if (!NPlugin_001_Auth(client, notificationsettings.User, notificationsettings.Pass)) break; if (!NPlugin_001_MTA(client, String(F("MAIL FROM:<")) + notificationsettings.Sender + ">", F("250 "))) break; bool nextAddressAvailable = true; int i = 0; String emailTo; if (!getNextMailAddress(notificationsettings.Receiver, emailTo, i)) { addLog(LOG_LEVEL_ERROR, F("Email: No recipient given")); break; } while (nextAddressAvailable) { if (loglevelActiveFor(LOG_LEVEL_INFO)) { String log = F("Email: To "); log += emailTo; addLogMove(LOG_LEVEL_INFO, log); } if (!NPlugin_001_MTA(client, String(F("RCPT TO:<")) + emailTo + ">", F("250 "))) break; ++i; nextAddressAvailable = getNextMailAddress(notificationsettings.Receiver, emailTo, i); } if (!NPlugin_001_MTA(client, F("DATA"), F("354 "))) break; if (!NPlugin_001_MTA(client, mailheader + aMesg + F("\r\n.\r\n"), F("250 "))) break; myStatus = true; break; } client.flush(); client.stop(); if (myStatus == true) { addLog(LOG_LEVEL_INFO, F("EMAIL: Connection Closed Successfully")); }else { if (loglevelActiveFor(LOG_LEVEL_ERROR)) { String log = F("EMAIL: Connection Closed With Error. Used header: "); log += mailheader; addLogMove(LOG_LEVEL_ERROR, log); } } } return myStatus; } bool NPlugin_001_Auth(WiFiClient& client, const String& user, const String& pass) { if (user.isEmpty() || pass.isEmpty()) { // No user/password given. return true; } if (!NPlugin_001_MTA(client, String(F("AUTH LOGIN")), F("334 "))) return false; base64 encoder; String auth; auth = encoder.encode(user); if (!NPlugin_001_MTA(client, auth, F("334 "))) return false; auth = encoder.encode(pass); if (!NPlugin_001_MTA(client, auth, F("235 "))) return false; return true; } bool NPlugin_001_MTA(WiFiClient& client, const String& aStr, const String &aWaitForPattern) { if (loglevelActiveFor(LOG_LEVEL_DEBUG)) addLog(LOG_LEVEL_DEBUG, aStr); if (aStr.length()) client.println(aStr); // Wait For Response unsigned long timer = millis() + NPLUGIN_001_TIMEOUT; backgroundtasks(); while (true) { if (timeOutReached(timer)) { if (loglevelActiveFor(LOG_LEVEL_ERROR)) { String log = F("NPlugin_001_MTA: timeout. "); log += aStr; addLogMove(LOG_LEVEL_ERROR, log); } return false; } delay(0); // String line = client.readStringUntil('\n'); String line; safeReadStringUntil(client, line, '\n'); const bool patternFound = line.indexOf(aWaitForPattern) >= 0; #ifndef BUILD_NO_DEBUG if (loglevelActiveFor(LOG_LEVEL_DEBUG)) addLogMove(LOG_LEVEL_DEBUG, line); #endif if (patternFound) { return true; } } return false; } bool getNextMailAddress(const String& data, String& address, int index) { int found = 0; int strIndex[] = { 0, -1 }; const int maxIndex = data.length() - 1; for (int i = 0; i <= maxIndex && found <= index; i++) { if (data.charAt(i) == ',' || i == maxIndex) { found++; strIndex[0] = strIndex[1] + 1; strIndex[1] = (i == maxIndex) ? i + 1 : i; } } if (found > index) { address = data.substring(strIndex[0], strIndex[1]); return true; } return false; } #endif