From 44e761df47dffa29f1e10fd6fd630be4b7093037 Mon Sep 17 00:00:00 2001 From: Ray Date: Sun, 23 Jun 2024 20:38:25 -0400 Subject: [PATCH 01/61] support for https station (per PR 260) and new feature of remote otc station --- OpenSprinkler.cpp | 111 ++++++++++++++++++++++++++++++--------- OpenSprinkler.h | 24 ++++++--- defines.h | 14 +++-- opensprinkler_server.cpp | 2 +- 4 files changed, 111 insertions(+), 40 deletions(-) diff --git a/OpenSprinkler.cpp b/OpenSprinkler.cpp index 89dd2012..10e9af12 100644 --- a/OpenSprinkler.cpp +++ b/OpenSprinkler.cpp @@ -469,7 +469,7 @@ byte OpenSprinkler::start_network() { useEth = false; } - if((useEth || get_wifi_mode()==WIFI_MODE_STA) && otc.en>0 && otc.token.length()>=32) { + if((useEth || get_wifi_mode()==WIFI_MODE_STA) && otc.en>0 && otc.token.length()>=DEFAULT_OTC_TOKEN_LENGTH) { otf = new OTF::OpenThingsFramework(httpport, otc.server, otc.port, otc.token, false, ether_buffer, ETHER_BUFFER_SIZE); DEBUG_PRINTLN(F("Started OTF with remote connection")); } else { @@ -1714,8 +1714,12 @@ void OpenSprinkler::switch_special_station(byte sid, byte value, uint16_t dur) { switch_rfstation((RFStationData *)pdata->sped, value); break; - case STN_TYPE_REMOTE: - switch_remotestation((RemoteStationData *)pdata->sped, value, dur); + case STN_TYPE_REMOTE_IP: + switch_remotestation((RemoteIPStationData *)pdata->sped, value, dur); + break; + + case STN_TYPE_REMOTE_OTC: + switch_remotestation((RemoteOTCStationData *)pdata->sped, value, dur); break; case STN_TYPE_GPIO: @@ -1723,8 +1727,13 @@ void OpenSprinkler::switch_special_station(byte sid, byte value, uint16_t dur) { break; case STN_TYPE_HTTP: - switch_httpstation((HTTPStationData *)pdata->sped, value); + switch_httpstation((HTTPStationData *)pdata->sped, value, false); break; + + case STN_TYPE_HTTPS: + switch_httpstation((HTTPStationData *)pdata->sped, value, true); + break; + } } } @@ -1861,19 +1870,33 @@ void OpenSprinkler::switch_gpiostation(GPIOStationData *data, bool turnon) { /** Callback function for switching remote station */ void remote_http_callback(char* buffer) { -/* + DEBUG_PRINTLN(buffer); -*/ + } -int8_t OpenSprinkler::send_http_request(const char* server, uint16_t port, char* p, void(*callback)(char*), uint16_t timeout) { +int8_t OpenSprinkler::send_http_request(const char* server, uint16_t port, char* p, void(*callback)(char*), bool usessl, uint16_t timeout) { #if defined(ARDUINO) - Client *client; + Client *client = NULL; #if defined(ESP8266) - WiFiClient wifiClient; - client = &wifiClient; + if(usessl) { + static WiFiClientSecure _c; + _c.setInsecure(); + bool mfln = _c.probeMaxFragmentLength(server, port, 512); + DEBUG_PRINTF("MFLN supported: %s\n", mfln ? "yes" : "no"); + if (mfln) { + _c.setBufferSizes(512, 512); + } else { + _c.setBufferSizes(2048, 2048); + } + client = &_c; + } else { + static WiFiClient _c; + //client = new WiFiClient(); + client = &_c; + } #else EthernetClient etherClient; client = ðerClient; @@ -1962,12 +1985,13 @@ int8_t OpenSprinkler::send_http_request(const char* server, uint16_t port, char* #endif ether_buffer[pos]=0; // properly end buffer with 0 client->stop(); + //delete client; if(strlen(ether_buffer)==0) return HTTP_RQT_EMPTY_RETURN; if(callback) callback(ether_buffer); return HTTP_RQT_SUCCESS; } -int8_t OpenSprinkler::send_http_request(uint32_t ip4, uint16_t port, char* p, void(*callback)(char*), uint16_t timeout) { +int8_t OpenSprinkler::send_http_request(uint32_t ip4, uint16_t port, char* p, void(*callback)(char*), bool usessl, uint16_t timeout) { char server[20]; byte ip[4]; ip[0] = ip4>>24; @@ -1975,25 +1999,25 @@ int8_t OpenSprinkler::send_http_request(uint32_t ip4, uint16_t port, char* p, vo ip[2] = (ip4>>8)&0xff; ip[3] = ip4&0xff; sprintf(server, "%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]); - return send_http_request(server, port, p, callback, timeout); + return send_http_request(server, port, p, callback, usessl, timeout); } -int8_t OpenSprinkler::send_http_request(char* server_with_port, char* p, void(*callback)(char*), uint16_t timeout) { +int8_t OpenSprinkler::send_http_request(char* server_with_port, char* p, void(*callback)(char*), bool usessl, uint16_t timeout) { char * server = strtok(server_with_port, ":"); char * port = strtok(NULL, ":"); - return send_http_request(server, (port==NULL)?80:atoi(port), p, callback, timeout); + return send_http_request(server, (port==NULL)?80:atoi(port), p, callback, usessl, timeout); } -/** Switch remote station +/** Switch remote IP station * This function takes a remote station code, * parses it into remote IP, port, station index, * and makes a HTTP GET request. * The remote controller is assumed to have the same * password as the main controller */ -void OpenSprinkler::switch_remotestation(RemoteStationData *data, bool turnon, uint16_t dur) { - RemoteStationData copy; - memcpy((char*)©, (char*)data, sizeof(RemoteStationData)); +void OpenSprinkler::switch_remotestation(RemoteIPStationData *data, bool turnon, uint16_t dur) { + RemoteIPStationData copy; + memcpy((char*)©, (char*)data, sizeof(RemoteIPStationData)); uint32_t ip4 = hex2ulong(copy.ip, sizeof(copy.ip)); uint16_t port = (uint16_t)hex2ulong(copy.port, sizeof(copy.port)); @@ -2004,8 +2028,6 @@ void OpenSprinkler::switch_remotestation(RemoteStationData *data, bool turnon, u ip[2] = (ip4>>8)&0xff; ip[3] = ip4&0xff; - // use tmp_buffer starting at a later location - // because remote station data is loaded at the beginning char *p = tmp_buffer; BufferFiller bf = p; // if turning on the zone and duration is defined, give duration as the timer value @@ -2032,11 +2054,48 @@ void OpenSprinkler::switch_remotestation(RemoteStationData *data, bool turnon, u send_http_request(server, port, p, remote_http_callback); } -/** Switch http station - * This function takes an http station code, +/** Switch remote OTC station + * This function takes a remote station code, + * parses it into OTC token and station index, + * and makes a HTTPS GET request. + * The remote controller is assumed to have the same + * password as the main controller + */ +void OpenSprinkler::switch_remotestation(RemoteOTCStationData *data, bool turnon, uint16_t dur) { + RemoteOTCStationData copy; + memcpy((char*)©, (char*)data, sizeof(RemoteOTCStationData)); + copy.token[sizeof(copy.token)-1] = 0; // ensure the string ends properly + DEBUG_PRINTLN((char*)copy.token); + DEBUG_PRINTLN((int)hex2ulong(copy.sid, sizeof(copy.sid))); + char *p = tmp_buffer; + BufferFiller bf = p; + // if turning on the zone and duration is defined, give duration as the timer value + // otherwise: + // if autorefresh is defined, we give a fixed duration each time, and auto refresh will renew it periodically + // if no auto refresh, we will give the maximum allowed duration, and station will be turned off when off command is sent + uint16_t timer = 0; + if(turnon) { + if(dur>0) { + timer = dur; + } else { + timer = iopts[IOPT_SPE_AUTO_REFRESH]?4*MAX_NUM_STATIONS:64800; + } + } + bf.emit_p(PSTR("GET /forward/v1/$S/cm?pw=$O&sid=$D&en=$D&t=$D"), + copy.token, + SOPT_PASSWORD, + (int)hex2ulong(copy.sid, sizeof(copy.sid)), + turnon, timer); + bf.emit_p(PSTR(" HTTP/1.0\r\nHOST: $S\r\n\r\n"), DEFAULT_OTC_SERVER_APP); + + send_http_request(DEFAULT_OTC_SERVER_APP, DEFAULT_OTC_PORT_APP, p, remote_http_callback, true); +} + +/** Switch http(s) station + * This function takes an http(s) station code, * parses it into a server name and two HTTP GET requests. */ -void OpenSprinkler::switch_httpstation(HTTPStationData *data, bool turnon) { +void OpenSprinkler::switch_httpstation(HTTPStationData *data, bool turnon, bool usessl) { HTTPStationData copy; // make a copy of the HTTP station data and work with it @@ -2054,7 +2113,7 @@ void OpenSprinkler::switch_httpstation(HTTPStationData *data, bool turnon) { bf.emit_p(PSTR("GET /$S HTTP/1.0\r\nHOST: $S\r\n\r\n"), cmd, server); - send_http_request(server, atoi(port), p, remote_http_callback); + send_http_request(server, atoi(port), p, remote_http_callback, usessl); } /** Prepare factory reset */ @@ -2138,7 +2197,7 @@ void OpenSprinkler::factory_reset() { void OpenSprinkler::parse_otc_config() { char server[MAX_SOPTS_SIZE+1] = {0}; char token[MAX_SOPTS_SIZE+1] = {0}; - int port = DEFAULT_OTC_PORT; + int port = DEFAULT_OTC_PORT_DEV; int en = 0; char *config = tmp_buffer; @@ -2152,7 +2211,7 @@ void OpenSprinkler::parse_otc_config() { otc.en = en; otc.token = String(token); otc.server = String(server); - otc.port = 80; + otc.port = port; } #endif diff --git a/OpenSprinkler.h b/OpenSprinkler.h index c57c5844..76a35a57 100644 --- a/OpenSprinkler.h +++ b/OpenSprinkler.h @@ -144,13 +144,19 @@ struct RFStationData { byte timing[4]; }; -/** Remote station data structures - Must fit in STATION_SPECIAL_DATA_SIZE */ -struct RemoteStationData { +/** Remote IP station data structures - Must fit in STATION_SPECIAL_DATA_SIZE */ +struct RemoteIPStationData { byte ip[8]; byte port[4]; byte sid[2]; }; +/** Remote OTC station data structures - Must fit in STATION_SPECIAL_DATA_SIZE */ +struct RemoteOTCStationData { + byte token[DEFAULT_OTC_TOKEN_LENGTH+1]; + byte sid[2]; +}; + /** GPIO station data structures - Must fit in STATION_SPECIAL_DATA_SIZE */ struct GPIOStationData { byte pin[2]; @@ -283,10 +289,11 @@ class OpenSprinkler { static void attribs_load(); // load and repackage attrib bits (backward compatibility) static uint16_t parse_rfstation_code(RFStationData *data, ulong *on, ulong *off); // parse rf code into on/off/time sections static void switch_rfstation(RFStationData *data, bool turnon); // switch rf station - static void switch_remotestation(RemoteStationData *data, bool turnon, uint16_t dur=0); // switch remote station + static void switch_remotestation(RemoteIPStationData *data, bool turnon, uint16_t dur=0); // switch remote IP station + static void switch_remotestation(RemoteOTCStationData *data, bool turnon, uint16_t dur=0); // switch remote OTC station static void switch_gpiostation(GPIOStationData *data, bool turnon); // switch gpio station - static void switch_httpstation(HTTPStationData *data, bool turnon); // switch http station - + static void switch_httpstation(HTTPStationData *data, bool turnon, bool usessl=false); // switch http station + // -- options and data storeage static void nvdata_load(); static void nvdata_save(); @@ -322,9 +329,10 @@ class OpenSprinkler { static void clear_all_station_bits(); // clear all station bits static void apply_all_station_bits(); // apply all station bits (activate/deactive values) - static int8_t send_http_request(uint32_t ip4, uint16_t port, char* p, void(*callback)(char*)=NULL, uint16_t timeout=5000); - static int8_t send_http_request(const char* server, uint16_t port, char* p, void(*callback)(char*)=NULL, uint16_t timeout=5000); - static int8_t send_http_request(char* server_with_port, char* p, void(*callback)(char*)=NULL, uint16_t timeout=5000); + static int8_t send_http_request(uint32_t ip4, uint16_t port, char* p, void(*callback)(char*)=NULL, bool usessl=false, uint16_t timeout=5000); + static int8_t send_http_request(const char* server, uint16_t port, char* p, void(*callback)(char*)=NULL, bool usessl=false, uint16_t timeout=5000); + static int8_t send_http_request(char* server_with_port, char* p, void(*callback)(char*)=NULL, bool usessl=false, uint16_t timeout=5000); + // -- LCD functions #if defined(ARDUINO) // LCD functions for Arduino #if defined(ESP8266) diff --git a/defines.h b/defines.h index ac2c2309..6c28d406 100755 --- a/defines.h +++ b/defines.h @@ -24,7 +24,7 @@ #ifndef _DEFINES_H #define _DEFINES_H -//#define ENABLE_DEBUG // enable serial debug +#define ENABLE_DEBUG // enable serial debug typedef unsigned char byte; typedef unsigned long ulong; @@ -36,7 +36,7 @@ typedef unsigned long ulong; // if this number is different from the one stored in non-volatile memory // a device reset will be automatically triggered -#define OS_FW_MINOR 5 // Firmware minor version +#define OS_FW_MINOR 6 // Firmware minor version /** Hardware version base numbers */ #define OS_HW_VERSION_BASE 0x00 // OpenSprinkler @@ -61,10 +61,11 @@ typedef unsigned long ulong; /** Station macro defines */ #define STN_TYPE_STANDARD 0x00 // standard solenoid station #define STN_TYPE_RF 0x01 // Radio Frequency (RF) station -#define STN_TYPE_REMOTE 0x02 // Remote OpenSprinkler station +#define STN_TYPE_REMOTE_IP 0x02 // Remote OpenSprinkler station (by IP) #define STN_TYPE_GPIO 0x03 // direct GPIO station #define STN_TYPE_HTTP 0x04 // HTTP station #define STN_TYPE_HTTPS 0x05 // HTTPS station +#define STN_TYPE_REMOTE_OTC 0x06 // Remote OpenSprinkler station (by OTC) #define STN_TYPE_OTHER 0xFF /** Notification macro defines */ @@ -144,8 +145,11 @@ typedef unsigned long ulong; #define DEFAULT_JAVASCRIPT_URL "https://ui.opensprinkler.com/js" #define DEFAULT_WEATHER_URL "weather.opensprinkler.com" #define DEFAULT_IFTTT_URL "maker.ifttt.com" -#define DEFAULT_OTC_SERVER "ws.cloud.openthings.io" -#define DEFAULT_OTC_PORT 80 +#define DEFAULT_OTC_SERVER_DEV "ws.cloud.openthings.io" +#define DEFAULT_OTC_PORT_DEV 80 +#define DEFAULT_OTC_SERVER_APP "cloud.openthings.io" +#define DEFAULT_OTC_PORT_APP 443 +#define DEFAULT_OTC_TOKEN_LENGTH 32 #define DEFAULT_DEVICE_NAME "My OpenSprinkler" #define DEFAULT_EMPTY_STRING "" diff --git a/opensprinkler_server.cpp b/opensprinkler_server.cpp index 5ee33472..57510782 100644 --- a/opensprinkler_server.cpp +++ b/opensprinkler_server.cpp @@ -600,7 +600,7 @@ void server_change_stations(OTF_PARAMS_DEF) { if (!found || activeState > 1) { handle_return(HTML_DATA_OUTOFBOUND); } - } else if (tmp_buffer[0] == STN_TYPE_HTTP) { + } else if (tmp_buffer[0] == STN_TYPE_HTTP || tmp_buffer[0] == STN_TYPE_HTTPS) { #if !defined(ESP8266) urlDecode(tmp_buffer + 1); #endif From 55fe4f23a35f2d82562a767be9cbb349f29026a8 Mon Sep 17 00:00:00 2001 From: Ray Date: Mon, 24 Jun 2024 15:22:04 -0400 Subject: [PATCH 02/61] getting https to work for pi/linux --- OpenSprinkler.cpp | 37 ++++---- build.sh | 6 +- etherport.cpp | 213 +++++++++++++++++++++++++++++++++++++++++----- etherport.h | 29 ++++++- 4 files changed, 240 insertions(+), 45 deletions(-) diff --git a/OpenSprinkler.cpp b/OpenSprinkler.cpp index 10e9af12..8d8475f9 100644 --- a/OpenSprinkler.cpp +++ b/OpenSprinkler.cpp @@ -1882,24 +1882,21 @@ int8_t OpenSprinkler::send_http_request(const char* server, uint16_t port, char* Client *client = NULL; #if defined(ESP8266) if(usessl) { - static WiFiClientSecure _c; - _c.setInsecure(); - bool mfln = _c.probeMaxFragmentLength(server, port, 512); + WiFiClientSecure *_c = new WiFiClientSecure(); + _c->setInsecure(); + bool mfln = _c->probeMaxFragmentLength(server, port, 512); DEBUG_PRINTF("MFLN supported: %s\n", mfln ? "yes" : "no"); if (mfln) { - _c.setBufferSizes(512, 512); + _c->setBufferSizes(512, 512); } else { - _c.setBufferSizes(2048, 2048); + _c->setBufferSizes(2048, 2048); } - client = &_c; + client = _c; } else { - static WiFiClient _c; - //client = new WiFiClient(); - client = &_c; + client = new WiFiClient(); } #else - EthernetClient etherClient; - client = ðerClient; + client = new EthernetClient(); #endif #define HTTP_CONNECT_NTRIES 3 @@ -1919,21 +1916,25 @@ int8_t OpenSprinkler::send_http_request(const char* server, uint16_t port, char* if(tries==HTTP_CONNECT_NTRIES) { DEBUG_PRINTLN(F("failed.")); client->stop(); + delete client; return HTTP_RQT_CONNECT_ERR; } #else + EthernetClient *client = NULL; + + if (usessl) { + client = new EthernetClientSsl(); + } else { + client = new EthernetClient(); + } - EthernetClient etherClient; - EthernetClient *client = ðerClient; - struct hostent *host; DEBUG_PRINT(server); DEBUG_PRINT(":"); DEBUG_PRINTLN(port); - host = gethostbyname(server); - if (!host) { return HTTP_RQT_CONNECT_ERR; } - if(!client->connect((uint8_t*)host->h_addr, port)) { + if(!client->connect(server, port)) { DEBUG_PRINT(F("failed.")); client->stop(); + delete client; return HTTP_RQT_CONNECT_ERR; } @@ -1985,7 +1986,7 @@ int8_t OpenSprinkler::send_http_request(const char* server, uint16_t port, char* #endif ether_buffer[pos]=0; // properly end buffer with 0 client->stop(); - //delete client; + delete client; if(strlen(ether_buffer)==0) return HTTP_RQT_EMPTY_RETURN; if(callback) callback(ether_buffer); return HTTP_RQT_SUCCESS; diff --git a/build.sh b/build.sh index ae257c8d..fe9854be 100755 --- a/build.sh +++ b/build.sh @@ -13,13 +13,15 @@ echo "Building OpenSprinkler..." if [ "$1" == "demo" ]; then echo "Installing required libraries..." apt-get install -y libmosquitto-dev + apt-get install -y libssl-dev echo "Compiling demo firmware..." - g++ -o OpenSprinkler -DDEMO -std=c++14 main.cpp OpenSprinkler.cpp program.cpp opensprinkler_server.cpp utils.cpp weather.cpp gpio.cpp etherport.cpp mqtt.cpp -lpthread -lmosquitto + g++ -o OpenSprinkler -DDEMO -std=c++14 main.cpp OpenSprinkler.cpp program.cpp opensprinkler_server.cpp utils.cpp weather.cpp gpio.cpp etherport.cpp mqtt.cpp -lpthread -lmosquitto -l crypto -lssl elif [ "$1" == "osbo" ]; then echo "Installing required libraries..." apt-get install -y libmosquitto-dev + apt-get install -y libssl-dev echo "Compiling osbo firmware..." - g++ -o OpenSprinkler -DOSBO -std=c++14 main.cpp OpenSprinkler.cpp program.cpp opensprinkler_server.cpp utils.cpp weather.cpp gpio.cpp etherport.cpp mqtt.cpp -lpthread -lmosquitto + g++ -o OpenSprinkler -DOSBO -std=c++14 main.cpp OpenSprinkler.cpp program.cpp opensprinkler_server.cpp utils.cpp weather.cpp gpio.cpp etherport.cpp mqtt.cpp -lpthread -lmosquitto -l crypto -lssl else echo "Installing required libraries..." apt-get update diff --git a/etherport.cpp b/etherport.cpp index a2cea4ad..858b496a 100644 --- a/etherport.cpp +++ b/etherport.cpp @@ -33,11 +33,19 @@ #include #include #include +#include #include #include #include #include "defines.h" +#include +#include +#include +#include +#include +#include + EthernetServer::EthernetServer(uint16_t port) : m_port(port), m_sock(0) { @@ -91,28 +99,25 @@ bool EthernetServer::begin() } // This function blocks until we get a client connected. -// It will timeout after 50ms and return a blank client. +// It will timeout after 5ms and return a blank client. // If it succeeds it will return an EthernetClient. EthernetClient EthernetServer::available() { - fd_set sock_set; - FD_ZERO(&sock_set); - FD_SET(m_sock, &sock_set); - struct timeval timeout; - timeout.tv_sec = 0; - timeout.tv_usec = 50 * 1000; // 50ms + struct pollfd fds; + memset(&fds, 0, sizeof(fds)); + fds.fd = m_sock; + fds.events = POLLIN; + int timeout = 5; // reduce this timeout for less blocking time - select(m_sock + 1, &sock_set, NULL, NULL, &timeout); - if (FD_ISSET(m_sock, &sock_set)) + int rc = poll(&fds, 1, timeout); + if (rc > 0) { int client_sock = 0; - struct sockaddr_in6 cli_addr; - unsigned int clilen = sizeof(cli_addr); - if ((client_sock = accept(m_sock, (struct sockaddr *) &cli_addr, &clilen)) <= 0) - return EthernetClient(0); + if ((client_sock = accept(m_sock, NULL, NULL)) <= 0) + return EthernetClient(); return EthernetClient(client_sock); } - return EthernetClient(0); + return EthernetClient(); } EthernetClient::EthernetClient() @@ -130,18 +135,32 @@ EthernetClient::~EthernetClient() stop(); } -int EthernetClient::connect(uint8_t ip[4], uint16_t port) +int EthernetClient::connect(const char* server, uint16_t port) { if (m_sock) return 0; + + struct hostent *host = gethostbyname(server); + if (!host) { + DEBUG_PRINTLN("Error: DNS look up failed\n"); + return 0; + } + struct sockaddr_in sin = {0}; sin.sin_family = AF_INET; sin.sin_port = htons(port); - sin.sin_addr.s_addr = *(uint32_t*) (ip); + sin.sin_addr.s_addr = *(uint32_t*) (host->h_addr); m_sock = socket(AF_INET, SOCK_STREAM, 0); + + struct timeval timeout; + timeout.tv_sec = 2; + timeout.tv_usec = 0; + setsockopt (m_sock, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)); + setsockopt (m_sock, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout)); + if (::connect(m_sock, (struct sockaddr *) &sin, sizeof(sin)) < 0) { - DEBUG_PRINTLN("error connecting to server"); + DEBUG_PRINTLN("Error: connecting to the server"); return 0; } m_connected = true; @@ -178,6 +197,150 @@ EthernetClient::operator bool() // If an error occurs or a timeout happens, we set the disconnect flag on the socket // and return 0; int EthernetClient::read(uint8_t *buf, size_t size) +{ + struct pollfd fds; + memset(&fds, 0, sizeof(fds)); + fds.fd = m_sock; + fds.events = POLLIN; + int timeout = 3000; + + int rc = poll(&fds, 1, timeout); + if (rc > 0) + { + rc = recv(m_sock, buf, size, 0); + if (errno == EWOULDBLOCK) + return 0; + + if (rc <= 0) // socket closed + m_connected = false; + return rc; + } + if (rc < 0) + m_connected = false; + return 0; +} + +size_t EthernetClient::write(const uint8_t *buf, size_t size) +{ + return ::send(m_sock, buf, size, MSG_NOSIGNAL); +} + +/** + * SSL Client +*/ + +EthernetClientSsl::EthernetClientSsl() + : m_sock(0), m_connected(false) +{ +} + +EthernetClientSsl::EthernetClientSsl(int sock) + : m_sock(sock), m_connected(true) +{ +} + +EthernetClientSsl::~EthernetClientSsl() +{ + stop(); +} + +static bool sslInit; +//static BIO* certbio; +static SSL_CTX* ctx; + +/** + * https://github.com/angstyloop/c-web/blob/main/openssl-fetch-example.c +*/ +int EthernetClientSsl::connect(const char* server, uint16_t port) +{ + if (m_sock) + return 0; + + struct hostent *host = gethostbyname(server); + if (!host) { + DEBUG_PRINTLN("Error: DNS look up failed\n"); + return 0; + } + + if (!sslInit) { + OpenSSL_add_all_algorithms(); + //ERR_load_BIO_strings(); + ERR_load_crypto_strings(); + SSL_load_error_strings(); + //BIO* certbio = BIO_new(BIO_s_file()); + if (SSL_library_init() < 0) { + DEBUG_PRINTLN("Error: could not initialize the OpenSSL library.\n"); + return 0; + } + const SSL_METHOD* method = SSLv23_client_method(); + ctx = SSL_CTX_new(method); + if (!ctx) { + DEBUG_PRINTLN("Error: unable to create SSL context.\n"); + return 0; + } + SSL_CTX_set_options(ctx, SSL_OP_NO_SSLv2|SSL_OP_NO_SSLv3); + SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, NULL); //Accept all certs + sslInit = true; + } + + // Create a new SSL session. This does not connect the socket. + ssl = SSL_new(ctx); + + struct sockaddr_in sin = {0}; + sin.sin_family = AF_INET; + sin.sin_port = htons(port); + sin.sin_addr.s_addr = *(uint32_t*) (host->h_addr); + m_sock = socket(AF_INET, SOCK_STREAM, 0); + if (::connect(m_sock, (struct sockaddr *) &sin, sizeof(sin)) < 0) + { + DEBUG_PRINTLN("Error: connecting to the server failed"); + return 0; + } + SSL_set_fd(ssl, m_sock); + SSL_set_tlsext_host_name(ssl, server); // set correct host name + if (SSL_connect(ssl) < 1) { + close(m_sock); + m_sock = 0; + DEBUG_PRINTLN("Error: Could not build an SSL session"); + return 0; + } + m_connected = true; + return 1; +} + +bool EthernetClientSsl::connected() +{ + if (!m_sock || !ssl) + return false; + int error = 0; + socklen_t len = sizeof(error); + int retval = getsockopt(m_sock, SOL_SOCKET, SO_ERROR, &error, &len); + return (retval == 0) && m_connected; +} + +void EthernetClientSsl::stop() +{ + if (m_sock) { + close(m_sock); + m_sock = 0; + } + if (ssl) { + SSL_free(ssl); + ssl = NULL; + } + m_connected = false; +} + +EthernetClientSsl::operator bool() +{ + return m_sock != 0 && ssl != NULL; +} + +// read data from the client into the buffer provided +// This function will block until either data is received OR a timeout happens. +// If an error occurs or a timeout happens, we set the disconnect flag on the socket +// and return 0; +int EthernetClientSsl::read(uint8_t *buf, size_t size) { fd_set sock_set; FD_ZERO(&sock_set); @@ -186,10 +349,14 @@ int EthernetClient::read(uint8_t *buf, size_t size) timeout.tv_sec = 3; timeout.tv_usec = 0; - select(m_sock + 1, &sock_set, NULL, NULL, &timeout); - if (FD_ISSET(m_sock, &sock_set)) + //select(m_sock + 1, &sock_set, NULL, NULL, &timeout); + //if (FD_ISSET(m_sock, &sock_set)) + size_t pending = SSL_pending(ssl); + if (pending > 0) { - int retval = ::read(m_sock, buf, size); + if (size > pending) + size = pending; + int retval = SSL_read(ssl, buf, size); if (retval <= 0) // socket closed m_connected = false; return retval; @@ -198,9 +365,9 @@ int EthernetClient::read(uint8_t *buf, size_t size) return 0; } -size_t EthernetClient::write(const uint8_t *buf, size_t size) +size_t EthernetClientSsl::write(const uint8_t *buf, size_t size) { - return ::send(m_sock, buf, size, MSG_NOSIGNAL); + return SSL_write(ssl, buf, size); + //return ::send(m_sock, buf, size, MSG_NOSIGNAL); } - #endif diff --git a/etherport.h b/etherport.h index a0788c8b..20e75787 100644 --- a/etherport.h +++ b/etherport.h @@ -27,11 +27,14 @@ #if defined(ARDUINO) -#else // headers for RPI/BBB +#else // headers for RPI/BBB/Linux #include #include #include +#include +#include +#include "defines.h" #ifdef __APPLE__ #define MSG_NOSIGNAL SO_NOSIGPIPE @@ -44,7 +47,28 @@ class EthernetClient { EthernetClient(); EthernetClient(int sock); ~EthernetClient(); - int connect(uint8_t ip[4], uint16_t port); + int connect(const char *server, uint16_t port); + bool connected(); + void stop(); + int read(uint8_t *buf, size_t size); + size_t write(const uint8_t *buf, size_t size); + operator bool(); + int GetSocket() + { + return m_sock; + } +private: + int m_sock = 0; + bool m_connected; + friend class EthernetServer; +}; + +class EthernetClientSsl : EthernetClient { +public: + EthernetClientSsl(); + EthernetClientSsl(int sock); + ~EthernetClientSsl(); + int connect(const char *server, uint16_t port); bool connected(); void stop(); int read(uint8_t *buf, size_t size); @@ -56,6 +80,7 @@ class EthernetClient { } private: int m_sock; + SSL* ssl; bool m_connected; friend class EthernetServer; }; From f0b1e2e3a765f363ece72d14aec6926149882db1 Mon Sep 17 00:00:00 2001 From: "Nathan P." Date: Wed, 26 Jun 2024 13:44:18 -0400 Subject: [PATCH 03/61] Initial support for Arduino email notifications --- OpenSprinkler.cpp | 7 ++- defines.h | 3 +- main.cpp | 125 +++++++++++++++++++++++++++++++++++---- opensprinkler_server.cpp | 12 +++- platformio.ini | 33 +++-------- 5 files changed, 137 insertions(+), 43 deletions(-) diff --git a/OpenSprinkler.cpp b/OpenSprinkler.cpp index 10e9af12..7781b59e 100644 --- a/OpenSprinkler.cpp +++ b/OpenSprinkler.cpp @@ -154,7 +154,7 @@ const char iopt_json_names[] PROGMEM = "dns3\0" "dns4\0" "sar\0\0" - "ife\0\0" + "nfe\0\0" "sn1t\0" "sn1o\0" "sn2t\0" @@ -224,7 +224,7 @@ const char iopt_prompts[] PROGMEM = "DNS server.ip3: " "DNS server.ip4: " "Special Refresh?" - "IFTTT Enable: " + "Notif Enable: " "Sensor 1 type: " "Normally open? " "Sensor 2 type: " @@ -368,7 +368,7 @@ byte OpenSprinkler::iopts[] = { 8, 8, 0, // special station auto refresh - 0, // ifttt enable bits + 0, // notif enable bits 0, // sensor 1 type (see SENSOR_TYPE macro defines) 1, // sensor 1 option. 0: normally closed; 1: normally open. default 1. 0, // sensor 2 type @@ -399,6 +399,7 @@ const char *OpenSprinkler::sopts[] = { DEFAULT_EMPTY_STRING, // SOPT_OTC_OPTS DEFAULT_DEVICE_NAME, DEFAULT_EMPTY_STRING, // SOPT_STA_BSSID_CHL + DEFAULT_EMPTY_STRING, // SOPT_EMAIL_OPTS }; /** Weekday strings (stored in PROGMEM to reduce RAM usage) */ diff --git a/defines.h b/defines.h index 6c28d406..bf088f29 100755 --- a/defines.h +++ b/defines.h @@ -234,7 +234,7 @@ enum { IOPT_DNS_IP3, IOPT_DNS_IP4, IOPT_SPE_AUTO_REFRESH, - IOPT_IFTTT_ENABLE, + IOPT_NOTIF_ENABLE, IOPT_SENSOR1_TYPE, IOPT_SENSOR1_OPTION, IOPT_SENSOR2_TYPE, @@ -265,6 +265,7 @@ enum { SOPT_OTC_OPTS, SOPT_DEVICE_NAME, SOPT_STA_BSSID_CHL, // wifi extra info: bssid and channel + SOPT_EMAIL_OPTS, NUM_SOPTS // total number of string options }; diff --git a/main.cpp b/main.cpp index 81113530..95fd8ba6 100644 --- a/main.cpp +++ b/main.cpp @@ -30,6 +30,10 @@ #include "opensprinkler_server.h" #include "mqtt.h" #include "main.h" +#include "EmailSender.h" + +#define str(s) #s +#define xstr(s) str(s) #if defined(ARDUINO) #if defined(ESP8266) @@ -89,6 +93,9 @@ float flow_last_gpm=0; uint32_t reboot_timer = 0; +//boolean to delay reboot notification until wifi is reconnected +bool delayed = false; + void flow_poll() { #if defined(ESP8266) if(os.hw_rev>=2) pinModeExt(PIN_SENSOR1, INPUT_PULLUP); // this seems necessary for OS 3.2 @@ -441,6 +448,14 @@ void do_loop() os.status.mas2= os.iopts[IOPT_MASTER_STATION_2]; time_os_t curr_time = os.now_tz(); + //handle delayed reboot notification until wifi connection + if(delayed){ + if(WiFi.status() == WL_CONNECTED){ + push_message(NOTIFY_REBOOT); + delayed = false; + } + } + // ====== Process Ethernet packets ====== #if defined(ARDUINO) // Process Ethernet packets for Arduino #if defined(ESP8266) @@ -1371,10 +1386,44 @@ void push_message(int type, uint32_t lval, float fval, const char* sval) { char* postval = tmp_buffer; uint32_t volume; - bool ifttt_enabled = os.iopts[IOPT_IFTTT_ENABLE]&type; + //define email variables + char host[50 + 1] = {0}; + char username[32 + 1] = {0}; + char password[32 + 1] = {0}; + char recipient[32 + 1] = {0}; + int port = 465; + int enabled = 0; + + //pull email variables + os.sopt_load(SOPT_EMAIL_OPTS, postval); + if (*postval != 0) { + sscanf( + postval, + "\"en\":%d,\"host\":\"%" xstr(32) "[^\"]\",\"port\":%d,\"user\":\"%" xstr(50) "[^\"]\",\"pass\":\"%" xstr(32) "[^\"]\",\"recipient\":\"%" xstr(32) "[^\"]\"", + &enabled, host, &port, username, password, recipient + ); + } + //assign email variables necessary + EMailSender emailSend(username, password); + EMailSender::EMailMessage message; + + //check if ifttt key exists + bool ifttt_enabled; + if(SOPT_IFTTT_KEY != 0){ + ifttt_enabled = false; + }else{ + ifttt_enabled = os.iopts[IOPT_NOTIF_ENABLE]&type; + } + + bool email_enabled; + if(!enabled){ + email_enabled = false; + }else{ + email_enabled = os.iopts[IOPT_NOTIF_ENABLE]&type; + } // check if this type of event is enabled for push notification - if (!ifttt_enabled && !os.mqtt.enabled()) + if (!ifttt_enabled && !os.mqtt.enabled() && !email_enabled) return; if (ifttt_enabled) { @@ -1388,6 +1437,14 @@ void push_message(int type, uint32_t lval, float fval, const char* sval) { payload[0] = 0; } + if (email_enabled) { + strcpy_P(postval, PSTR("On site [")); + os.sopt_load(SOPT_DEVICE_NAME, postval+strlen(postval)); + strcat_P(postval, PSTR("], ")); + emailSend.setSMTPServer(strdup(host)); + emailSend.setSMTPPort(port); + } + switch(type) { case NOTIFY_STATION_ON: @@ -1396,7 +1453,7 @@ void push_message(int type, uint32_t lval, float fval, const char* sval) { sprintf_P(payload, PSTR("{\"state\":1,\"duration\":%d}"), (int)fval); } - // todo: add IFTTT support for this event as well. + // todo: add IFTTT and email support for this event as well. // currently no support due to the number of events exceeds 8 so need to use more than 1 byte break; @@ -1410,8 +1467,8 @@ void push_message(int type, uint32_t lval, float fval, const char* sval) { sprintf_P(payload, PSTR("{\"state\":0,\"duration\":%d}"), (int)fval); } } - if (ifttt_enabled) { - strcat_P(postval, PSTR("station [")); + if (ifttt_enabled || email_enabled) { + strcat_P(postval, PSTR("Station [")); os.get_station_name(lval, postval+strlen(postval)); strcat_P(postval, PSTR("] closed. It ran for ")); sprintf_P(postval+strlen(postval), PSTR(" %d minutes %d seconds."), (int)fval/60, (int)fval%60); @@ -1420,11 +1477,15 @@ void push_message(int type, uint32_t lval, float fval, const char* sval) { sprintf_P(postval+strlen(postval), PSTR(" Flow rate: %d.%02d"), (int)flow_last_gpm, (int)(flow_last_gpm*100)%100); } } + if (email_enabled) { + message.subject = "Station Off"; + message.message = postval; + } break; case NOTIFY_PROGRAM_SCHED: - if (ifttt_enabled) { + if (ifttt_enabled || email_enabled) { if (sval) strcat_P(postval, PSTR("manually scheduled ")); else strcat_P(postval, PSTR("automatically scheduled ")); strcat_P(postval, PSTR("Program ")); @@ -1435,6 +1496,10 @@ void push_message(int type, uint32_t lval, float fval, const char* sval) { } sprintf_P(postval+strlen(postval), PSTR(" with %d%% water level."), (int)fval); } + if (email_enabled) { + message.subject = "Program Scheduled"; + message.message = postval; + } break; case NOTIFY_SENSOR1: @@ -1443,10 +1508,14 @@ void push_message(int type, uint32_t lval, float fval, const char* sval) { strcpy_P(topic, PSTR("opensprinkler/sensor1")); sprintf_P(payload, PSTR("{\"state\":%d}"), (int)fval); } - if (ifttt_enabled) { + if (ifttt_enabled || email_enabled) { strcat_P(postval, PSTR("sensor 1 ")); strcat_P(postval, ((int)fval)?PSTR("activated."):PSTR("de-activated.")); } + if (email_enabled) { + message.subject = "Sensor 1 Notification"; + message.message = postval; + } break; case NOTIFY_SENSOR2: @@ -1455,10 +1524,14 @@ void push_message(int type, uint32_t lval, float fval, const char* sval) { strcpy_P(topic, PSTR("opensprinkler/sensor2")); sprintf_P(payload, PSTR("{\"state\":%d}"), (int)fval); } - if (ifttt_enabled) { + if (ifttt_enabled || email_enabled) { strcat_P(postval, PSTR("sensor 2 ")); strcat_P(postval, ((int)fval)?PSTR("activated."):PSTR("de-activated.")); } + if (email_enabled) { + message.subject = "Sensor 2 Notification"; + message.message = postval; + } break; case NOTIFY_RAINDELAY: @@ -1467,10 +1540,14 @@ void push_message(int type, uint32_t lval, float fval, const char* sval) { strcpy_P(topic, PSTR("opensprinkler/raindelay")); sprintf_P(payload, PSTR("{\"state\":%d}"), (int)fval); } - if (ifttt_enabled) { + if (ifttt_enabled || email_enabled) { strcat_P(postval, PSTR("rain delay ")); strcat_P(postval, ((int)fval)?PSTR("activated."):PSTR("de-activated.")); } + if (email_enabled) { + message.subject = "Rain Delay"; + message.message = postval; + } break; case NOTIFY_FLOWSENSOR: @@ -1482,14 +1559,18 @@ void push_message(int type, uint32_t lval, float fval, const char* sval) { strcpy_P(topic, PSTR("opensprinkler/sensor/flow")); sprintf_P(payload, PSTR("{\"count\":%u,\"volume\":%d.%02d}"), lval, (int)volume/100, (int)volume%100); } - if (ifttt_enabled) { + if (ifttt_enabled || email_enabled) { sprintf_P(postval+strlen(postval), PSTR("Flow count: %u, volume: %d.%02d"), lval, (int)volume/100, (int)volume%100); } + if (email_enabled) { + message.subject = "Flow Sensor Notification"; + message.message = postval; + } break; case NOTIFY_WEATHER_UPDATE: - if (ifttt_enabled) { + if (ifttt_enabled || email_enabled) { if(lval>0) { strcat_P(postval, PSTR("external IP updated: ")); byte ip[4] = {(byte)((lval>>24)&0xFF), @@ -1502,15 +1583,23 @@ void push_message(int type, uint32_t lval, float fval, const char* sval) { sprintf_P(postval+strlen(postval), PSTR("water level updated: %d%%."), (int)fval); } } + if (email_enabled) { + message.subject = "Weather Update"; + message.message = postval; + } break; case NOTIFY_REBOOT: + if(!delayed){ + delayed = true; + return; + } if (os.mqtt.enabled()) { strcpy_P(topic, PSTR("opensprinkler/system")); strcpy_P(payload, PSTR("{\"state\":\"started\"}")); } - if (ifttt_enabled) { + if (ifttt_enabled || email_enabled) { #if defined(ARDUINO) strcat_P(postval, PSTR("rebooted. Device IP: ")); #if defined(ESP8266) @@ -1534,6 +1623,10 @@ void push_message(int type, uint32_t lval, float fval, const char* sval) { strcat_P(postval, PSTR("process restarted.")); #endif } + if (email_enabled) { + message.subject = "Reboot Notification"; + message.message = postval; + } break; } @@ -1554,6 +1647,14 @@ void push_message(int type, uint32_t lval, float fval, const char* sval) { os.send_http_request(DEFAULT_IFTTT_URL, 80, ether_buffer, remote_http_callback); } + + if(email_enabled){ + EMailSender::Response resp = emailSend.send(recipient, message); + // DEBUG_PRINTLN(F("Sending Status:")); + // DEBUG_PRINTLN(resp.status); + // DEBUG_PRINTLN(resp.code); + // DEBUG_PRINTLN(resp.desc); + } } // ================================ diff --git a/opensprinkler_server.cpp b/opensprinkler_server.cpp index 57510782..e3688f20 100644 --- a/opensprinkler_server.cpp +++ b/opensprinkler_server.cpp @@ -1135,13 +1135,14 @@ void server_json_controller_main(OTF_PARAMS_DEF) { bfill.emit_p(PSTR("\"mac\":\"$X:$X:$X:$X:$X:$X\","), mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); - bfill.emit_p(PSTR("\"loc\":\"$O\",\"jsp\":\"$O\",\"wsp\":\"$O\",\"wto\":{$O},\"ifkey\":\"$O\",\"mqtt\":{$O},\"wtdata\":$S,\"wterr\":$D,\"dname\":\"$O\","), + bfill.emit_p(PSTR("\"loc\":\"$O\",\"jsp\":\"$O\",\"wsp\":\"$O\",\"wto\":{$O},\"ifkey\":\"$O\",\"mqtt\":{$O},\"email\":{$O},\"wtdata\":$S,\"wterr\":$D,\"dname\":\"$O\","), SOPT_LOCATION, SOPT_JAVASCRIPTURL, SOPT_WEATHERURL, SOPT_WEATHER_OPTS, SOPT_IFTTT_KEY, SOPT_MQTT_OPTS, + SOPT_EMAIL_OPTS, strlen(wt_rawData)==0?"{}":wt_rawData, wt_errCode, SOPT_DEVICE_NAME); @@ -1476,6 +1477,15 @@ void server_change_options(OTF_PARAMS_DEF) os.status.req_mqtt_restart = true; } + keyfound = 0; + if(findKeyVal(FKV_SOURCE, tmp_buffer, TMP_BUFFER_SIZE, PSTR("email"), true, &keyfound)) { + urlDecode(tmp_buffer); + os.sopt_save(SOPT_EMAIL_OPTS, tmp_buffer); + } else if (keyfound) { + tmp_buffer[0]=0; + os.sopt_save(SOPT_EMAIL_OPTS, tmp_buffer); + } + if (findKeyVal(FKV_SOURCE, tmp_buffer, TMP_BUFFER_SIZE, PSTR("dname"), true)) { urlDecode(tmp_buffer); strReplace(tmp_buffer, '\"', '\''); diff --git a/platformio.ini b/platformio.ini index 681d87a4..8e68b6be 100644 --- a/platformio.ini +++ b/platformio.ini @@ -8,9 +8,6 @@ ; Please visit documentation for the other options and examples ; https://docs.platformio.org/page/projectconf.html -; adapt to the already existing folder structure. -; usually there's src/ and include/ folders. -; redirect them both. [platformio] src_dir = . include_dir = . @@ -20,32 +17,16 @@ platform = espressif8266@4.2.1 board = d1_mini framework = arduino lib_ldf_mode = deep -lib_deps = - sui77/rc-switch @ ^2.6.3 - https://github.com/ThingPulse/esp8266-oled-ssd1306/archive/4.2.0.zip - knolleary/PubSubClient @ ^2.8 - https://github.com/OpenThingsIO/OpenThings-Framework-Firmware-Library @ ^0.1.0 -; ignore html2raw.cpp source file for firmware compilation (external helper program) +lib_deps = + sui77/rc-switch @ ^2.6.3 + https://github.com/ThingPulse/esp8266-oled-ssd1306/archive/4.2.0.zip + knolleary/PubSubClient @ ^2.8 + https://github.com/OpenThingsIO/OpenThings-Framework-Firmware-Library @ ^0.1.0 + xreef/EMailSender@^3.0.14 build_src_filter = +<*> - upload_speed = 460800 -monitor_speed=115200 +monitor_speed = 115200 board_build.flash_mode = dio board_build.ldscript = eagle.flash.4m2m.ld board_build.f_cpu = 160000000L board_build.f_flash = 80000000L - -;[env:sanguino_atmega1284p] -;platform = atmelavr -;board = ATmega1284P -;board_build.f_cpu = 16000000L -;board_build.variant = sanguino -;framework = arduino -;lib_ldf_mode = deep -;lib_deps = -; https://github.com/UIPEthernet/UIPEthernet/archive/refs/tags/v2.0.12.zip -; knolleary/PubSubClient @ ^2.8 -; greiman/SdFat @ 1.0.7 -; Wire -;build_src_filter = +<*> - -;monitor_speed=115200 - From f565cc475dcbf2986cbfc7e70676b62506af6c10 Mon Sep 17 00:00:00 2001 From: Ray Date: Thu, 27 Jun 2024 14:54:22 -0400 Subject: [PATCH 04/61] fixing issues for ospi https --- OpenSprinkler.cpp | 16 +++++----- etherport.cpp | 68 ++++++++++++++++++---------------------- etherport.h | 44 +++++++++++--------------- opensprinkler_server.cpp | 8 +++-- 4 files changed, 61 insertions(+), 75 deletions(-) diff --git a/OpenSprinkler.cpp b/OpenSprinkler.cpp index 8d8475f9..2e30437e 100644 --- a/OpenSprinkler.cpp +++ b/OpenSprinkler.cpp @@ -1945,7 +1945,7 @@ int8_t OpenSprinkler::send_http_request(const char* server, uint16_t port, char* if(client->connected()) { client->write((uint8_t *)p, len); } else { - DEBUG_PRINTLN(F("clint no longer connected")); + DEBUG_PRINTLN(F("client no longer connected")); } memset(ether_buffer, 0, ETHER_BUFFER_SIZE); uint32_t stoptime = millis()+timeout; @@ -1973,16 +1973,18 @@ int8_t OpenSprinkler::send_http_request(const char* server, uint16_t port, char* } } #else - while(client->connected()) { + /*while(client->connected()) { int len=client->read((uint8_t *)ether_buffer+pos, ETHER_BUFFER_SIZE); - if (len<=0) continue; + if (len==0) continue; pos+=len; if(millis()>stoptime) { DEBUG_PRINTLN(F("host timeout occured")); //return HTTP_RQT_TIMEOUT; // instead of returning with timeout, we'll work with data received so far break; } - } + }*/ + int n = client->read((uint8_t *)ether_buffer+pos, ETHER_BUFFER_SIZE); + pos+=n; #endif ether_buffer[pos]=0; // properly end buffer with 0 client->stop(); @@ -2066,8 +2068,6 @@ void OpenSprinkler::switch_remotestation(RemoteOTCStationData *data, bool turnon RemoteOTCStationData copy; memcpy((char*)©, (char*)data, sizeof(RemoteOTCStationData)); copy.token[sizeof(copy.token)-1] = 0; // ensure the string ends properly - DEBUG_PRINTLN((char*)copy.token); - DEBUG_PRINTLN((int)hex2ulong(copy.sid, sizeof(copy.sid))); char *p = tmp_buffer; BufferFiller bf = p; // if turning on the zone and duration is defined, give duration as the timer value @@ -2087,9 +2087,9 @@ void OpenSprinkler::switch_remotestation(RemoteOTCStationData *data, bool turnon SOPT_PASSWORD, (int)hex2ulong(copy.sid, sizeof(copy.sid)), turnon, timer); - bf.emit_p(PSTR(" HTTP/1.0\r\nHOST: $S\r\n\r\n"), DEFAULT_OTC_SERVER_APP); + bf.emit_p(PSTR(" HTTP/1.0\r\nHOST: $S\r\nConnection:close\r\n\r\n"), DEFAULT_OTC_SERVER_APP); - send_http_request(DEFAULT_OTC_SERVER_APP, DEFAULT_OTC_PORT_APP, p, remote_http_callback, true); + int x = send_http_request(DEFAULT_OTC_SERVER_APP, DEFAULT_OTC_PORT_APP, p, remote_http_callback, true); } /** Switch http(s) station diff --git a/etherport.cpp b/etherport.cpp index 858b496a..6e8db0e9 100644 --- a/etherport.cpp +++ b/etherport.cpp @@ -228,14 +228,15 @@ size_t EthernetClient::write(const uint8_t *buf, size_t size) /** * SSL Client */ +static SSL_CTX* ctx = NULL; EthernetClientSsl::EthernetClientSsl() - : m_sock(0), m_connected(false) + : EthernetClient() { } EthernetClientSsl::EthernetClientSsl(int sock) - : m_sock(sock), m_connected(true) + : EthernetClient(sock) { } @@ -244,10 +245,6 @@ EthernetClientSsl::~EthernetClientSsl() stop(); } -static bool sslInit; -//static BIO* certbio; -static SSL_CTX* ctx; - /** * https://github.com/angstyloop/c-web/blob/main/openssl-fetch-example.c */ @@ -256,18 +253,14 @@ int EthernetClientSsl::connect(const char* server, uint16_t port) if (m_sock) return 0; - struct hostent *host = gethostbyname(server); - if (!host) { - DEBUG_PRINTLN("Error: DNS look up failed\n"); - return 0; - } - + static bool sslInit = false; if (!sslInit) { OpenSSL_add_all_algorithms(); //ERR_load_BIO_strings(); ERR_load_crypto_strings(); SSL_load_error_strings(); //BIO* certbio = BIO_new(BIO_s_file()); + //BIO* outbio = BIO_new_fp(stdout, BIO_NOCLOSE); if (SSL_library_init() < 0) { DEBUG_PRINTLN("Error: could not initialize the OpenSSL library.\n"); return 0; @@ -278,30 +271,36 @@ int EthernetClientSsl::connect(const char* server, uint16_t port) DEBUG_PRINTLN("Error: unable to create SSL context.\n"); return 0; } - SSL_CTX_set_options(ctx, SSL_OP_NO_SSLv2|SSL_OP_NO_SSLv3); + SSL_CTX_set_options(ctx, SSL_OP_NO_SSLv2); SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, NULL); //Accept all certs sslInit = true; } - // Create a new SSL session. This does not connect the socket. - ssl = SSL_new(ctx); + struct hostent *host = gethostbyname(server); + if (!host) { + DEBUG_PRINTLN("Error: DNS look up failed\n"); + return 0; + } + // Create a new SSL session. This does not connect the socket. + ssl = SSL_new(ctx); struct sockaddr_in sin = {0}; sin.sin_family = AF_INET; sin.sin_port = htons(port); - sin.sin_addr.s_addr = *(uint32_t*) (host->h_addr); + sin.sin_addr.s_addr = *(long*) (host->h_addr); m_sock = socket(AF_INET, SOCK_STREAM, 0); - if (::connect(m_sock, (struct sockaddr *) &sin, sizeof(sin)) < 0) + if (::connect(m_sock, (struct sockaddr *) &sin, sizeof(struct sockaddr)) < 0) { DEBUG_PRINTLN("Error: connecting to the server failed"); return 0; } SSL_set_fd(ssl, m_sock); SSL_set_tlsext_host_name(ssl, server); // set correct host name + if (SSL_connect(ssl) < 1) { close(m_sock); m_sock = 0; - DEBUG_PRINTLN("Error: Could not build an SSL session"); + DEBUG_PRINTLN("Error: Could not build an SSL session"); return 0; } m_connected = true; @@ -342,27 +341,20 @@ EthernetClientSsl::operator bool() // and return 0; int EthernetClientSsl::read(uint8_t *buf, size_t size) { - fd_set sock_set; - FD_ZERO(&sock_set); - FD_SET(m_sock, &sock_set); - struct timeval timeout; - timeout.tv_sec = 3; - timeout.tv_usec = 0; - - //select(m_sock + 1, &sock_set, NULL, NULL, &timeout); - //if (FD_ISSET(m_sock, &sock_set)) - size_t pending = SSL_pending(ssl); - if (pending > 0) - { - if (size > pending) - size = pending; - int retval = SSL_read(ssl, buf, size); - if (retval <= 0) // socket closed - m_connected = false; - return retval; + /*int retval = SSL_read(ssl, buf, size); + if (retval < 0) // socket closed + m_connected = false; + return retval;*/ + int n=0; + for (;;) { + if ((n = SSL_read(ssl, buf, size)) < 0) { + DEBUG_PRINTLN("ERROR reading from socket."); + break; + } + if (!n) break; + printf("%s", buf); } - m_connected = false; - return 0; + return n; } size_t EthernetClientSsl::write(const uint8_t *buf, size_t size) diff --git a/etherport.h b/etherport.h index 20e75787..767d8ff3 100644 --- a/etherport.h +++ b/etherport.h @@ -47,42 +47,34 @@ class EthernetClient { EthernetClient(); EthernetClient(int sock); ~EthernetClient(); - int connect(const char *server, uint16_t port); - bool connected(); - void stop(); - int read(uint8_t *buf, size_t size); - size_t write(const uint8_t *buf, size_t size); - operator bool(); - int GetSocket() - { + virtual int connect(const char *server, uint16_t port); + virtual bool connected(); + virtual void stop(); + virtual int read(uint8_t *buf, size_t size); + virtual size_t write(const uint8_t *buf, size_t size); + virtual operator bool(); + virtual int GetSocket() { return m_sock; } -private: +protected: int m_sock = 0; bool m_connected; friend class EthernetServer; }; -class EthernetClientSsl : EthernetClient { +class EthernetClientSsl : public EthernetClient { public: EthernetClientSsl(); EthernetClientSsl(int sock); ~EthernetClientSsl(); - int connect(const char *server, uint16_t port); - bool connected(); - void stop(); - int read(uint8_t *buf, size_t size); - size_t write(const uint8_t *buf, size_t size); - operator bool(); - int GetSocket() - { - return m_sock; - } -private: - int m_sock; + virtual int connect(const char *server, uint16_t port); + virtual bool connected(); + virtual void stop(); + virtual int read(uint8_t *buf, size_t size); + virtual size_t write(const uint8_t *buf, size_t size); + virtual operator bool(); +protected: SSL* ssl; - bool m_connected; - friend class EthernetServer; }; class EthernetServer { @@ -90,8 +82,8 @@ class EthernetServer { EthernetServer(uint16_t port); ~EthernetServer(); - bool begin(); - EthernetClient available(); + virtual bool begin(); + virtual EthernetClient available(); private: uint16_t m_port; int m_sock; diff --git a/opensprinkler_server.cpp b/opensprinkler_server.cpp index 57510782..8bb226ff 100644 --- a/opensprinkler_server.cpp +++ b/opensprinkler_server.cpp @@ -586,9 +586,9 @@ void server_change_stations(OTF_PARAMS_DEF) { tmp_buffer[0]-='0'; tmp_buffer[STATION_SPECIAL_DATA_SIZE] = 0; - // only process GPIO and HTTP stations for OS 2.3, above, and OSPi + DEBUG_PRINTF("%d\n", tmp_buffer[0]); if(tmp_buffer[0] == STN_TYPE_GPIO) { - // check that pin does not clash with OSPi pins + // check that pin does not clash with free pins byte gpio = (tmp_buffer[1] - '0') * 10 + tmp_buffer[2] - '0'; byte activeState = tmp_buffer[3] - '0'; @@ -600,10 +600,12 @@ void server_change_stations(OTF_PARAMS_DEF) { if (!found || activeState > 1) { handle_return(HTML_DATA_OUTOFBOUND); } - } else if (tmp_buffer[0] == STN_TYPE_HTTP || tmp_buffer[0] == STN_TYPE_HTTPS) { + } else if ((tmp_buffer[0] == STN_TYPE_HTTP) || (tmp_buffer[0] == STN_TYPE_HTTPS) || (tmp_buffer[0] == STN_TYPE_REMOTE_OTC)) { + DEBUG_PRINTLN(tmp_buffer+1); #if !defined(ESP8266) urlDecode(tmp_buffer + 1); #endif + DEBUG_PRINTLN(tmp_buffer+1); if (strlen(tmp_buffer+1) > sizeof(HTTPStationData)) { handle_return(HTML_DATA_OUTOFBOUND); } From b185c1bc866e43e3724fd796ca6d410085478bae Mon Sep 17 00:00:00 2001 From: Nathan P Date: Thu, 27 Jun 2024 15:55:54 -0400 Subject: [PATCH 05/61] Initial support for Raspberry Pi email notifications. --- build.sh | 2 +- main.cpp | 78 ++++++++++++++++++++++++++++++++++++++++++-------------- 2 files changed, 60 insertions(+), 20 deletions(-) diff --git a/build.sh b/build.sh index ae257c8d..44591633 100755 --- a/build.sh +++ b/build.sh @@ -45,7 +45,7 @@ else fi echo "Compiling ospi firmware..." - g++ -o OpenSprinkler -DOSPI $USEGPIO -std=c++14 main.cpp OpenSprinkler.cpp program.cpp opensprinkler_server.cpp utils.cpp weather.cpp gpio.cpp etherport.cpp mqtt.cpp -lpthread -lmosquitto $GPIOLIB + g++ -o OpenSprinkler -DOSPI $USEGPIO -DSMTP_OPENSSL -fpermissive -std=c++14 main.cpp OpenSprinkler.cpp program.cpp opensprinkler_server.cpp utils.cpp weather.cpp gpio.cpp etherport.cpp mqtt.cpp smtp.c -lpthread -lmosquitto -lssl -lcrypto $GPIOLIB fi if [ ! "$SILENT" = true ] && [ -f OpenSprinkler.launch ] && [ ! -f /etc/init.d/OpenSprinkler.sh ]; then diff --git a/main.cpp b/main.cpp index 95fd8ba6..57ccd7fb 100644 --- a/main.cpp +++ b/main.cpp @@ -30,7 +30,14 @@ #include "opensprinkler_server.h" #include "mqtt.h" #include "main.h" -#include "EmailSender.h" +#if defined(ARUDINO) + #include "EmailSender.h" +#else + #include "smtp.h" + #define MAIL_CONNECTION_SECURITY SMTP_SECURITY_TLS + #define MAIL_FLAGS (SMTP_DEBUG | SMTP_NO_CERT_VERIFY) + #define MAIL_AUTH SMTP_AUTH_PLAIN +#endif #define str(s) #s #define xstr(s) str(s) @@ -449,12 +456,14 @@ void do_loop() time_os_t curr_time = os.now_tz(); //handle delayed reboot notification until wifi connection - if(delayed){ - if(WiFi.status() == WL_CONNECTED){ - push_message(NOTIFY_REBOOT); - delayed = false; + #if defined(ARDUINO) + if(delayed){ + if(WiFi.status() == WL_CONNECTED){ + push_message(NOTIFY_REBOOT); + delayed = false; + } } - } + #endif // ====== Process Ethernet packets ====== #if defined(ARDUINO) // Process Ethernet packets for Arduino @@ -1403,9 +1412,19 @@ void push_message(int type, uint32_t lval, float fval, const char* sval) { &enabled, host, &port, username, password, recipient ); } + //assign email variables necessary - EMailSender emailSend(username, password); - EMailSender::EMailMessage message; + #if defined(ARDUINO) + EMailSender emailSend(username, password); + EMailSender::EMailMessage message; + #else + struct smtp *smtp; + int rc; + struct { + String subject; + String message; + }message; + #endif //check if ifttt key exists bool ifttt_enabled; @@ -1441,8 +1460,10 @@ void push_message(int type, uint32_t lval, float fval, const char* sval) { strcpy_P(postval, PSTR("On site [")); os.sopt_load(SOPT_DEVICE_NAME, postval+strlen(postval)); strcat_P(postval, PSTR("], ")); - emailSend.setSMTPServer(strdup(host)); - emailSend.setSMTPPort(port); + #if defined(ARDUINO) + emailSend.setSMTPServer(strdup(host)); + emailSend.setSMTPPort(port); + #endif } switch(type) { @@ -1590,10 +1611,12 @@ void push_message(int type, uint32_t lval, float fval, const char* sval) { break; case NOTIFY_REBOOT: - if(!delayed){ - delayed = true; - return; - } + #if defined(ARUDINO) + if(!delayed){ + delayed = true; + return; + } + #endif if (os.mqtt.enabled()) { strcpy_P(topic, PSTR("opensprinkler/system")); @@ -1649,11 +1672,28 @@ void push_message(int type, uint32_t lval, float fval, const char* sval) { } if(email_enabled){ - EMailSender::Response resp = emailSend.send(recipient, message); - // DEBUG_PRINTLN(F("Sending Status:")); - // DEBUG_PRINTLN(resp.status); - // DEBUG_PRINTLN(resp.code); - // DEBUG_PRINTLN(resp.desc); + #if defined(ARDUINO) + EMailSender::Response resp = emailSend.send(recipient, message); + // DEBUG_PRINTLN(F("Sending Status:")); + // DEBUG_PRINTLN(resp.status); + // DEBUG_PRINTLN(resp.code); + // DEBUG_PRINTLN(resp.desc); + #else + char piSubject[message.subject.length() + 1]; + char piMessage[message.message.length() + 1]; + String sPort = to_string(port); + char piPort[sPort.length() + 1]; + strcpy(piSubject, message.subject.c_str()); + strcpy(piMessage, message.message.c_str()); + strcpy(piPort, sPort.c_str()); + rc = smtp_open(host, piPort, MAIL_CONNECTION_SECURITY, MAIL_FLAGS, NULL, &smtp); + rc = smtp_auth(smtp, MAIL_AUTH, username, password); + rc = smtp_address_add(smtp, SMTP_ADDRESS_FROM, username, "OpenSprinkler"); + rc = smtp_address_add(smtp, SMTP_ADDRESS_TO, recipient, "User"); + rc = smtp_header_add(smtp, "Subject", piSubject); + rc = smtp_mail(smtp, piMessage); + rc = smtp_close(smtp); + #endif } } From c5a5fcc81c1ebb81e83b8936ade169435aecdff9 Mon Sep 17 00:00:00 2001 From: Nathan P Date: Thu, 27 Jun 2024 15:58:46 -0400 Subject: [PATCH 06/61] add missing smtp.c and .h files --- smtp.c | 3528 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ smtp.h | 635 ++++++++++ 2 files changed, 4163 insertions(+) create mode 100644 smtp.c create mode 100644 smtp.h diff --git a/smtp.c b/smtp.c new file mode 100644 index 00000000..40e77b49 --- /dev/null +++ b/smtp.c @@ -0,0 +1,3528 @@ +/** + * @file + * @brief SMTP client library. + * @author James Humphrey (mail@somnisoft.com) + * @version 1.00 + * + * This SMTP client library allows the user to send emails to an SMTP server. + * The user can include custom headers and MIME attachments. + * + * This software has been placed into the public domain using CC0. + */ + +/** + * @mainpage smtp-client + * + * This section contains documentation generated directly from the source + * code. + * + * To view the repository details, visit the main smtp-client page at + * + * www.somnisoft.com/smtp-client + * . + */ + +#if defined(_WIN32) || defined(WIN32) +# define SMTP_IS_WINDOWS +#endif /* SMTP_IS_WINDOWS */ + +#ifdef SMTP_IS_WINDOWS +# include +# include +#else /* POSIX */ +# include +# include +# include +# include +# include +#endif /* SMTP_IS_WINDOWS */ + +#include +#include +#include +#include +#include +#include +#include + +#ifdef SMTP_OPENSSL +# include +# include +# include +# include +# include +#endif /* SMTP_OPENSSL */ + +/** + * Get access to the @ref smtp_result_code and @ref smtp_command definitions. + */ +#define SMTP_INTERNAL_DEFINE + +#include "smtp.h" + +/* + * The SMTP_TEST converts some library routines into special test seams which + * allows the test program to control whether they fail. For example, we can + * control when malloc() fails under certain conditions with an out of + * memory condition. + */ +#ifdef SMTP_TEST +/** + * Declare extern linkage on some functions so we can redefine their behavior + * in the external test suite. + */ +# define SMTP_LINKAGE extern +# include "../test/seams.h" +#else /* !(SMTP_TEST) */ +/** + * When not testing, all functions should have static linkage except for those + * in the header. + */ +# define SMTP_LINKAGE static +#endif /* SMTP_TEST */ + +/** + * Increment the read buffer size by this amount if the delimiter + * has not been found. + */ +#define SMTP_GETDELIM_READ_SZ 1000 + +/** + * Stores source and destination email addresses. + */ +struct smtp_address{ + /** + * Email address without any special formatting. + * + * For example: mail@example.com + */ + char *email; + + /** + * Description of the email address. + */ + char *name; + + /** + * Specify from, to, cc, bcc. + */ + enum smtp_address_type type; + + /** + * Padding structure to align. + */ + char pad[4]; +}; + +/** + * Attachment data which gets placed in the MIME email section. + */ +struct smtp_attachment{ + /** + * File name of the attachment. + */ + char *name; + + /** + * Base64-encoded file data. + */ + char *b64_data; +}; + +/** + * List of email headers to send before the mail body. + */ +struct smtp_header{ + /** + * Header name which will get sorted alphabetically in the header list. + */ + char *key; + + /** + * Content of the corresponding header key. + */ + char *value; +}; + +/** + * Main data structure that holds the SMTP client context. + */ +struct smtp{ + /** + * Bitwise list of flags controlling the behavior of this SMTP client. + */ + enum smtp_flag flags; + + /** + * Standard network socket connection. + */ + int sock; + + /** + * Read buffer and line parsing structure. + */ + struct str_getdelimfd gdfd; + + /** + * List of headers to print before the mail body. + */ + struct smtp_header *header_list; + + /** + * Number of headers in header_list. + */ + size_t num_headers; + + /** + * List of from, to, cc, and bcc email addresses. + */ + struct smtp_address *address_list; + + /** + * Number of addresses in address_list. + */ + size_t num_address; + + /** + * List of attachments to send. + */ + struct smtp_attachment *attachment_list; + + /** + * Number of attachments in attachment_list. + */ + size_t num_attachment; + + /** + * Timeout in seconds to wait before returning with an error. + * + * This applies to both writing to and reading from a network socket. + */ + long timeout_sec; + + /** + * Status code indicating success/failure. + * + * This code gets returned by most of the header functions. + */ + enum smtp_status_code status_code; + + /** + * Indicates if this context has an active TLS connection. + * - Set to 0 if TLS connection inactive. + * - Set to 1 if TLS connection currently active. + */ + int tls_on; + + /** + * Path to certificate file if using self-signed or untrusted certificate + * not in the default key store. + */ + const char *cafile; + +#ifdef SMTP_OPENSSL + /** + * OpenSSL TLS object. + */ + SSL *tls; + + /** + * OpenSSL TLS context. + */ + SSL_CTX *tls_ctx; + + /** + * OpenSSL TLS I/O abstraction. + */ + BIO *tls_bio; +#endif /* SMTP_OPENSSL */ +}; + +/** + * Check if adding a size_t value will cause a wrap. + * + * @param[in] a Add this value with @p b. + * @param[in] b Add this value with @p a. + * @param[out] result Save the addition to this buffer. Does not + * perform the addition if set to NULL. + * @retval 1 Value wrapped. + * @retval 0 Value did not wrap. + */ +SMTP_LINKAGE int +smtp_si_add_size_t(const size_t a, + const size_t b, + size_t *const result){ + int wraps; + +#ifdef SMTP_TEST + if(smtp_test_seam_dec_err_ctr(&g_smtp_test_err_si_add_size_t_ctr)){ + return 1; + } +#endif /* SMTP_TEST */ + + if(SIZE_MAX - a < b){ + wraps = 1; + } + else{ + wraps = 0; + } + if(result){ + *result = a + b; + } + return wraps; +} + +/** + * Check if subtracting a size_t value will cause wrap. + * + * @param[in] a Subtract this value by @p b. + * @param[in] b Subtract this value from @p a. + * @param[out] result Save the subtraction to this buffer. Does not + * perform the subtraction if set to NULL. + * @retval 1 Value wrapped. + * @retval 0 Value did not wrap. + */ +SMTP_LINKAGE int +smtp_si_sub_size_t(const size_t a, + const size_t b, + size_t *const result){ + int wraps; + +#ifdef SMTP_TEST + if(smtp_test_seam_dec_err_ctr(&g_smtp_test_err_si_sub_size_t_ctr)){ + return 1; + } +#endif /* SMTP_TEST */ + + if(a < b){ + wraps = 1; + } + else{ + wraps = 0; + } + if(result){ + *result = a - b; + } + return wraps; +} + +/** + * Check if multiplying a size_t value will cause a wrap. + * + * @param[in] a Multiply this value with @p b. + * @param[in] b Multiply this value with @p a. + * @param[out] result Save the multiplication to this buffer. Does not + * perform the multiplication if set to NULL. + * @retval 1 Value wrapped. + * @retval 0 Value did not wrap. + */ +SMTP_LINKAGE int +smtp_si_mul_size_t(const size_t a, + const size_t b, + size_t *const result){ + int wraps; + +#ifdef SMTP_TEST + if(smtp_test_seam_dec_err_ctr(&g_smtp_test_err_si_mul_size_t_ctr)){ + return 1; + } +#endif /* SMTP_TEST */ + + if(b != 0 && a > SIZE_MAX / b){ + wraps = 1; + } + else{ + wraps = 0; + } + if(result){ + *result = a * b; + } + return wraps; +} + +/** + * Wait until more data has been made available on the socket read end. + * + * @param[in] smtp SMTP client context. + * @retval SMTP_STATUS_OK If data available to read on the socket. + * @retval SMTP_STATUS_RECV If the connection times out before any data + * appears on the socket. + */ +static enum smtp_status_code +smtp_str_getdelimfd_read_timeout(struct smtp *const smtp){ + fd_set readfds; + struct timeval timeout; + int sel_rc; + + FD_ZERO(&readfds); + FD_SET(smtp->sock, &readfds); + timeout.tv_sec = smtp->timeout_sec; + timeout.tv_usec = 0; + sel_rc = select(smtp->sock + 1, &readfds, NULL, NULL, &timeout); + if(sel_rc < 1){ + return smtp_status_code_set(smtp, SMTP_STATUS_RECV); + } + return SMTP_STATUS_OK; +} + +/** + * This function gets called by the @ref smtp_str_getdelimfd interface when it + * needs to read in more data. + * + * It reads using either the plain socket connection if encryption not + * enabled, or it reads using OpenSSL if it has an active TLS connection. + * + * @param[in] gdfd See @ref str_getdelimfd. + * @param[out] buf Pointer to buffer for storing bytes read. + * @param[in] count Maximum number of bytes to try reading. + * @retval >=0 Number of bytes read. + * @retval -1 Failed to read from the socket. + */ +static long +smtp_str_getdelimfd_read(struct str_getdelimfd *const gdfd, + void *buf, + size_t count){ + struct smtp *smtp; + long bytes_read; + + smtp = gdfd->user_data; + + if(smtp_str_getdelimfd_read_timeout(smtp) != SMTP_STATUS_OK){ + return -1; + } + + bytes_read = 0; + if(smtp->tls_on){ +#ifdef SMTP_OPENSSL + do{ + /* + * Count will never have a value greater than SMTP_GETDELIM_READ_SZ, + * so we can safely convert this to an int. + */ + bytes_read = SSL_read(smtp->tls, buf, (int)count); + } while(bytes_read <= 0 && BIO_should_retry(smtp->tls_bio)); +#endif /* SMTP_OPENSSL */ + } + else{ + bytes_read = recv(smtp->sock, buf, count, 0); + } + return bytes_read; +} + +/** + * Find and return the location of the delimiter character in the + * search buffer. + * + * This function gets used by the main socket parsing function which + * continually reads from the socket and expands the buffer until it + * encounters the expected delimiter. This function provides the logic + * to check for the delimiter character in order to simplify the code + * in the main parse function. + * + * @param[in] buf Search buffer used to find the delimiter. + * @param[in] buf_len Number of bytes to search for in buf. + * @param[in] delim The delimiter to search for in buf. + * @param[out] delim_pos If delimiter found in buf, return the delimiter + * position in this parameter. + * @retval 1 If the delimiter character found. + * @retval 0 If the delimiter character not found. + */ +static int +smtp_str_getdelimfd_search_delim(const char *const buf, + size_t buf_len, + int delim, + size_t *const delim_pos){ + size_t i; + + *delim_pos = 0; + for(i = 0; i < buf_len; i++){ + if(buf[i] == delim){ + *delim_pos = i; + return 1; + } + } + return 0; +} + +/** + * Set the internal line buffer to the number of bytes specified. + * + * @param[in] gdfd See @ref str_getdelimfd. + * @param[in] copy_len Number of bytes to copy to the internal line buffer. + * @retval 0 Successfully allocated and copied data over to the new + * line buffer. + * @retval -1 Failed to allocate memory for the new line buffer. + */ +SMTP_LINKAGE int +smtp_str_getdelimfd_set_line_and_buf(struct str_getdelimfd *const gdfd, + size_t copy_len){ + size_t copy_len_inc; + size_t nbytes_to_shift; + size_t new_buf_len; + + if(gdfd->line){ + free(gdfd->line); + gdfd->line = NULL; + } + + if(smtp_si_add_size_t(copy_len, 1, ©_len_inc) || + smtp_si_add_size_t((size_t)gdfd->_buf, copy_len_inc, NULL) || + smtp_si_sub_size_t(gdfd->_buf_len, copy_len, &nbytes_to_shift) || + (gdfd->line = calloc(1, copy_len_inc)) == NULL){ + return -1; + } + memcpy(gdfd->line, gdfd->_buf, copy_len); + gdfd->line_len = copy_len; + memmove(gdfd->_buf, gdfd->_buf + copy_len_inc, nbytes_to_shift); + if(smtp_si_sub_size_t(nbytes_to_shift, 1, &new_buf_len) == 0){ + gdfd->_buf_len = new_buf_len; + } + return 0; +} + +/** + * Free memory in the @ref str_getdelimfd data structure. + * + * @param[in] gdfd Frees memory stored in this socket parsing structure. + */ +SMTP_LINKAGE void +smtp_str_getdelimfd_free(struct str_getdelimfd *const gdfd){ + free(gdfd->_buf); + free(gdfd->line); + gdfd->_buf = NULL; + gdfd->_bufsz = 0; + gdfd->_buf_len = 0; + gdfd->line = NULL; + gdfd->line_len = 0; +} + +/** + * Free the @ref str_getdelimfd and return the @ref STRING_GETDELIMFD_ERROR + * error code. + * + * @param[in] gdfd See @ref str_getdelimfd. + * @return See @ref str_getdelim_retcode. + */ +static enum str_getdelim_retcode +smtp_str_getdelimfd_throw_error(struct str_getdelimfd *const gdfd){ + smtp_str_getdelimfd_free(gdfd); + return STRING_GETDELIMFD_ERROR; +} + +/** + * Read and parse a delimited string using a custom socket read function. + * + * This interface handles all of the logic for expanding the buffer, + * parsing the delimiter in the buffer, and returning each "line" + * to the caller for handling. + * + * @param[in] gdfd See @ref str_getdelimfd. + * @return See @ref str_getdelim_retcode. + */ +SMTP_LINKAGE enum str_getdelim_retcode +smtp_str_getdelimfd(struct str_getdelimfd *const gdfd){ + size_t delim_pos; + long bytes_read; + void *read_buf_ptr; + char *buf_new; + size_t buf_sz_remaining; + size_t buf_sz_new; + + if(gdfd->getdelimfd_read == NULL){ + return STRING_GETDELIMFD_ERROR; + } + + bytes_read = -1; + + while(1){ + if(smtp_str_getdelimfd_search_delim(gdfd->_buf, + gdfd->_buf_len, + gdfd->delim, + &delim_pos)){ + if(smtp_str_getdelimfd_set_line_and_buf(gdfd, delim_pos) < 0){ + return smtp_str_getdelimfd_throw_error(gdfd); + } + return STRING_GETDELIMFD_NEXT; + }else if(bytes_read == 0){ + if(smtp_str_getdelimfd_set_line_and_buf(gdfd, gdfd->_buf_len) < 0){ + return smtp_str_getdelimfd_throw_error(gdfd); + } + return STRING_GETDELIMFD_DONE; + } + + if(smtp_si_sub_size_t(gdfd->_bufsz, gdfd->_buf_len, &buf_sz_remaining)){ + return smtp_str_getdelimfd_throw_error(gdfd); + } + + if(buf_sz_remaining < SMTP_GETDELIM_READ_SZ){ + if(smtp_si_add_size_t(buf_sz_remaining, + SMTP_GETDELIM_READ_SZ, + &buf_sz_new)){ + return smtp_str_getdelimfd_throw_error(gdfd); + } + buf_new = realloc(gdfd->_buf, buf_sz_new); + if(buf_new == NULL){ + return smtp_str_getdelimfd_throw_error(gdfd); + } + gdfd->_buf = buf_new; + gdfd->_bufsz = buf_sz_new; + } + + if(smtp_si_add_size_t((size_t)gdfd->_buf, gdfd->_buf_len, NULL)){ + return smtp_str_getdelimfd_throw_error(gdfd); + } + read_buf_ptr = gdfd->_buf + gdfd->_buf_len; + bytes_read = (*gdfd->getdelimfd_read)(gdfd, + read_buf_ptr, + SMTP_GETDELIM_READ_SZ); + if(bytes_read < 0 || + smtp_si_add_size_t(gdfd->_buf_len, + (size_t)bytes_read, + &gdfd->_buf_len)){ + return smtp_str_getdelimfd_throw_error(gdfd); + } + } +} + +/** + * Copy a string and get the pointer to the end of the copied buffer. + * + * This function behaves similar to POSIX stpcpy(), useful for + * concatenating multiple strings onto a buffer. It always adds a + * null-terminated byte at the end of the string. + * + * @param[in] s1 Destination buffer. + * @param[in] s2 Null-terminated source string to copy to @p s1. + * @return Pointer to location in @p s1 after the last copied byte. + */ +SMTP_LINKAGE char * +smtp_stpcpy(char *s1, + const char *s2){ + size_t i; + + i = 0; + do{ + s1[i] = s2[i]; + } while(s2[i++] != '\0'); + return &s1[i-1]; +} + +/** + * Reallocate memory with unsigned wrapping checks. + * + * @param[in] ptr Existing allocation buffer, or NULL when allocating a + * new buffer. + * @param[in] nmemb Number of elements to allocate. + * @param[in] size Size of each element in @p nmemb. + * @retval void* Pointer to a reallocated buffer containing + * @p nmemb * @p size bytes. + * @retval NULL Failed to reallocate memory. + */ +SMTP_LINKAGE void * +smtp_reallocarray(void *ptr, + size_t nmemb, + size_t size){ + void *alloc; + size_t size_mul; + + if(smtp_si_mul_size_t(nmemb, size, &size_mul)){ + alloc = NULL; + errno = ENOMEM; + } + else{ + alloc = realloc(ptr, size_mul); + } + return alloc; +} + +/** + * Copy a string into a new dynamically allocated buffer. + * + * Returns a dynamically allocated string, with the same contents as the + * input string. The caller must free the returned string when finished. + * + * @param[in] s String to duplicate. + * @retval char* Pointer to a new dynamically allocated string duplicated + * from @p s. + * @retval NULL Failed to allocate memory for the new duplicate string. + */ +SMTP_LINKAGE char * +smtp_strdup(const char *s){ + char *dup; + size_t dup_len; + size_t slen; + + slen = strlen(s); + if(smtp_si_add_size_t(slen, 1, &dup_len)){ + dup = NULL; + errno = ENOMEM; + } + else if((dup = malloc(dup_len)) != NULL){ + memcpy(dup, s, dup_len); + } + return dup; +} + +/** + * Search for all substrings in a string and replace each instance with a + * replacement string. + * + * @param[in] search Substring to search for in @p s. + * @param[in] replace Replace each instance of the search string with this. + * @param[in] s Null-terminated string to search and replace. + * @retval char* A dynamically allocated string with the replaced instances + * as described above. The caller must free the allocated + * memory when finished. + * @retval NULL Memory allocation failure. + */ +SMTP_LINKAGE char * +smtp_str_replace(const char *const search, + const char *const replace, + const char *const s){ + size_t search_len; + size_t replace_len; + size_t replace_len_inc; + size_t slen; + size_t slen_inc; + size_t s_idx; + size_t snew_len; + size_t snew_len_inc; + size_t snew_sz; + size_t snew_sz_dup; + size_t snew_sz_plus_slen; + size_t snew_replace_len_inc; + char *snew; + char *stmp; + + search_len = strlen(search); + replace_len = strlen(replace); + slen = strlen(s); + s_idx = 0; + snew = NULL; + snew_len = 0; + snew_sz = 0; + + if(smtp_si_add_size_t(replace_len, 1, &replace_len_inc) || + smtp_si_add_size_t(slen, 1, &slen_inc)){ + return NULL; + } + + if(s[0] == '\0'){ + return smtp_strdup(""); + } + else if(search_len < 1){ + return smtp_strdup(s); + } + + while(s[s_idx]){ + if(smtp_si_add_size_t(snew_len, 1, &snew_len_inc) || + smtp_si_add_size_t(snew_sz, snew_sz, &snew_sz_dup) || + smtp_si_add_size_t(snew_sz, slen, &snew_sz_plus_slen)){ + free(snew); + return NULL; + } + + if(strncmp(&s[s_idx], search, search_len) == 0){ + if(smtp_si_add_size_t(snew_len, replace_len_inc, &snew_replace_len_inc)){ + free(snew); + return NULL; + } + if(snew_replace_len_inc >= snew_sz){ + /* snew_sz += snew_sz + slen + replace_len + 1 */ + if(smtp_si_add_size_t(snew_sz_dup, slen_inc, &snew_sz) || + smtp_si_add_size_t(snew_sz, replace_len, &snew_sz) || + (stmp = realloc(snew, snew_sz)) == NULL){ + free(snew); + return NULL; + } + snew = stmp; + } + memcpy(&snew[snew_len], replace, replace_len); + snew_len += replace_len; + s_idx += search_len; + } + else{ + if(snew_len_inc >= snew_sz){ + /* snew_sz += snew_sz + slen + snew_len + 1 */ + if(smtp_si_add_size_t(snew_sz_dup, slen, &snew_sz) || + smtp_si_add_size_t(snew_sz, snew_len_inc, &snew_sz) || + (stmp = realloc(snew, snew_sz)) == NULL){ + free(snew); + return NULL; + } + snew = stmp; + } + snew[snew_len] = s[s_idx]; + s_idx += 1; + snew_len = snew_len_inc; + } + } + snew[snew_len] = '\0'; + + return snew; +} + +/** + * Lookup table used to encode data into base64. + * + * Base64 encoding takes six bits of data and encodes those bits using this + * table. Since 2^6 = 64, this array has 64 entries which maps directly from + * the 6 bit value into the corresponding array value. + */ +static char g_base64_encode_table[] = { + 'A','B','C','D','E','F','G','H','I','J', + 'K','L','M','N','O','P','Q','R','S','T', + 'U','V','W','X','Y','Z', + 'a','b','c','d','e','f','g','h','i','j', + 'k','l','m','n','o','p','q','r','s','t', + 'u','v','w','x','y','z', + '0','1','2','3','4','5','6','7','8','9', + '+','/' +}; + +/** + * Encode a single block of binary data into base64. + * + * @param[in] buf Buffer with data to encode. + * @param[in] buf_block_sz Number of bytes in buf to encode (min 1, max 3). + * @param[out] b64 Pointer to buffer with at least 4 bytes for + * storing the base64 encoded result. + */ +static void +smtp_base64_encode_block(const char *const buf, + size_t buf_block_sz, + char *const b64){ + unsigned char inb[3] = {0}; + unsigned char in_idx[4] = {0}; + char outb[5] = {'=', '=', '=', '=', '\0'}; + size_t i; + + memcpy(inb, buf, buf_block_sz); + + in_idx[0] = ((inb[0] >> 2)) & 0x3F; + in_idx[1] = ((inb[0] << 4) | ((inb[1] >> 4) & 0xF)) & 0x3F; + in_idx[2] = ((inb[1] << 2) | ((inb[2] >> 6) & 0x3)) & 0x3F; + in_idx[3] = ((inb[2] )) & 0x3F; + for(i = 0; i < 4; i++){ + if(i < buf_block_sz + 1){ + outb[i] = g_base64_encode_table[in_idx[i]]; + } + b64[i] = outb[i]; + } +} + +/** + * Encode binary data into a base64 string. + * + * @param[in] buf Binary data to encode in base64. + * @param[in] buflen Number of bytes in the @p buf parameter, or -1 if + * null-terminated. + * @retval char* Dynamically allocated base64 encoded string. The caller + * must free this string when finished. + * @retval NULL Memory allocation failure. + */ +SMTP_LINKAGE char * +smtp_base64_encode(const char *const buf, + size_t buflen){ + char *b64; + size_t b64_sz; + size_t buf_i; + size_t b64_i; + size_t remaining_block_sz; + size_t buf_block_sz; + + if(buflen == SIZE_MAX){ + buflen = strlen(buf); + } + + /* + * base64 size expands by 33% + * +1 to round integer division up + * +2 for '=' padding + * +1 null terminator + */ + if(smtp_si_mul_size_t(buflen, 4, NULL)){ + return NULL; + } + b64_sz = (4 * buflen / 3) + 1 + 2 + 1; + if((b64 = calloc(1, b64_sz)) == NULL){ + return NULL; + } + + if(buflen == 0){ + return b64; + } + + buf_i = 0; + b64_i = 0; + remaining_block_sz = buflen; + while(remaining_block_sz > 0){ + if(remaining_block_sz >= 3){ + buf_block_sz = 3; + } + else{ + buf_block_sz = remaining_block_sz; + } + + smtp_base64_encode_block(&buf[buf_i], buf_block_sz, &b64[b64_i]); + + /* + * Do not need to check for wrapping because these values restricted to + * range of b64_sz, which has already been checked for wrapping above. + */ + buf_i += 3; + b64_i += 4; + + remaining_block_sz -= buf_block_sz; + } + + return b64; +} + +#ifdef SMTP_OPENSSL +/** + * Lookup table used to decode base64 data. + * + * For base64 encoding, every six bits have been encoded using only the ASCII + * characters from @ref g_base64_encode_table. This table has entries which + * allow the reversal of that process. It has 128 entries which map over to + * the index value from the encoding table. If an indexing result ends up + * with -1 during the decoding process, then that indicates an invalid base64 + * character in the encoded data. + */ +static signed char +g_base64_decode_table[] = { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, + 62, /* + */ + -1, -1, -1, + 63, /* / */ + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, /* 0 - 9 */ + -1, -1, -1, -1, -1, -1, -1, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, /* A - J */ + 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, /* K - T */ + 20, 21, 22, 23, 24, 25, /* U - Z */ + -1, -1, -1, -1, -1, -1, + 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, /* a - j */ + 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, /* k - t */ + 46, 47, 48, 49, 50, 51, /* u - z */ + -1, -1, -1, -1, -1 +}; + +/** + * Decodes a base64 block of up to four bytes at a time. + * + * @param[in] buf Buffer containing bytes to decode. + * @param[out] decode Buffer for storing base64 decoded bytes. + * @retval >0 Length of the decoded block. + * @retval 0 If the block contains invalid base64 data. + */ +static size_t +smtp_base64_decode_block(const unsigned char *const buf, + unsigned char *const decode){ + size_t decode_block_len; + size_t i; + signed char decode_table[4]; + unsigned char outb[3]; + + decode_block_len = 0; + for(i = 0; i < 4; i++){ + if(buf[i] == '='){ + decode_table[i] = 0; + continue; + } + decode_table[i] = g_base64_decode_table[buf[i]]; + if(decode_table[i] < 0){ + return 0; + } + } + + outb[0] = ((decode_table[0] << 2) & 0xFC) | ((decode_table[1] >> 4) & 0x03); + outb[1] = ((decode_table[1] << 4) & 0xF0) | ((decode_table[2] >> 2) & 0x0F); + outb[2] = ((decode_table[2] << 6) & 0xC0) | ((decode_table[3] ) & 0x3F); + + decode[0] = outb[0]; + decode_block_len += 1; + + if(buf[2] == '='){ + decode[1] = '\0'; + } + else{ + decode[1] = outb[1]; + decode_block_len += 1; + } + + if(buf[3] == '='){ + decode[2] = '\0'; + } + else{ + decode[2] = outb[2]; + decode_block_len += 1; + } + + return decode_block_len; +} + +/** + * Decode a base64 string. + * + * The decode parameter will get dynamically allocated by this function + * if it successfully completes. Therefore, the caller must free the decode + * parameter after use. + * + * @param[in] buf Null-terminated base64 string. + * @param[out] decode Pointer to buffer which will get dynamically allocated + * and will contain the decoded binary data. This parameter + * will get set to NULL if the memory allocation fails. + * @retval >=0 Length of the data stored in the decode parameter. + * @retval -1 Memory allocation failure or invalid base64 byte sequences. + */ +SMTP_LINKAGE size_t +smtp_base64_decode(const char *const buf, + unsigned char **decode){ + size_t buf_len; + size_t buf_len_inc; + size_t buf_i; + unsigned char *b64_decode; + size_t decode_len; + size_t decode_block_len; + + *decode = NULL; + + buf_len = strlen(buf); + if(buf_len % 4 != 0){ + return SIZE_MAX; + } + + if(smtp_si_add_size_t(buf_len, 1, &buf_len_inc) || + (b64_decode = calloc(1, buf_len_inc)) == NULL){ + return SIZE_MAX; + } + + decode_len = 0; + for(buf_i = 0; buf_i < buf_len; buf_i += 4){ + decode_block_len = smtp_base64_decode_block( + (const unsigned char*)&buf[buf_i], + &b64_decode[decode_len]); + if(decode_block_len == 0){ + free(b64_decode); + return SIZE_MAX; + } + decode_len += decode_block_len; + } + *decode = b64_decode; + return decode_len; +} + +/** + * Convert binary data to lowercase hexadecimal representation. + * + * @param[in] s Buffer containing binary data to convert. + * @param[in] slen Number of bytes in @p s. + * @retval char* Dynamically allocated string consisting of a hexadecimal + * representation of binary data in @p s. The caller must free + * this memory when finished. + * @retval NULL Memory allocation or encoding error. + */ +SMTP_LINKAGE char * +smtp_bin2hex(const unsigned char *const s, + size_t slen){ + char *snew; + size_t alloc_sz; + size_t i; + size_t j; + unsigned hex; + int rc; + + /* alloc_sz = slen * 2 + 1 */ + if(smtp_si_mul_size_t(slen, 2, &alloc_sz) || + smtp_si_add_size_t(alloc_sz, 1, &alloc_sz)){ + return NULL; + } + if((snew = malloc(alloc_sz)) == NULL){ + return NULL; + } + + j = 0; + for(i = 0; i < slen; i++){ + hex = s[i]; + rc = sprintf(&snew[j], "%02x", hex); + if(rc < 0 || (size_t)rc >= 3){ + free(snew); + return NULL; + } + j += 2; + } + snew[j] = '\0'; + + return snew; +} +#endif /* SMTP_OPENSSL */ + +/** + * Get the length in bytes of a UTF-8 character. + * + * This consists of a very simple check and assumes the user provides a valid + * UTF-8 byte sequence. It gets the length from the first byte in the sequence + * and does not validate any other bytes in the character sequence or any other + * bits in the first byte of the character sequence. + * + * @param[in] c The first byte in a valid UTF-8 character sequence. + * @retval >0 Number of bytes for the current UTF-8 character sequence. + * @retval -1 Invalid byte sequence. + */ +SMTP_LINKAGE size_t +smtp_utf8_charlen(char c){ + unsigned char uc; + + uc = (unsigned char)c; + if((uc & 0x80) == 0){ /* 0XXXXXXX */ + return 1; + } + else if((uc & 0xE0) == 0xC0){ /* 110XXXXX */ + return 2; + } + else if((uc & 0xF0) == 0xE0){ /* 1110XXXX */ + return 3; + } + else if((uc & 0xF8) == 0xF0){ /* 11110XXX */ + return 4; + } + else{ /* invalid */ + return 0; + } +} + +/** + * Check if a string contains non-ASCII UTF-8 characters. + * + * Uses the simple algorithm from @ref smtp_utf8_charlen to check for + * non-ASCII UTF-8 characters. + * + * @param[in] s UTF-8 string. + * @retval 1 String contains non-ASCII UTF-8 characters. + * @retval 0 String contains only ASCII characters. + */ +SMTP_LINKAGE int +smtp_str_has_nonascii_utf8(const char *const s){ + size_t i; + size_t charlen; + + for(i = 0; s[i]; i++){ + charlen = smtp_utf8_charlen(s[i]); + if(charlen != 1){ + return 1; + } + } + return 0; +} + +/** + * Get the number of bytes in a UTF-8 string, or a shorter count if + * the string exceeds a maximum specified length. + * + * See @p maxlen for more information on multi-byte parsing. + * + * @param[in] s Null-terminated UTF-8 string. + * @param[in] maxlen Do not check more than @p maxlen bytes of string @p s + * except if in the middle of a multi-byte character. + * @retval strlen(s) If length of s has less bytes than maxlen or the same + * number of bytes as maxlen. See @p maxlen for more details. + * @retval maxlen If length of s has more bytes than maxlen. + * @retval -1 If @p s contains an invalid UTF-8 byte sequence. + */ +SMTP_LINKAGE size_t +smtp_strnlen_utf8(const char *s, + size_t maxlen){ + size_t i; + size_t utf8_i; + size_t utf8_len; + + for(i = 0; *s && i < maxlen; i += utf8_len){ + utf8_len = smtp_utf8_charlen(*s); + if(utf8_len == 0){ + return SIZE_MAX; + } + + for(utf8_i = 0; utf8_i < utf8_len; utf8_i++){ + if(!*s){ + return SIZE_MAX; + } + s += 1; + } + } + return i; +} + +/** + * Get the offset of the next whitespace block to process folding. + * + * If a string does not have whitespace before @p maxlen, then the index + * will get returned past @p maxlen. Also returns the index of NULL character + * if that fits within the next block. The caller must check for the NULL + * index to indicate the last block. It will skip past any leading whitespace + * even if that means going over maxlen. + * + * Examples: + * @ref smtp_fold_whitespace_get_offset ("Subject: Test WS", 1/2/8/9/10/13) -> 8 + * @ref smtp_fold_whitespace_get_offset ("Subject: Test WS", 14/15) -> 13 + * @ref smtp_fold_whitespace_get_offset ("Subject: Test WS", 17/18) -> 16 + * + * @param[in] s String to get offset from. + * @param[in] maxlen Number of bytes for each line in the string (soft limit). + * @return Index in @p s. + */ +SMTP_LINKAGE size_t +smtp_fold_whitespace_get_offset(const char *const s, + unsigned int maxlen){ + size_t i; + size_t offset_i; + + i = 0; + offset_i = 0; + + while(s[i] == ' ' || s[i] == '\t'){ + i += 1; + } + + while(s[i]){ + if(s[i] == ' ' || s[i] == '\t'){ + do{ + i += 1; + } while(s[i] == ' ' || s[i] == '\t'); + i -= 1; + if(i < maxlen || !offset_i){ + offset_i = i; + } + else{ + break; + } + } + i += 1; + } + + if(!offset_i || i < maxlen){ + offset_i = i; + } + + return offset_i; +} + +/** + * Email header lines should have no more than 78 characters and must + * not be more than 998 characters. + */ +#define SMTP_LINE_MAX 78 + +/** + * Fold a line at whitespace characters. + * + * This function tries to keep the total number of characters per line under + * @p maxlen, but does not guarantee this. For really long text with no + * whitespace, the line will still extend beyond @p maxlen and possibly + * beyond the RFC limit as defined in @ref SMTP_LINE_MAX. This is by design + * and intended to keep the algorithm simpler to implement. Users sending + * long headers with no space characters should not assume that will work, + * but modern email systems may correctly process those headers anyways. + * + * Lines get folded by adding a [CR][LF] and then two space characters on the + * beginning of the next line. For example, this Subject line: + * + * Subject: Email[WS][WS]Header + * + * Would get folded like this (assuming a small @p maxlen): + * + * Subject: Email[WS][CR][LF] + * [WS][WS]Header + * + * @param[in] s String to fold. + * @param[in] maxlen Number of bytes for each line in the string (soft limit). + * The minimum value of this parameter is 3 and it will get + * forced to 3 if the provided value is less. + * @retval char* Pointer to an allocated string with the contents split into + * separate lines. The caller must free this memory when done. + * @retval NULL Memory allocation failed. + */ +SMTP_LINKAGE char * +smtp_fold_whitespace(const char *const s, + unsigned int maxlen){ + const char *const SMTP_LINE_FOLD_STR = "\r\n "; + size_t end_slen; + size_t s_i; + size_t buf_i; + size_t bufsz; + size_t ws_offset; + char *buf; + char *buf_new; + + if(maxlen < 3){ + maxlen = 3; + } + + end_slen = strlen(SMTP_LINE_FOLD_STR); + + s_i = 0; + buf_i = 0; + bufsz = 0; + buf = NULL; + + while(1){ + ws_offset = smtp_fold_whitespace_get_offset(&s[s_i], maxlen - 2); + + /* bufsz += ws_offset + end_slen + 1 */ + if(smtp_si_add_size_t(bufsz, ws_offset, &bufsz) || + smtp_si_add_size_t(bufsz, end_slen, &bufsz) || + smtp_si_add_size_t(bufsz, 1, &bufsz) || + (buf_new = realloc(buf, bufsz)) == NULL){ + free(buf); + return NULL; + } + buf = buf_new; + memcpy(&buf[buf_i], &s[s_i], ws_offset); + buf[buf_i + ws_offset] = '\0'; + + if(s[s_i + ws_offset] == '\0'){ + break; + } + + buf_i += ws_offset; + strcat(&buf[buf_i], SMTP_LINE_FOLD_STR); + buf_i += end_slen; + + /* WS */ + s_i += ws_offset + 1; + } + return buf; +} + +/** + * Splits a string into smaller chunks separated by a terminating string. + * + * @param[in] s The string to chunk. + * @param[in] chunklen Number of bytes for each chunk in the string. + * @param[in] end Terminating string placed at the end of each chunk. + * @retval char* Pointer to an allocated string with the contents split into + * separate chunks. The caller must free this memory when done. + * @retval NULL Memory allocation failure. + */ +SMTP_LINKAGE char * +smtp_chunk_split(const char *const s, + size_t chunklen, + const char *const end){ + char *snew; + size_t bodylen; + size_t bodylen_inc; + size_t endlen; + size_t endlen_inc; + size_t snewlen; + size_t chunk_i; + size_t snew_i; + size_t body_i; + size_t body_copy_len; + + if(chunklen < 1){ + errno = EINVAL; + return NULL; + } + + bodylen = strlen(s); + endlen = strlen(end); + + if(bodylen < 1){ + return smtp_strdup(end); + } + + /* + * \0 + * snewlen = bodylen + (endlen + 1) * (bodylen / chunklen + 1) + 1 + */ + if(smtp_si_add_size_t(endlen, 1, &endlen_inc) || + smtp_si_add_size_t(bodylen, 1, &bodylen_inc) || + smtp_si_mul_size_t(endlen_inc, bodylen / chunklen + 1, &snewlen) || + smtp_si_add_size_t(snewlen, bodylen_inc, &snewlen) || + (snew = calloc(1, snewlen)) == NULL){ + return NULL; + } + + body_i = 0; + snew_i = 0; + for(chunk_i = 0; chunk_i < bodylen / chunklen + 1; chunk_i++){ + body_copy_len = smtp_strnlen_utf8(&s[body_i], chunklen); + if(body_copy_len == SIZE_MAX){ + free(snew); + return NULL; + } + memcpy(&snew[snew_i], &s[body_i], body_copy_len); + snew_i += body_copy_len; + if(s[body_i] == '\0'){ + snew_i += 1; + } + body_i += body_copy_len; + + if(endlen > 0){ + memcpy(&snew[snew_i], end, endlen); + } + snew_i += endlen; + } + + return snew; +} + +/** + * Read the entire contents of a file stream and store the data into a + * dynamically allocated buffer. + * + * @param[in] stream File stream already opened by the caller. + * @param[out] bytes_read Number of bytes stored in the return buffer. + * @retval char* A dynamically allocated buffer which contains the entire + * contents of @p stream. The caller must free this memory + * when done. + * @retval NULL Memory allocation or file read error. + */ +SMTP_LINKAGE char * +smtp_ffile_get_contents(FILE *stream, + size_t *bytes_read){ + char *read_buf; + size_t bufsz; + size_t bufsz_inc; + char *new_buf; + size_t bytes_read_loop; + const size_t BUFSZ_INCREMENT = 512; + + read_buf = NULL; + bufsz = 0; + + if(bytes_read){ + *bytes_read = 0; + } + + do{ + if(smtp_si_add_size_t(bufsz, BUFSZ_INCREMENT, &bufsz_inc) || + (new_buf = realloc(read_buf, bufsz_inc)) == NULL){ + free(read_buf); + return NULL; + } + read_buf = new_buf; + bufsz = bufsz_inc; + + bytes_read_loop = fread(&read_buf[bufsz - BUFSZ_INCREMENT], + sizeof(char), + BUFSZ_INCREMENT, + stream); + if(bytes_read){ + *bytes_read += bytes_read_loop; + } + if(ferror(stream)){ + free(read_buf); + return NULL; + } + } while(!feof(stream)); + + return read_buf; +} + +/** + * Read the entire contents of a file from a given path, and store the data + * into a dynamically allocated buffer. + * + * @param[in] filename Path of file to open and read from. + * @param[out] bytes_read Number of bytes stored in the return buffer. + * @retval char* A dynamically allocated buffer which has the contents of + * the file at @p filename. The caller must free this memory when + * done. + * @retval NULL Memory allocation or file read error. + */ +SMTP_LINKAGE char * +smtp_file_get_contents(const char *const filename, + size_t *bytes_read){ + FILE *fp; + char *read_buf; + + if((fp = fopen(filename, "rb")) == NULL){ + return NULL; + } + + read_buf = smtp_ffile_get_contents(fp, bytes_read); + + if(fclose(fp) == EOF){ + free(read_buf); + read_buf = NULL; + } + + return read_buf; +} + +/** + * Parse a server response line into the @ref smtp_command data structure. + * + * @param[in] line Server response string. + * @param[out] cmd Structure containing the server response data broken up + * into its separate components. + * @return See @ref smtp_result_code. + */ +SMTP_LINKAGE int +smtp_parse_cmd_line(char *const line, + struct smtp_command *const cmd){ + char *ep; + char code_str[4]; + size_t line_len; + unsigned long int ulcode; + + line_len = strlen(line); + if(line_len < 5){ + cmd->code = SMTP_INTERNAL_ERROR; + cmd->more = 0; + cmd->text = line; + return cmd->code; + } + + cmd->text = &line[4]; + + memcpy(code_str, line, 3); + code_str[3] = '\0'; + ulcode = strtoul(code_str, &ep, 10); + if(*ep != '\0' || ulcode > SMTP_BEGIN_MAIL){ + cmd->code = SMTP_INTERNAL_ERROR; + } + else{ + cmd->code = (enum smtp_result_code)ulcode; + } + + if(line[3] == '-'){ + cmd->more = 1; + } + else{ + cmd->more = 0; + } + return cmd->code; +} + +/** + * Prints communication between the client and server to stderr only if + * the debug flag has been set. + * + * @param[in] smtp SMTP client context. + * @param[in] prefix Print this prefix before the main debug line text. + * @param[in] str Debug text to print out. + */ +static void +smtp_puts_dbg(struct smtp *const smtp, + const char *const prefix, + const char *const str){ + char *sdup; + size_t i; + + if(smtp->flags & SMTP_DEBUG){ + if((sdup = smtp_strdup(str)) == NULL){ + return; + } + + /* Remove carriage return and newline when printing to stderr. */ + for(i = 0; sdup[i]; i++){ + if(sdup[i] == '\r' || sdup[i] == '\n'){ + sdup[i] = ' '; + } + } + + if(fprintf(stderr, "[smtp %s]: %s\n", prefix, sdup) < 0){ + /* Do not care if this fails. */ + } + free(sdup); + } +} + +/** + * Read a server response line. + * + * @param[in] smtp SMTP client context. + * @return See @ref str_getdelim_retcode. + */ +static enum str_getdelim_retcode +smtp_getline(struct smtp *const smtp){ + enum str_getdelim_retcode rc; + + errno = 0; + rc = smtp_str_getdelimfd(&smtp->gdfd); + if(errno == ENOMEM){ + smtp_status_code_set(smtp, SMTP_STATUS_NOMEM); + return rc; + } + else if(rc == STRING_GETDELIMFD_ERROR){ + smtp_status_code_set(smtp, SMTP_STATUS_RECV); + return STRING_GETDELIMFD_ERROR; + } + + if(smtp->gdfd.line_len > 0){ + /* Remove the carriage-return character ('\r'). */ + smtp->gdfd.line[smtp->gdfd.line_len - 1] = '\0'; + smtp_puts_dbg(smtp, "Server", smtp->gdfd.line); + } + return rc; +} + +/** + * Loop through all of the server response lines until the last line, and + * then return the status code from the last response line. + * + * @param[in] smtp SMTP client context. + * @return See @ref smtp_result_code. + */ +static int +smtp_read_and_parse_code(struct smtp *const smtp){ + struct smtp_command cmd; + enum str_getdelim_retcode rc; + + do{ + rc = smtp_getline(smtp); + if(rc == STRING_GETDELIMFD_ERROR){ + return SMTP_INTERNAL_ERROR; + } + + smtp_parse_cmd_line(smtp->gdfd.line, &cmd); + }while (rc != STRING_GETDELIMFD_DONE && cmd.more); + + return cmd.code; +} + +/** + * Send data to the SMTP server. + * + * Writes a buffer of length len into either the unencrypted TCP socket or + * the TLS encrypted socket, depending on the current underlying mode of + * the socket. + * + * @param[in] smtp SMTP client context. + * @param[in] buf Data to send to the SMTP server. + * @param[in] len Number of bytes in buf. + * @return See @ref smtp_status_code. + */ +SMTP_LINKAGE enum smtp_status_code +smtp_write(struct smtp *const smtp, + const char *const buf, + size_t len){ + size_t bytes_to_send; + long bytes_sent; + const char *buf_offset; + int ssl_bytes_to_send; + + smtp_puts_dbg(smtp, "Client", buf); + + bytes_to_send = len; + buf_offset = buf; + while(bytes_to_send){ + if(bytes_to_send > INT_MAX){ + return smtp_status_code_set(smtp, SMTP_STATUS_SEND); + } + + if(smtp->tls_on){ +#ifdef SMTP_OPENSSL + /* bytes_to_send <= INT_MAX */ + ssl_bytes_to_send = (int)bytes_to_send; + bytes_sent = SSL_write(smtp->tls, buf_offset, ssl_bytes_to_send); + if(bytes_sent <= 0){ + return smtp_status_code_set(smtp, SMTP_STATUS_SEND); + } +#else /* !(SMTP_OPENSSL) */ + /* unreachable */ + bytes_sent = 0; + (void)ssl_bytes_to_send; +#endif /* SMTP_OPENSSL */ + } + else{ + bytes_sent = send(smtp->sock, buf_offset, bytes_to_send, 0); + if(bytes_sent < 0){ + return smtp_status_code_set(smtp, SMTP_STATUS_SEND); + } + } + bytes_to_send -= (size_t)bytes_sent; + buf_offset += bytes_sent; + } + + return smtp->status_code; +} + +/** + * Send a null-terminated string to the SMTP server. + * + * @param[in] smtp SMTP client context. + * @param[in] s Null-terminated string to send to the SMTP server. + * @return See @ref smtp_status_code and @ref smtp_write. + */ +static enum smtp_status_code +smtp_puts(struct smtp *const smtp, + const char *const s){ + return smtp_write(smtp, s, strlen(s)); +} + +/** + * Same as @ref smtp_puts except this function also appends the line + * terminating carriage return and newline bytes at the end of the string. + * + * @param[in] smtp SMTP client context. + * @param[in] s Null-terminated string to send to the SMTP server. + * @return See @ref smtp_status_code and @ref smtp_puts. + */ +static enum smtp_status_code +smtp_puts_terminate(struct smtp *const smtp, + const char *const s){ + enum smtp_status_code rc; + char *line; + char *concat; + size_t slen; + size_t allocsz; + + slen = strlen(s); + if(smtp_si_add_size_t(slen, 3, &allocsz) || + (line = malloc(allocsz)) == NULL){ + return smtp_status_code_set(smtp, SMTP_STATUS_NOMEM); + } + concat = smtp_stpcpy(line, s); + smtp_stpcpy(concat, "\r\n"); + rc = smtp_puts(smtp, line); + free(line); + return rc; +} + +/** + * Connect to the server using a standard TCP socket. + * + * This function handles the server name lookup to get an IP address + * for the server, and then to connect to that IP using a normal TCP + * connection. + * + * @param[in] smtp SMTP client context. + * @param[in] server Mail server name or IP address. + * @param[in] port Mail server port number. + * @retval 0 Successfully connected to server. + * @retval -1 Failed to connect to server. + */ +static int +smtp_connect(struct smtp *const smtp, + const char *const server, + const char *const port){ + struct addrinfo hints; + struct addrinfo *res0; + struct addrinfo *res; + + /* + * Windows requires initializing the socket library before we call any + * socket functions. + */ +#ifdef SMTP_IS_WINDOWS + /* Windows global network socket data structure. */ + WSADATA wsa_data; + if(WSAStartup(MAKEWORD(2, 2), &wsa_data) != 0){ + return -1; + } +#endif /* SMTP_IS_WINDOWS */ + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + hints.ai_flags = 0; + hints.ai_protocol = IPPROTO_TCP; + + if(getaddrinfo(server, port, &hints, &res0) != 0){ + return -1; + } + + for(res = res0; res; res = res->ai_next){ + smtp->sock = socket(res->ai_family, res->ai_socktype, res->ai_protocol); + if(smtp->sock < 0){ + continue; + } + + if(connect(smtp->sock, res->ai_addr, res->ai_addrlen) < 0){ +#ifdef SMTP_IS_WINDOWS + closesocket(smtp->sock); +#else /* POSIX */ + close(smtp->sock); +#endif /* SMTP_IS_WINDOWS */ + smtp->sock = -1; + } + else{ + break; + } + } + + freeaddrinfo(res0); + if(smtp->sock < 0){ + return -1; + } + + return 0; +} + +#ifdef SMTP_OPENSSL +/** + * Initialize the TLS library and establish a TLS handshake with the server + * over the existing socket connection. + * + * @param[in] smtp SMTP client context. + * @param[in] server Server name or IP address. + * @retval 0 Successfully established a TLS connection with the server. + * @retval -1 Failed to establish a TLS connection with the server. + */ +static int +smtp_tls_init(struct smtp *const smtp, + const char *const server){ + X509 *X509_cert_peer; + + /* Do not need to check the return value since this always returns 1. */ + SSL_library_init(); + + SSL_load_error_strings(); + /*ERR_load_BIO_strings();*/ + OpenSSL_add_all_algorithms(); + + if((smtp->tls_ctx = SSL_CTX_new(SSLv23_client_method())) == NULL){ + return -1; + } + + /* Disable SSLv2, SSLv3, and TLSv1.0. */ + SSL_CTX_set_options(smtp->tls_ctx, + SSL_OP_NO_SSLv2 | + SSL_OP_NO_SSLv3 | + SSL_OP_NO_TLSv1); + + SSL_CTX_set_mode(smtp->tls_ctx, SSL_MODE_AUTO_RETRY); + if((smtp->flags & SMTP_NO_CERT_VERIFY) == 0){ + SSL_CTX_set_verify(smtp->tls_ctx, SSL_VERIFY_PEER, NULL); + } + + /* + * Set the path to the user-provided CA file or use the default cert paths + * if not provided. + */ + if(smtp->cafile){ + if(SSL_CTX_load_verify_locations(smtp->tls_ctx, smtp->cafile, NULL) != 1){ + SSL_CTX_free(smtp->tls_ctx); + return -1; + } + } + else{ + X509_STORE_set_default_paths(SSL_CTX_get_cert_store(smtp->tls_ctx)); + if(ERR_peek_error() != 0){ + SSL_CTX_free(smtp->tls_ctx); + return -1; + } + } + + if((smtp->tls = SSL_new(smtp->tls_ctx)) == NULL){ + SSL_CTX_free(smtp->tls_ctx); + return -1; + } + + if((smtp->tls_bio = BIO_new_socket(smtp->sock, 0)) == NULL){ + SSL_CTX_free(smtp->tls_ctx); + SSL_free(smtp->tls); + return -1; + } + + SSL_set_bio(smtp->tls, smtp->tls_bio, smtp->tls_bio); + SSL_set_connect_state(smtp->tls); + if(SSL_connect(smtp->tls) != 1){ + SSL_CTX_free(smtp->tls_ctx); + SSL_free(smtp->tls); + return -1; + } + + if(SSL_do_handshake(smtp->tls) != 1){ + SSL_CTX_free(smtp->tls_ctx); + SSL_free(smtp->tls); + return -1; + } + + /* Verify matching subject in certificate. */ + if((smtp->flags & SMTP_NO_CERT_VERIFY) == 0){ + if((X509_cert_peer = SSL_get_peer_certificate(smtp->tls)) == NULL){ + SSL_CTX_free(smtp->tls_ctx); + SSL_free(smtp->tls); + return -1; + } + if(X509_check_host(X509_cert_peer, server, 0, 0, NULL) != 1){ + SSL_CTX_free(smtp->tls_ctx); + SSL_free(smtp->tls); + return -1; + } + X509_free(X509_cert_peer); + } + + smtp->tls_on = 1; + return 0; +} +#endif /* SMTP_OPENSSL */ + +/** + * Send the EHLO command and parse through the responses. + * + * Ignores all of the server extensions that get returned. If a server + * doesn't support an extension we need, then we should receive an error + * later on when we try to use that extension. + * + * @param[in] smtp SMTP client context. + * @return See @ref smtp_status_code. + */ +static enum smtp_status_code +smtp_ehlo(struct smtp *const smtp){ + if(smtp_puts(smtp, "EHLO smtp\r\n") == SMTP_STATUS_OK){ + smtp_read_and_parse_code(smtp); + } + return smtp->status_code; +} + +/** + * Authenticate using the PLAIN method. + * + * 1. Set the text to the following format: "\0\0", + * or as shown in the format string: "\0%s\0%s", email, password. + * 2. Base64 encode the text from (1). + * 3. Send the constructed auth text from (2) to the server: + * "AUTH PLAIN ". + * + * @param[in] smtp SMTP client context. + * @param[in] user SMTP account user name. + * @param[in] pass SMTP account password. + * @retval 0 Successfully authenticated. + * @retval -1 Failed to authenticate. + */ +static int +smtp_auth_plain(struct smtp *const smtp, + const char *const user, + const char *const pass){ + size_t user_len; + size_t pass_len; + char *login_str; + size_t login_len; + char *login_b64; + size_t login_b64_len; + char *login_send; + char *concat; + + /* (1) */ + user_len = strlen(user); + pass_len = strlen(pass); + /* login_len = 1 + user_len + 1 + pass_len */ + if(smtp_si_add_size_t(user_len, pass_len, &login_len) || + smtp_si_add_size_t(login_len, 2, &login_len) || + (login_str = malloc(login_len)) == NULL){ + return -1; + } + login_str[0] = '\0'; + memcpy(&login_str[1], user, user_len); + login_str[1 + user_len] = '\0'; + memcpy(&login_str[1 + user_len + 1], pass, pass_len); + + /* (2) */ + login_b64 = smtp_base64_encode(login_str, login_len); + free(login_str); + if(login_b64 == NULL){ + return -1; + } + + /* (3) */ + login_b64_len = strlen(login_b64); + if(smtp_si_add_size_t(login_b64_len, 14, &login_b64_len) || + (login_send = malloc(login_b64_len)) == NULL){ + free(login_b64); + return -1; + } + concat = smtp_stpcpy(login_send, "AUTH PLAIN "); + concat = smtp_stpcpy(concat, login_b64); + smtp_stpcpy(concat, "\r\n"); + + free(login_b64); + smtp_puts(smtp, login_send); + free(login_send); + if(smtp->status_code != SMTP_STATUS_OK){ + return -1; + } + + if(smtp_read_and_parse_code(smtp) != SMTP_AUTH_SUCCESS){ + return -1; + } + return 0; +} + +/** + * Authenticate using the LOGIN method. + * + * 1. Base64 encode the user name. + * 2. Send string from (1) as part of the login: + * "AUTH LOGIN ". + * 3. Base64 encode the password and send that by itself on a separate + * line: "". + * + * @param[in] smtp SMTP client context. + * @param[in] user SMTP account user name. + * @param[in] pass SMTP account password. + * @retval 0 Successfully authenticated. + * @retval -1 Failed to authenticate. + */ +static int +smtp_auth_login(struct smtp *const smtp, + const char *const user, + const char *const pass){ + char *b64_user; + char *b64_pass; + size_t b64_user_len; + char *login_str; + char *concat; + + /* (1) */ + if((b64_user = smtp_base64_encode(user, SIZE_MAX)) == NULL){ + return -1; + } + + /* (2) */ + b64_user_len = strlen(b64_user); + if(smtp_si_add_size_t(b64_user_len, 14, &b64_user_len) || + (login_str = malloc(b64_user_len)) == NULL){ + free(b64_user); + return -1; + } + concat = smtp_stpcpy(login_str, "AUTH LOGIN "); + concat = smtp_stpcpy(concat, b64_user); + smtp_stpcpy(concat, "\r\n"); + + free(b64_user); + smtp_puts(smtp, login_str); + free(login_str); + if(smtp->status_code != SMTP_STATUS_OK){ + return -1; + } + + if(smtp_read_and_parse_code(smtp) != SMTP_AUTH_CONTINUE){ + return -1; + } + + /* (3) */ + if((b64_pass = smtp_base64_encode(pass, SIZE_MAX)) == NULL){ + return -1; + } + smtp_puts_terminate(smtp, b64_pass); + free(b64_pass); + if(smtp->status_code != SMTP_STATUS_OK){ + return -1; + } + + if(smtp_read_and_parse_code(smtp) != SMTP_AUTH_SUCCESS){ + return -1; + } + return 0; +} + +#ifdef SMTP_OPENSSL +/** + * Authenticate using the CRAM-MD5 method. + * + * 1. Send "AUTH CRAM-MD5" to the server. + * 2. Decode the base64 challenge response from the server. + * 3. Do an MD5 HMAC on (2) using the account password as the key. + * 4. Convert the binary data in (3) to lowercase hex characters. + * 5. Construct the string: " <(4)>". + * 6. Encode (5) into base64 format. + * 7. Send the final string from (6) to the server and check the response. + * + * @param[in] smtp SMTP client context. + * @param[in] user SMTP account user name. + * @param[in] pass SMTP account password. + * @retval 0 Successfully authenticated. + * @retval -1 Failed to authenticate. + */ +static int +smtp_auth_cram_md5(struct smtp *const smtp, + const char *const user, + const char *const pass){ + struct smtp_command cmd; + unsigned char *challenge_decoded; + size_t challenge_decoded_len; + const char *key; + int key_len; + unsigned char hmac[EVP_MAX_MD_SIZE]; + unsigned int hmac_len; + unsigned char *hmac_ret; + char *challenge_hex; + size_t user_len; + size_t challenge_hex_len; + char *auth_concat; + char *concat; + size_t auth_concat_len; + char *b64_auth; + + /* (1) */ + if(smtp_puts(smtp, "AUTH CRAM-MD5\r\n") != SMTP_STATUS_OK){ + return -1; + } + if(smtp_getline(smtp) == STRING_GETDELIMFD_ERROR){ + return -1; + } + if(smtp_parse_cmd_line(smtp->gdfd.line, &cmd) != SMTP_AUTH_CONTINUE){ + return -1; + } + + /* (2) */ + challenge_decoded_len = smtp_base64_decode(cmd.text, + &challenge_decoded); + if(challenge_decoded_len == SIZE_MAX){ + return -1; + } + + /* (3) */ + key = pass; + key_len = (int)strlen(pass);/*********************cast*/ + hmac_ret = HMAC(EVP_md5(), + key, + key_len, + challenge_decoded, + challenge_decoded_len, + hmac, + &hmac_len); + free(challenge_decoded); + if(hmac_ret == NULL){ + return -1; + } + + /* (4) */ + challenge_hex = smtp_bin2hex(hmac, hmac_len); + if(challenge_hex == NULL){ + return -1; + } + + /* (5) */ + user_len = strlen(user); + challenge_hex_len = strlen(challenge_hex); + /* auth_concat_len = user_len + 1 + challenge_hex_len + 1 */ + if(smtp_si_add_size_t(user_len, challenge_hex_len, &auth_concat_len) || + smtp_si_add_size_t(auth_concat_len, 2, &auth_concat_len) || + (auth_concat = malloc(auth_concat_len)) == NULL){ + free(challenge_hex); + return -1; + } + concat = smtp_stpcpy(auth_concat, user); + concat = smtp_stpcpy(concat, " "); + smtp_stpcpy(concat, challenge_hex); + free(challenge_hex); + + /* (6) */ + b64_auth = smtp_base64_encode(auth_concat, auth_concat_len - 1); + free(auth_concat); + if(b64_auth == NULL){ + return -1; + } + + /* (7) */ + smtp_puts_terminate(smtp, b64_auth); + free(b64_auth); + if(smtp->status_code != SMTP_STATUS_OK){ + return -1; + } + + if(smtp_read_and_parse_code(smtp) != SMTP_AUTH_SUCCESS){ + return -1; + } + return 0; +} +#endif /* SMTP_OPENSSL */ + +/** + * Set the timeout for the next socket read operation. + * + * @param[in] smtp SMTP client context. + * @param[in] seconds Timeout in seconds. + */ +static void +smtp_set_read_timeout(struct smtp *const smtp, + long seconds){ + smtp->timeout_sec = seconds; +} + +/** + * Perform a handshake with the SMTP server which includes optionally + * setting up TLS and sending the EHLO greeting. + * + * At this point, the client has already connected to the SMTP server + * through its socket connection. In this function, the client will: + * 1. Optionally convert the connection to TLS (SMTP_SECURITY_TLS). + * 2. Read the initial server greeting. + * 3. Send an EHLO to the server. + * 4. Optionally initiate STARTTLS and resend the EHLO + * (SMTP_SECURITY_STARTTLS). + * + * @param[in] smtp SMTP client context. + * @param[in] server Server name or IP address. + * @param[in] connection_security See @ref smtp_connection_security. + * @return See @ref smtp_status_code. + */ +static enum smtp_status_code +smtp_initiate_handshake(struct smtp *const smtp, + const char *const server, + enum smtp_connection_security connection_security){ + /* Eliminate unused warnings if not using SMTP_OPENSSL. */ + (void)server; + (void)connection_security; + + /* (1) */ +#ifdef SMTP_OPENSSL + if(connection_security == SMTP_SECURITY_TLS){ + if(smtp_tls_init(smtp, server) < 0){ + return smtp_status_code_set(smtp, SMTP_STATUS_HANDSHAKE); + } + } +#endif /* SMTP_OPENSSL */ + + /* (2) */ + /* Get initial 220 message - 5 minute timeout. */ + smtp_set_read_timeout(smtp, 60 * 5); + if(smtp_getline(smtp) == STRING_GETDELIMFD_ERROR){ + return smtp->status_code; + } + + /* (3) */ + if(smtp_ehlo(smtp) != SMTP_STATUS_OK){ + return smtp->status_code; + } + + /* (4) */ +#ifdef SMTP_OPENSSL + if(connection_security == SMTP_SECURITY_STARTTLS){ + if(smtp_puts(smtp, "STARTTLS\r\n") != SMTP_STATUS_OK){ + return smtp->status_code; + } + if(smtp_read_and_parse_code(smtp) != SMTP_READY){ + return smtp_status_code_set(smtp, SMTP_STATUS_HANDSHAKE); + } + if(smtp_tls_init(smtp, server) < 0){ + return smtp_status_code_set(smtp, SMTP_STATUS_HANDSHAKE); + } + if(smtp_ehlo(smtp) != SMTP_STATUS_OK){ + return smtp->status_code; + } + } +#endif /* SMTP_OPENSSL */ + + return smtp->status_code; +} + +/** + Maximum size of an RFC 2822 date string. + + @verbatim + Thu, 21 May 1998 05:33:29 -0700 + 12345678901234567890123456789012 + 10 20 30 32 (bytes) + @endverbatim + + Add more bytes to the 32 maximum size to silence compiler warning on the + computed UTF offset. + */ +#define SMTP_DATE_MAX_SZ (32 + 15) + +/** + * Convert the time into an RFC 2822 formatted string. + * + * Example date format: + * Thu, 21 May 1998 05:33:29 -0700 + * + * @param[out] date Buffer that has at least SMTP_DATE_MAX_SZ bytes. + * @retval 0 Successfully copied the current date to the buffer. + * @retval -1 Failed to establish the current date or an output + * format error occurred. + */ +SMTP_LINKAGE int +smtp_date_rfc_2822(char *const date){ + time_t t; + time_t t_local; + time_t t_utc; + struct tm tm_local; + struct tm tm_utc; + long offset_utc; + double diff_local_utc; + int rc; + + const char weekday_abbreviation[7][4] = { + "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" + }; + + const char month_abbreviation[12][4] = { + "Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" + }; + + if((t = time(NULL)) == (time_t)(-1)){ + return -1; + } + +#ifdef SMTP_IS_WINDOWS + if(localtime_s(&tm_local, &t) || + gmtime_s(&tm_utc, &t)){ + return -1; + } +#else /* POSIX */ + /* Not defined if system does not have localtime_r or gmtime_r. */ +# ifdef SMTP_TIME_NO_REENTRANT + struct tm *tm; + + /* localtime() not thread-safe. */ + if((tm = localtime(&t)) == NULL){ + return -1; + } + memcpy(&tm_local, tm, sizeof(tm_local)); + + /* gmtime() not thread-safe. */ + if((tm = gmtime(&t)) == NULL){ + return -1; + } + memcpy(&tm_utc, tm, sizeof(tm_utc)); +# else /* Reentrant versions: localtime_r() and gmtime_r(). */ + if(localtime_r(&t, &tm_local) == NULL || + gmtime_r(&t, &tm_utc) == NULL){ + return -1; + } +# endif /* SMTP_TIME_NO_REENTRANT */ +#endif /* SMTP_IS_WINDOWS */ + + if((t_local = mktime(&tm_local)) == (time_t)(-1)){ + return -1; + } + + if((t_utc = mktime(&tm_utc)) == (time_t)(-1)){ + return -1; + } + + /* + * After computing the offset, it will contain a maximum of 4 digits. + * For example, PST time zone will have an offset of -800 which will get + * formatted as -0800 in the sprintf call below. + */ + diff_local_utc = difftime(t_local, t_utc); + offset_utc = (long)diff_local_utc; + offset_utc = offset_utc / 60 / 60 * 100; + + rc = sprintf(date, + "%s, %02d %s %d %02d:%02d:%02d %0+5ld", + weekday_abbreviation[tm_local.tm_wday], + tm_local.tm_mday, + month_abbreviation[tm_local.tm_mon], + tm_local.tm_year + 1900, + tm_local.tm_hour, + tm_local.tm_min, + tm_local.tm_sec, /* 0 - 60 (leap second) */ + offset_utc); + + if(rc + 1 != SMTP_DATE_MAX_SZ - 15){ /* See @ref SMTP_DATE_MAX_SZ for -5. */ + return -1; + } + + return 0; +} + +/** + * Search function used by bsearch, allowing the caller to check for + * headers with existing keys. + * + * @param v1 String to search for in the list. + * @param v2 The @ref smtp_header to compare. + * @retval 0 If the keys match. + * @retval !0 If the keys do not match. + */ +static int +smtp_header_cmp_key(const void *const v1, + const void *const v2){ + const char *key; + const struct smtp_header *header2; + + key = v1; + header2 = v2; + return strcmp(key, header2->key); +} + +/** + * Determine if the header key has already been defined in this context. + * + * @param[in] smtp SMTP client context. + * @param[in] key Header key value to search for. + * @retval 1 If the header already exists in this context. + * @retval 0 If the header does not exist in this context. + */ +static int +smtp_header_exists(const struct smtp *const smtp, + const char *const key){ + if(bsearch(key, + smtp->header_list, + smtp->num_headers, + sizeof(*smtp->header_list), + smtp_header_cmp_key) == NULL){ + return 0; + } + return 1; +} + +/** + * Minimum length of buffer required to hold the MIME boundary test: + * mimeXXXXXXXXXX + * 123456789012345 + * 1 10 15 bytes + */ +#define SMTP_MIME_BOUNDARY_LEN 15 + +/** + * Generate the MIME boundary text field and store it in a user-supplied + * buffer. + * + * For example: + * mimeXXXXXXXXXX + * where each X gets set to a pseudo-random uppercase ASCII character. + * + * This uses a simple pseudo-random number generator since we only care + * about preventing accidental boundary collisions. + * + * @param[out] boundary Buffer that has at least SMTP_MIME_BOUNDARY_LEN bytes. + */ +static void +smtp_gen_mime_boundary(char *const boundary){ + size_t i; + unsigned int seed; + + seed = (unsigned int)time(NULL); + srand(seed); + + strcpy(boundary, "mime"); + for(i = 4; i < SMTP_MIME_BOUNDARY_LEN - 1; i++){ + /* Modulo bias okay since we only need to prevent accidental collision. */ + boundary[i] = rand() % 26 + 'A'; + } + boundary[SMTP_MIME_BOUNDARY_LEN - 1] = '\0'; +} + +/** + * Print the MIME header and the MIME section containing the email body. + * + * @param[in] smtp SMTP client context. + * @param[in] boundary MIME boundary text. + * @param[in] body_dd Email body with double dots added at the + * beginning of each line. + * @return See @ref smtp_status_code. + */ +static enum smtp_status_code +smtp_print_mime_header_and_body(struct smtp *const smtp, + const char *const boundary, + const char *const body_dd){ + /* Buffer size for the static MIME text used below. */ + const size_t MIME_TEXT_BUFSZ = 1000; + size_t data_double_dot_len; + char *data_header_and_body; + char *concat; + + data_double_dot_len = strlen(body_dd); + if(smtp_si_add_size_t(data_double_dot_len, + MIME_TEXT_BUFSZ, + &data_double_dot_len) || + (data_header_and_body = malloc(data_double_dot_len)) == NULL){ + return smtp_status_code_set(smtp, SMTP_STATUS_NOMEM); + } + + concat = smtp_stpcpy(data_header_and_body, + "MIME-Version: 1.0\r\n" + "Content-Type: multipart/mixed; boundary="); + concat = smtp_stpcpy(concat, + boundary); + concat = smtp_stpcpy(concat, + "\r\n" + "\r\n" + "Multipart MIME message.\r\n" + "--"); + concat = smtp_stpcpy(concat, + boundary); + concat = smtp_stpcpy(concat, + "\r\n" + "Content-Type: text/plain; charset=\"UTF-8\"\r\n" + "\r\n"); + concat = smtp_stpcpy(concat, + body_dd); + smtp_stpcpy(concat, + "\r\n" + "\r\n"); + + smtp_puts(smtp, data_header_and_body); + free(data_header_and_body); + return smtp->status_code; +} + +/** + * Print a MIME section containing an attachment. + * + * @param[in] smtp SMTP client context. + * @param[in] boundary MIME boundary text. + * @param[in] attachment See @ref smtp_attachment. + * @return See @ref smtp_status_code. + */ +static enum smtp_status_code +smtp_print_mime_attachment(struct smtp *const smtp, + const char *const boundary, + const struct smtp_attachment *const attachment){ + /* Buffer size for the static MIME text used below. */ + const size_t MIME_TEXT_BUFSZ = 1000; + size_t name_len; + size_t b64_data_len; + size_t bufsz; + char *mime_attach_text; + char *concat; + + name_len = strlen(attachment->name); + b64_data_len = strlen(attachment->b64_data); + /* + * bufsz = SMTP_MIME_BOUNDARY_LEN + name_len + b64_data_len + MIME_TEXT_BUFSZ + */ + if(smtp_si_add_size_t(name_len, b64_data_len, &bufsz) || + smtp_si_add_size_t(bufsz, + SMTP_MIME_BOUNDARY_LEN + MIME_TEXT_BUFSZ, + &bufsz) || + (mime_attach_text = malloc(bufsz)) == NULL){ + return smtp_status_code_set(smtp, SMTP_STATUS_NOMEM); + } + + concat = smtp_stpcpy(mime_attach_text, + "--"); + concat = smtp_stpcpy(concat, + boundary); + concat = smtp_stpcpy(concat, + "\r\n" + "Content-Type: application/octet-stream\r\n" + "Content-Disposition: attachment; filename=\""); + concat = smtp_stpcpy(concat, + attachment->name); + concat = smtp_stpcpy(concat, + "\"\r\n" + "Content-Transfer-Encoding: base64\r\n" + "\r\n"); + concat = smtp_stpcpy(concat, + attachment->b64_data); + smtp_stpcpy(concat, + "\r\n" + "\r\n"); + smtp_puts(smtp, mime_attach_text); + free(mime_attach_text); + return smtp->status_code; +} + +/** + * Prints double hyphen on both sides of the MIME boundary which indicates + * the end of the MIME sections. + * + * @param[in] smtp SMTP client context. + * @param[in] boundary MIME boundary text. + * @return See @ref smtp_status_code and @ref smtp_puts. + */ +static enum smtp_status_code +smtp_print_mime_end(struct smtp *const smtp, + const char *const boundary){ + char *concat; + char mime_end[2 + SMTP_MIME_BOUNDARY_LEN + 4 + 1]; + + concat = smtp_stpcpy(mime_end, "--"); + concat = smtp_stpcpy(concat, boundary); + smtp_stpcpy(concat, "--\r\n"); + return smtp_puts(smtp, mime_end); +} + +/** + * Send the main email body to the SMTP server. + * + * This includes the MIME sections for the email body and attachments. + * + * @param[in] smtp SMTP client context. + * @param[in] body_dd Email body with double dots added at the + * beginning of each line. + * @return See @ref smtp_status_code. + */ +static enum smtp_status_code +smtp_print_mime_email(struct smtp *const smtp, + const char *const body_dd){ + char boundary[SMTP_MIME_BOUNDARY_LEN]; + size_t i; + struct smtp_attachment *attachment; + + smtp_gen_mime_boundary(boundary); + + if(smtp_print_mime_header_and_body(smtp, boundary, body_dd) != SMTP_STATUS_OK){ + return smtp->status_code; + } + + for(i = 0; i < smtp->num_attachment; i++){ + attachment = &smtp->attachment_list[i]; + if(smtp_print_mime_attachment(smtp, + boundary, + attachment) != SMTP_STATUS_OK){ + return smtp->status_code; + } + } + + return smtp_print_mime_end(smtp, boundary); +} + +/** + * Print the email data provided by the user without MIME formatting. + * + * @param[in,out] smtp SMTP client context. + * @param[in] body_dd Email body with double dots added at the + * beginning of each line. + * @return See @ref smtp_status_code. + */ +static enum smtp_status_code +smtp_print_nomime_email(struct smtp *const smtp, + const char *const body_dd){ + return smtp_puts_terminate(smtp, body_dd); +} + +/** + * Send the email body to the mail server. + * + * @param[in,out] smtp SMTP client context. + * @param[in] body Email body text. + * @return See @ref smtp_status_code. + */ +static enum smtp_status_code +smtp_print_email(struct smtp *const smtp, + const char *const body){ + char *body_double_dot; + + /* + * Insert an extra dot for each line that begins with a dot. This will + * prevent data in the body parameter from prematurely ending the DATA + * segment. + */ + if((body_double_dot = smtp_str_replace("\n.", "\n..", body)) == NULL){ + return smtp_status_code_set(smtp, SMTP_STATUS_NOMEM); + } + + if(smtp_header_exists(smtp, "Content-Type")){ + smtp_print_nomime_email(smtp, body_double_dot); + } + else{ + smtp_print_mime_email(smtp, body_double_dot); + } + free(body_double_dot); + return smtp->status_code; +} + +/** + * Convert a header into an RFC 5322 formatted string and send it to the + * SMTP server. + * + * This will adding proper line wrapping and indentation for long + * header lines. + * + * @param[in] smtp SMTP client context. + * @param[in] header See @ref smtp_header. + * @return See @ref smtp_status_code. + */ +static enum smtp_status_code +smtp_print_header(struct smtp *const smtp, + const struct smtp_header *const header){ + size_t key_len; + size_t value_len; + size_t concat_len; + char *header_concat; + char *concat; + char *header_fmt; + + if(header->value == NULL){ + return smtp->status_code; + } + + key_len = strlen(header->key); + value_len = strlen(header->value); + + /* concat_len = key_len + 2 + value_len + 1 */ + if(smtp_si_add_size_t(key_len, value_len, &concat_len) || + smtp_si_add_size_t(concat_len, 3, &concat_len) || + (header_concat = malloc(concat_len)) == NULL){ + return smtp_status_code_set(smtp, SMTP_STATUS_NOMEM); + } + concat = smtp_stpcpy(header_concat, header->key); + concat = smtp_stpcpy(concat, ": "); + smtp_stpcpy(concat, header->value); + + header_fmt = smtp_fold_whitespace(header_concat, SMTP_LINE_MAX); + free(header_concat); + if(header_fmt == NULL){ + return smtp_status_code_set(smtp, SMTP_STATUS_NOMEM); + } + + smtp_puts_terminate(smtp, header_fmt); + free(header_fmt); + return smtp->status_code; +} + +/** + * Take a FROM, TO, and CC address and add it into the email header list. + * + * The following example shows what the final header might look like when + * the client sends an email to two CC addresses: + * Cc: mail1\@example.com, mail2\@example.com + * + * @param[in] smtp SMTP client context. + * @param[in] address_type See @ref smtp_address_type. + * @param[in] key Header key value, for example, To From Cc. + * @return See @ref smtp_status_code. + */ +static enum smtp_status_code +smtp_append_address_to_header(struct smtp *const smtp, + enum smtp_address_type address_type, + const char *const key){ + size_t i; + size_t num_address_in_header; + size_t header_value_sz; + size_t name_slen; + size_t email_slen; + size_t concat_len; + struct smtp_address *address; + char *header_value; + char *header_value_new; + char *concat; + + num_address_in_header = 0; + header_value_sz = 0; + header_value = NULL; + concat_len = 0; + for(i = 0; i < smtp->num_address; i++){ + address = &smtp->address_list[i]; + if(address->type == address_type){ + name_slen = 0; + if(address->name){ + name_slen = strlen(address->name); + } + email_slen = strlen(address->email); + + /* + * ', "' NAME '" <' EMAIL > \0 + * header_value_sz += 3 + name_slen + 3 + email_slen + 1 + 1 + */ + if(smtp_si_add_size_t(header_value_sz, name_slen, &header_value_sz) || + smtp_si_add_size_t(header_value_sz, email_slen, &header_value_sz) || + smtp_si_add_size_t(header_value_sz, 3 + 3 + 1 + 1, &header_value_sz)|| + (header_value_new = realloc(header_value, + header_value_sz)) == NULL){ + free(header_value); + return smtp_status_code_set(smtp, SMTP_STATUS_NOMEM); + } + header_value = header_value_new; + concat = header_value + concat_len; + if(num_address_in_header > 0){ + concat = smtp_stpcpy(concat, ", "); + } + + if(name_slen){ + concat = smtp_stpcpy(concat, "\""); + concat = smtp_stpcpy(concat, address->name); + concat = smtp_stpcpy(concat, "\" "); + } + concat = smtp_stpcpy(concat, "<"); + concat = smtp_stpcpy(concat, address->email); + concat = smtp_stpcpy(concat, ">"); + num_address_in_header += 1; + concat_len = (size_t)(concat - header_value); + } + } + + if(header_value){ + smtp_header_add(smtp, key, header_value); + free(header_value); + } + return smtp->status_code; +} + +/** + * Send envelope MAIL FROM or RCPT TO header address. + * + * Examples: + * MAIL FROM: + * RCPT TO: + * + * @param[in] smtp SMTP client context. + * @param[in] header Either "MAIL FROM" or "RCPT TO". + * @param[in] address See @ref smtp_address -> email field. + * @return See @ref smtp_status_code. + */ +static enum smtp_status_code +smtp_mail_envelope_header(struct smtp *const smtp, + const char *const header, + const struct smtp_address *const address){ + const char *const SMTPUTF8 = " SMTPUTF8"; + const size_t SMTPUTF8_LEN = strlen(SMTPUTF8); + size_t email_len; + size_t bufsz; + char *envelope_address; + char *concat; + const char *smtputf8_opt; + + email_len = strlen(address->email); + + /* bufsz = 14 + email_len + SMTPUTF8_LEN + 1 */ + if(smtp_si_add_size_t(email_len, SMTPUTF8_LEN + 14 + 1, &bufsz) || + (envelope_address = malloc(bufsz)) == NULL){ + return smtp_status_code_set(smtp, SMTP_STATUS_NOMEM); + } + + smtputf8_opt = ""; + if(smtp_str_has_nonascii_utf8(address->email)){ + smtputf8_opt = SMTPUTF8; + } + + concat = smtp_stpcpy(envelope_address, header); + concat = smtp_stpcpy(concat, ":<"); + concat = smtp_stpcpy(concat, address->email); + concat = smtp_stpcpy(concat, ">"); + concat = smtp_stpcpy(concat, smtputf8_opt); + smtp_stpcpy(concat, "\r\n"); + smtp_puts(smtp, envelope_address); + free(envelope_address); + + if(smtp->status_code != SMTP_STATUS_OK){ + return smtp->status_code; + } + smtp_read_and_parse_code(smtp); + return smtp->status_code; +} + +/** + * Comparison function for qsort which sorts headers alphabetically based + * on the key. + * + * @param[in] v1 The first @ref smtp_header to compare. + * @param[in] v2 The second @ref smtp_header to compare. + * @retval 0 If the keys match. + * @retval !0 If the keys do not match. + */ +static int +smtp_header_cmp(const void *v1, + const void *v2){ + const struct smtp_header *header1; + const struct smtp_header *header2; + + header1 = v1; + header2 = v2; + return strcmp(header1->key, header2->key); +} + +/** + * Validate characters in the email header key. + * + * Must consist only of printable US-ASCII characters except colon. + * + * @param[in] key Header key to validate. + * @retval 0 Successful validation. + * @retval -1 Failed to validate. + */ +SMTP_LINKAGE int +smtp_header_key_validate(const char *const key){ + unsigned char uc; + size_t i; + size_t keylen; + + keylen = strlen(key); + if(keylen < 1){ + return -1; + } + + for(i = 0; i < keylen; i++){ + uc = (unsigned char)key[i]; + if(uc <= ' ' || uc > 126 || uc == ':'){ + return -1; + } + } + + return 0; +} + +/** + * Validate characters in the email header contents. + * + * Must consist only of printable character, space, or horizontal tab. + * + * @param[in] value Header value to validate. + * @retval 0 Successful validation. + * @retval -1 Failed to validate. + */ +SMTP_LINKAGE int +smtp_header_value_validate(const char *const value){ + size_t i; + unsigned char uc; + + for(i = 0; value[i]; i++){ + uc = (unsigned char)value[i]; + if((uc < ' ' || uc > 126) && + uc != '\t' && + uc < 0x80){ /* Allow UTF-8 byte sequence. */ + return -1; + } + } + return 0; +} + +/** + * Validate characters in the email address. + * + * The email address must consist only of printable characters excluding + * the angle brackets (<) and (>). + * + * @param[in] email The email address of the party. + * @retval 0 Successful validation. + * @retval -1 Failed to validate. + */ +SMTP_LINKAGE int +smtp_address_validate_email(const char *const email){ + size_t i; + unsigned char uc; + + for(i = 0; email[i]; i++){ + uc = (unsigned char)email[i]; + if(uc <= ' ' || uc == 127 || + uc == '<' || uc == '>'){ + return -1; + } + } + return 0; +} + +/** + * Validate characters in the email name. + * + * Email user name must consist only of printable characters, excluding the + * double quote character. + * + * @param[in] name Email name to validate. + * @retval 0 Successful validation. + * @retval -1 Failed to validate. + */ +SMTP_LINKAGE int +smtp_address_validate_name(const char *const name){ + size_t i; + unsigned char uc; + + for(i = 0; name[i]; i++){ + uc = (unsigned char)name[i]; + if(uc < ' ' || uc == 127 || uc == '\"'){ + return -1; + } + } + return 0; +} + +/** + * Validate characters in the attachment file name. + * + * Must consist only of printable characters or the space character ( ), and + * excluding the quote characters (') and ("). + * + * @param[in] name Filename of the attachment shown to recipients. + * @retval 0 Successful validation. + * @retval -1 Failed to validate. + */ +SMTP_LINKAGE int +smtp_attachment_validate_name(const char *const name){ + size_t i; + unsigned char uc; + + for(i = 0; name[i]; i++){ + uc = (unsigned char)name[i]; + if(uc < ' ' || uc == 127 || + uc == '\'' || uc == '\"'){ + return -1; + } + } + return 0; +} + +/** + * Special flag value for the SMTP context used to determine if the initial + * memory allocation failed to create the context. + */ +#define SMTP_FLAG_INVALID_MEMORY (enum smtp_flag)(0xFFFFFFFF) + +/** + * This error structure used for the single error case where we cannot + * initially allocate memory. This makes it easier to propagate any + * error codes when calling the other header functions because the + * caller will always get a valid SMTP structure returned. + */ +static struct smtp +g_smtp_error = { + SMTP_FLAG_INVALID_MEMORY, /* flags */ + 0, /* sock */ + { /* gdfd */ + NULL, /* _buf */ + 0, /* _bufsz */ + 0, /* _buf_len */ + NULL, /* line */ + 0, /* line_len */ + NULL, /* getdelimfd_read */ + NULL, /* user_data */ + 0, /* delim */ + {0} /* pad */ + }, /* gdfd */ + NULL, /* header_list */ + 0, /* num_headers */ + NULL, /* address_list */ + 0, /* num_address */ + NULL, /* attachment_list */ + 0, /* num_attachment */ + 0, /* timeout_sec */ + SMTP_STATUS_NOMEM, /* smtp_status_code status_code */ + 0, /* tls_on */ + NULL /* cafile */ +#ifdef SMTP_OPENSSL + , + NULL, /* tls */ + NULL, /* tls_ctx */ + NULL /* tls_bio */ +#endif /* SMTP_OPENSSL */ +}; + +enum smtp_status_code +smtp_open(const char *const server, + const char *const port, + enum smtp_connection_security connection_security, + enum smtp_flag flags, + const char *const cafile, + struct smtp **smtp){ + struct smtp *snew; + + if((snew = calloc(1, sizeof(**smtp))) == NULL){ + *smtp = &g_smtp_error; + return smtp_status_code_get(*smtp); + } + *smtp = snew; + + snew->flags = flags; + snew->sock = -1; + snew->gdfd.delim = '\n'; + snew->gdfd.getdelimfd_read = smtp_str_getdelimfd_read; + snew->gdfd.user_data = snew; + snew->cafile = cafile; + +#ifndef SMTP_IS_WINDOWS + signal(SIGPIPE, SIG_IGN); +#endif /* !(SMTP_IS_WINDOWS) */ + + if(smtp_connect(snew, server, port) < 0){ + return smtp_status_code_set(*smtp, SMTP_STATUS_CONNECT); + } + + if(smtp_initiate_handshake(snew, + server, + connection_security) != SMTP_STATUS_OK){ + return smtp_status_code_set(*smtp, SMTP_STATUS_HANDSHAKE); + } + + return snew->status_code; +} + +enum smtp_status_code +smtp_auth(struct smtp *const smtp, + enum smtp_authentication_method auth_method, + const char *const user, + const char *const pass){ + int auth_rc; + + if(smtp->status_code != SMTP_STATUS_OK){ + return smtp->status_code; + } + + if(auth_method == SMTP_AUTH_PLAIN){ + auth_rc = smtp_auth_plain(smtp, user, pass); + } + else if(auth_method == SMTP_AUTH_LOGIN){ + auth_rc = smtp_auth_login(smtp, user, pass); + } +#ifdef SMTP_OPENSSL + else if(auth_method == SMTP_AUTH_CRAM_MD5){ + auth_rc = smtp_auth_cram_md5(smtp, user, pass); + } +#endif /* SMTP_OPENSSL */ + else if(auth_method == SMTP_AUTH_NONE){ + auth_rc = 0; + } + else{ + return smtp_status_code_set(smtp, SMTP_STATUS_PARAM); + } + + if(auth_rc < 0){ + return smtp_status_code_set(smtp, SMTP_STATUS_AUTH); + } + + return smtp->status_code; +} + +enum smtp_status_code +smtp_mail(struct smtp *const smtp, + const char *const body){ + size_t i; + int has_mail_from; + struct smtp_address *address; + char date[SMTP_DATE_MAX_SZ]; + + if(smtp->status_code != SMTP_STATUS_OK){ + return smtp->status_code; + } + + /* MAIL timeout 5 minutes. */ + smtp_set_read_timeout(smtp, 60 * 5); + has_mail_from = 0; + for(i = 0; i < smtp->num_address; i++){ + address = &smtp->address_list[i]; + if(address->type == SMTP_ADDRESS_FROM){ + if(smtp_mail_envelope_header(smtp, + "MAIL FROM", + address) != SMTP_STATUS_OK){ + return smtp->status_code; + } + has_mail_from = 1; + break; + } + } + + if(!has_mail_from){ + return smtp_status_code_set(smtp, SMTP_STATUS_PARAM); + } + + /* RCPT timeout 5 minutes. */ + smtp_set_read_timeout(smtp, 60 * 5); + + for(i = 0; i < smtp->num_address; i++){ + address = &smtp->address_list[i]; + if(address->type != SMTP_ADDRESS_FROM){ + if(smtp_mail_envelope_header(smtp, + "RCPT TO", + address) != SMTP_STATUS_OK){ + return smtp->status_code; + } + } + } + + /* DATA timeout 2 minutes. */ + smtp_set_read_timeout(smtp, 60 * 2); + + if(smtp_puts(smtp, "DATA\r\n") != SMTP_STATUS_OK){ + return smtp->status_code; + } + + /* 354 response to DATA must get returned before we can send the message. */ + if(smtp_read_and_parse_code(smtp) != SMTP_BEGIN_MAIL){ + return smtp_status_code_set(smtp, SMTP_STATUS_SERVER_RESPONSE); + } + + if(!smtp_header_exists(smtp, "Date")){ + if(smtp_date_rfc_2822(date) < 0){ + return smtp_status_code_set(smtp, SMTP_STATUS_DATE); + } + if(smtp_header_add(smtp, "Date", date) != SMTP_STATUS_OK){ + return smtp->status_code; + } + } + + /* DATA block timeout 3 minutes. */ + smtp_set_read_timeout(smtp, 60 * 3); + + if(smtp_append_address_to_header(smtp, + SMTP_ADDRESS_FROM, + "From") != SMTP_STATUS_OK || + smtp_append_address_to_header(smtp, + SMTP_ADDRESS_TO, + "To") != SMTP_STATUS_OK || + smtp_append_address_to_header(smtp, + SMTP_ADDRESS_CC, + "Cc") != SMTP_STATUS_OK){ + return smtp->status_code; + } + + for(i = 0; i < smtp->num_headers; i++){ + if(smtp_print_header(smtp, &smtp->header_list[i]) != SMTP_STATUS_OK){ + return smtp->status_code; + } + } + + if(smtp_print_email(smtp, body)){ + return smtp->status_code; + } + + /* End of DATA segment. */ + if(smtp_puts(smtp, ".\r\n") != SMTP_STATUS_OK){ + return smtp->status_code; + } + + /* DATA termination timeout 250 return code - 10 minutes. */ + smtp_set_read_timeout(smtp, 60 * 10); + if(smtp_read_and_parse_code(smtp) != SMTP_DONE){ + return smtp_status_code_set(smtp, SMTP_STATUS_SERVER_RESPONSE); + } + + return smtp->status_code; +} + +enum smtp_status_code +smtp_close(struct smtp *smtp){ + enum smtp_status_code status_code; + + status_code = smtp->status_code; + + if(smtp->flags == SMTP_FLAG_INVALID_MEMORY){ + return status_code; + } + + if(smtp->sock != -1){ + /* + * Do not error out if this fails because we still need to free the + * SMTP client resources. + */ + smtp->status_code = SMTP_STATUS_OK; + smtp_puts(smtp, "QUIT\r\n"); + +#ifdef SMTP_OPENSSL + if(smtp->tls_on){ + SSL_free(smtp->tls); + SSL_CTX_free(smtp->tls_ctx); + } +#endif /* SMTP_OPENSSL */ + +#ifdef SMTP_IS_WINDOWS + closesocket(smtp->sock); + WSACleanup(); +#else /* POSIX */ + if(close(smtp->sock) < 0){ + if(smtp->status_code == SMTP_STATUS_OK){ + smtp_status_code_set(smtp, SMTP_STATUS_CLOSE); + } + } +#endif /* SMTP_IS_WINDOWS */ + } + + smtp_str_getdelimfd_free(&smtp->gdfd); + smtp_header_clear_all(smtp); + smtp_address_clear_all(smtp); + smtp_attachment_clear_all(smtp); + if(status_code == SMTP_STATUS_OK){ + status_code = smtp->status_code; + } + free(smtp); + + return status_code; +} + +enum smtp_status_code +smtp_status_code_get(const struct smtp *const smtp){ + return smtp->status_code; +} + +enum smtp_status_code +smtp_status_code_clear(struct smtp *const smtp){ + enum smtp_status_code old_status; + + old_status = smtp_status_code_get(smtp); + smtp_status_code_set(smtp, SMTP_STATUS_OK); + return old_status; +} + +enum smtp_status_code +smtp_status_code_set(struct smtp *const smtp, + enum smtp_status_code status_code){ + if((unsigned)status_code >= SMTP_STATUS__LAST){ + return smtp_status_code_set(smtp, SMTP_STATUS_PARAM); + } + smtp->status_code = status_code; + return status_code; +} + +const char * +smtp_status_code_errstr(enum smtp_status_code status_code){ + const char *const status_code_err_str[] = { + /* SMTP_STATUS_OK */ + "Success", + /* SMTP_STATUS_NOMEM */ + "Memory allocation failed", + /* SMTP_STATUS_CONNECT */ + "Failed to connect to the mail server", + /* SMTP_STATUS_HANDSHAKE */ + "Failed to handshake or negotiate a TLS connection with the server", + /* SMTP_STATUS_AUTH */ + "Failed to authenticate with the given credentials", + /* SMTP_STATUS_SEND */ + "Failed to send bytes to the server", + /* SMTP_STATUS_RECV */ + "Failed to receive bytes from the server", + /* SMTP_STATUS_CLOSE */ + "Failed to properly close a connection", + /* SMTP_STATUS_SERVER_RESPONSE */ + "SMTP server sent back an unexpected status code", + /* SMTP_STATUS_PARAM */ + "Invalid parameter", + /* SMTP_STATUS_FILE */ + "Failed to read or open a local file", + /* SMTP_STATUS_DATE */ + "Failed to get the local date and time", + /* SMTP_STATUS__LAST */ + "Unknown error" + }; + + if((unsigned)status_code > SMTP_STATUS__LAST){ + status_code = SMTP_STATUS__LAST; + } + return status_code_err_str[status_code]; +} + +enum smtp_status_code +smtp_header_add(struct smtp *const smtp, + const char *const key, + const char *const value){ + struct smtp_header *new_header_list; + struct smtp_header *new_header; + size_t num_headers_inc; + + if(smtp->status_code != SMTP_STATUS_OK){ + return smtp->status_code; + } + + if(smtp_header_key_validate(key) < 0){ + return smtp_status_code_set(smtp, SMTP_STATUS_PARAM); + } + + if(value && smtp_header_value_validate(value) < 0){ + return smtp_status_code_set(smtp, SMTP_STATUS_PARAM); + } + + if(smtp_si_add_size_t(smtp->num_headers, 1, &num_headers_inc) || + (new_header_list = smtp_reallocarray( + smtp->header_list, + num_headers_inc, + sizeof(*smtp->header_list))) == NULL){ + return smtp_status_code_set(smtp, SMTP_STATUS_NOMEM); + } + smtp->header_list = new_header_list; + new_header = &smtp->header_list[smtp->num_headers]; + + new_header->key = smtp_strdup(key); + if(value){ + new_header->value = smtp_strdup(value); + } + else{ + new_header->value = NULL; + } + if(new_header->key == NULL || + (new_header->value == NULL && value)){ + free(new_header->key); + free(new_header->value); + return smtp_status_code_set(smtp, SMTP_STATUS_NOMEM); + } + + smtp->num_headers = num_headers_inc; + + qsort(smtp->header_list, + smtp->num_headers, + sizeof(*smtp->header_list), + smtp_header_cmp); + + return smtp->status_code; +} + +void +smtp_header_clear_all(struct smtp *const smtp){ + size_t i; + struct smtp_header *header; + + for(i = 0; i < smtp->num_headers; i++){ + header = &smtp->header_list[i]; + free(header->key); + free(header->value); + } + free(smtp->header_list); + smtp->header_list = NULL; + smtp->num_headers = 0; +} + +enum smtp_status_code +smtp_address_add(struct smtp *const smtp, + enum smtp_address_type type, + const char *const email, + const char *const name){ + struct smtp_address *new_address_list; + struct smtp_address *new_address; + size_t num_address_inc; + + if(smtp->status_code != SMTP_STATUS_OK){ + return smtp->status_code; + } + + if(smtp_address_validate_email(email) < 0){ + return smtp_status_code_set(smtp, SMTP_STATUS_PARAM); + } + + if(name && smtp_address_validate_name(name) < 0){ + return smtp_status_code_set(smtp, SMTP_STATUS_PARAM); + } + + if(smtp_si_add_size_t(smtp->num_address, 1, &num_address_inc)){ + return smtp_status_code_set(smtp, SMTP_STATUS_NOMEM); + } + + new_address_list = smtp_reallocarray(smtp->address_list, + num_address_inc, + sizeof(*new_address_list)); + if(new_address_list == NULL){ + return smtp_status_code_set(smtp, SMTP_STATUS_NOMEM); + } + new_address = &new_address_list[smtp->num_address]; + + smtp->address_list = new_address_list; + + new_address->type = type; + new_address->email = smtp_strdup(email); + if(name){ + new_address->name = smtp_strdup(name); + } + else{ + new_address->name = NULL; + } + if(new_address->email == NULL || + (new_address->name == NULL && name)){ + free(new_address->email); + free(new_address->name); + return smtp_status_code_set(smtp, SMTP_STATUS_NOMEM); + } + smtp->num_address = num_address_inc; + + return smtp->status_code; +} + +void +smtp_address_clear_all(struct smtp *const smtp){ + size_t i; + struct smtp_address *address; + + for(i = 0; i < smtp->num_address; i++){ + address = &smtp->address_list[i]; + free(address->email); + free(address->name); + } + free(smtp->address_list); + smtp->address_list = NULL; + smtp->num_address = 0; +} + +enum smtp_status_code +smtp_attachment_add_path(struct smtp *const smtp, + const char *const name, + const char *const path){ + char *data; + size_t bytes_read; + + if(smtp->status_code != SMTP_STATUS_OK){ + return smtp->status_code; + } + + errno = 0; + if((data = smtp_file_get_contents(path, &bytes_read)) == NULL){ + if(errno == ENOMEM){ + return smtp_status_code_set(smtp, SMTP_STATUS_NOMEM); + } + return smtp_status_code_set(smtp, SMTP_STATUS_FILE); + } + smtp_attachment_add_mem(smtp, name, data, bytes_read); + free(data); + return smtp->status_code; +} + +enum smtp_status_code +smtp_attachment_add_fp(struct smtp *const smtp, + const char *const name, + FILE *fp){ + char *data; + size_t bytes_read; + + if(smtp->status_code != SMTP_STATUS_OK){ + return smtp->status_code; + } + + errno = 0; + if((data = smtp_ffile_get_contents(fp, &bytes_read)) == NULL){ + if(errno == ENOMEM){ + return smtp_status_code_set(smtp, SMTP_STATUS_NOMEM); + } + return smtp_status_code_set(smtp, SMTP_STATUS_FILE); + } + smtp_attachment_add_mem(smtp, name, data, bytes_read); + free(data); + return smtp->status_code; +} + +enum smtp_status_code +smtp_attachment_add_mem(struct smtp *const smtp, + const char *const name, + const void *const data, + size_t datasz){ + size_t num_attachment_inc; + struct smtp_attachment *new_attachment_list; + struct smtp_attachment *new_attachment; + char *b64_encode; + + if(smtp->status_code != SMTP_STATUS_OK){ + return smtp->status_code; + } + + if(smtp_attachment_validate_name(name) < 0){ + return smtp_status_code_set(smtp, SMTP_STATUS_PARAM); + } + + if(datasz == SIZE_MAX){ + datasz = strlen(data); + } + + if(smtp_si_add_size_t(smtp->num_attachment, 1, &num_attachment_inc) || + (new_attachment_list = smtp_reallocarray( + smtp->attachment_list, + num_attachment_inc, + sizeof(*new_attachment_list))) == NULL){ + return smtp_status_code_set(smtp, SMTP_STATUS_NOMEM); + } + smtp->attachment_list = new_attachment_list; + new_attachment = &new_attachment_list[smtp->num_attachment]; + + new_attachment->name = smtp_strdup(name); + b64_encode = smtp_base64_encode(data, datasz); + if(new_attachment->name == NULL || b64_encode == NULL){ + free(new_attachment->name); + free(b64_encode); + return smtp_status_code_set(smtp, SMTP_STATUS_NOMEM); + } + + new_attachment->b64_data = smtp_chunk_split(b64_encode, + SMTP_LINE_MAX, + "\r\n"); + free(b64_encode); + if(new_attachment->b64_data == NULL){ + free(new_attachment->name); + return smtp_status_code_set(smtp, SMTP_STATUS_NOMEM); + } + + smtp->num_attachment = num_attachment_inc; + return smtp->status_code; +} + +void +smtp_attachment_clear_all(struct smtp *const smtp){ + size_t i; + struct smtp_attachment *attachment; + + for(i = 0; i < smtp->num_attachment; i++){ + attachment = &smtp->attachment_list[i]; + free(attachment->name); + free(attachment->b64_data); + } + free(smtp->attachment_list); + smtp->attachment_list = NULL; + smtp->num_attachment = 0; +} + diff --git a/smtp.h b/smtp.h new file mode 100644 index 00000000..f035cc5b --- /dev/null +++ b/smtp.h @@ -0,0 +1,635 @@ +/** + * @file + * @brief SMTP client library. + * @author James Humphrey (mail@somnisoft.com) + * @version 1.00 + * + * This SMTP client library allows the user to send emails to an SMTP server. + * The user can include custom headers and MIME attachments. + * + * This software has been placed into the public domain using CC0. + */ +#ifndef SMTP_H +#define SMTP_H + +#include +#include +#include + +#ifndef SIZE_MAX +/** + * Maximum value of size_t type. + */ +# define SIZE_MAX ((size_t)(-1)) +#endif /* SIZE_MAX */ + +/** + * Status codes indicating success or failure from calling any of the + * SMTP library functions. + * + * This code gets returned by all functions in this header. + */ +enum smtp_status_code{ + /** + * Successful operation completed. + */ + SMTP_STATUS_OK = 0, + + /** + * Memory allocation failed. + */ + SMTP_STATUS_NOMEM = 1, + + /** + * Failed to connect to the mail server. + */ + SMTP_STATUS_CONNECT = 2, + + /** + * Failed to handshake or negotiate a TLS connection with the server. + */ + SMTP_STATUS_HANDSHAKE = 3, + + /** + * Failed to authenticate with the given credentials. + */ + SMTP_STATUS_AUTH = 4, + + /** + * Failed to send bytes to the server. + */ + SMTP_STATUS_SEND = 5, + + /** + * Failed to receive bytes from the server. + */ + SMTP_STATUS_RECV = 6, + + /** + * Failed to properly close a connection. + */ + SMTP_STATUS_CLOSE = 7, + + /** + * SMTP server sent back an unexpected status code. + */ + SMTP_STATUS_SERVER_RESPONSE = 8, + + /** + * Invalid parameter. + */ + SMTP_STATUS_PARAM = 9, + + /** + * Failed to open or read a local file. + */ + SMTP_STATUS_FILE = 10, + + /** + * Failed to get the local date and time. + */ + SMTP_STATUS_DATE = 11, + + /** + * Indicates the last status code in the enumeration, useful for + * bounds checking. + * + * Not a valid status code. + */ + SMTP_STATUS__LAST +}; + +/** + * Address source and destination types. + */ +enum smtp_address_type{ + /** + * From address. + */ + SMTP_ADDRESS_FROM = 0, + + /** + * To address. + */ + SMTP_ADDRESS_TO = 1, + + /** + * Copy address. + */ + SMTP_ADDRESS_CC = 2, + + /** + * Blind copy address. + * + * Recipients should not see any of the BCC addresses when they receive + * their email. However, some SMTP server implementations may copy this + * information into the mail header, so do not assume that this will + * always get hidden. If the BCC addresses must not get shown to the + * receivers, then send one separate email to each BCC party and add + * the TO and CC addresses manually as a header property using + * @ref smtp_header_add instead of as an address using + * @ref smtp_address_add. + */ + SMTP_ADDRESS_BCC = 3 +}; + +/** + * Connect to the SMTP server using either an unencrypted socket or + * TLS encryption. + */ +enum smtp_connection_security{ +#ifdef SMTP_OPENSSL + /** + * First connect without encryption, then negotiate an encrypted connection + * by issuing a STARTTLS command. + * + * Typically used when connecting to a mail server on port 25 or 587. + */ + SMTP_SECURITY_STARTTLS = 0, + + /** + * Use TLS when initially connecting to server. + * + * Typically used when connecting to a mail server on port 465. + */ + SMTP_SECURITY_TLS = 1, +#endif /* SMTP_OPENSSL */ + + /** + * Do not use TLS encryption. + * + * Not recommended unless connecting to the SMTP server locally. + */ + SMTP_SECURITY_NONE = 2 +}; + +/** + * List of supported methods for authenticating a mail user account on + * the server. + */ +enum smtp_authentication_method{ +#ifdef SMTP_OPENSSL + /** + * Use HMAC-MD5. + */ + SMTP_AUTH_CRAM_MD5 = 0, +#endif /* SMTP_OPENSSL */ + /** + * No authentication required. + * + * Some servers support this option if connecting locally. + */ + SMTP_AUTH_NONE = 1, + + /** + * Authenticate using base64 user and password. + */ + SMTP_AUTH_PLAIN = 2, + + /** + * Another base64 authentication method, similar to SMTP_AUTH_PLAIN. + */ + SMTP_AUTH_LOGIN = 3 +}; + +/** + * Special flags defining certain behaviors for the SMTP client context. + */ +enum smtp_flag{ + /** + * Print client and server communication on stderr. + */ + SMTP_DEBUG = 1 << 0, + + /** + * Do not verify TLS certificate. + * + * By default, the TLS handshake function will check if a certificate + * has expired or if using a self-signed certificate. Either of those + * conditions will cause the connection to fail. This option allows the + * connection to proceed even if those checks fail. + */ + SMTP_NO_CERT_VERIFY = 1 << 1 +}; + +struct smtp; + +#ifdef __cplusplus +extern "C" { +#endif /* __cplusplus */ + +/** + * Open a connection to an SMTP server and return the context. + * + * After successfully connecting and performing a handshake with the + * SMTP server, this will return a valid SMTP client context that + * the application can use in the other library functions. + * + * This always returns a valid SMTP client context even if + * the server connection or memory allocation fails. In this scenario, the + * error status will continue to propagate to future library calls for + * the SMTP context while in this failure mode. + * + * This function will ignore the SIGPIPE signal. Applications that require a + * handler for that signal should set it up after calling this function. + * + * @param[in] server Server name or IP address. + * @param[in] port Server port number. + * @param[in] connection_security See @ref smtp_connection_security. + * @param[in] flags See @ref smtp_flag. + * @param[in] cafile Path to certificate file, or NULL to use + * certificates in the default path. + * @param[out] smtp Pointer to a new SMTP context. When + * finished, the caller must free this + * context using @ref smtp_close. + * @return See @ref smtp_status_code. + */ +enum smtp_status_code +smtp_open(const char *const server, + const char *const port, + enum smtp_connection_security connection_security, + enum smtp_flag flags, + const char *const cafile, + struct smtp **smtp); + +/** + * Authenticate the user using one of the methods listed in + * @ref smtp_authentication_method. + * + * @param[in] smtp SMTP client context. + * @param[in] auth_method See @ref smtp_authentication_method. + * @param[in] user SMTP user name. + * @param[in] pass SMTP password. + * @return See @ref smtp_status_code. + */ +enum smtp_status_code +smtp_auth(struct smtp *const smtp, + enum smtp_authentication_method auth_method, + const char *const user, + const char *const pass); + +/** + * Sends an email using the addresses, attachments, and headers defined + * in the current SMTP context. + * + * The caller must call the @ref smtp_open function prior to this. + * + * The 'Date' header will automatically get generated here if it hasn't + * already been set using @ref smtp_header_add. + * + * If the application overrides the default 'Content-Type' header, then + * this function will output the @p body as raw data just below the email + * headers, and it will not output the attachments added using the + * smtp_attachment_add_* functions. In other words, the application must + * create its own MIME sections (if needed) when overriding the + * 'Content-Type' header. + * + * @param[in] smtp SMTP client context. + * @param[in] body Null-terminated string to send in the email body. + * @return See @ref smtp_status_code. + */ +enum smtp_status_code +smtp_mail(struct smtp *const smtp, + const char *const body); + +/** + * Close the SMTP connection and frees all resources held by the + * SMTP context. + * + * @param[in] smtp SMTP client context. + * @return See @ref smtp_status_code. + */ +enum smtp_status_code +smtp_close(struct smtp *smtp); + +/** + * Get the current status/error code. + * + * @param[in] smtp SMTP client context. + * @return See @ref smtp_status_code. + */ +enum smtp_status_code +smtp_status_code_get(const struct smtp *const smtp); + +/** + * Clear the current error code set in the SMTP client context. + * + * @param[in,out] smtp SMTP client context. + * @return Previous error code before clearing. + */ +enum smtp_status_code +smtp_status_code_clear(struct smtp *const smtp); + +/** + * Set the error status of the SMTP client context and return the same code. + * + * This allows the caller to clear an error status to SMTP_STATUS_OK + * so that previous errors will stop propagating. However, this will only + * work correctly for clearing the SMTP_STATUS_PARAM and SMTP_STATUS_FILE + * errors. Do not use this to clear any other error codes. + * + * @deprecated Use @ref smtp_status_code_clear instead. + * + * @param[in] smtp SMTP client context. + * @param[in] new_status_code See @ref smtp_status_code. + * @return See @ref smtp_status_code. + */ +enum smtp_status_code +smtp_status_code_set(struct smtp *const smtp, + enum smtp_status_code new_status_code); + +/** + * Convert a standard SMTP client status code to a descriptive string. + * + * @param[in] status_code Status code returned from one of the other + * library functions. + * @return String containing a description of the @p status_code. The caller + * must not free or modify this string. + */ +const char * +smtp_status_code_errstr(enum smtp_status_code status_code); + +/** + * Add a key/value header to the header list in the SMTP context. + * + * If adding a header with an existing key, this will insert instead of + * replacing the existing header. See @ref smtp_header_clear_all. + * + * See @ref smtp_mail when overriding the default 'Content-Type' header. + * + * @param[in] smtp SMTP client context. + * @param[in] key Key name for new header. It must consist only of + * printable US-ASCII characters except colon. + * @param[in] value Value for new header. It must consist only of printable + * US-ASCII, space, or horizontal tab. If set to NULL, + * this will prevent the header from printing out. + * @return See @ref smtp_status_code. + */ +enum smtp_status_code +smtp_header_add(struct smtp *const smtp, + const char *const key, + const char *const value); + +/** + * Free all memory related to email headers. + * + * @param[in] smtp SMTP client context. + */ +void +smtp_header_clear_all(struct smtp *const smtp); + +/** + * Add a FROM, TO, CC, or BCC address destination to this SMTP context. + * + * @note Some SMTP servers may reject over 100 recipients. + * + * @param[in] smtp SMTP client context. + * @param[in] type See @ref smtp_address_type. + * @param[in] email The email address of the party. Must consist only of + * printable characters excluding the angle brackets + * (<) and (>). + * @param[in] name Name or description of the party. Must consist only of + * printable characters, excluding the quote characters. If + * set to NULL or empty string, no name will get associated + * with this email. + * @return See @ref smtp_status_code. + */ +enum smtp_status_code +smtp_address_add(struct smtp *const smtp, + enum smtp_address_type type, + const char *const email, + const char *const name); + +/** + * Free all memory related to the address list. + * + * @param[in] smtp SMTP client context. + */ +void +smtp_address_clear_all(struct smtp *const smtp); + +/** + * Add a file attachment from a path. + * + * See @ref smtp_attachment_add_mem for more details. + * + * @param[in] smtp SMTP client context. + * @param[in] name Filename of the attachment shown to recipients. Must + * consist only of printable ASCII characters, excluding + * the quote characters (') and ("). + * @param[in] path Path to file. + * @return See @ref smtp_status_code. + */ +enum smtp_status_code +smtp_attachment_add_path(struct smtp *const smtp, + const char *const name, + const char *const path); + +/** + * Add an attachment using a file pointer. + * + * See @ref smtp_attachment_add_mem for more details. + * + * @param[in] smtp SMTP client context. + * @param[in] name Filename of the attachment shown to recipients. Must + * consist only of printable ASCII characters, excluding + * the quote characters (') and ("). + * @param[in] fp File pointer already opened by the caller. + * @return See @ref smtp_status_code. + */ +enum smtp_status_code +smtp_attachment_add_fp(struct smtp *const smtp, + const char *const name, + FILE *fp); + +/** + * Add a MIME attachment to this SMTP context with the data retrieved + * from memory. + * + * The attachment data will get base64 encoded before sending to the server. + * + * @param[in] smtp SMTP client context. + * @param[in] name Filename of the attachment shown to recipients. Must + * consist only of printable ASCII characters, excluding + * the quote characters (') and ("). + * @param[in] data Raw attachment data stored in memory. + * @param[in] datasz Number of bytes in @p data, or -1 if data + * null-terminated. + * @return See @ref smtp_status_code. + */ +enum smtp_status_code +smtp_attachment_add_mem(struct smtp *const smtp, + const char *const name, + const void *const data, + size_t datasz); + +/** + * Remove all attachments from the SMTP client context. + * + * @param[in] smtp SMTP client context. + */ +void +smtp_attachment_clear_all(struct smtp *const smtp); + + +/* + * The SMTP_INTERNAL DEFINE section contains definitions that get used + * internally by the SMTP client library. + */ +#ifdef SMTP_INTERNAL_DEFINE +/** + * SMTP codes returned by the server and parsed by the client. + */ +enum smtp_result_code{ + /** + * Client error code which does not get set by the server. + */ + SMTP_INTERNAL_ERROR = -1, + + /** + * Returned when ready to begin processing next step. + */ + SMTP_READY = 220, + + /** + * Returned in response to QUIT. + */ + SMTP_CLOSE = 221, + + /** + * Returned if client successfully authenticates. + */ + SMTP_AUTH_SUCCESS = 235, + + /** + * Returned when some commands successfully complete. + */ + SMTP_DONE = 250, + + /** + * Returned for some multi-line authentication mechanisms where this code + * indicates the next stage in the authentication step. + */ + SMTP_AUTH_CONTINUE = 334, + + /** + * Returned in response to DATA command. + */ + SMTP_BEGIN_MAIL = 354 +}; + +/** + * Used for parsing out the responses from the SMTP server. + * + * For example, if the server sends back '250-STARTTLS', then code would + * get set to 250, more would get set to 1, and text would get set to STARTTLS. + */ +struct smtp_command{ + /** + * Result code converted to an integer. + */ + enum smtp_result_code code; + + /** + * Indicates if more server commands follow. + * + * This will get set to 1 if the fourth character in the response line + * contains a '-', otherwise this will get set to 0. + */ + int more; + + /** + * The text shown after the status code. + */ + const char *text; +}; + +/** + * Return codes for the getdelim interface which allows the caller to check + * if more delimited lines can get processed. + */ +enum str_getdelim_retcode{ + /** + * An error occurred during the getdelim processing. + */ + STRING_GETDELIMFD_ERROR = -1, + + /** + * Found a new line and can process more lines in the next call. + */ + STRING_GETDELIMFD_NEXT = 0, + + /** + * Found a new line and unable to read any more lines at this time. + */ + STRING_GETDELIMFD_DONE = 1 +}; + +/** + * Data structure for read buffer and line parsing. + * + * This assists with getting and parsing the server response lines. + */ +struct str_getdelimfd{ + /** + * Read buffer which may include bytes past the delimiter. + */ + char *_buf; + + /** + * Number of allocated bytes in the read buffer. + */ + size_t _bufsz; + + /** + * Number of actual stored bytes in the read buffer. + */ + size_t _buf_len; + + /** + * Current line containing the text up to the delimiter. + */ + char *line; + + /** + * Number of stored bytes in the line buffer. + */ + size_t line_len; + + /** + * Function pointer to a custom read function for the + * @ref smtp_str_getdelimfd interface. + * + * This function prototype has similar semantics to the read function. + * The @p gdfd parameter allows the custom function to pull the user_data + * info from the @ref str_getdelimfd struct which can contain file pointer, + * socket connection, etc. + */ + long (*getdelimfd_read)(struct str_getdelimfd *const gdfd, + void *buf, + size_t count); + + /** + * User data which gets sent to the read handler function. + */ + void *user_data; + + /** + * Character delimiter used for determining line separation. + */ + int delim; + + /** + * Padding structure to align. + */ + char pad[4]; +}; + +#endif /* SMTP_INTERNAL_DEFINE */ + +#ifdef __cplusplus +} +#endif /* __cplusplus */ + +#endif /* SMTP_H */ + From 63a8ce36fabbbd9f0a355c8d47beb4891036b1e3 Mon Sep 17 00:00:00 2001 From: "Nathan P." Date: Fri, 28 Jun 2024 12:24:22 -0400 Subject: [PATCH 07/61] Fixes for Arduino and RPI email notifications. --- EMailSender.cpp | 1114 ++++++++++++++++++++++++++++++++++++++++++++++ EMailSender.h | 525 ++++++++++++++++++++++ EMailSenderKey.h | 157 +++++++ main.cpp | 11 +- platformio.ini | 1 - smtp.c | 76 ++-- smtp.h | 3 + 7 files changed, 1845 insertions(+), 42 deletions(-) create mode 100644 EMailSender.cpp create mode 100644 EMailSender.h create mode 100644 EMailSenderKey.h diff --git a/EMailSender.cpp b/EMailSender.cpp new file mode 100644 index 00000000..dfdc9d5b --- /dev/null +++ b/EMailSender.cpp @@ -0,0 +1,1114 @@ +/* + * EMail Sender Arduino, esp8266, stm32 and esp32 library to send email + * + * AUTHOR: Renzo Mischianti + * VERSION: 3.0.14 + * + * https://www.mischianti.org/ + * + * The MIT License (MIT) + * + * Copyright (c) 2017 Renzo Mischianti www.mischianti.org All right reserved. + * + * You may copy, alter and reuse this code in any way you like, but please leave + * reference to www.mischianti.org in your comments if you redistribute this code. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "EMailSender.h" +#include +#if defined(ESP32) +#include +#endif + +//#include +//#include + +//#define SD SPIFFS + +// BASE64 ----------------------------------------------------------- +const char PROGMEM b64_alphabet[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789+/"; + +#define encode64(arr) encode64_f(arr,strlen(arr)) + +inline void a3_to_a4(unsigned char * a4, unsigned char * a3) { + a4[0] = (a3[0] & 0xfc) >> 2; + a4[1] = ((a3[0] & 0x03) << 4) + ((a3[1] & 0xf0) >> 4); + a4[2] = ((a3[1] & 0x0f) << 2) + ((a3[2] & 0xc0) >> 6); + a4[3] = (a3[2] & 0x3f); +} + +int base64_encode(char *output, char *input, int inputLen) { + int i = 0, j = 0; + int encLen = 0; + unsigned char a3[3]; + unsigned char a4[4]; + + while (inputLen--) { + a3[i++] = *(input++); + if (i == 3) { + a3_to_a4(a4, a3); + + for (i = 0; i < 4; i++) { + output[encLen++] = pgm_read_byte(&b64_alphabet[a4[i]]); + } + + i = 0; + } + } + + if (i) { + for (j = i; j < 3; j++) { + a3[j] = '\0'; + } + + a3_to_a4(a4, a3); + + for (j = 0; j < i + 1; j++) { + output[encLen++] = pgm_read_byte(&b64_alphabet[a4[j]]); + } + + while ((i++ < 3)) { + output[encLen++] = '='; + } + } + output[encLen] = '\0'; + return encLen; +} + +int base64_enc_length(int plainLen) { + int n = plainLen; + return (n + 2 - ((n + 2) % 3)) / 3 * 4; +} + +const char* encode64_f(char* input, uint8_t len) { + // encoding + + DEBUG_PRINTLN(F("Encoding")); + + DEBUG_PRINTLN(input); + DEBUG_PRINTLN(len); + + //int encodedLen = + base64_enc_length(len); + static char encoded[256]; + // note input is consumed in this step: it will be empty afterwards + base64_encode(encoded, input, len); + return encoded; +} + +// END BASE64 --------------------------------------------------------- + +EMailSender::EMailSender(const char* email_login, const char* email_password, const char* email_from, const char* name_from , + const char* smtp_server, uint16_t smtp_port) { + this->setEMailLogin(email_login); + this->setEMailFrom(email_from); + this->setEMailPassword(email_password); + this->setSMTPServer(smtp_server); + this->setSMTPPort(smtp_port); + this->setNameFrom(name_from); +// this->isSecure = isSecure; +} +EMailSender::EMailSender(const char* email_login, const char* email_password, const char* email_from, + const char* smtp_server, uint16_t smtp_port) { + this->setEMailLogin(email_login); + this->setEMailFrom(email_from); + this->setEMailPassword(email_password); + this->setSMTPServer(smtp_server); + this->setSMTPPort(smtp_port); + +// this->isSecure = isSecure; +} + +EMailSender::EMailSender(const char* email_login, const char* email_password, const char* email_from, const char* name_from ) { + this->setEMailLogin(email_login); + this->setEMailFrom(email_from); + this->setEMailPassword(email_password); + this->setNameFrom(name_from); + this->setNameFrom(name_from); + +// this->isSecure = isSecure; +} +EMailSender::EMailSender(const char* email_login, const char* email_password, const char* email_from) { + this->setEMailLogin(email_login); + this->setEMailFrom(email_from); + this->setEMailPassword(email_password); + +// this->isSecure = isSecure; +} + +EMailSender::EMailSender(const char* email_login, const char* email_password){ + this->setEMailLogin(email_login); + this->setEMailFrom(email_login); + this->setEMailPassword(email_password); + +// this->isSecure = isSecure; +} + +void EMailSender::setSMTPPort(uint16_t smtp_port){ + this->smtp_port = smtp_port; +}; +void EMailSender::setSMTPServer(const char* smtp_server){ + delete [] this->smtp_server; + this->smtp_server = new char[strlen(smtp_server)+1]; + strcpy(this->smtp_server, smtp_server); +}; + +void EMailSender::setEMailLogin(const char* email_login){ + delete [] this->email_login; + this->email_login = new char[strlen(email_login)+1]; + strcpy(this->email_login, email_login); +}; +void EMailSender::setEMailFrom(const char* email_from){ + delete [] this->email_from; + this->email_from = new char[strlen(email_from)+1]; + strcpy(this->email_from, email_from); +}; +void EMailSender::setNameFrom(const char* name_from){ + delete [] this->name_from; + this->name_from = new char[strlen(name_from)+1]; + strcpy(this->name_from, name_from); +}; +void EMailSender::setEMailPassword(const char* email_password){ + delete [] this->email_password; + this->email_password = new char[strlen(email_password)+1]; + strcpy(this->email_password, email_password); +}; + +void EMailSender::setIsSecure(bool isSecure) { + this->isSecure = isSecure; +} + +#ifdef SSLCLIENT_WRAPPER +EMailSender::Response EMailSender::awaitSMTPResponse(SSLClient &client, + const char* resp, const char* respDesc, uint16_t timeOut) { + EMailSender::Response response; + uint32_t ts = millis(); + while (!client.available()) { + if (millis() > (ts + timeOut)) { + response.code = F("1"); + response.desc = String(respDesc) + "! " + F("SMTP Response TIMEOUT!"); + response.status = false; + + return response; + } + } + _serverResponce = client.readStringUntil('\n'); + + DEBUG_PRINTLN(_serverResponce); + if (resp && _serverResponce.indexOf(resp) == -1){ + response.code = resp; + response.desc = respDesc +String(" (") + _serverResponce + String(")"); + response.status = false; + + return response; + } + + response.status = true; + return response; +} +#else +EMailSender::Response EMailSender::awaitSMTPResponse(EMAIL_NETWORK_CLASS &client, + const char* resp, const char* respDesc, uint16_t timeOut) { + EMailSender::Response response; + uint32_t ts = millis(); + while (!client.available()) { + if (millis() > (ts + timeOut)) { + response.code = F("1"); + response.desc = String(respDesc) + "! " + F("SMTP Response TIMEOUT!"); + response.status = false; + return response; + } + } + _serverResponce = client.readStringUntil('\n'); + + DEBUG_PRINTLN(_serverResponce); + if (resp && _serverResponce.indexOf(resp) == -1){ + response.code = resp; + response.desc = respDesc +String(" (") + _serverResponce + String(")"); + response.status = false; + return response; + } + + response.status = true; + return response; +} +#endif + +static const char cb64[]="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; +void encodeblock(unsigned char in[3],unsigned char out[4],int len) { + out[0]=cb64[in[0]>>2]; out[1]=cb64[((in[0]&0x03)<<4)|((in[1]&0xF0)>>4)]; + out[2]=(unsigned char) (len>1 ? cb64[((in[1]&0x0F)<<2)|((in[2]&0xC0)>>6)] : '='); + out[3]=(unsigned char) (len>2 ? cb64[in[2]&0x3F] : '='); +} + +#ifdef ENABLE_ATTACHMENTS + #ifdef STORAGE_EXTERNAL_ENABLED + #if (defined(DIFFERENT_FILE_MANAGE) && defined(EMAIL_FILE_EX)) || !defined(STORAGE_INTERNAL_ENABLED) + #ifdef SSLCLIENT_WRAPPER + void encode(EMAIL_FILE_EX *file, SSLClient *client) { + unsigned char in[3],out[4]; + int i,len,blocksout=0; + + while (file->available()!=0) { + len=0; + for (i=0;i<3;i++){ + in[i]=(unsigned char) file->read(); + if (file->available()!=0) len++; + else in[i]=0; + } + if (len){ + encodeblock(in,out,len); + // for(i=0;i<4;i++) client->write(out[i]); + client->write(out, 4); + blocksout++; } + if (blocksout>=19||file->available()==0){ + if (blocksout) { + client->print("\r\n"); + } + blocksout=0; + } + } + } + + #else + void encode(EMAIL_FILE_EX *file, EMAIL_NETWORK_CLASS *client) { + unsigned char in[3],out[4]; + int i,len,blocksout=0; + + while (file->available()!=0) { + len=0; + for (i=0;i<3;i++){ + in[i]=(unsigned char) file->read(); + if (file->available()!=0) len++; + else in[i]=0; + } + if (len){ + encodeblock(in,out,len); + // for(i=0;i<4;i++) client->write(out[i]); + client->write(out, 4); + blocksout++; } + if (blocksout>=19||file->available()==0){ + if (blocksout) { + client->print("\r\n"); + } + blocksout=0; + } + } + } + #endif + + #endif + #endif + #ifdef STORAGE_INTERNAL_ENABLED + #if defined(DIFFERENT_FILE_MANAGE) || (!defined(DIFFERENT_FILE_MANAGE) && defined(EMAIL_FILE)) || !defined(STORAGE_EXTERNAL_ENABLED) + + #ifdef SSLCLIENT_WRAPPER + void encode(EMAIL_FILE *file, SSLClient *client) { + unsigned char in[3],out[4]; + int i,len,blocksout=0; + + while (file->available()!=0) { + len=0; + for (i=0;i<3;i++){ + in[i]=(unsigned char) file->read(); + if (file->available()!=0) len++; + else in[i]=0; + } + if (len){ + encodeblock(in,out,len); + // for(i=0;i<4;i++) client->write(out[i]); + client->write(out, 4); + blocksout++; } + if (blocksout>=19||file->available()==0){ + if (blocksout) { + client->print("\r\n"); + } + blocksout=0; + } + } + } + + #else + void encode(EMAIL_FILE *file, EMAIL_NETWORK_CLASS *client) { + unsigned char in[3],out[4]; + int i,len,blocksout=0; + + while (file->available()!=0) { + len=0; + for (i=0;i<3;i++){ + in[i]=(unsigned char) file->read(); + if (file->available()!=0) len++; + else in[i]=0; + } + if (len){ + encodeblock(in,out,len); + // for(i=0;i<4;i++) client->write(out[i]); + client->write(out, 4); + blocksout++; } + if (blocksout>=19||file->available()==0){ + if (blocksout) { + client->print("\r\n"); + } + blocksout=0; + } + } + } + #endif + #endif + #endif +#endif + +const char** toCharArray(String arr[], int num) { + // If we ever alloc with new with have to delete + const char** buffer = new const char*[num]; + + for(int i = 0; i < num; i++) { + buffer[i] = arr[i].c_str(); + } + + return buffer; +} +const char** toCharArray(char* arr[], int num) { + // If we ever alloc with new with have to delete + const char** buffer = new const char*[num]; + + for(int i = 0; i < num; i++) { + buffer[i] = arr[i]; + } + + return buffer; +} + +EMailSender::Response EMailSender::send(char* tos[], byte sizeOfTo, EMailMessage &email, Attachments attachments) { + return send(toCharArray(tos, sizeOfTo), sizeOfTo, 0, 0, email, attachments); +} +EMailSender::Response EMailSender::send(char* tos[], byte sizeOfTo, byte sizeOfCc, EMailMessage &email, Attachments attachments) { + return send(toCharArray(tos, sizeOfTo+sizeOfCc), sizeOfTo, sizeOfCc, 0, email, attachments); +} +EMailSender::Response EMailSender::send(char* tos[], byte sizeOfTo, byte sizeOfCc,byte sizeOfCCn, EMailMessage &email, Attachments attachments){ + return send(toCharArray(tos, sizeOfTo+sizeOfCc+sizeOfCCn), sizeOfTo, sizeOfCc, sizeOfCCn, email, attachments); +} + + +EMailSender::Response EMailSender::send(String to, EMailMessage &email, Attachments attachments){ + DEBUG_PRINT(F("ONLY ONE RECIPIENT")); + + const char* arrEmail[] = {to.c_str()}; + return send(arrEmail, 1, email, attachments); +} + +EMailSender::Response EMailSender::send(String tos[], byte sizeOfTo, EMailMessage &email, Attachments attachments) { + return send(toCharArray(tos, sizeOfTo), sizeOfTo, 0, 0, email, attachments); +} + +EMailSender::Response EMailSender::send(String tos[], byte sizeOfTo, byte sizeOfCc, EMailMessage &email, Attachments attachments) { + return send(toCharArray(tos, sizeOfTo+sizeOfCc), sizeOfTo, sizeOfCc, 0, email, attachments); +} + +EMailSender::Response EMailSender::send(String tos[], byte sizeOfTo, byte sizeOfCc,byte sizeOfCCn, EMailMessage &email, Attachments attachments){ + return send(toCharArray(tos, sizeOfTo+sizeOfCc+sizeOfCCn), sizeOfTo, sizeOfCc, sizeOfCCn, email, attachments); +} + +EMailSender::Response EMailSender::send(const char* to, EMailMessage &email, Attachments attachments){ + DEBUG_PRINT(F("ONLY ONE RECIPIENT")); + + const char* arrEmail[] = {to}; + return send(arrEmail, 1, email, attachments); +} + +EMailSender::Response EMailSender::send(const char* to[], byte sizeOfTo, EMailMessage &email, Attachments attachments) { + DEBUG_PRINTLN(F("miltiple destination and attachments")); + return send(to, sizeOfTo, 0, email, attachments); +} + +EMailSender::Response EMailSender::send(const char* to[], byte sizeOfTo, byte sizeOfCc, EMailMessage &email, Attachments attachments) +{ + return send(to, sizeOfTo, sizeOfCc, 0, email, attachments); +} + +#ifdef SSLCLIENT_WRAPPER +#ifdef PUT_OUTSIDE_SCOPE_CLIENT_DECLARATION + // Initialize the SSL client library + // We input an EthernetClient, our trust anchors, and the analog pin + EMAIL_NETWORK_CLASS base_client; + SSLClient client(base_client, TAs, (size_t)TAs_NUM, ANALOG_PIN, 2); +#else + #error "You must put outside scope the client declaration if you want use SSLClient!" +#endif +#else + #ifdef PUT_OUTSIDE_SCOPE_CLIENT_DECLARATION + EMAIL_NETWORK_CLASS client; + #endif +#endif + +EMailSender::Response EMailSender::send(const char* to[], byte sizeOfTo, byte sizeOfCc,byte sizeOfCCn, EMailMessage &email, Attachments attachments) +{ +#ifdef SSLCLIENT_WRAPPER + DEBUG_PRINTLN(F("SSLClient active!")); +#else + #ifndef PUT_OUTSIDE_SCOPE_CLIENT_DECLARATION + EMAIL_NETWORK_CLASS client; + #endif + + DEBUG_PRINT(F("Insecure client:")); + DEBUG_PRINTLN(this->isSecure); + #ifndef FORCE_DISABLE_SSL + #if (EMAIL_NETWORK_TYPE == NETWORK_ESP8266 || EMAIL_NETWORK_TYPE == NETWORK_ESP8266_242) + #ifndef ARDUINO_ESP8266_RELEASE_2_4_2 + if (this->isSecure == false){ + client.setInsecure(); + bool mfln = client.probeMaxFragmentLength(this->smtp_server, this->smtp_port, 512); + + DEBUG_PRINT("MFLN supported: "); + DEBUG_PRINTLN(mfln?"yes":"no"); + + if (mfln) { + client.setBufferSizes(512, 512); + } else { + client.setBufferSizes(2048, 2048); + } + } + #endif + #elif (EMAIL_NETWORK_TYPE == NETWORK_ESP32) + // String coreVersion = String(ESP.getSdkVersion()); + // uint8_t firstdot = coreVersion.indexOf('.'); + // + // DEBUG_PRINTLN(coreVersion.substring(1, coreVersion.indexOf('.', firstdot+1)).toFloat()); + // DEBUG_PRINTLN(coreVersion.substring(1, coreVersion.indexOf('.', firstdot+1)).toFloat() >= 3.3f); + // if (coreVersion.substring(1, coreVersion.indexOf('.', firstdot+1)).toFloat() >= 3.3f) { + // client.setInsecure(); + // } + #ifndef ARDUINO_ARCH_RP2040 + #include + #if ((!defined(ARDUINO_ESP32_RELEASE_1_0_4)) && (!defined(ARDUINO_ESP32_RELEASE_1_0_3)) && (!defined(ARDUINO_ESP32_RELEASE_1_0_2))) + client.setInsecure(); + #endif + #else + client.setInsecure(); + #endif + #endif + #endif +#endif + EMailSender::Response response; + + DEBUG_PRINTLN(this->smtp_server); + DEBUG_PRINTLN(this->smtp_port); + + if(!client.connect(this->smtp_server, this->smtp_port)) { + response.desc = F("Could not connect to mail server"); + response.code = F("2"); + response.status = false; + + client.flush(); + client.stop(); + + return response; + } + + response = awaitSMTPResponse(client, "220", "Connection Error"); + if (!response.status) { + client.flush(); + client.stop(); + return response; + } + + if (this->additionalResponseLineOnConnection == 0) { + if (DEFAULT_CONNECTION_RESPONSE_COUNT == 'a'){ + this->additionalResponseLineOnConnection = 255; + } else { + this->additionalResponseLineOnConnection = DEFAULT_CONNECTION_RESPONSE_COUNT; + } + } + + if (this->additionalResponseLineOnConnection > 0){ + for (int i = 0; i<=this->additionalResponseLineOnConnection; i++) { + response = awaitSMTPResponse(client, "220", "Connection response error ", 2500); + //if additionalResponseLineOnConnection is set to 255: wait out all code 250 responses, then continue + if (this->additionalResponseLineOnConnection == 255) break; + else { + if (!response.status && response.code == F("1")) { + response.desc = F("Connection error! Reduce the HELO response line!"); + client.flush(); + client.stop(); + return response; + } + } + } + } + + String commandHELO = "HELO"; + if (this->useEHLO == true) { + commandHELO = "EHLO"; + } + String helo = commandHELO + " "+String(publicIPDescriptor)+" "; + DEBUG_PRINTLN(helo); + client.println(helo); + + response = awaitSMTPResponse(client, "250", "Identification error"); + if (!response.status) { + client.flush(); + client.stop(); + return response; + } + + if (this->useEHLO == true && this->additionalResponseLineOnHELO == 0) { + if (DEFAULT_EHLO_RESPONSE_COUNT == 'a'){ + this->additionalResponseLineOnHELO = 255; + } else { + this->additionalResponseLineOnHELO = DEFAULT_EHLO_RESPONSE_COUNT; + } + } + + if (this->additionalResponseLineOnHELO > 0){ + for (int i = 0; i<=this->additionalResponseLineOnHELO; i++) { + response = awaitSMTPResponse(client, "250", "EHLO error", 2500); + if (!response.status && response.code == F("1")) { + //if additionalResponseLineOnHELO is set to 255: wait out all code 250 responses, then continue + if (this->additionalResponseLineOnHELO == 255) break; + else { + response.desc = F("Timeout! Reduce additional HELO response line count or set it to 255!"); + client.flush(); + client.stop(); + return response; + } + } + } + } + + if (useAuth){ + if (this->isSASLLogin == true){ + + int size = 1 + strlen(this->email_login)+ strlen(this->email_password)+2; + char * logPass = (char *) malloc(size); + +// strcpy(logPass, " "); +// strcat(logPass, this->email_login); +// strcat(logPass, " "); +// strcat(logPass, this->email_password); + +// String logPass; + int maincont = 0; + + logPass[maincont++] = ' '; + logPass[maincont++] = (char) 0; + + for (unsigned int i = 0;iemail_login);i++){ + logPass[maincont++] = this->email_login[i]; + } + logPass[maincont++] = (char) 0; + for (unsigned int i = 0;iemail_password);i++){ + logPass[maincont++] = this->email_password[i]; + } + + +// strcpy(logPass, "\0"); +// strcat(logPass, this->email_login); +// strcat(logPass, "\0"); +// strcat(logPass, this->email_password); + + String auth = "AUTH PLAIN "+String(encode64_f(logPass, size)); +// String auth = "AUTH PLAIN "+String(encode64(logPass)); + DEBUG_PRINTLN(auth); + client.println(auth); + } +#if defined(ESP32) + else if (this->isCramMD5Login == true) { + DEBUG_PRINTLN(F("AUTH CRAM-MD5")); + client.println(F("AUTH CRAM-MD5")); + + // read back the base64 encoded digest. + // + response = awaitSMTPResponse(client,"334","No digest error"); + if (!response.status) { + client.flush(); + client.stop(); + return response; + }; + _serverResponce = _serverResponce.substring(4); // '334' + + size_t b64l = _serverResponce.length()-1; // C vs C++ counting of \0 + const unsigned char * b64 = (const unsigned char *)_serverResponce.c_str(); + DEBUG_PRINTLN("B64digest="+String((char *)b64) + " Len=" + String((int)b64l)); + + unsigned char digest[256]; + size_t len; + + int e = mbedtls_base64_decode(digest, sizeof(digest), &len, b64, b64l); + if (e || len < 3 || len >= 256) { + response.code = F("999"); + response.desc = F("Invalid digest"); + response.status = false; + client.flush(); + client.stop(); + return response; + }; + + // calculate HMAC with the password as the key of this digest. + // + mbedtls_md_context_t ctx; + mbedtls_md_type_t md_type = MBEDTLS_MD_MD5; + unsigned char md5[16]; + + mbedtls_md_init(&ctx); + mbedtls_md_setup(&ctx, mbedtls_md_info_from_type(md_type), 1); + mbedtls_md_hmac_starts(&ctx, + (const unsigned char *)this->email_password, strlen(this->email_password)); + mbedtls_md_hmac_update(&ctx, digest, len); + mbedtls_md_hmac_finish(&ctx, md5); + mbedtls_md_free(&ctx); + + // build an output string of the username followed by the __lowercase__ hex of the md5 + // + String rsp = String(this->email_login) + " "; + for(int i = 0; i < sizeof(md5); i++) { + unsigned char c = md5[i]; + char h[16+1] = "0123456789abcdef"; + rsp += String(h[ (c >> 4) &0xF]) + String(h[ (c >> 0) &0xF]); + }; + + // And submit this to the server as a login string. + DEBUG_PRINTLN(encode64((char*)rsp.c_str())); + client.println(encode64((char*)rsp.c_str())); + + // now exepct the normal login confirmation to continue. + } +#endif + else{ + DEBUG_PRINTLN(F("AUTH LOGIN:")); + client.println(F("AUTH LOGIN")); + awaitSMTPResponse(client); + + DEBUG_PRINTLN(encode64(this->email_login)); + client.println(encode64(this->email_login)); + awaitSMTPResponse(client); + + DEBUG_PRINTLN(encode64(this->email_password)); + client.println(encode64(this->email_password)); + } + response = awaitSMTPResponse(client, "235", "SMTP AUTH error"); + if (!response.status) { + client.flush(); + client.stop(); + return response; + } + } + DEBUG_PRINT(F("MAIL FROM: <")); + DEBUG_PRINT(this->email_from); + DEBUG_PRINTLN(F(">")); + + client.print(F("MAIL FROM: <")); + client.print(this->email_from); + client.println(F(">")); + awaitSMTPResponse(client); + +// String rcpt = "RCPT TO: <" + String(to) + '>'; +// +// DEBUG_PRINTLN(rcpt); +// client.println(rcpt); + + int cont; + for (cont=0;cont<(sizeOfTo+sizeOfCc+sizeOfCCn);cont++){ + DEBUG_PRINT(F("RCPT TO: <")); + DEBUG_PRINT(to[cont]); + DEBUG_PRINTLN(F(">")); + + client.print(F("RCPT TO: <")); + client.print(to[cont]); + client.println(F(">")); + awaitSMTPResponse(client); + } + + DEBUG_PRINTLN(F("DATA:")); + client.println(F("DATA")); + + response = awaitSMTPResponse(client, "354", "SMTP DATA error"); + if (!response.status) { + client.flush(); + client.stop(); + return response; + } + +// client.println("From: <" + String(this->email_from) + '>'); + + client.print(F("From: ")); + if (this->name_from){ + client.print(this->name_from); + } + client.print(F(" <")); + client.print(this->email_from); + client.println(F(">")); + +// client.println("To: <" + String(to) + '>'); + + client.print(F("To: ")); + for (cont=0;cont"); + if (cont!=sizeOfTo-1){ + client.print(","); + } + } + client.println(); + + if (sizeOfCc>0){ + client.print(F("Cc: ")); + for (;cont"); + if (cont!=sizeOfCc-1){ + client.print(","); + } + } + client.println(); + } + + if (sizeOfCCn>0){ + client.print(F("CCn: ")); + for (;cont"); + if (cont!=sizeOfCCn-1){ + client.print(","); + } + } + client.println(); + } + + client.print(F("Subject: ")); + client.println(email.subject); + +// client.println(F("Mime-Version: 1.0")); + + client.println(F("MIME-Version: 1.0")); + client.println(F("Content-Type: Multipart/mixed; boundary=frontier")); + + client.println(F("--frontier")); + + client.print(F("Content-Type: ")); + client.print(email.mime); + client.println(F("; charset=\"UTF-8\"")); +// client.println(F("Content-Type: text/html; charset=\"UTF-8\"")); + client.println(F("Content-Transfer-Encoding: 7bit")); + client.println(); + if (email.mime==F("text/html")){ +// String body = "" + String(email.message) + ""; + + client.print(F("")); + client.print(email.message); + client.println(F("")); + +// client.println(body); + }else{ + client.println(email.message); + } + client.println(); + +#ifdef STORAGE_INTERNAL_ENABLED + bool spiffsActive = false; +#endif +#ifdef STORAGE_EXTERNAL_ENABLED + bool sdActive = false; +#endif + +#if defined(ENABLE_ATTACHMENTS) && (defined(STORAGE_EXTERNAL_ENABLED) || defined(STORAGE_INTERNAL_ENABLED)) +// if ((sizeof(attachs) / sizeof(attachs[0]))>0){ + if (sizeof(attachments)>0 && attachments.number>0){ + + DEBUG_PRINT(F("Array: ")); +// for (int i = 0; i<(sizeof(attachs) / sizeof(attachs[0])); i++){ + for (int i = 0; i ")); + DEBUG_PRINTLN(myFile.name()); + if (attachments.fileDescriptor[i].encode64){ + encode(&myFile, &client); + }else{ + while(myFile.available()) { + clientCount = myFile.read(tBuf,64); + DEBUG_PRINTLN(clientCount); + client.write((byte*)tBuf,clientCount); + } + } + myFile.close(); + + client.println(); + } // Else myfile + else { + EMailSender::Response response; + response.code = F("404"); + response.desc = "Error opening attachments file "+attachments.fileDescriptor[i].url; + response.status = false; + client.flush(); + client.stop(); + + return response; + } // Close myfile + } // Close storageType + +#endif +#ifdef STORAGE_EXTERNAL_ENABLED + if (attachments.fileDescriptor[i].storageType==EMAIL_STORAGE_TYPE_SD){ +#ifdef OPEN_CLOSE_SD + DEBUG_PRINTLN(F("SD Check")); + if (!EXTERNAL_STORAGE_CLASS.exists(attachments.fileDescriptor[i].url.c_str())){ +#if EXTERNAL_STORAGE == STORAGE_SD || EXTERNAL_STORAGE == STORAGE_SDFAT2 || EXTERNAL_STORAGE == STORAGE_SDFAT_RP2040_ESP8266 + if(!EXTERNAL_STORAGE_CLASS.begin(SD_CS_PIN)){ + response.code = F("500"); + response.desc = F("Error on startup SD filesystem!"); + response.status = false; + client.flush(); + client.stop(); + + return response; + } // Close EXTERNAL_STORAGE_CLASS.begin +#elif EXTERNAL_STORAGE == STORAGE_SPIFM + Adafruit_FlashTransport_SPI flashTransport(SS, SPI); // Set CS and SPI interface + Adafruit_SPIFlash flash(&flashTransport); + + if (!EXTERNAL_STORAGE_CLASS.begin(&flash)) { + response.code = F("500"); + response.desc = F("Error on startup SDFAT2 filesystem!"); + response.status = false; + client.flush(); + client.stop(); + + return response; + } +#endif + sdActive = true; + } // Close EXTERNAL_STORAGE_CLASS.exists +#endif + + DEBUG_PRINTLN(F("Open file: ")); + EMAIL_FILE_EX myFile = EXTERNAL_STORAGE_CLASS.open(attachments.fileDescriptor[i].url.c_str()); + + if(myFile) { + myFile.seek(0); + DEBUG_PRINTLN(F("OK")); + if (attachments.fileDescriptor[i].encode64){ + DEBUG_PRINTLN(F("BASE 64")); + encode(&myFile, &client); + }else{ + DEBUG_PRINTLN(F("NORMAL")); + while(myFile.available()) { + clientCount = myFile.read(tBuf,64); + client.write((byte*)tBuf,clientCount); + } + } + myFile.close(); + + client.println(); + } // Else myfile + else { + response.code = F("404"); + response.desc = "Error opening attachments file "+attachments.fileDescriptor[i].url; + response.status = false; + client.flush(); + client.stop(); + + return response; + } // Close myFile + + } // Close storageType==EMAIL_STORAGE_TYPE_SD +#else + if (attachments.fileDescriptor[i].storageType==EMAIL_STORAGE_TYPE_SD){ + response.code = F("500"); + response.desc = F("EMAIL_STORAGE_TYPE_SD not enabled on EMailSenderKey.h"); + response.status = false; + client.flush(); + client.stop(); + + return response; + } +#endif + + } // Close attachment cycle + client.println(); + client.println(F("--frontier--")); +#ifdef STORAGE_EXTERNAL_ENABLED + #ifdef OPEN_CLOSE_SD + if (sdActive){ + DEBUG_PRINTLN(F("SD end")); + #ifndef ARDUINO_ESP8266_RELEASE_2_4_2 + #if EXTERNAL_STORAGE != STORAGE_SDFAT_RP2040_ESP8266 + EXTERNAL_STORAGE_CLASS.end(); + #endif + #endif + DEBUG_PRINTLN(F("SD end 2")); + } + #endif +#endif + +#ifdef STORAGE_INTERNAL_ENABLED + #ifdef OPEN_CLOSE_INTERNAL + #if INTERNAL_STORAGE != STORAGE_SPIFM + if (spiffsActive){ + INTERNAL_STORAGE_CLASS.end(); + DEBUG_PRINTLN(F("SPIFFS END")); + } + #endif +#endif + + +#endif + } // Close attachement enable + +#endif + DEBUG_PRINTLN(F("Message end")); + client.println(F(".")); + + response = awaitSMTPResponse(client, "250", "Sending message error"); + if (!response.status) { + client.flush(); + client.stop(); + return response; + } + + client.println(F("QUIT")); + + response = awaitSMTPResponse(client, "221", "SMTP QUIT error"); + if (!response.status) { + client.flush(); + client.stop(); + return response; + } + + response.status = true; + response.code = F("0"); + response.desc = F("Message sent!"); + + client.flush(); + client.stop(); + + return response; +} + diff --git a/EMailSender.h b/EMailSender.h new file mode 100644 index 00000000..4249da08 --- /dev/null +++ b/EMailSender.h @@ -0,0 +1,525 @@ +/* + * EMail Sender Arduino, esp8266, stm32 and esp32 library to send email + * + * AUTHOR: Renzo Mischianti + * VERSION: 3.0.14 + * + * https://www.mischianti.org/ + * + * The MIT License (MIT) + * + * Copyright (c) 2017 Renzo Mischianti www.mischianti.org All right reserved. + * + * You may copy, alter and reuse this code in any way you like, but please leave + * reference to www.mischianti.org in your comments if you redistribute this code. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef EMailSender_h +#define EMailSender_h + +#include "EMailSenderKey.h" + +#if ARDUINO >= 100 +#include "Arduino.h" +#else +#include "WProgram.h" +#endif + +#if(NETWORK_ESP8266_242 == DEFAULT_EMAIL_NETWORK_TYPE_ESP8266) + #define ARDUINO_ESP8266_RELEASE_2_4_2 + #define DEFAULT_EMAIL_NETWORK_TYPE_ESP8266 NETWORK_ESP8266 +#endif +// +//#if(NETWORK_ESP8266_SD == DEFAULT_EMAIL_NETWORK_TYPE_ESP8266) +// #define ESP8266_GT_2_4_2_SD_STORAGE_SELECTED +// #define DEFAULT_EMAIL_NETWORK_TYPE_ESP8266 NETWORK_ESP8266 +//#endif +#if !defined(EMAIL_NETWORK_TYPE) +// select Network type based + #if defined(ESP8266) || defined(ESP31B) + #define EMAIL_NETWORK_TYPE DEFAULT_EMAIL_NETWORK_TYPE_ESP8266 + #define INTERNAL_STORAGE DEFAULT_INTERNAL_ESP8266_STORAGE + #define EXTERNAL_STORAGE DEFAULT_EXTERNAL_ESP8266_STORAGE + #elif defined(ARDUINO_ARCH_STM32) || defined(__STM32F1__) || defined(__STM32F4__) + #define EMAIL_NETWORK_TYPE DEFAULT_EMAIL_NETWORK_TYPE_STM32 + #define INTERNAL_STORAGE DEFAULT_INTERNAL_STM32_STORAGE + #define EXTERNAL_STORAGE DEFAULT_EXTERNAL_STM32_STORAGE + #elif defined(ESP32) + #define EMAIL_NETWORK_TYPE DEFAULT_EMAIL_NETWORK_TYPE_ESP32 + #define INTERNAL_STORAGE DEFAULT_INTERNAL_ESP32_STORAGE + #define EXTERNAL_STORAGE DEFAULT_EXTERNAL_ESP32_STORAGE + #elif defined(ARDUINO_ARCH_RP2040) + #define EMAIL_NETWORK_TYPE DEFAULT_EMAIL_NETWORK_TYPE_RP2040 + #define INTERNAL_STORAGE DEFAULT_INTERNAL_ARDUINO_RP2040_STORAGE + #define EXTERNAL_STORAGE DEFAULT_EXTERNAL_ARDUINO_RP2040_STORAGE + #elif defined(ARDUINO_ARCH_SAMD) + #define EMAIL_NETWORK_TYPE DEFAULT_EMAIL_NETWORK_TYPE_SAMD + #define INTERNAL_STORAGE DEFAULT_INTERNAL_ARDUINO_SAMD_STORAGE + #define EXTERNAL_STORAGE DEFAULT_EXTERNAL_ARDUINO_SAMD_STORAGE + #elif defined(ARDUINO_ARCH_MBED) + #define EMAIL_NETWORK_TYPE DEFAULT_EMAIL_NETWORK_TYPE_MBED + #define INTERNAL_STORAGE DEFAULT_INTERNAL_ARDUINO_MBED_STORAGE + #define EXTERNAL_STORAGE DEFAULT_EXTERNAL_ARDUINO_MBED_STORAGE + #else + #define EMAIL_NETWORK_TYPE DEFAULT_EMAIL_NETWORK_TYPE_ARDUINO + #define INTERNAL_STORAGE DEFAULT_INTERNAL_ARDUINO_STORAGE + #define EXTERNAL_STORAGE DEFAULT_EXTERNAL_ARDUINO_STORAGE + #endif +#endif + +//#if defined(ESP8266) || defined(ESP31B) +// #ifndef STORAGE_EXTERNAL_FORCE_DISABLE +// #define STORAGE_EXTERNAL_ENABLED +// #endif +// #ifndef STORAGE_INTERNAL_FORCE_DISABLE +// #define STORAGE_INTERNAL_ENABLED +// #endif +//#elif defined(ESP32) +// #ifndef STORAGE_EXTERNAL_FORCE_DISABLE +// #define STORAGE_EXTERNAL_ENABLED +// #endif +// #ifndef STORAGE_INTERNAL_FORCE_DISABLE +// #define STORAGE_INTERNAL_ENABLED +// #endif +//#elif defined(ARDUINO_ARCH_STM32) +// #ifndef STORAGE_EXTERNAL_FORCE_DISABLE +// #define STORAGE_EXTERNAL_ENABLED +// #endif +//#else +// #ifndef STORAGE_EXTERNAL_FORCE_DISABLE +// #define STORAGE_EXTERNAL_ENABLED +// #endif +//#endif + +#ifdef ENABLE_ATTACHMENTS + #if !defined(STORAGE_EXTERNAL_FORCE_DISABLE) && (EXTERNAL_STORAGE != STORAGE_NONE) + #define STORAGE_EXTERNAL_ENABLED + #endif + #if !defined(STORAGE_INTERNAL_FORCE_DISABLE) && (INTERNAL_STORAGE != STORAGE_NONE) + #define STORAGE_INTERNAL_ENABLED + #endif +#endif + +// Includes and defined based on Network Type +#if(EMAIL_NETWORK_TYPE == NETWORK_ESP8266_ASYNC) + +// Note: +// No SSL/WSS support for client in Async mode +// TLS lib need a sync interface! + +#if defined(ESP8266) +#include +#include +#elif defined(ESP32) +#include +#include + +#ifndef FORCE_DISABLE_SSL + #define EMAIL_NETWORK_CLASS WiFiClientSecure +#else + #define EMAIL_NETWORK_CLASS WiFiClient +#endif +//#define EMAIL_NETWORK_SSL_CLASS WiFiClientSecure +#define EMAIL_NETWORK_SERVER_CLASS WiFiServer + +#elif defined(ESP31B) +#include +#else +#error "network type ESP8266 ASYNC only possible on the ESP mcu!" +#endif +// +//#include +//#include +//#define EMAIL_NETWORK_CLASS AsyncTCPbuffer +//#define EMAIL_NETWORK_SERVER_CLASS AsyncServer + +#elif(EMAIL_NETWORK_TYPE == NETWORK_ESP8266 || EMAIL_NETWORK_TYPE == NETWORK_ESP8266_242) + +#if !defined(ESP8266) && !defined(ESP31B) +#error "network type ESP8266 only possible on the ESP mcu!" +#endif + +#ifdef ESP8266 +#include +#else +#include +#endif +#ifndef FORCE_DISABLE_SSL + #define EMAIL_NETWORK_CLASS WiFiClientSecure +#else + #define EMAIL_NETWORK_CLASS WiFiClient +#endif +//#define EMAIL_NETWORK_SSL_CLASS WiFiClientSecure +#define EMAIL_NETWORK_SERVER_CLASS WiFiServer + +#elif(EMAIL_NETWORK_TYPE == NETWORK_W5100 || EMAIL_NETWORK_TYPE == NETWORK_ETHERNET_ENC) + +#include +#include +#define EMAIL_NETWORK_CLASS EthernetClient +#define EMAIL_NETWORK_SERVER_CLASS EthernetServer + +#elif(EMAIL_NETWORK_TYPE == NETWORK_ETHERNET_GENERIC) + +#include +#include +#define EMAIL_NETWORK_CLASS EthernetClient +#define EMAIL_NETWORK_SERVER_CLASS EthernetServer + +#elif(EMAIL_NETWORK_TYPE == NETWORK_ENC28J60 || EMAIL_NETWORK_TYPE == NETWORK_UIPETHERNET) + +#include + +#define EMAIL_NETWORK_CLASS UIPClient +#define EMAIL_NETWORK_SERVER_CLASS UIPServer + +//#include +//UIPClient base_client; +//SSLClient client(base_client, TAs, (size_t)TAs_NUM, A5); +// +//#define EMAIL_NETWORK_CLASS SSLClient +//#define EMAIL_NETWORK_SERVER_CLASS UIPServer + +#elif(EMAIL_NETWORK_TYPE == NETWORK_ESP32) + +#include +#include +#ifndef FORCE_DISABLE_SSL + #define EMAIL_NETWORK_CLASS WiFiClientSecure +#else + #define EMAIL_NETWORK_CLASS WiFiClient +#endif +//#define EMAIL_NETWORK_SSL_CLASS WiFiClientSecure +#define EMAIL_NETWORK_SERVER_CLASS WiFiServer + +#elif(EMAIL_NETWORK_TYPE == NETWORK_ESP32_ETH) + +#include +#define EMAIL_NETWORK_CLASS WiFiClient +#define EMAIL_NETWORK_SERVER_CLASS WiFiServer + +#elif(EMAIL_NETWORK_TYPE == NETWORK_ETHERNET_LARGE) + +#include +#include +#define EMAIL_NETWORK_CLASS EthernetClient +#define EMAIL_NETWORK_SERVER_CLASS EthernetServer + +#elif(EMAIL_NETWORK_TYPE == NETWORK_ETHERNET_2) + +#include +#include +#define EMAIL_NETWORK_CLASS EthernetClient +#define EMAIL_NETWORK_SERVER_CLASS EthernetServer + +#elif(EMAIL_NETWORK_TYPE == NETWORK_ETHERNET_STM) + +#include +#include +#define EMAIL_NETWORK_CLASS EthernetClient +#define EMAIL_NETWORK_SERVER_CLASS EthernetServer + +#elif(EMAIL_NETWORK_TYPE == NETWORK_WiFiNINA) + +#include +#ifndef FORCE_DISABLE_SSL + #define EMAIL_NETWORK_CLASS WiFiSSLClient +#else + #define EMAIL_NETWORK_CLASS WiFiClient +#endif +//#define EMAIL_NETWORK_SSL_CLASS WiFiSSLClient +#define EMAIL_NETWORK_SERVER_CLASS WiFiServer +#elif(EMAIL_NETWORK_TYPE == NETWORK_MBED_WIFI) + +#include +#include + +#ifndef FORCE_DISABLE_SSL + #define EMAIL_NETWORK_CLASS WiFiSSLClient +#else + #define EMAIL_NETWORK_CLASS WiFiClient +#endif +//#define EMAIL_NETWORK_SSL_CLASS WiFiSSLClient +#define EMAIL_NETWORK_SERVER_CLASS WiFiServer + +#else +#error "no network type selected!" +#endif + +#ifdef STORAGE_INTERNAL_ENABLED +// #define FS_NO_GLOBALS + #if (INTERNAL_STORAGE == STORAGE_SPIFFS) + #if defined(ESP32) + #include + #define INTERNAL_STORAGE_CLASS SPIFFS + + #define EMAIL_FILE_READ "r" + #elif defined(ESP8266) + #ifdef ARDUINO_ESP8266_RELEASE_2_4_2 + #define DIFFERENT_FILE_MANAGE + #endif + #include "FS.h" + + #define INTERNAL_STORAGE_CLASS SPIFFS + #define EMAIL_FILE_READ "r" + #endif + #define EMAIL_FILE fs::File + #elif (INTERNAL_STORAGE == STORAGE_LITTLEFS) + #if defined(ESP32) + #if ESP_ARDUINO_VERSION_MAJOR >= 2 + #include "FS.h" + #include "LittleFS.h" + #define INTERNAL_STORAGE_CLASS LittleFS + #else + #include "LITTLEFS.h" + #define INTERNAL_STORAGE_CLASS LITTLEFS + #endif + #else + #include "LittleFS.h" + #define INTERNAL_STORAGE_CLASS LittleFS + #endif + #define EMAIL_FILE_READ "r" + #define EMAIL_FILE fs::File + #elif (INTERNAL_STORAGE == STORAGE_FFAT) + #include "FFat.h" + #define INTERNAL_STORAGE_CLASS FFat + #define EMAIL_FILE_READ 'r' + #define EMAIL_FILE fs::File + #elif (INTERNAL_STORAGE == STORAGE_SPIFM) + #include + + #include "SdFat.h" + #include "Adafruit_SPIFlash.h" + + #define INTERNAL_STORAGE_CLASS fatfs + extern FatFileSystem INTERNAL_STORAGE_CLASS; + + #define EMAIL_FILE_READ O_READ + #define EMAIL_FILE File + #endif +#endif + +#ifdef STORAGE_EXTERNAL_ENABLED + #include + #if (EXTERNAL_STORAGE == STORAGE_SDFAT_RP2040_ESP8266) + #include + #include + + #define EXTERNAL_STORAGE_CLASS sd + extern SdFat EXTERNAL_STORAGE_CLASS; + + #define EMAIL_FILE_READ_EX FILE_READ + #define EMAIL_FILE_EX File32 + + #define DIFFERENT_FILE_MANAGE + #elif (EXTERNAL_STORAGE == STORAGE_SDFAT2) + #include + #include + + #define EXTERNAL_STORAGE_CLASS sd + extern SdFat EXTERNAL_STORAGE_CLASS; + + #define EMAIL_FILE_READ_EX FILE_READ + #define EMAIL_FILE_EX FsFile + + #define DIFFERENT_FILE_MANAGE + #elif (EXTERNAL_STORAGE == STORAGE_SD) + #include + + #define EXTERNAL_STORAGE_CLASS SD + #define EMAIL_FILE_READ_EX 'r' + + #define EMAIL_FILE_EX File + #endif +#endif + +//#ifdef EMAIL_NETWORK_SSL_CLASS +// #ifndef FORCE_DISABLE_SSL +// #define EMAIL_NETWORK_CLASS WiFiClientSecure +// #else +// #define EMAIL_NETWORK_CLASS WiFiClient +// #endif +//#endif + +#define OPEN_CLOSE_INTERNAL +#define OPEN_CLOSE_SD + +// Setup debug printing macros. +#ifdef EMAIL_SENDER_DEBUG + #define DEBUG_PRINT(...) { DEBUG_PRINTER.print(__VA_ARGS__); } + #define DEBUG_PRINTLN(...) { DEBUG_PRINTER.println(__VA_ARGS__); } +#else + #define DEBUG_PRINT(...) {} + #define DEBUG_PRINTLN(...) {} +#endif + +// Debug level for SSLClient +#ifndef EMAIL_SENDER_SSL_CLIENT_DEBUG + #define EMAIL_SENDER_SSL_CLIENT_DEBUG 2 +#endif + +class EMailSender { +public: + EMailSender(const char* email_login, const char* email_password, const char* email_from, const char* name_from, const char* smtp_server, uint16_t smtp_port ); + EMailSender(const char* email_login, const char* email_password, const char* email_from, const char* smtp_server, uint16_t smtp_port); + EMailSender(const char* email_login, const char* email_password, const char* email_from, const char* name_from ); + EMailSender(const char* email_login, const char* email_password, const char* email_from); + EMailSender(const char* email_login, const char* email_password); + +#define STORAGE_SPIFFS (1) +#define STORAGE_LITTLEFS (2) +#define STORAGE_FFAT (3) +#define STORAGE_SPIFM (5) // Libraries Adafruit_SPIFlash and SdFat-Adafruit-Fork +// EXTERNAL STORAGE +#define STORAGE_SD (4) +#define STORAGE_SDFAT2 (6) // Library SdFat version >= 2.0.2 +#define STORAGE_SDFAT_RP2040_ESP8266 (7) // Library ESP8266SdFat on Raspberry Pi Pico + + + enum StorageType { + EMAIL_STORAGE_TYPE_SPIFFS, + EMAIL_STORAGE_TYPE_LITTLE_FS, + EMAIL_STORAGE_TYPE_FFAT, + EMAIL_STORAGE_TYPE_SPIFM, + EMAIL_STORAGE_TYPE_SD + }; + +#define MIME_TEXT_HTML F("text/html") +#define MIME_TEXT_PLAIN F("text/plain") +#define MIME_IMAGE_JPG F("image/jpg") +#define MIME_IMAGE_PNG F("image/png") + + typedef struct { + String mime = "text/html"; + String subject; + String message; + } EMailMessage; + +#ifdef ENABLE_ATTACHMENTS + typedef struct { + StorageType storageType = EMAIL_STORAGE_TYPE_SD; + String mime; + bool encode64 = false; + String filename; + String url; + } FileDescriptior; + + typedef struct { + byte number; + FileDescriptior *fileDescriptor; + } Attachments; +#else + typedef struct { + char* noop1; + char* noop2; + } Attachments; +#endif + + typedef struct { + String code; + String desc; + bool status = false; + } Response; + + void setSMTPPort(uint16_t smtp_port); + void setSMTPServer(const char* smtp_server); + void setEMailLogin(const char* email_login); + void setEMailFrom(const char* email_from); + void setNameFrom(const char* name_from); + void setEMailPassword(const char* email_password); + + EMailSender::Response send(char* to[], byte sizeOfTo, EMailMessage &email, Attachments att = {0, 0}); + EMailSender::Response send(char* to[], byte sizeOfTo, byte sizeOfCc, EMailMessage &email, Attachments att = {0, 0}); + EMailSender::Response send(char* to[], byte sizeOfTo, byte sizeOfCc, byte sizeOfCCn, EMailMessage &email, Attachments att = {0, 0}); + + EMailSender::Response send(const char* to, EMailMessage &email, Attachments att = {0, 0}); + EMailSender::Response send(const char* to[], byte sizeOfTo, EMailMessage &email, Attachments att = {0, 0}); + EMailSender::Response send(const char* to[], byte sizeOfTo, byte sizeOfCc, EMailMessage &email, Attachments att = {0, 0}); + EMailSender::Response send(const char* to[], byte sizeOfTo, byte sizeOfCc, byte sizeOfCCn, EMailMessage &email, Attachments att = {0, 0}); + + EMailSender::Response send(String to, EMailMessage &email, Attachments att = {0, 0}); + EMailSender::Response send(String to[], byte sizeOfTo, EMailMessage &email, Attachments att = {0, 0}); + EMailSender::Response send(String to[], byte sizeOfTo, byte sizeOfCc, EMailMessage &email, Attachments att = {0, 0}); + EMailSender::Response send(String to[], byte sizeOfTo, byte sizeOfCc, byte sizeOfCCn, EMailMessage &email, Attachments att = {0, 0}); + + void setIsSecure(bool isSecure = false); + + void setUseAuth(bool useAuth = true) { + this->useAuth = useAuth; + } + + void setPublicIpDescriptor(const char *publicIpDescriptor = "mischianti") { + publicIPDescriptor = publicIpDescriptor; + } + + void setEHLOCommand(bool useEHLO = false) { + this->useEHLO = useEHLO; + } + + void setSASLLogin(bool isSASLLogin = false) { + this->isSASLLogin = isSASLLogin; + } + +#if defined(ESP32) + // Conditional - as it relies on considerable crypto infra. + void setCramMD5Login(bool onoff= false) { + this->isCramMD5Login = onoff; + } +#endif + + void setAdditionalResponseLineOnConnection(uint8_t numLines = 0) { + this->additionalResponseLineOnConnection = numLines; + } + void setAdditionalResponseLineOnHELO(uint8_t numLines = 0) { + this->additionalResponseLineOnHELO = numLines; + } + +private: + uint16_t smtp_port = 465; + char* smtp_server = strdup("smtp.gmail.com"); + char* email_login = 0; + char* email_from = 0; + char* name_from = 0; + char* email_password = 0; + + const char* publicIPDescriptor = "mischianti"; + + bool isSecure = false; + + bool useEHLO = false; + bool isSASLLogin = false; + + bool useAuth = true; + bool isCramMD5Login = false; + + String _serverResponce; + + uint8_t additionalResponseLineOnConnection = 0; + uint8_t additionalResponseLineOnHELO = 0; + +#ifdef SSLCLIENT_WRAPPER + Response awaitSMTPResponse(SSLClient &client, const char* resp = "", const char* respDesc = "", uint16_t timeOut = 10000); +#else + Response awaitSMTPResponse(EMAIL_NETWORK_CLASS &client, const char* resp = "", const char* respDesc = "", uint16_t timeOut = 10000); +#endif +}; + +#endif diff --git a/EMailSenderKey.h b/EMailSenderKey.h new file mode 100644 index 00000000..75694b50 --- /dev/null +++ b/EMailSenderKey.h @@ -0,0 +1,157 @@ +/* + * EMail Sender Arduino, esp8266, stm32 and esp32 library to send email + * + * AUTHOR: Renzo Mischianti + * VERSION: 3.0.14 + * + * https://www.mischianti.org/ + * + * The MIT License (MIT) + * + * Copyright (c) 2017 Renzo Mischianti www.mischianti.org All right reserved. + * + * You may copy, alter and reuse this code in any way you like, but please leave + * reference to www.mischianti.org in your comments if you redistribute this code. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#ifndef EMailSenderKey_h +#define EMailSenderKey_h + +// Uncomment if you use esp8266 core <= 2.4.2 +//#define ARDUINO_ESP8266_RELEASE_2_4_2 + +// If you want disable attachments and save memory comment this define +//#define ENABLE_ATTACHMENTS + +// Uncomment to enable printing out nice debug messages. +//#define EMAIL_SENDER_DEBUG + +// Define where debug output will be printed. +#define DEBUG_PRINTER Serial + +#define STORAGE_NONE (0) +// INTERNAL STORAGE +#define STORAGE_SPIFFS (1) +#define STORAGE_LITTLEFS (2) +#define STORAGE_FFAT (3) +#define STORAGE_SPIFM (5) // Libraries Adafruit_SPIFlash and SdFat-Adafruit-Fork +// EXTERNAL STORAGE +#define STORAGE_SD (4) +#define STORAGE_SDFAT2 (6) // Library SdFat version >= 2.0.2 +#define STORAGE_SDFAT_RP2040_ESP8266 (7) // Library ESP8266SdFat on Raspberry Pi Pico + +#define NETWORK_ESP8266_ASYNC (0) +#define NETWORK_ESP8266 (1) +#define NETWORK_ESP8266_242 (6) +#define NETWORK_W5100 (2) +#define NETWORK_ETHERNET (2) // Standard Arduino Ethernet library +#define NETWORK_ENC28J60 (3) +#define NETWORK_ESP32 (4) +#define NETWORK_RP2040_WIFI (4) +#define NETWORK_ESP32_ETH (5) +#define NETWORK_WiFiNINA (7) +#define NETWORK_ETHERNET_LARGE (8) +#define NETWORK_ETHERNET_ENC (9) +#define NETWORK_ETHERNET_STM (10) +#define NETWORK_UIPETHERNET (11) +#define NETWORK_ETHERNET_2 (12) +#define NETWORK_ETHERNET_GENERIC (13) // Ethernet generic +#define NETWORK_MBED_WIFI (14) // Arduino GIGA R1 WiFi + +// if you want force disable SSL if present uncomment this define +// #define FORCE_DISABLE_SSL + +// If you want add a wrapper to emulate SSL over Client like EthernetClient +// #define SSLCLIENT_WRAPPER + +// esp8266 microcontrollers configuration +#ifndef DEFAULT_EMAIL_NETWORK_TYPE_ESP8266 + #define DEFAULT_EMAIL_NETWORK_TYPE_ESP8266 NETWORK_ESP8266 + #define DEFAULT_INTERNAL_ESP8266_STORAGE STORAGE_LITTLEFS + #define DEFAULT_EXTERNAL_ESP8266_STORAGE STORAGE_NONE +#endif +// esp32 microcontrollers configuration +#ifndef DEFAULT_EMAIL_NETWORK_TYPE_ESP32 + #define DEFAULT_EMAIL_NETWORK_TYPE_ESP32 NETWORK_ESP32 + #define DEFAULT_INTERNAL_ESP32_STORAGE STORAGE_SPIFFS + #define DEFAULT_EXTERNAL_ESP32_STORAGE STORAGE_SD +#endif +// stm32 microcontrollers configuration +#ifndef DEFAULT_EMAIL_NETWORK_TYPE_STM32 + #define DEFAULT_EMAIL_NETWORK_TYPE_STM32 NETWORK_W5100 + #define DEFAULT_INTERNAL_STM32_STORAGE STORAGE_NONE + #define DEFAULT_EXTERNAL_STM32_STORAGE STORAGE_SDFAT2 +#endif +// Arduino microcontrollers configuration +#ifndef DEFAULT_EMAIL_NETWORK_TYPE_ARDUINO + #define DEFAULT_EMAIL_NETWORK_TYPE_ARDUINO NETWORK_W5100 + #define DEFAULT_INTERNAL_ARDUINO_STORAGE STORAGE_NONE + #define DEFAULT_EXTERNAL_ARDUINO_STORAGE STORAGE_SD +#endif +// Arduino SAMD microcontrollers configuration +#ifndef DEFAULT_EMAIL_NETWORK_TYPE_ARDUINO_SAMD + #define DEFAULT_EMAIL_NETWORK_TYPE_SAMD NETWORK_WiFiNINA + #define DEFAULT_INTERNAL_ARDUINO_SAMD_STORAGE STORAGE_NONE + #define DEFAULT_EXTERNAL_ARDUINO_SAMD_STORAGE STORAGE_SD +#endif +// Raspberry Pi Pico (rp2040) configuration +#ifndef DEFAULT_EMAIL_NETWORK_TYPE_RP2040 + #define DEFAULT_EMAIL_NETWORK_TYPE_RP2040 NETWORK_RP2040_WIFI + #define DEFAULT_INTERNAL_ARDUINO_RP2040_STORAGE STORAGE_LITTLEFS + #define DEFAULT_EXTERNAL_ARDUINO_RP2040_STORAGE STORAGE_NONE +#endif +// Arduino MBED microcontrollers configuration LIKE Arduino GIGA +#ifndef DEFAULT_EMAIL_NETWORK_TYPE_ARDUINO_MBED + #define DEFAULT_EMAIL_NETWORK_TYPE_MBED NETWORK_MBED_WIFI + #define DEFAULT_INTERNAL_ARDUINO_MBED_STORAGE STORAGE_NONE + #define DEFAULT_EXTERNAL_ARDUINO_MBED_STORAGE STORAGE_SD +#endif + +#ifdef SSLCLIENT_WRAPPER + // Generate the trust_anchors.h with this online generator + // https://openslab-osu.github.io/bearssl-certificate-utility/ + /** + * For Ethernet w5x00 card you must follow the step given + * from this link https://github.com/OPEnSLab-OSU/SSLClient#sslclient-with-ethernet + * you can modify Ethernet library or use directly this modified one + * https://github.com/OPEnSLab-OSU/EthernetLarge + * + * For enc28j60 use EthernetENC available from library manager or + * https://github.com/jandrassy/EthernetENC + */ + #define ANALOG_PIN A7 + #include + #include "trust_anchors.h" + + #define PUT_OUTSIDE_SCOPE_CLIENT_DECLARATION +#endif + +// This two value can take a number 1..20 or you can specify 'a' +// but with auto you can lost specified response error +#define DEFAULT_EHLO_RESPONSE_COUNT 6 +#define DEFAULT_CONNECTION_RESPONSE_COUNT 0 + +#define SD_CS_PIN SS +#define SPIFM_CS_PIN SS + +//#define STORAGE_INTERNAL_FORCE_DISABLE +//#define STORAGE_EXTERNAL_FORCE_DISABLE + +#endif diff --git a/main.cpp b/main.cpp index 57ccd7fb..d8c2dba1 100644 --- a/main.cpp +++ b/main.cpp @@ -30,12 +30,13 @@ #include "opensprinkler_server.h" #include "mqtt.h" #include "main.h" -#if defined(ARUDINO) - #include "EmailSender.h" +#include "EmailSender.h" +#include "smtp.h" + +#if defined(ARDUINO) #else - #include "smtp.h" #define MAIL_CONNECTION_SECURITY SMTP_SECURITY_TLS - #define MAIL_FLAGS (SMTP_DEBUG | SMTP_NO_CERT_VERIFY) + #define MAIL_FLAGS (smtp_flag)(SMTP_NO_CERT_VERIFY) #define MAIL_AUTH SMTP_AUTH_PLAIN #endif @@ -1611,7 +1612,7 @@ void push_message(int type, uint32_t lval, float fval, const char* sval) { break; case NOTIFY_REBOOT: - #if defined(ARUDINO) + #if defined(ARDUINO) if(!delayed){ delayed = true; return; diff --git a/platformio.ini b/platformio.ini index 8e68b6be..8af89200 100644 --- a/platformio.ini +++ b/platformio.ini @@ -22,7 +22,6 @@ lib_deps = https://github.com/ThingPulse/esp8266-oled-ssd1306/archive/4.2.0.zip knolleary/PubSubClient @ ^2.8 https://github.com/OpenThingsIO/OpenThings-Framework-Firmware-Library @ ^0.1.0 - xreef/EMailSender@^3.0.14 build_src_filter = +<*> - upload_speed = 460800 monitor_speed = 115200 diff --git a/smtp.c b/smtp.c index 40e77b49..c256a4f6 100644 --- a/smtp.c +++ b/smtp.c @@ -22,6 +22,9 @@ * . */ +#if defined(ARDUINO) +#else + #if defined(_WIN32) || defined(WIN32) # define SMTP_IS_WINDOWS #endif /* SMTP_IS_WINDOWS */ @@ -385,7 +388,7 @@ smtp_str_getdelimfd_read(struct str_getdelimfd *const gdfd, struct smtp *smtp; long bytes_read; - smtp = gdfd->user_data; + smtp = (struct smtp*)(gdfd->user_data); if(smtp_str_getdelimfd_read_timeout(smtp) != SMTP_STATUS_OK){ return -1; @@ -468,7 +471,7 @@ smtp_str_getdelimfd_set_line_and_buf(struct str_getdelimfd *const gdfd, if(smtp_si_add_size_t(copy_len, 1, ©_len_inc) || smtp_si_add_size_t((size_t)gdfd->_buf, copy_len_inc, NULL) || smtp_si_sub_size_t(gdfd->_buf_len, copy_len, &nbytes_to_shift) || - (gdfd->line = calloc(1, copy_len_inc)) == NULL){ + (gdfd->line = (char*)(calloc(1, copy_len_inc))) == NULL){ return -1; } memcpy(gdfd->line, gdfd->_buf, copy_len); @@ -560,7 +563,7 @@ smtp_str_getdelimfd(struct str_getdelimfd *const gdfd){ &buf_sz_new)){ return smtp_str_getdelimfd_throw_error(gdfd); } - buf_new = realloc(gdfd->_buf, buf_sz_new); + buf_new = (char*)(realloc(gdfd->_buf, buf_sz_new)); if(buf_new == NULL){ return smtp_str_getdelimfd_throw_error(gdfd); } @@ -657,7 +660,7 @@ smtp_strdup(const char *s){ dup = NULL; errno = ENOMEM; } - else if((dup = malloc(dup_len)) != NULL){ + else if((dup = (char*)(malloc(dup_len))) != NULL){ memcpy(dup, s, dup_len); } return dup; @@ -731,7 +734,7 @@ smtp_str_replace(const char *const search, /* snew_sz += snew_sz + slen + replace_len + 1 */ if(smtp_si_add_size_t(snew_sz_dup, slen_inc, &snew_sz) || smtp_si_add_size_t(snew_sz, replace_len, &snew_sz) || - (stmp = realloc(snew, snew_sz)) == NULL){ + (stmp = (char*)(realloc(snew, snew_sz))) == NULL){ free(snew); return NULL; } @@ -746,7 +749,7 @@ smtp_str_replace(const char *const search, /* snew_sz += snew_sz + slen + snew_len + 1 */ if(smtp_si_add_size_t(snew_sz_dup, slen, &snew_sz) || smtp_si_add_size_t(snew_sz, snew_len_inc, &snew_sz) || - (stmp = realloc(snew, snew_sz)) == NULL){ + (stmp = (char*)(realloc(snew, snew_sz))) == NULL){ free(snew); return NULL; } @@ -845,7 +848,7 @@ smtp_base64_encode(const char *const buf, return NULL; } b64_sz = (4 * buflen / 3) + 1 + 2 + 1; - if((b64 = calloc(1, b64_sz)) == NULL){ + if((b64 = (char*)(calloc(1, b64_sz))) == NULL){ return NULL; } @@ -998,7 +1001,7 @@ smtp_base64_decode(const char *const buf, } if(smtp_si_add_size_t(buf_len, 1, &buf_len_inc) || - (b64_decode = calloc(1, buf_len_inc)) == NULL){ + (b64_decode = (unsigned char*)(calloc(1, buf_len_inc))) == NULL){ return SIZE_MAX; } @@ -1042,7 +1045,7 @@ smtp_bin2hex(const unsigned char *const s, smtp_si_add_size_t(alloc_sz, 1, &alloc_sz)){ return NULL; } - if((snew = malloc(alloc_sz)) == NULL){ + if((snew = (char*)(malloc(alloc_sz))) == NULL){ return NULL; } @@ -1276,7 +1279,7 @@ smtp_fold_whitespace(const char *const s, if(smtp_si_add_size_t(bufsz, ws_offset, &bufsz) || smtp_si_add_size_t(bufsz, end_slen, &bufsz) || smtp_si_add_size_t(bufsz, 1, &bufsz) || - (buf_new = realloc(buf, bufsz)) == NULL){ + (buf_new = (char*)(realloc(buf, bufsz))) == NULL){ free(buf); return NULL; } @@ -1343,7 +1346,7 @@ smtp_chunk_split(const char *const s, smtp_si_add_size_t(bodylen, 1, &bodylen_inc) || smtp_si_mul_size_t(endlen_inc, bodylen / chunklen + 1, &snewlen) || smtp_si_add_size_t(snewlen, bodylen_inc, &snewlen) || - (snew = calloc(1, snewlen)) == NULL){ + (snew = (char*)(calloc(1, snewlen))) == NULL){ return NULL; } @@ -1401,7 +1404,7 @@ smtp_ffile_get_contents(FILE *stream, do{ if(smtp_si_add_size_t(bufsz, BUFSZ_INCREMENT, &bufsz_inc) || - (new_buf = realloc(read_buf, bufsz_inc)) == NULL){ + (new_buf = (char*)(realloc(read_buf, bufsz_inc))) == NULL){ free(read_buf); return NULL; } @@ -1676,7 +1679,7 @@ smtp_puts_terminate(struct smtp *const smtp, slen = strlen(s); if(smtp_si_add_size_t(slen, 3, &allocsz) || - (line = malloc(allocsz)) == NULL){ + (line = (char*)(malloc(allocsz))) == NULL){ return smtp_status_code_set(smtp, SMTP_STATUS_NOMEM); } concat = smtp_stpcpy(line, s); @@ -1908,7 +1911,7 @@ smtp_auth_plain(struct smtp *const smtp, /* login_len = 1 + user_len + 1 + pass_len */ if(smtp_si_add_size_t(user_len, pass_len, &login_len) || smtp_si_add_size_t(login_len, 2, &login_len) || - (login_str = malloc(login_len)) == NULL){ + (login_str = (char*)(malloc(login_len))) == NULL){ return -1; } login_str[0] = '\0'; @@ -1926,7 +1929,7 @@ smtp_auth_plain(struct smtp *const smtp, /* (3) */ login_b64_len = strlen(login_b64); if(smtp_si_add_size_t(login_b64_len, 14, &login_b64_len) || - (login_send = malloc(login_b64_len)) == NULL){ + (login_send = (char*)(malloc(login_b64_len))) == NULL){ free(login_b64); return -1; } @@ -1980,7 +1983,7 @@ smtp_auth_login(struct smtp *const smtp, /* (2) */ b64_user_len = strlen(b64_user); if(smtp_si_add_size_t(b64_user_len, 14, &b64_user_len) || - (login_str = malloc(b64_user_len)) == NULL){ + (login_str = (char*)(malloc(b64_user_len))) == NULL){ free(b64_user); return -1; } @@ -2098,7 +2101,7 @@ smtp_auth_cram_md5(struct smtp *const smtp, /* auth_concat_len = user_len + 1 + challenge_hex_len + 1 */ if(smtp_si_add_size_t(user_len, challenge_hex_len, &auth_concat_len) || smtp_si_add_size_t(auth_concat_len, 2, &auth_concat_len) || - (auth_concat = malloc(auth_concat_len)) == NULL){ + (auth_concat = (char*)(malloc(auth_concat_len))) == NULL){ free(challenge_hex); return -1; } @@ -2335,8 +2338,8 @@ smtp_header_cmp_key(const void *const v1, const char *key; const struct smtp_header *header2; - key = v1; - header2 = v2; + key = (char*)(v1); + header2 = (smtp_header*)(v2); return strcmp(key, header2->key); } @@ -2421,7 +2424,7 @@ smtp_print_mime_header_and_body(struct smtp *const smtp, if(smtp_si_add_size_t(data_double_dot_len, MIME_TEXT_BUFSZ, &data_double_dot_len) || - (data_header_and_body = malloc(data_double_dot_len)) == NULL){ + (data_header_and_body = (char*)(malloc(data_double_dot_len))) == NULL){ return smtp_status_code_set(smtp, SMTP_STATUS_NOMEM); } @@ -2481,7 +2484,7 @@ smtp_print_mime_attachment(struct smtp *const smtp, smtp_si_add_size_t(bufsz, SMTP_MIME_BOUNDARY_LEN + MIME_TEXT_BUFSZ, &bufsz) || - (mime_attach_text = malloc(bufsz)) == NULL){ + (mime_attach_text = (char*)(malloc(bufsz))) == NULL){ return smtp_status_code_set(smtp, SMTP_STATUS_NOMEM); } @@ -2640,7 +2643,7 @@ smtp_print_header(struct smtp *const smtp, /* concat_len = key_len + 2 + value_len + 1 */ if(smtp_si_add_size_t(key_len, value_len, &concat_len) || smtp_si_add_size_t(concat_len, 3, &concat_len) || - (header_concat = malloc(concat_len)) == NULL){ + (header_concat = (char*)(malloc(concat_len))) == NULL){ return smtp_status_code_set(smtp, SMTP_STATUS_NOMEM); } concat = smtp_stpcpy(header_concat, header->key); @@ -2705,8 +2708,8 @@ smtp_append_address_to_header(struct smtp *const smtp, if(smtp_si_add_size_t(header_value_sz, name_slen, &header_value_sz) || smtp_si_add_size_t(header_value_sz, email_slen, &header_value_sz) || smtp_si_add_size_t(header_value_sz, 3 + 3 + 1 + 1, &header_value_sz)|| - (header_value_new = realloc(header_value, - header_value_sz)) == NULL){ + (header_value_new = (char*)(realloc(header_value, + header_value_sz))) == NULL){ free(header_value); return smtp_status_code_set(smtp, SMTP_STATUS_NOMEM); } @@ -2764,7 +2767,7 @@ smtp_mail_envelope_header(struct smtp *const smtp, /* bufsz = 14 + email_len + SMTPUTF8_LEN + 1 */ if(smtp_si_add_size_t(email_len, SMTPUTF8_LEN + 14 + 1, &bufsz) || - (envelope_address = malloc(bufsz)) == NULL){ + (envelope_address = (char*)(malloc(bufsz))) == NULL){ return smtp_status_code_set(smtp, SMTP_STATUS_NOMEM); } @@ -2804,8 +2807,8 @@ smtp_header_cmp(const void *v1, const struct smtp_header *header1; const struct smtp_header *header2; - header1 = v1; - header2 = v2; + header1 = (smtp_header*)(v1); + header2 = (smtp_header*)(v2); return strcmp(header1->key, header2->key); } @@ -2992,7 +2995,7 @@ smtp_open(const char *const server, struct smtp **smtp){ struct smtp *snew; - if((snew = calloc(1, sizeof(**smtp))) == NULL){ + if((snew = (struct smtp*)(calloc(1, sizeof(**smtp)))) == NULL){ *smtp = &g_smtp_error; return smtp_status_code_get(*smtp); } @@ -3295,10 +3298,10 @@ smtp_header_add(struct smtp *const smtp, } if(smtp_si_add_size_t(smtp->num_headers, 1, &num_headers_inc) || - (new_header_list = smtp_reallocarray( + (new_header_list = (smtp_header*)(smtp_reallocarray( smtp->header_list, num_headers_inc, - sizeof(*smtp->header_list))) == NULL){ + sizeof(*smtp->header_list)))) == NULL){ return smtp_status_code_set(smtp, SMTP_STATUS_NOMEM); } smtp->header_list = new_header_list; @@ -3368,9 +3371,9 @@ smtp_address_add(struct smtp *const smtp, return smtp_status_code_set(smtp, SMTP_STATUS_NOMEM); } - new_address_list = smtp_reallocarray(smtp->address_list, + new_address_list = (smtp_address*)(smtp_reallocarray(smtp->address_list, num_address_inc, - sizeof(*new_address_list)); + sizeof(*new_address_list))); if(new_address_list == NULL){ return smtp_status_code_set(smtp, SMTP_STATUS_NOMEM); } @@ -3477,21 +3480,21 @@ smtp_attachment_add_mem(struct smtp *const smtp, } if(datasz == SIZE_MAX){ - datasz = strlen(data); + datasz = strlen((char*)(data)); } if(smtp_si_add_size_t(smtp->num_attachment, 1, &num_attachment_inc) || - (new_attachment_list = smtp_reallocarray( + (new_attachment_list = (smtp_attachment*)(smtp_reallocarray( smtp->attachment_list, num_attachment_inc, - sizeof(*new_attachment_list))) == NULL){ + sizeof(*new_attachment_list)))) == NULL){ return smtp_status_code_set(smtp, SMTP_STATUS_NOMEM); } smtp->attachment_list = new_attachment_list; new_attachment = &new_attachment_list[smtp->num_attachment]; new_attachment->name = smtp_strdup(name); - b64_encode = smtp_base64_encode(data, datasz); + b64_encode = smtp_base64_encode((char*)(data), datasz); if(new_attachment->name == NULL || b64_encode == NULL){ free(new_attachment->name); free(b64_encode); @@ -3526,3 +3529,4 @@ smtp_attachment_clear_all(struct smtp *const smtp){ smtp->num_attachment = 0; } +#endif \ No newline at end of file diff --git a/smtp.h b/smtp.h index f035cc5b..5316801d 100644 --- a/smtp.h +++ b/smtp.h @@ -1,3 +1,5 @@ +#if defined(ARDUINO) +#else /** * @file * @brief SMTP client library. @@ -633,3 +635,4 @@ struct str_getdelimfd{ #endif /* SMTP_H */ +#endif \ No newline at end of file From 47993ec8c7fdcca1715cb54b4a8ec54d037fd85a Mon Sep 17 00:00:00 2001 From: Ray Date: Fri, 28 Jun 2024 14:06:35 -0400 Subject: [PATCH 08/61] relocate some macros, change option name nfe back to ife for backward compatibility --- OpenSprinkler.cpp | 2 +- main.cpp | 9 +-------- opensprinkler_server.cpp | 7 +++++-- smtp.c | 1 + smtp.h | 4 ++++ 5 files changed, 12 insertions(+), 11 deletions(-) diff --git a/OpenSprinkler.cpp b/OpenSprinkler.cpp index 953139ad..74b27da1 100644 --- a/OpenSprinkler.cpp +++ b/OpenSprinkler.cpp @@ -154,7 +154,7 @@ const char iopt_json_names[] PROGMEM = "dns3\0" "dns4\0" "sar\0\0" - "nfe\0\0" + "ife\0\0" "sn1t\0" "sn1o\0" "sn2t\0" diff --git a/main.cpp b/main.cpp index d8c2dba1..bbdeaea1 100644 --- a/main.cpp +++ b/main.cpp @@ -30,16 +30,9 @@ #include "opensprinkler_server.h" #include "mqtt.h" #include "main.h" -#include "EmailSender.h" +#include "EMailSender.h" #include "smtp.h" -#if defined(ARDUINO) -#else - #define MAIL_CONNECTION_SECURITY SMTP_SECURITY_TLS - #define MAIL_FLAGS (smtp_flag)(SMTP_NO_CERT_VERIFY) - #define MAIL_AUTH SMTP_AUTH_PLAIN -#endif - #define str(s) #s #define xstr(s) str(s) diff --git a/opensprinkler_server.cpp b/opensprinkler_server.cpp index 8e018036..d7321db2 100644 --- a/opensprinkler_server.cpp +++ b/opensprinkler_server.cpp @@ -1137,18 +1137,21 @@ void server_json_controller_main(OTF_PARAMS_DEF) { bfill.emit_p(PSTR("\"mac\":\"$X:$X:$X:$X:$X:$X\","), mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); - bfill.emit_p(PSTR("\"loc\":\"$O\",\"jsp\":\"$O\",\"wsp\":\"$O\",\"wto\":{$O},\"ifkey\":\"$O\",\"mqtt\":{$O},\"email\":{$O},\"wtdata\":$S,\"wterr\":$D,\"dname\":\"$O\","), + bfill.emit_p(PSTR("\"loc\":\"$O\",\"jsp\":\"$O\",\"wsp\":\"$O\",\"wto\":{$O},\"ifkey\":\"$O\",\"mqtt\":{$O},\"wtdata\":$S,\"wterr\":$D,\"dname\":\"$O\","), SOPT_LOCATION, SOPT_JAVASCRIPTURL, SOPT_WEATHERURL, SOPT_WEATHER_OPTS, SOPT_IFTTT_KEY, - SOPT_MQTT_OPTS, SOPT_EMAIL_OPTS, strlen(wt_rawData)==0?"{}":wt_rawData, wt_errCode, SOPT_DEVICE_NAME); +#if !(defined(__AVR_ATmega1284__)||defined(__AVR_ATmega1284P__)) + bfill.emit_p(PSTR("\"email\":{$O},"), SOPT_MQTT_OPTS); +#endif + #if defined(ARDUINO) if(os.status.has_curr_sense) { uint16_t current = os.read_current(); diff --git a/smtp.c b/smtp.c index c256a4f6..9b9db72c 100644 --- a/smtp.c +++ b/smtp.c @@ -23,6 +23,7 @@ */ #if defined(ARDUINO) + #else #if defined(_WIN32) || defined(WIN32) diff --git a/smtp.h b/smtp.h index 5316801d..d587018c 100644 --- a/smtp.h +++ b/smtp.h @@ -14,6 +14,10 @@ #ifndef SMTP_H #define SMTP_H +#define MAIL_CONNECTION_SECURITY SMTP_SECURITY_TLS +#define MAIL_FLAGS (smtp_flag)(SMTP_NO_CERT_VERIFY) +#define MAIL_AUTH SMTP_AUTH_PLAIN + #include #include #include From ac1dc101151a43d48ef199719239bdb9d8244ba0 Mon Sep 17 00:00:00 2001 From: Ray Date: Fri, 28 Jun 2024 15:11:25 -0400 Subject: [PATCH 09/61] fix compilation errors for all three platforms --- OpenSprinkler.h | 2 + build.sh | 6 +- main.cpp | 166 +++++++++++++++++++++--------------------------- platformio.ini | 44 +++++++++---- smtp.c | 2 +- smtp.h | 6 +- 6 files changed, 111 insertions(+), 115 deletions(-) diff --git a/OpenSprinkler.h b/OpenSprinkler.h index 76a35a57..69a3872b 100644 --- a/OpenSprinkler.h +++ b/OpenSprinkler.h @@ -49,6 +49,7 @@ #include #include "SSD1306Display.h" #include "espconnect.h" + #include "EMailSender.h" #else // for AVR #include #include @@ -62,6 +63,7 @@ #include #include #include "etherport.h" + #include "smtp.h" #endif // end of headers #if defined(ARDUINO) diff --git a/build.sh b/build.sh index 3f009214..e2553dde 100755 --- a/build.sh +++ b/build.sh @@ -15,13 +15,13 @@ if [ "$1" == "demo" ]; then apt-get install -y libmosquitto-dev apt-get install -y libssl-dev echo "Compiling demo firmware..." - g++ -o OpenSprinkler -DDEMO -std=c++14 main.cpp OpenSprinkler.cpp program.cpp opensprinkler_server.cpp utils.cpp weather.cpp gpio.cpp etherport.cpp mqtt.cpp -lpthread -lmosquitto -l crypto -lssl + g++ -o OpenSprinkler -DDEMO -DSMTP_OPENSSL -std=c++14 main.cpp OpenSprinkler.cpp program.cpp opensprinkler_server.cpp utils.cpp weather.cpp gpio.cpp etherport.cpp mqtt.cpp smtp.c -lpthread -lmosquitto -l crypto -lssl elif [ "$1" == "osbo" ]; then echo "Installing required libraries..." apt-get install -y libmosquitto-dev apt-get install -y libssl-dev echo "Compiling osbo firmware..." - g++ -o OpenSprinkler -DOSBO -std=c++14 main.cpp OpenSprinkler.cpp program.cpp opensprinkler_server.cpp utils.cpp weather.cpp gpio.cpp etherport.cpp mqtt.cpp -lpthread -lmosquitto -l crypto -lssl + g++ -o OpenSprinkler -DOSBO -DSMTP_OPENSSL -std=c++14 main.cpp OpenSprinkler.cpp program.cpp opensprinkler_server.cpp utils.cpp weather.cpp gpio.cpp etherport.cpp mqtt.cpp smtp.c -lpthread -lmosquitto -l crypto -lssl else echo "Installing required libraries..." apt-get update @@ -47,7 +47,7 @@ else fi echo "Compiling ospi firmware..." - g++ -o OpenSprinkler -DOSPI $USEGPIO -DSMTP_OPENSSL -fpermissive -std=c++14 main.cpp OpenSprinkler.cpp program.cpp opensprinkler_server.cpp utils.cpp weather.cpp gpio.cpp etherport.cpp mqtt.cpp smtp.c -lpthread -lmosquitto -lssl -lcrypto $GPIOLIB + g++ -o OpenSprinkler -DOSPI $USEGPIO -DSMTP_OPENSLL -std=c++14 main.cpp OpenSprinkler.cpp program.cpp opensprinkler_server.cpp utils.cpp weather.cpp gpio.cpp etherport.cpp mqtt.cpp smtp.c -lpthread -lmosquitto -lssl -lcrypto $GPIOLIB fi if [ ! "$SILENT" = true ] && [ -f OpenSprinkler.launch ] && [ ! -f /etc/init.d/OpenSprinkler.sh ]; then diff --git a/main.cpp b/main.cpp index bbdeaea1..2ff16aad 100644 --- a/main.cpp +++ b/main.cpp @@ -30,8 +30,6 @@ #include "opensprinkler_server.h" #include "mqtt.h" #include "main.h" -#include "EMailSender.h" -#include "smtp.h" #define str(s) #s #define xstr(s) str(s) @@ -94,9 +92,6 @@ float flow_last_gpm=0; uint32_t reboot_timer = 0; -//boolean to delay reboot notification until wifi is reconnected -bool delayed = false; - void flow_poll() { #if defined(ESP8266) if(os.hw_rev>=2) pinModeExt(PIN_SENSOR1, INPUT_PULLUP); // this seems necessary for OS 3.2 @@ -449,16 +444,6 @@ void do_loop() os.status.mas2= os.iopts[IOPT_MASTER_STATION_2]; time_os_t curr_time = os.now_tz(); - //handle delayed reboot notification until wifi connection - #if defined(ARDUINO) - if(delayed){ - if(WiFi.status() == WL_CONNECTED){ - push_message(NOTIFY_REBOOT); - delayed = false; - } - } - #endif - // ====== Process Ethernet packets ====== #if defined(ARDUINO) // Process Ethernet packets for Arduino #if defined(ESP8266) @@ -997,8 +982,13 @@ void do_loop() } static byte reboot_notification = 1; if(reboot_notification) { + #if defined(ESP266) + if(useEth || WiFi.status()==WL_CONNECTED) + #endif + { reboot_notification = 0; push_message(NOTIFY_REBOOT); + } } } @@ -1389,47 +1379,48 @@ void push_message(int type, uint32_t lval, float fval, const char* sval) { char* postval = tmp_buffer; uint32_t volume; + //check if ifttt key exists and also if the enable bit is set + bool ifttt_enabled; + if(strlen(os.sopts[SOPT_IFTTT_KEY]) != 0){ + ifttt_enabled = false; + }else{ + ifttt_enabled = os.iopts[IOPT_NOTIF_ENABLE]&type; + } + //define email variables - char host[50 + 1] = {0}; - char username[32 + 1] = {0}; - char password[32 + 1] = {0}; - char recipient[32 + 1] = {0}; - int port = 465; - int enabled = 0; + char email_host[32 + 1] = {0}; + char email_username[32 + 1] = {0}; + char email_password[32 + 1] = {0}; + char email_recipient[32 + 1] = {0}; + int email_port = 465; + int email_en = 0; //pull email variables - os.sopt_load(SOPT_EMAIL_OPTS, postval); - if (*postval != 0) { - sscanf( - postval, - "\"en\":%d,\"host\":\"%" xstr(32) "[^\"]\",\"port\":%d,\"user\":\"%" xstr(50) "[^\"]\",\"pass\":\"%" xstr(32) "[^\"]\",\"recipient\":\"%" xstr(32) "[^\"]\"", - &enabled, host, &port, username, password, recipient - ); - } + #if defined(__AVR_ATmega1284__) | defined(__AVR_ATmega1284P__) + // AVR doesn't support email, so no need to pull + #else + os.sopt_load(SOPT_EMAIL_OPTS, postval); + if (*postval != 0) { + sscanf( + postval, + "\"en\":%d,\"host\":\"%" xstr(32) "[^\"]\",\"port\":%d,\"user\":\"%" xstr(32) "[^\"]\",\"pass\":\"%" xstr(32) "[^\"]\",\"recipient\":\"%" xstr(32) "[^\"]\"", + &email_en, email_host, &email_port, email_username, email_password, email_recipient + ); + } + #endif - //assign email variables necessary - #if defined(ARDUINO) - EMailSender emailSend(username, password); - EMailSender::EMailMessage message; + #if defined(ESP8266) + EMailSender::EMailMessage email_message; #else - struct smtp *smtp; int rc; struct { String subject; String message; - }message; + } email_message; #endif - //check if ifttt key exists - bool ifttt_enabled; - if(SOPT_IFTTT_KEY != 0){ - ifttt_enabled = false; - }else{ - ifttt_enabled = os.iopts[IOPT_NOTIF_ENABLE]&type; - } - bool email_enabled; - if(!enabled){ + if(!email_en){ email_enabled = false; }else{ email_enabled = os.iopts[IOPT_NOTIF_ENABLE]&type; @@ -1439,7 +1430,8 @@ void push_message(int type, uint32_t lval, float fval, const char* sval) { if (!ifttt_enabled && !os.mqtt.enabled() && !email_enabled) return; - if (ifttt_enabled) { + if (ifttt_enabled || email_enabled) { + // todo: modify the format for email strcpy_P(postval, PSTR("{\"value1\":\"On site [")); os.sopt_load(SOPT_DEVICE_NAME, postval+strlen(postval)); strcat_P(postval, PSTR("], ")); @@ -1450,16 +1442,6 @@ void push_message(int type, uint32_t lval, float fval, const char* sval) { payload[0] = 0; } - if (email_enabled) { - strcpy_P(postval, PSTR("On site [")); - os.sopt_load(SOPT_DEVICE_NAME, postval+strlen(postval)); - strcat_P(postval, PSTR("], ")); - #if defined(ARDUINO) - emailSend.setSMTPServer(strdup(host)); - emailSend.setSMTPPort(port); - #endif - } - switch(type) { case NOTIFY_STATION_ON: @@ -1493,8 +1475,8 @@ void push_message(int type, uint32_t lval, float fval, const char* sval) { } } if (email_enabled) { - message.subject = "Station Off"; - message.message = postval; + email_message.subject = "Station Off"; + email_message.message = postval; } break; @@ -1512,8 +1494,8 @@ void push_message(int type, uint32_t lval, float fval, const char* sval) { sprintf_P(postval+strlen(postval), PSTR(" with %d%% water level."), (int)fval); } if (email_enabled) { - message.subject = "Program Scheduled"; - message.message = postval; + email_message.subject = "Program Scheduled"; + email_message.message = postval; } break; @@ -1528,8 +1510,8 @@ void push_message(int type, uint32_t lval, float fval, const char* sval) { strcat_P(postval, ((int)fval)?PSTR("activated."):PSTR("de-activated.")); } if (email_enabled) { - message.subject = "Sensor 1 Notification"; - message.message = postval; + email_message.subject = "Sensor 1 Notification"; + email_message.message = postval; } break; @@ -1544,8 +1526,8 @@ void push_message(int type, uint32_t lval, float fval, const char* sval) { strcat_P(postval, ((int)fval)?PSTR("activated."):PSTR("de-activated.")); } if (email_enabled) { - message.subject = "Sensor 2 Notification"; - message.message = postval; + email_message.subject = "Sensor 2 Notification"; + email_message.message = postval; } break; @@ -1560,8 +1542,8 @@ void push_message(int type, uint32_t lval, float fval, const char* sval) { strcat_P(postval, ((int)fval)?PSTR("activated."):PSTR("de-activated.")); } if (email_enabled) { - message.subject = "Rain Delay"; - message.message = postval; + email_message.subject = "Rain Delay"; + email_message.message = postval; } break; @@ -1578,8 +1560,8 @@ void push_message(int type, uint32_t lval, float fval, const char* sval) { sprintf_P(postval+strlen(postval), PSTR("Flow count: %u, volume: %d.%02d"), lval, (int)volume/100, (int)volume%100); } if (email_enabled) { - message.subject = "Flow Sensor Notification"; - message.message = postval; + email_message.subject = "Flow Sensor Notification"; + email_message.message = postval; } break; @@ -1599,19 +1581,12 @@ void push_message(int type, uint32_t lval, float fval, const char* sval) { } } if (email_enabled) { - message.subject = "Weather Update"; - message.message = postval; + email_message.subject = "Weather Update"; + email_message.message = postval; } break; case NOTIFY_REBOOT: - #if defined(ARDUINO) - if(!delayed){ - delayed = true; - return; - } - #endif - if (os.mqtt.enabled()) { strcpy_P(topic, PSTR("opensprinkler/system")); strcpy_P(payload, PSTR("{\"state\":\"started\"}")); @@ -1641,8 +1616,8 @@ void push_message(int type, uint32_t lval, float fval, const char* sval) { #endif } if (email_enabled) { - message.subject = "Reboot Notification"; - message.message = postval; + email_message.subject = "Reboot Notification"; + email_message.message = postval; } break; } @@ -1667,23 +1642,30 @@ void push_message(int type, uint32_t lval, float fval, const char* sval) { if(email_enabled){ #if defined(ARDUINO) - EMailSender::Response resp = emailSend.send(recipient, message); - // DEBUG_PRINTLN(F("Sending Status:")); - // DEBUG_PRINTLN(resp.status); - // DEBUG_PRINTLN(resp.code); - // DEBUG_PRINTLN(resp.desc); + #if defined(ESP8266) + EMailSender emailSend(email_username, email_password); + emailSend.setSMTPServer(strdup(email_host)); + emailSend.setSMTPPort(email_port); + EMailSender::Response resp = emailSend.send(email_recipient, email_message); + // DEBUG_PRINTLN(F("Sending Status:")); + // DEBUG_PRINTLN(resp.status); + // DEBUG_PRINTLN(resp.code); + // DEBUG_PRINTLN(resp.desc); + #endif #else - char piSubject[message.subject.length() + 1]; - char piMessage[message.message.length() + 1]; - String sPort = to_string(port); + struct smtp *smtp = NULL; + char piSubject[email_message.subject.length() + 1]; + char piMessage[email_message.message.length() + 1]; + String sPort = to_string(email_port); char piPort[sPort.length() + 1]; - strcpy(piSubject, message.subject.c_str()); - strcpy(piMessage, message.message.c_str()); + strcpy(piSubject, email_message.subject.c_str()); + strcpy(piMessage, email_message.message.c_str()); strcpy(piPort, sPort.c_str()); - rc = smtp_open(host, piPort, MAIL_CONNECTION_SECURITY, MAIL_FLAGS, NULL, &smtp); - rc = smtp_auth(smtp, MAIL_AUTH, username, password); - rc = smtp_address_add(smtp, SMTP_ADDRESS_FROM, username, "OpenSprinkler"); - rc = smtp_address_add(smtp, SMTP_ADDRESS_TO, recipient, "User"); + // todo: check error? + rc = smtp_open(email_host, piPort, SMTP_SECURITY_TLS, SMTP_NO_CERT_VERIFY, NULL, &smtp); + rc = smtp_auth(smtp, SMTP_AUTH_PLAIN, email_username, email_password); + rc = smtp_address_add(smtp, SMTP_ADDRESS_FROM, email_username, "OpenSprinkler"); + rc = smtp_address_add(smtp, SMTP_ADDRESS_TO, email_recipient, "User"); rc = smtp_header_add(smtp, "Subject", piSubject); rc = smtp_mail(smtp, piMessage); rc = smtp_close(smtp); diff --git a/platformio.ini b/platformio.ini index 8af89200..0ed3b310 100644 --- a/platformio.ini +++ b/platformio.ini @@ -12,20 +12,36 @@ src_dir = . include_dir = . -[env:d1_mini] -platform = espressif8266@4.2.1 -board = d1_mini +; [env:d1_mini] +; platform = espressif8266@4.2.1 +; board = d1_mini +; framework = arduino +; lib_ldf_mode = deep +; lib_deps = +; sui77/rc-switch @ ^2.6.3 +; https://github.com/ThingPulse/esp8266-oled-ssd1306/archive/4.2.0.zip +; knolleary/PubSubClient @ ^2.8 +; https://github.com/OpenThingsIO/OpenThings-Framework-Firmware-Library @ ^0.1.0 +; build_src_filter = +<*> - +; upload_speed = 460800 +; monitor_speed = 115200 +; board_build.flash_mode = dio +; board_build.ldscript = eagle.flash.4m2m.ld +; board_build.f_cpu = 160000000L +; board_build.f_flash = 80000000L + + +[env:sanguino_atmega1284p] +platform = atmelavr +board = ATmega1284P +board_build.f_cpu = 16000000L +board_build.variant = sanguino framework = arduino lib_ldf_mode = deep -lib_deps = - sui77/rc-switch @ ^2.6.3 - https://github.com/ThingPulse/esp8266-oled-ssd1306/archive/4.2.0.zip - knolleary/PubSubClient @ ^2.8 - https://github.com/OpenThingsIO/OpenThings-Framework-Firmware-Library @ ^0.1.0 +lib_deps = + https://github.com/UIPEthernet/UIPEthernet/archive/refs/tags/v2.0.12.zip + knolleary/PubSubClient @ ^2.8 + greiman/SdFat @ 1.0.7 + Wire build_src_filter = +<*> - -upload_speed = 460800 -monitor_speed = 115200 -board_build.flash_mode = dio -board_build.ldscript = eagle.flash.4m2m.ld -board_build.f_cpu = 160000000L -board_build.f_flash = 80000000L +monitor_speed=115200 \ No newline at end of file diff --git a/smtp.c b/smtp.c index 9b9db72c..bcd91a35 100644 --- a/smtp.c +++ b/smtp.c @@ -3530,4 +3530,4 @@ smtp_attachment_clear_all(struct smtp *const smtp){ smtp->num_attachment = 0; } -#endif \ No newline at end of file +#endif diff --git a/smtp.h b/smtp.h index d587018c..c1dac854 100644 --- a/smtp.h +++ b/smtp.h @@ -14,10 +14,6 @@ #ifndef SMTP_H #define SMTP_H -#define MAIL_CONNECTION_SECURITY SMTP_SECURITY_TLS -#define MAIL_FLAGS (smtp_flag)(SMTP_NO_CERT_VERIFY) -#define MAIL_AUTH SMTP_AUTH_PLAIN - #include #include #include @@ -639,4 +635,4 @@ struct str_getdelimfd{ #endif /* SMTP_H */ -#endif \ No newline at end of file +#endif From 338672cc3ba765345329025030514e171679861e Mon Sep 17 00:00:00 2001 From: Ray Date: Fri, 28 Jun 2024 21:11:33 -0400 Subject: [PATCH 10/61] streamline email notification message; fix a couple of bugs --- main.cpp | 80 ++++++++++++++-------------------------- opensprinkler_server.cpp | 4 +- 2 files changed, 29 insertions(+), 55 deletions(-) diff --git a/main.cpp b/main.cpp index 2ff16aad..5a20fcf4 100644 --- a/main.cpp +++ b/main.cpp @@ -1379,15 +1379,15 @@ void push_message(int type, uint32_t lval, float fval, const char* sval) { char* postval = tmp_buffer; uint32_t volume; - //check if ifttt key exists and also if the enable bit is set + // check if ifttt key exists and also if the enable bit is set bool ifttt_enabled; - if(strlen(os.sopts[SOPT_IFTTT_KEY]) != 0){ + if(strlen(os.sopts[SOPT_IFTTT_KEY]) == 0) { ifttt_enabled = false; - }else{ + } else { ifttt_enabled = os.iopts[IOPT_NOTIF_ENABLE]&type; } - //define email variables + // define email variables char email_host[32 + 1] = {0}; char email_username[32 + 1] = {0}; char email_password[32 + 1] = {0}; @@ -1395,9 +1395,9 @@ void push_message(int type, uint32_t lval, float fval, const char* sval) { int email_port = 465; int email_en = 0; - //pull email variables + // parse email variables #if defined(__AVR_ATmega1284__) | defined(__AVR_ATmega1284P__) - // AVR doesn't support email, so no need to pull + // AVR doesn't support email, so no need to parse #else os.sopt_load(SOPT_EMAIL_OPTS, postval); if (*postval != 0) { @@ -1426,15 +1426,19 @@ void push_message(int type, uint32_t lval, float fval, const char* sval) { email_enabled = os.iopts[IOPT_NOTIF_ENABLE]&type; } - // check if this type of event is enabled for push notification + // if none if enabled, return here if (!ifttt_enabled && !os.mqtt.enabled() && !email_enabled) return; if (ifttt_enabled || email_enabled) { - // todo: modify the format for email strcpy_P(postval, PSTR("{\"value1\":\"On site [")); - os.sopt_load(SOPT_DEVICE_NAME, postval+strlen(postval)); + os.sopt_load(SOPT_DEVICE_NAME, topic); + strcat(postval+strlen(postval), topic); strcat_P(postval, PSTR("], ")); + if(email_enabled) { + strcat(topic, " "); + email_message.subject = topic; // prefix the email subject with device name + } } if (os.mqtt.enabled()) { @@ -1473,10 +1477,7 @@ void push_message(int type, uint32_t lval, float fval, const char* sval) { if(os.iopts[IOPT_SENSOR1_TYPE]==SENSOR_TYPE_FLOW) { sprintf_P(postval+strlen(postval), PSTR(" Flow rate: %d.%02d"), (int)flow_last_gpm, (int)(flow_last_gpm*100)%100); } - } - if (email_enabled) { - email_message.subject = "Station Off"; - email_message.message = postval; + if(email_enabled) { email_message.subject += PSTR("station event"); } } break; @@ -1492,10 +1493,7 @@ void push_message(int type, uint32_t lval, float fval, const char* sval) { if(lval=0) { sprintf_P(postval+strlen(postval), PSTR("water level updated: %d%%."), (int)fval); } - } - if (email_enabled) { - email_message.subject = "Weather Update"; - email_message.message = postval; + if(email_enabled) { email_message.subject += PSTR("weather update event"); } } break; @@ -1612,12 +1595,9 @@ void push_message(int type, uint32_t lval, float fval, const char* sval) { //strcat(postval, ":"); //itoa(_port, postval+strlen(postval), 10); #else - strcat_P(postval, PSTR("process restarted.")); + strcat_P(postval, PSTR("controller process restarted.")); #endif - } - if (email_enabled) { - email_message.subject = "Reboot Notification"; - email_message.message = postval; + if(email_enabled) { email_message.subject += PSTR("reboot event"); } } break; } @@ -1628,7 +1608,6 @@ void push_message(int type, uint32_t lval, float fval, const char* sval) { if (ifttt_enabled) { strcat_P(postval, PSTR("\"}")); - //char postBuffer[1500]; BufferFiller bf = ether_buffer; bf.emit_p(PSTR("POST /trigger/sprinkler/with/key/$O HTTP/1.0\r\n" "Host: $S\r\n" @@ -1641,6 +1620,7 @@ void push_message(int type, uint32_t lval, float fval, const char* sval) { } if(email_enabled){ + email_message.message = strchr(postval, 'O'); // ad-hoc: remove the value1 part from the ifttt message #if defined(ARDUINO) #if defined(ESP8266) EMailSender emailSend(email_username, email_password); @@ -1654,20 +1634,14 @@ void push_message(int type, uint32_t lval, float fval, const char* sval) { #endif #else struct smtp *smtp = NULL; - char piSubject[email_message.subject.length() + 1]; - char piMessage[email_message.message.length() + 1]; - String sPort = to_string(email_port); - char piPort[sPort.length() + 1]; - strcpy(piSubject, email_message.subject.c_str()); - strcpy(piMessage, email_message.message.c_str()); - strcpy(piPort, sPort.c_str()); + String email_port_str = to_string(email_port); // todo: check error? - rc = smtp_open(email_host, piPort, SMTP_SECURITY_TLS, SMTP_NO_CERT_VERIFY, NULL, &smtp); + rc = smtp_open(email_host, email_port_str.c_str(), SMTP_SECURITY_TLS, SMTP_NO_CERT_VERIFY, NULL, &smtp); rc = smtp_auth(smtp, SMTP_AUTH_PLAIN, email_username, email_password); rc = smtp_address_add(smtp, SMTP_ADDRESS_FROM, email_username, "OpenSprinkler"); rc = smtp_address_add(smtp, SMTP_ADDRESS_TO, email_recipient, "User"); - rc = smtp_header_add(smtp, "Subject", piSubject); - rc = smtp_mail(smtp, piMessage); + rc = smtp_header_add(smtp, "Subject", email_message.subject.c_str()); + rc = smtp_mail(smtp, email_message.message.c_str()); rc = smtp_close(smtp); #endif } diff --git a/opensprinkler_server.cpp b/opensprinkler_server.cpp index d7321db2..0312ffdc 100644 --- a/opensprinkler_server.cpp +++ b/opensprinkler_server.cpp @@ -1143,13 +1143,13 @@ void server_json_controller_main(OTF_PARAMS_DEF) { SOPT_WEATHERURL, SOPT_WEATHER_OPTS, SOPT_IFTTT_KEY, - SOPT_EMAIL_OPTS, + SOPT_MQTT_OPTS, strlen(wt_rawData)==0?"{}":wt_rawData, wt_errCode, SOPT_DEVICE_NAME); #if !(defined(__AVR_ATmega1284__)||defined(__AVR_ATmega1284P__)) - bfill.emit_p(PSTR("\"email\":{$O},"), SOPT_MQTT_OPTS); + bfill.emit_p(PSTR("\"email\":{$O},"), SOPT_EMAIL_OPTS); #endif #if defined(ARDUINO) From 2aa61afd43b42fd0ca9bb306ac7d73e5f0738e93 Mon Sep 17 00:00:00 2001 From: Ray Date: Fri, 28 Jun 2024 22:46:34 -0400 Subject: [PATCH 11/61] modify email library's DEBUG_PRINT macro to avoid conflicting with main DEBUG_PRINT --- EMailSender.cpp | 119 ++++++++++++++++++++++++------------------------ EMailSender.h | 8 ++-- 2 files changed, 63 insertions(+), 64 deletions(-) diff --git a/EMailSender.cpp b/EMailSender.cpp index dfdc9d5b..22b87894 100644 --- a/EMailSender.cpp +++ b/EMailSender.cpp @@ -103,10 +103,9 @@ int base64_enc_length(int plainLen) { const char* encode64_f(char* input, uint8_t len) { // encoding - DEBUG_PRINTLN(F("Encoding")); - - DEBUG_PRINTLN(input); - DEBUG_PRINTLN(len); + EMAIL_DEBUG_PRINTLN(F("Encoding")); + EMAIL_DEBUG_PRINTLN(input); + EMAIL_DEBUG_PRINTLN(len); //int encodedLen = base64_enc_length(len); @@ -214,7 +213,7 @@ EMailSender::Response EMailSender::awaitSMTPResponse(SSLClient &client, } _serverResponce = client.readStringUntil('\n'); - DEBUG_PRINTLN(_serverResponce); + EMAIL_DEBUG_PRINTLN(_serverResponce); if (resp && _serverResponce.indexOf(resp) == -1){ response.code = resp; response.desc = respDesc +String(" (") + _serverResponce + String(")"); @@ -241,7 +240,7 @@ EMailSender::Response EMailSender::awaitSMTPResponse(EMAIL_NETWORK_CLASS &client } _serverResponce = client.readStringUntil('\n'); - DEBUG_PRINTLN(_serverResponce); + EMAIL_DEBUG_PRINTLN(_serverResponce); if (resp && _serverResponce.indexOf(resp) == -1){ response.code = resp; response.desc = respDesc +String(" (") + _serverResponce + String(")"); @@ -411,7 +410,7 @@ EMailSender::Response EMailSender::send(char* tos[], byte sizeOfTo, byte sizeOf EMailSender::Response EMailSender::send(String to, EMailMessage &email, Attachments attachments){ - DEBUG_PRINT(F("ONLY ONE RECIPIENT")); + EMAIL_DEBUG_PRINT(F("ONLY ONE RECIPIENT")); const char* arrEmail[] = {to.c_str()}; return send(arrEmail, 1, email, attachments); @@ -430,14 +429,14 @@ EMailSender::Response EMailSender::send(String tos[], byte sizeOfTo, byte sizeO } EMailSender::Response EMailSender::send(const char* to, EMailMessage &email, Attachments attachments){ - DEBUG_PRINT(F("ONLY ONE RECIPIENT")); + EMAIL_DEBUG_PRINT(F("ONLY ONE RECIPIENT")); const char* arrEmail[] = {to}; return send(arrEmail, 1, email, attachments); } EMailSender::Response EMailSender::send(const char* to[], byte sizeOfTo, EMailMessage &email, Attachments attachments) { - DEBUG_PRINTLN(F("miltiple destination and attachments")); + EMAIL_DEBUG_PRINTLN(F("miltiple destination and attachments")); return send(to, sizeOfTo, 0, email, attachments); } @@ -464,14 +463,14 @@ EMailSender::Response EMailSender::send(const char* to[], byte sizeOfTo, byte s EMailSender::Response EMailSender::send(const char* to[], byte sizeOfTo, byte sizeOfCc,byte sizeOfCCn, EMailMessage &email, Attachments attachments) { #ifdef SSLCLIENT_WRAPPER - DEBUG_PRINTLN(F("SSLClient active!")); + EMAIL_DEBUG_PRINTLN(F("SSLClient active!")); #else #ifndef PUT_OUTSIDE_SCOPE_CLIENT_DECLARATION EMAIL_NETWORK_CLASS client; #endif - DEBUG_PRINT(F("Insecure client:")); - DEBUG_PRINTLN(this->isSecure); + EMAIL_DEBUG_PRINT(F("Insecure client:")); + EMAIL_DEBUG_PRINTLN(this->isSecure); #ifndef FORCE_DISABLE_SSL #if (EMAIL_NETWORK_TYPE == NETWORK_ESP8266 || EMAIL_NETWORK_TYPE == NETWORK_ESP8266_242) #ifndef ARDUINO_ESP8266_RELEASE_2_4_2 @@ -479,8 +478,8 @@ EMailSender::Response EMailSender::send(const char* to[], byte sizeOfTo, byte s client.setInsecure(); bool mfln = client.probeMaxFragmentLength(this->smtp_server, this->smtp_port, 512); - DEBUG_PRINT("MFLN supported: "); - DEBUG_PRINTLN(mfln?"yes":"no"); + EMAIL_DEBUG_PRINT("MFLN supported: "); + EMAIL_DEBUG_PRINTLN(mfln?"yes":"no"); if (mfln) { client.setBufferSizes(512, 512); @@ -493,8 +492,8 @@ EMailSender::Response EMailSender::send(const char* to[], byte sizeOfTo, byte s // String coreVersion = String(ESP.getSdkVersion()); // uint8_t firstdot = coreVersion.indexOf('.'); // - // DEBUG_PRINTLN(coreVersion.substring(1, coreVersion.indexOf('.', firstdot+1)).toFloat()); - // DEBUG_PRINTLN(coreVersion.substring(1, coreVersion.indexOf('.', firstdot+1)).toFloat() >= 3.3f); + // EMAIL_DEBUG_PRINTLN(coreVersion.substring(1, coreVersion.indexOf('.', firstdot+1)).toFloat()); + // EMAIL_DEBUG_PRINTLN(coreVersion.substring(1, coreVersion.indexOf('.', firstdot+1)).toFloat() >= 3.3f); // if (coreVersion.substring(1, coreVersion.indexOf('.', firstdot+1)).toFloat() >= 3.3f) { // client.setInsecure(); // } @@ -511,8 +510,8 @@ EMailSender::Response EMailSender::send(const char* to[], byte sizeOfTo, byte s #endif EMailSender::Response response; - DEBUG_PRINTLN(this->smtp_server); - DEBUG_PRINTLN(this->smtp_port); + EMAIL_DEBUG_PRINTLN(this->smtp_server); + EMAIL_DEBUG_PRINTLN(this->smtp_port); if(!client.connect(this->smtp_server, this->smtp_port)) { response.desc = F("Could not connect to mail server"); @@ -561,7 +560,7 @@ EMailSender::Response EMailSender::send(const char* to[], byte sizeOfTo, byte s commandHELO = "EHLO"; } String helo = commandHELO + " "+String(publicIPDescriptor)+" "; - DEBUG_PRINTLN(helo); + EMAIL_DEBUG_PRINTLN(helo); client.println(helo); response = awaitSMTPResponse(client, "250", "Identification error"); @@ -628,12 +627,12 @@ EMailSender::Response EMailSender::send(const char* to[], byte sizeOfTo, byte s String auth = "AUTH PLAIN "+String(encode64_f(logPass, size)); // String auth = "AUTH PLAIN "+String(encode64(logPass)); - DEBUG_PRINTLN(auth); + EMAIL_DEBUG_PRINTLN(auth); client.println(auth); } #if defined(ESP32) else if (this->isCramMD5Login == true) { - DEBUG_PRINTLN(F("AUTH CRAM-MD5")); + EMAIL_DEBUG_PRINTLN(F("AUTH CRAM-MD5")); client.println(F("AUTH CRAM-MD5")); // read back the base64 encoded digest. @@ -648,7 +647,7 @@ EMailSender::Response EMailSender::send(const char* to[], byte sizeOfTo, byte s size_t b64l = _serverResponce.length()-1; // C vs C++ counting of \0 const unsigned char * b64 = (const unsigned char *)_serverResponce.c_str(); - DEBUG_PRINTLN("B64digest="+String((char *)b64) + " Len=" + String((int)b64l)); + EMAIL_DEBUG_PRINTLN("B64digest="+String((char *)b64) + " Len=" + String((int)b64l)); unsigned char digest[256]; size_t len; @@ -687,22 +686,22 @@ EMailSender::Response EMailSender::send(const char* to[], byte sizeOfTo, byte s }; // And submit this to the server as a login string. - DEBUG_PRINTLN(encode64((char*)rsp.c_str())); + EMAIL_DEBUG_PRINTLN(encode64((char*)rsp.c_str())); client.println(encode64((char*)rsp.c_str())); // now exepct the normal login confirmation to continue. } #endif else{ - DEBUG_PRINTLN(F("AUTH LOGIN:")); + EMAIL_DEBUG_PRINTLN(F("AUTH LOGIN:")); client.println(F("AUTH LOGIN")); awaitSMTPResponse(client); - DEBUG_PRINTLN(encode64(this->email_login)); + EMAIL_DEBUG_PRINTLN(encode64(this->email_login)); client.println(encode64(this->email_login)); awaitSMTPResponse(client); - DEBUG_PRINTLN(encode64(this->email_password)); + EMAIL_DEBUG_PRINTLN(encode64(this->email_password)); client.println(encode64(this->email_password)); } response = awaitSMTPResponse(client, "235", "SMTP AUTH error"); @@ -712,9 +711,9 @@ EMailSender::Response EMailSender::send(const char* to[], byte sizeOfTo, byte s return response; } } - DEBUG_PRINT(F("MAIL FROM: <")); - DEBUG_PRINT(this->email_from); - DEBUG_PRINTLN(F(">")); + EMAIL_DEBUG_PRINT(F("MAIL FROM: <")); + EMAIL_DEBUG_PRINT(this->email_from); + EMAIL_DEBUG_PRINTLN(F(">")); client.print(F("MAIL FROM: <")); client.print(this->email_from); @@ -723,14 +722,14 @@ EMailSender::Response EMailSender::send(const char* to[], byte sizeOfTo, byte s // String rcpt = "RCPT TO: <" + String(to) + '>'; // -// DEBUG_PRINTLN(rcpt); +// EMAIL_DEBUG_PRINTLN(rcpt); // client.println(rcpt); int cont; for (cont=0;cont<(sizeOfTo+sizeOfCc+sizeOfCCn);cont++){ - DEBUG_PRINT(F("RCPT TO: <")); - DEBUG_PRINT(to[cont]); - DEBUG_PRINTLN(F(">")); + EMAIL_DEBUG_PRINT(F("RCPT TO: <")); + EMAIL_DEBUG_PRINT(to[cont]); + EMAIL_DEBUG_PRINTLN(F(">")); client.print(F("RCPT TO: <")); client.print(to[cont]); @@ -738,7 +737,7 @@ EMailSender::Response EMailSender::send(const char* to[], byte sizeOfTo, byte s awaitSMTPResponse(client); } - DEBUG_PRINTLN(F("DATA:")); + EMAIL_DEBUG_PRINTLN(F("DATA:")); client.println(F("DATA")); response = awaitSMTPResponse(client, "354", "SMTP DATA error"); @@ -837,7 +836,7 @@ EMailSender::Response EMailSender::send(const char* to[], byte sizeOfTo, byte s // if ((sizeof(attachs) / sizeof(attachs[0]))>0){ if (sizeof(attachments)>0 && attachments.number>0){ - DEBUG_PRINT(F("Array: ")); + EMAIL_DEBUG_PRINT(F("Array: ")); // for (int i = 0; i<(sizeof(attachs) / sizeof(attachs[0])); i++){ for (int i = 0; i ")); - DEBUG_PRINTLN(myFile.name()); + EMAIL_DEBUG_PRINT(F("Filename -> ")); + EMAIL_DEBUG_PRINTLN(myFile.name()); if (attachments.fileDescriptor[i].encode64){ encode(&myFile, &client); }else{ while(myFile.available()) { clientCount = myFile.read(tBuf,64); - DEBUG_PRINTLN(clientCount); + EMAIL_DEBUG_PRINTLN(clientCount); client.write((byte*)tBuf,clientCount); } } @@ -978,7 +977,7 @@ EMailSender::Response EMailSender::send(const char* to[], byte sizeOfTo, byte s #ifdef STORAGE_EXTERNAL_ENABLED if (attachments.fileDescriptor[i].storageType==EMAIL_STORAGE_TYPE_SD){ #ifdef OPEN_CLOSE_SD - DEBUG_PRINTLN(F("SD Check")); + EMAIL_DEBUG_PRINTLN(F("SD Check")); if (!EXTERNAL_STORAGE_CLASS.exists(attachments.fileDescriptor[i].url.c_str())){ #if EXTERNAL_STORAGE == STORAGE_SD || EXTERNAL_STORAGE == STORAGE_SDFAT2 || EXTERNAL_STORAGE == STORAGE_SDFAT_RP2040_ESP8266 if(!EXTERNAL_STORAGE_CLASS.begin(SD_CS_PIN)){ @@ -1008,17 +1007,17 @@ EMailSender::Response EMailSender::send(const char* to[], byte sizeOfTo, byte s } // Close EXTERNAL_STORAGE_CLASS.exists #endif - DEBUG_PRINTLN(F("Open file: ")); + EMAIL_DEBUG_PRINTLN(F("Open file: ")); EMAIL_FILE_EX myFile = EXTERNAL_STORAGE_CLASS.open(attachments.fileDescriptor[i].url.c_str()); if(myFile) { myFile.seek(0); - DEBUG_PRINTLN(F("OK")); + EMAIL_DEBUG_PRINTLN(F("OK")); if (attachments.fileDescriptor[i].encode64){ - DEBUG_PRINTLN(F("BASE 64")); + EMAIL_DEBUG_PRINTLN(F("BASE 64")); encode(&myFile, &client); }else{ - DEBUG_PRINTLN(F("NORMAL")); + EMAIL_DEBUG_PRINTLN(F("NORMAL")); while(myFile.available()) { clientCount = myFile.read(tBuf,64); client.write((byte*)tBuf,clientCount); @@ -1057,13 +1056,13 @@ EMailSender::Response EMailSender::send(const char* to[], byte sizeOfTo, byte s #ifdef STORAGE_EXTERNAL_ENABLED #ifdef OPEN_CLOSE_SD if (sdActive){ - DEBUG_PRINTLN(F("SD end")); + EMAIL_DEBUG_PRINTLN(F("SD end")); #ifndef ARDUINO_ESP8266_RELEASE_2_4_2 #if EXTERNAL_STORAGE != STORAGE_SDFAT_RP2040_ESP8266 EXTERNAL_STORAGE_CLASS.end(); #endif #endif - DEBUG_PRINTLN(F("SD end 2")); + EMAIL_DEBUG_PRINTLN(F("SD end 2")); } #endif #endif @@ -1073,7 +1072,7 @@ EMailSender::Response EMailSender::send(const char* to[], byte sizeOfTo, byte s #if INTERNAL_STORAGE != STORAGE_SPIFM if (spiffsActive){ INTERNAL_STORAGE_CLASS.end(); - DEBUG_PRINTLN(F("SPIFFS END")); + EMAIL_DEBUG_PRINTLN(F("SPIFFS END")); } #endif #endif @@ -1083,7 +1082,7 @@ EMailSender::Response EMailSender::send(const char* to[], byte sizeOfTo, byte s } // Close attachement enable #endif - DEBUG_PRINTLN(F("Message end")); + EMAIL_DEBUG_PRINTLN(F("Message end")); client.println(F(".")); response = awaitSMTPResponse(client, "250", "Sending message error"); diff --git a/EMailSender.h b/EMailSender.h index 4249da08..36c42f58 100644 --- a/EMailSender.h +++ b/EMailSender.h @@ -364,11 +364,11 @@ // Setup debug printing macros. #ifdef EMAIL_SENDER_DEBUG - #define DEBUG_PRINT(...) { DEBUG_PRINTER.print(__VA_ARGS__); } - #define DEBUG_PRINTLN(...) { DEBUG_PRINTER.println(__VA_ARGS__); } + #define EMAIL_DEBUG_PRINT(...) { DEBUG_PRINTER.print(__VA_ARGS__); } + #define EMAIL_DEBUG_PRINTLN(...) { DEBUG_PRINTER.println(__VA_ARGS__); } #else - #define DEBUG_PRINT(...) {} - #define DEBUG_PRINTLN(...) {} + #define EMAIL_DEBUG_PRINT(...) {} + #define EMAIL_DEBUG_PRINTLN(...) {} #endif // Debug level for SSLClient From c91801622916112b2da44f4b8032eed0efc9f7b9 Mon Sep 17 00:00:00 2001 From: "Nathan P." Date: Tue, 2 Jul 2024 11:24:44 -0400 Subject: [PATCH 12/61] Initial support for MQTT subscribe on Arduino. --- main.cpp | 1 + mqtt.cpp | 366 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ mqtt.h | 3 + 3 files changed, 370 insertions(+) diff --git a/main.cpp b/main.cpp index 81113530..a9d4e121 100644 --- a/main.cpp +++ b/main.cpp @@ -604,6 +604,7 @@ void do_loop() DEBUG_PRINTLN(F("req_mqtt_restart")); os.mqtt.begin(); os.status.req_mqtt_restart = false; + os.mqtt.subscribe(); } os.mqtt.loop(); diff --git a/mqtt.cpp b/mqtt.cpp index d2f97136..ed6ba959 100644 --- a/mqtt.cpp +++ b/mqtt.cpp @@ -41,6 +41,9 @@ #endif #include "OpenSprinkler.h" +#include "opensprinkler_server.h" +#include "main.h" +#include "program.h" #include "types.h" #include "mqtt.h" @@ -67,6 +70,7 @@ #define xstr(s) str(s) extern OpenSprinkler os; +extern ProgramData pd; extern char tmp_buffer[]; #define OS_MQTT_KEEPALIVE 60 @@ -75,6 +79,7 @@ extern char tmp_buffer[]; #define MQTT_MAX_USERNAME_LEN 32 // Note: App is set to max 32 chars for username #define MQTT_MAX_PASSWORD_LEN 32 // Note: App is set to max 32 chars for password #define MQTT_MAX_ID_LEN 16 // MQTT Client Id to uniquely reference this unit +#define MQTT_MAX_TOPIC_LEN 20 // Note: App is set to max 20 chars for topic name #define MQTT_RECONNECT_DELAY 120 // Minumum of 60 seconds between reconnect attempts #define MQTT_ROOT_TOPIC "opensprinkler" @@ -91,6 +96,269 @@ char OSMqtt::_username[MQTT_MAX_USERNAME_LEN + 1] = {0}; // username to connect char OSMqtt::_password[MQTT_MAX_PASSWORD_LEN + 1] = {0}; // password to connect to the broker int OSMqtt::_port = MQTT_DEFAULT_PORT; // Port of the broker (default 1883) bool OSMqtt::_enabled = false; // Flag indicating whether MQTT is enabled +char OSMqtt::_topic[MQTT_MAX_TOPIC_LEN + 1] = {0}; // topic to subscribe to for commands + +//******************************** HELPER FUNCTIONS ********************************// + +//parsing a comma seperated value list +uint16_t parseListData(char **p) { + char* pv; + int i=0; + tmp_buffer[i]=0; + // copy to tmp_buffer until a non-number is encountered + for(pv=(*p);pv<(*p)+10;pv++) { + if ((*pv)=='-' || (*pv)=='+' || ((*pv)>='0'&&(*pv)<='9')) + tmp_buffer[i++] = (*pv); + else + break; + } + tmp_buffer[i]=0; + *p = pv+1; + return (uint16_t)atol(tmp_buffer); +} + +//finding a key value pair +bool findKeyVal(char *str, char *strbuf, uint16_t maxlen, const char *key, const char stop){ + DEBUG_LOGF("Finding Key Val\r\n"); + bool found = false; + uint16_t i = 0; + if(str == NULL || strbuf == NULL || key == NULL){return false;} + const char *kp = key; + while(*str && *str!= ' ' && *str!= '\n' && !found){ + if (*str == *kp){ + kp++; + if(*kp == '\0'){ + str++; + kp = key; + if(*str == stop){ + found=true; + } + } + } else { + kp = key; + } + str++; + } + if(found){ + while(*str && *str!=' ' && *str!='\n' && *str!='&' && i= os.nstations){ + DEBUG_LOGF("Invalid station ID.\r\n"); + return; + } + }else{ + DEBUG_LOGF("No station ID found.\r\n"); + return; + } + + byte en = 0; + if(findKeyVal(message, tmp_buffer, TMP_BUFFER_SIZE, "en", '=')){ + en = atoi(tmp_buffer); + }else{ + DEBUG_LOGF("No enable bit found.\r\n"); + return; + } + + uint16_t timer = 0; + unsigned long curr_time = os.now_tz(); + if(en){ + if(findKeyVal(message, tmp_buffer, TMP_BUFFER_SIZE, "t", '=')){ + timer = (uint16_t)atol(tmp_buffer); + if(timer==0 || timer>64800){ + DEBUG_LOGF("Time out of bounds.\r\n"); + return; + } + if((os.status.mas==sid+1) || (os.status.mas2==sid+1)){ + DEBUG_LOGF("Cannot independently schedule master.\r\n"); + return; + } + RuntimeQueueStruct *q = NULL; + byte sqi = pd.station_qid[sid]; + //check if station has schedule + if(sqi!=0xFF){ + q = pd.queue+sqi; + }else{ + q = pd.enqueue(); + } + + if(q){ + q->st = 0; + q->dur = timer; + q->sid = sid; + q->pid = 99; + schedule_all_stations(curr_time); + }else{ + DEBUG_LOGF("Queue is full.\r\n"); + return; + } + }else{ + DEBUG_LOGF("No time value found.\r\n") + return; + } + }else{ + byte ssta = 0; + if(findKeyVal(message, tmp_buffer, TMP_BUFFER_SIZE, "ssta",'=')){ + ssta = atoi(tmp_buffer); + } + RuntimeQueueStruct *q = pd.queue + pd.station_qid[sid]; + q->deque_time = curr_time; + turn_off_station(sid, curr_time, ssta); + } + return; +} + +//handles /mp command +void manual_start_program(byte, byte); +void programStart(char *message){ + if(!findKeyVal(message, tmp_buffer, TMP_BUFFER_SIZE, "pid",'=')){ + DEBUG_LOGF("Program ID missing.\r\n") + return; + } + int pid = atoi(tmp_buffer); + if(pid < 0 || pid >= pd.nprograms){ + DEBUG_LOGF("Program ID out of bounds.\r\n"); + } + + byte uwt = 0; + if(findKeyVal(message, tmp_buffer, TMP_BUFFER_SIZE, "uwt",'=')){ + if(tmp_buffer[0]=='1') uwt = 1; + } + + reset_all_stations_immediate(); + + manual_start_program(pid+1, uwt); + return; +} + +//handles /cr command +void runOnceProgram(char *message){ + char *pv; + bool found = false; + for(pv = message; (*pv) != 0 && pv> 3; + s = sid&0x07; + + if(dur > 0 && !(os.attrib_dis[bid]&(1<st = 0; + q->dur = water_time_resolve(dur); + q->pid = 254; + q->sid = sid; + match_found = true; + } + } + } + if(match_found){ + schedule_all_stations(os.now_tz()); + return; + } + return; +} + +//****************************** MQTT FUNCTIONS ******************************// // Initialise the client libraries and event handlers. void OSMqtt::init(void) { @@ -139,6 +407,14 @@ void OSMqtt::begin(void) { ); } + if(findKeyVal(config, tmp_buffer, TMP_BUFFER_SIZE, "\"topic\"", ':')){ + for(int i = 0; tmp_buffer[i+2] != 0; i++){ + _topic[i] = tmp_buffer[i+1]; + } + }else{ + DEBUG_LOGF("No topic found\r\n"); + } + begin(host, port, username, password, (bool)enabled); } @@ -180,6 +456,19 @@ void OSMqtt::publish(const char *topic, const char *payload) { _publish(topic, payload); } +//Subscribe to a specific topic +void OSMqtt::subscribe(void){ + DEBUG_LOGF("MQTT Subscribe: %s\r\n", _topic); + + if (mqtt_client == NULL || !_enabled || os.status.network_fails > 0) return; + + if (!_connected()) { + DEBUG_LOGF("MQTT Subscribe: Not connected\r\n"); + return; + } + + _subscribe(); +} // Regularly call the loop function to ensure "keep alive" messages are sent to the broker and to reconnect if needed. void OSMqtt::loop(void) { static unsigned long last_reconnect_attempt = 0; @@ -286,6 +575,43 @@ int OSMqtt::_publish(const char *topic, const char *payload) { return MQTT_SUCCESS; } +void callback(const char *topic, byte *payload, unsigned int length) { + DEBUG_LOGF("Callback\r\n"); + char message[length + 1]; + for (unsigned int i=0;isetCallback(callback); + if (!mqtt_client->subscribe(_topic)) { + DEBUG_LOGF("MQTT Subscribe: Failed (%d)\r\n", mqtt_client->state()); + return MQTT_ERROR; + } + return MQTT_SUCCESS; +} + int OSMqtt::_loop(void) { mqtt_client->loop(); return mqtt_client->state(); @@ -397,6 +723,46 @@ int OSMqtt::_publish(const char *topic, const char *payload) { return MQTT_SUCCESS; } +void piCallback(struct mosquitto *mosq, void *obj, const struct mosquitto_message *message){ + DEBUG_LOGF("Callback\r\n"); + char *topic = message->topic; + void *payload = message->payload; + char msg[message->payloadlen + 1]; + for (unsigned int i=0;ipayloadlen;i++) { + msg[i] = (char)payload[i]; + } + + if(!checkPassword(msg)){ + return; + } + DEBUG_LOGF("Password Passed\r\n"); + + if(msg[0]=='c'){ + if(msg[1]=='v'){ + changeValues(msg); + }else if(msg[1]=='m'){ + manualRun(msg); + }else if(msg[1]=='r'){ + runOnceProgram(msg); + } + }else if(msg[0]=='m' && msg[1]=='p'){ + programStart(msg); + }else{ + DEBUG_LOGF("Invalid request\r\n"); + return; + } +} + +int OSMQtt::_subscribe(void) { + mosquitto_message_callback_set(mqtt_client, piCallback) + int rc = mosquitto_subscribe(mqtt_client, NULL, _topic, 0); + if (rc != MOSQ_ERR_SUCCESS) { + DEBUG_LOGF("MQTT Subscribe: Failed (%s)\r\n", mosquitto_strerror(rc)); + return MQTT_ERROR; + } + return MQTT_SUCCESS; +} + int OSMqtt::_loop(void) { return mosquitto_loop(mqtt_client, 0 , 1); } diff --git a/mqtt.h b/mqtt.h index 3926d31b..18076493 100644 --- a/mqtt.h +++ b/mqtt.h @@ -32,6 +32,7 @@ class OSMqtt { static char _username[]; static char _password[]; static bool _enabled; + static char _topic[]; // Following routines are platform specific versions of the public interface static int _init(void); @@ -39,6 +40,7 @@ class OSMqtt { static int _disconnect(void); static bool _connected(void); static int _publish(const char *topic, const char *payload); + static int _subscribe(void); static int _loop(void); static const char * _state_string(int state); public: @@ -48,6 +50,7 @@ class OSMqtt { static void begin(const char * host, int port, const char * username, const char * password, bool enable); static bool enabled(void) { return _enabled; }; static void publish(const char *topic, const char *payload); + static void subscribe(); static void loop(void); }; From 03b109058545746446085219eebd52877e1dd101 Mon Sep 17 00:00:00 2001 From: "Nathan P." Date: Tue, 2 Jul 2024 12:34:47 -0400 Subject: [PATCH 13/61] Initial support for MQTT subsribe on OSPi. --- mqtt.cpp | 11 +++++++++-- mqtt.h | 1 + 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/mqtt.cpp b/mqtt.cpp index ed6ba959..73400e3c 100644 --- a/mqtt.cpp +++ b/mqtt.cpp @@ -97,6 +97,7 @@ char OSMqtt::_password[MQTT_MAX_PASSWORD_LEN + 1] = {0}; // password to connect int OSMqtt::_port = MQTT_DEFAULT_PORT; // Port of the broker (default 1883) bool OSMqtt::_enabled = false; // Flag indicating whether MQTT is enabled char OSMqtt::_topic[MQTT_MAX_TOPIC_LEN + 1] = {0}; // topic to subscribe to for commands +bool OSMqtt::_subscribed = false; //Flag indicating if command topic has been subscribed to //******************************** HELPER FUNCTIONS ********************************// @@ -395,6 +396,7 @@ void OSMqtt::begin(void) { char password[MQTT_MAX_PASSWORD_LEN + 1] = {0}; int port = MQTT_DEFAULT_PORT; int enabled = 0; + _subscribed = false; // JSON configuration settings in the form of {"en":0|1,"host":"server_name|IP address","port":1883,user:"",pass:""} char *config = tmp_buffer; @@ -467,6 +469,7 @@ void OSMqtt::subscribe(void){ return; } + _subscribed = true; _subscribe(); } // Regularly call the loop function to ensure "keep alive" messages are sent to the broker and to reconnect if needed. @@ -482,6 +485,10 @@ void OSMqtt::loop(void) { last_reconnect_attempt = millis(); } + if(!_subscribed){ + subscribe(); + } + #if defined(ENABLE_DEBUG) int state = _loop(); #else @@ -726,7 +733,7 @@ int OSMqtt::_publish(const char *topic, const char *payload) { void piCallback(struct mosquitto *mosq, void *obj, const struct mosquitto_message *message){ DEBUG_LOGF("Callback\r\n"); char *topic = message->topic; - void *payload = message->payload; + char *payload = (char*)(message->payload); char msg[message->payloadlen + 1]; for (unsigned int i=0;ipayloadlen;i++) { msg[i] = (char)payload[i]; @@ -754,7 +761,7 @@ void piCallback(struct mosquitto *mosq, void *obj, const struct mosquitto_messag } int OSMQtt::_subscribe(void) { - mosquitto_message_callback_set(mqtt_client, piCallback) + mosquitto_message_callback_set(mqtt_client, piCallback); int rc = mosquitto_subscribe(mqtt_client, NULL, _topic, 0); if (rc != MOSQ_ERR_SUCCESS) { DEBUG_LOGF("MQTT Subscribe: Failed (%s)\r\n", mosquitto_strerror(rc)); diff --git a/mqtt.h b/mqtt.h index 18076493..9f4d7d2a 100644 --- a/mqtt.h +++ b/mqtt.h @@ -33,6 +33,7 @@ class OSMqtt { static char _password[]; static bool _enabled; static char _topic[]; + static bool _subscribed; // Following routines are platform specific versions of the public interface static int _init(void); From ced4eab216e0be5bbb6baf6b2435aa9e331cc1d9 Mon Sep 17 00:00:00 2001 From: Ray Date: Wed, 10 Jul 2024 06:43:53 -0400 Subject: [PATCH 14/61] add a few more options, including reserved options for the future; increase tmp buffer and string option max length --- OpenSprinkler.cpp | 40 ++++++++++++++++++++++++++++++++++++++++ defines.h | 18 ++++++++++++++---- mqtt.cpp | 8 ++++---- 3 files changed, 58 insertions(+), 8 deletions(-) diff --git a/OpenSprinkler.cpp b/OpenSprinkler.cpp index 74b27da1..b41041d8 100644 --- a/OpenSprinkler.cpp +++ b/OpenSprinkler.cpp @@ -167,6 +167,16 @@ const char iopt_json_names[] PROGMEM = "subn2" "subn3" "subn4" + "sstag" + "fwire" + "resv1" + "resv2" + "resv3" + "resv4" + "resv5" + "resv6" + "resv7" + "resv8" "wimod" "reset" ; @@ -237,6 +247,16 @@ const char iopt_prompts[] PROGMEM = "Subnet mask2: " "Subnet mask3: " "Subnet mask4: " + "Station stagger?" + "Force wired? " + "Reserved 1 " + "Reserved 2 " + "Reserved 3 " + "Reserved 4 " + "Reserved 5 " + "Reserved 6 " + "Reserved 7 " + "Reserved 8 " "WiFi mode? " "Factory reset? "; @@ -306,6 +326,16 @@ const byte iopt_max[] PROGMEM = { 255, 255, 255, + 1, + 1, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, 255, 1 }; @@ -381,6 +411,16 @@ byte OpenSprinkler::iopts[] = { 255,// subnet mask 2 255,// subnet mask 3 0, + 1, // station stagger + 1, // force wired connection + 0, // reserved 1 + 0, // reserved 2 + 0, // reserved 3 + 0, // reserved 4 + 0, // reserved 5 + 0, // reserved 6 + 0, // reserved 7 + 0, // reserved 8 WIFI_MODE_AP, // wifi mode 0 // reset }; diff --git a/defines.h b/defines.h index bf088f29..04df087f 100755 --- a/defines.h +++ b/defines.h @@ -29,14 +29,14 @@ typedef unsigned char byte; typedef unsigned long ulong; -#define TMP_BUFFER_SIZE 255 // scratch buffer size +#define TMP_BUFFER_SIZE 320 // scratch buffer size /** Firmware version, hardware version, and maximal values */ -#define OS_FW_VERSION 220 // Firmware version: 220 means 2.2.0 +#define OS_FW_VERSION 221 // Firmware version: 220 means 2.2.0 // if this number is different from the one stored in non-volatile memory // a device reset will be automatically triggered -#define OS_FW_MINOR 6 // Firmware minor version +#define OS_FW_MINOR 0 // Firmware minor version /** Hardware version base numbers */ #define OS_HW_VERSION_BASE 0x00 // OpenSprinkler @@ -135,7 +135,7 @@ typedef unsigned long ulong; #define MAX_NUM_BOARDS (1+MAX_EXT_BOARDS) // maximum number of 8-zone boards including expanders #define MAX_NUM_STATIONS (MAX_NUM_BOARDS*8) // maximum number of stations #define STATION_NAME_SIZE 32 // maximum number of characters in each station name -#define MAX_SOPTS_SIZE 160 // maximum string option size +#define MAX_SOPTS_SIZE 320 // maximum string option size #define STATION_SPECIAL_DATA_SIZE (TMP_BUFFER_SIZE - STATION_NAME_SIZE - 12) @@ -247,6 +247,16 @@ enum { IOPT_SUBNET_MASK2, IOPT_SUBNET_MASK3, IOPT_SUBNET_MASK4, + IOPT_STATION_STAGGER, + IOPT_FORCE_WIRED, + IOPT_RESERVE_1, + IOPT_RESERVE_2, + IOPT_RESERVE_3, + IOPT_RESERVE_4, + IOPT_RESERVE_5, + IOPT_RESERVE_6, + IOPT_RESERVE_7, + IOPT_RESERVE_8, IOPT_WIFI_MODE, //ro IOPT_RESET, //ro NUM_IOPTS // total number of integer options diff --git a/mqtt.cpp b/mqtt.cpp index d2f97136..529ddbcf 100644 --- a/mqtt.cpp +++ b/mqtt.cpp @@ -71,11 +71,11 @@ extern char tmp_buffer[]; #define OS_MQTT_KEEPALIVE 60 #define MQTT_DEFAULT_PORT 1883 // Default port for MQTT. Can be overwritten through App config -#define MQTT_MAX_HOST_LEN 50 // Note: App is set to max 50 chars for broker name -#define MQTT_MAX_USERNAME_LEN 32 // Note: App is set to max 32 chars for username -#define MQTT_MAX_PASSWORD_LEN 32 // Note: App is set to max 32 chars for password +#define MQTT_MAX_HOST_LEN 100 // Note: App is set to max 100 chars for broker name +#define MQTT_MAX_USERNAME_LEN 50 // Note: App is set to max 50 chars for username +#define MQTT_MAX_PASSWORD_LEN 120 // Note: App is set to max 120 chars for password #define MQTT_MAX_ID_LEN 16 // MQTT Client Id to uniquely reference this unit -#define MQTT_RECONNECT_DELAY 120 // Minumum of 60 seconds between reconnect attempts +#define MQTT_RECONNECT_DELAY 100 // Minumum of 60 seconds between reconnect attempts #define MQTT_ROOT_TOPIC "opensprinkler" #define MQTT_AVAILABILITY_TOPIC MQTT_ROOT_TOPIC "/availability" From df603d129e2dc194ad4fae4321f2837644e74a30 Mon Sep 17 00:00:00 2001 From: Ray Date: Wed, 10 Jul 2024 08:06:56 -0400 Subject: [PATCH 15/61] add firmware upgrade message on the /update pages --- OpenSprinkler.cpp | 2 +- html/ap_home.html | 5 ++--- html/ap_update.html | 3 ++- html/sta_update.html | 3 ++- htmls.h | 5 +++-- 5 files changed, 10 insertions(+), 8 deletions(-) diff --git a/OpenSprinkler.cpp b/OpenSprinkler.cpp index b41041d8..774f2a3f 100644 --- a/OpenSprinkler.cpp +++ b/OpenSprinkler.cpp @@ -2261,7 +2261,7 @@ void OpenSprinkler::parse_otc_config() { void OpenSprinkler::options_setup() { // Check reset conditions: - if (file_read_byte(IOPTS_FILENAME, IOPT_FW_VERSION)<220 || // fw version is invalid (<219) + if (file_read_byte(IOPTS_FILENAME, IOPT_FW_VERSION)0) { + if(id('bssid').value.length>0) { if(id('channel').value.length>0) comm+='&extra='+encodeURIComponent(id('bssid').value+'@'+id('channel').value); else comm+='&extra='+encodeURIComponent(id('bssid').value+'@0'); } - xhr.open('POST', comm, true); xhr.send(); id('butt').disabled=true;id('ssid').disabled=true;id('pass').disabled=true; id('bssid').disabled=true;id('channel').disabled=true; @@ -84,4 +83,4 @@ } setTimeout(loadSSIDs, 1000); - \ No newline at end of file + diff --git a/html/ap_update.html b/html/ap_update.html index c6434c13..bfb9841c 100644 --- a/html/ap_update.html +++ b/html/ap_update.html @@ -11,6 +11,7 @@ Device password: + IMPORTANT: firmware update across major revisions (e.g. from 2.2.0 to 2.2.1) will trigger a factory reset. Save a copy of your current configurations before proceeding, so you can import and recover them after the upgrade.