/** * @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 * . */ #include "cfg.h" #ifdef USE_SMTP #include #include #include #define SMTP_OPENSSL #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 }; /** * 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]; }; struct smtp; 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); enum smtp_status_code smtp_auth(struct smtp *const smtp, enum smtp_authentication_method auth_method, const char *const user, const char *const pass); enum smtp_status_code smtp_mail(struct smtp *const smtp, const char *const body); enum smtp_status_code smtp_close(struct smtp *smtp); enum smtp_status_code smtp_status_code_get(const struct smtp *const smtp); enum smtp_status_code smtp_status_code_clear(struct smtp *const smtp); enum smtp_status_code smtp_status_code_set(struct smtp *const smtp, enum smtp_status_code new_status_code); const char * smtp_status_code_errstr(enum smtp_status_code status_code); enum smtp_status_code smtp_header_add(struct smtp *const smtp, const char *const key, const char *const value); void smtp_header_clear_all(struct smtp *const smtp); enum smtp_status_code smtp_address_add(struct smtp *const smtp, enum smtp_address_type type, const char *const email, const char *const name); void smtp_address_clear_all(struct smtp *const smtp); enum smtp_status_code smtp_attachment_add_path(struct smtp *const smtp, const char *const name, const char *const path); enum smtp_status_code smtp_attachment_add_fp(struct smtp *const smtp, const char *const name, FILE *fp); enum smtp_status_code smtp_attachment_add_mem(struct smtp *const smtp, const char *const name, const void *const data, size_t datasz); void smtp_attachment_clear_all(struct smtp *const smtp); ////////////////////////////////////////////////////////////////////// #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; } #endif ///////////////////////////////////////////////////// #include #include "common.h" #include "thread.h" #include "xlist.h" #include "elog.h" #include "lock.h" #include "sqlite_handle.h" #ifdef __USE_CURL_SMTP #include static char send_buff[1024] = {0}; struct upload_status { size_t bytes_read; }; static size_t payload_source(char *ptr, size_t size, size_t nmemb, void *userp) { struct upload_status *upload_ctx = (struct upload_status *)userp; const char *data; size_t room = size * nmemb; if((size == 0) || (nmemb == 0) || ((size*nmemb) < 1)) { return 0; } data = &send_buff[upload_ctx->bytes_read]; if(data) { size_t len = strlen(data); if(room < len) len = room; memcpy(ptr, data, len); upload_ctx->bytes_read += len; return len; } return 0; } static void fill_send_buff(const char *to,const char *from,const char *subject,const char *body) { time_t timer;//time_t就是long int 类型 struct tm *tblock; timer = time(NULL); tblock = localtime(&timer); memset(send_buff,0,1024); sprintf(send_buff,"Subject: %s\r\n" "From: %s\r\n" "To: %s\r\n" "Date: %s\r\n" "\r\n" "%s\r\n" "\r\n",subject,from,to,asctime(tblock),body); } #endif #if 1 #define LOGD log_d #define LOGE log_e #define LOGW log_w #else #define LOGD printf #define LOGE printf #define LOGW printf #endif #define MAIL_CONNECTION_SECURITY SMTP_SECURITY_STARTTLS #define MAIL_FLAGS (SMTP_DEBUG|SMTP_NO_CERT_VERIFY) /* Do not verify cert. */ #define MAIL_CAFILE NULL #define MAIL_AUTH SMTP_AUTH_PLAIN typedef struct { char sub[128]; char body[2048]; }smtp_pkt_t; typedef struct { handle_t list; smtp_info_t *info; #ifdef __USE_CURL_SMTP CURL *curl; #endif GlobalDeviceManager *dm; int inited; }smtp_handle_t; static smtp_handle_t smHandle; static void sleep_ms(int ms) { usleep(ms*1000); } static int send_mail(smtp_send_t *send, void *addr, char *sub, char *body, int multi) { #ifdef USE_SMTP int i,r=-1,flag=0; struct smtp *smtp=NULL; enum smtp_flag flags=(SMTP_DEBUG|SMTP_NO_CERT_VERIFY); enum smtp_connection_security security=SMTP_SECURITY_STARTTLS; enum smtp_authentication_method auth=SMTP_AUTH_PLAIN; lock_s_hold(LOCK_ID_SMTP); if(!sub || !body || send->mode==0) { goto out; } if(strcmp(send->auth, "LOGIN")==0) { auth = SMTP_AUTH_LOGIN; } else if(strcmp(send->auth, "CRAM-MD5")==0) { auth = SMTP_AUTH_CRAM_MD5; } if(strcmp(send->port,"465")==0) { security = SMTP_SECURITY_TLS; } r = smtp_open(send->server, send->port, security, flags, NULL, &smtp); if(r != SMTP_STATUS_OK) { LOGE("___ smtp_open failed, %s\n", smtp_status_code_errstr(r)); return -1; } r = smtp_auth(smtp, auth, send->account, send->password); if(r != SMTP_STATUS_OK) { LOGE("___ smtp_auth failed, %s\n", smtp_status_code_errstr(r)); goto quit; } r = smtp_address_add(smtp, SMTP_ADDRESS_FROM, send->account, send->account); if(r != SMTP_STATUS_OK) { LOGE("___ smtp_address_add fr failed\n, %s", smtp_status_code_errstr(r)); goto quit; } if(multi) { smtp_recv_t *recv=(smtp_recv_t*)addr; for(i=0; i0) { r = smtp_address_add(smtp, SMTP_ADDRESS_TO, recv[i].account, recv[i].account); if(r != SMTP_STATUS_OK) { LOGE("___ smtp_address_add to %s failed, %s\n", recv[i].account, smtp_status_code_errstr(r)); } else { flag = 1; } } } } else { char *sendto=(char*)addr; r = smtp_address_add(smtp, SMTP_ADDRESS_TO, sendto, sendto); if(r != SMTP_STATUS_OK) { LOGE("___ smtp_address_add to %s failed, %s\n", sendto, smtp_status_code_errstr(r)); } else { flag = 1; } } if(flag>0) { r = smtp_header_add(smtp, "Subject", sub); if(r != SMTP_STATUS_OK) { LOGE("___ smtp_header_add failed, %s\n", smtp_status_code_errstr(r)); goto quit; } //r = smtp_attachment_add_mem(smtp, "test.txt", "Test email attachment.", -1); smtp_mail(smtp, body); } quit: smtp_close(smtp); if(r != SMTP_STATUS_OK){ LOGE("___ send_mail failed, server: %s, port: %s failed\n", send->server, send->port); } out: lock_s_release(LOCK_ID_SMTP); return r; #endif #ifdef __USE_CURL_SMTP CURLcode res = CURLE_OK; smtp_handle_t *h=&smHandle; char url_buff[256] = {0}; sprintf(url_buff,"%s:%s",send->server,send->port); struct upload_status upload_ctx = { 0 }; struct curl_slist *recipients = NULL; smtp_recv_t *recv=(smtp_recv_t*)addr; res = curl_easy_setopt(h->curl, CURLOPT_URL,url_buff); LOGD("url------%s %d\n",url_buff,res); res = curl_easy_setopt(h->curl, CURLOPT_USERNAME,send->account); //setting user LOGD("usrname------%s %d\n",send->account,res); res = curl_easy_setopt(h->curl, CURLOPT_PASSWORD, send->password); //setting password LOGD("passworld------%s %d\n",send->password,res); // res = curl_easy_setopt(h->curl, CURLOPT_USE_SSL, (long)CURLUSESSL_ALL); //setting openssl/tls LOGD("SSL------ %d\n",res); //curl_easy_setopt(curl, CURLOPT_CAINFO, "/path/to/certificate.pem"); res = curl_easy_setopt(h->curl, CURLOPT_MAIL_FROM,send->account); LOGD("From------%s %d\n",send->account,res); curl_easy_setopt(h->curl, CURLOPT_READFUNCTION, payload_source); curl_easy_setopt(h->curl, CURLOPT_READDATA, &upload_ctx); char to_buff[256] = {0}; if(multi) { for(int i=0; i0) { recipients = curl_slist_append(recipients, recv[i].account); //recipients = curl_slist_append(recipients, CC_MAIL); strcat(to_buff,recv[i].account); strcat(to_buff,";"); } } }else { char *sendto=(char*)addr; recipients = curl_slist_append(recipients,sendto); strcat(to_buff,sendto); strcat(to_buff,";"); } fill_send_buff(to_buff,send->account,sub,body); curl_easy_setopt(h->curl, CURLOPT_UPLOAD, 1L); res = curl_easy_setopt(h->curl, CURLOPT_MAIL_RCPT, recipients); LOGD("rcpt------ %d\n",res); res = curl_easy_setopt(h->curl, CURLOPT_VERBOSE, 1L); LOGD("verbose------ %d\n",res); res = curl_easy_perform(h->curl); LOGD("send------ %d\n",res); if(res != CURLE_OK) { LOGE("send failed!!!!\n"); } curl_slist_free_all(recipients); //curl_easy_cleanup(h->curl); #endif return 0; } static void* smtp_thread(void *arg) { int r; thread_handle_t *th=(thread_handle_t*)arg; smtp_handle_t *h=(smtp_handle_t*)th->arg; list_node_t *ln=NULL; smtp_pkt_t *pkt=NULL; smtp_send_t *send=&h->info->send; smtp_recv_t *recv=h->info->recv; while(th->quit==0) { r = xlist_take_node(h->list, &ln, 0); if(r==0) { pkt = (smtp_pkt_t*)ln->data.buf; send_mail(send, recv, pkt->sub, pkt->body, 1); xlist_back_node(h->list, ln); } sleep_ms(100); } pthread_exit(NULL); } int smtp_init(void) { list_cfg_t lc; smtp_handle_t *h=&smHandle; GlobalDeviceManager *dm=&__globalDeviceManage; lc.mode = LIST_FULL_FIFO; lc.max = 100; lc.log = 0; h->list = xlist_init(&lc); h->dm = dm; h->info = &h->dm->smtpInfo; #ifdef __USE_CURL_SMTP if(!h->curl){ h->curl = curl_easy_init(); if(!h->curl) { return -1; } } #endif dev_smtp_init(dm->db, h->info); thread_start(THREAD_ID_SMTP, smtp_thread, h, 5*MB, 0); h->inited = 1; //smtp_test(); return 0; } int smtp_deinit(void) { smtp_handle_t *h=&smHandle; thread_stop(THREAD_ID_SMTP); xlist_free(h->list); #ifdef __USE_CURL_SMTP free(h->curl); h->curl = 0; #endif h->inited = 0; return 0; } static int post_data(char *sub, char *body) { int r=0; smtp_pkt_t pkt; smtp_handle_t *h=&smHandle; snprintf(pkt.sub, sizeof(pkt.sub), "%s", sub); snprintf(pkt.body, sizeof(pkt.body), "%s", body); r = xlist_append(h->list, 0, &pkt, sizeof(pkt)); return r; } int smtp_send(char *sub, char *body) { int r=-1; smtp_handle_t *h=&smHandle; if(!h->inited) { return -1; } r = send_mail(&h->info->send, h->info->recv, sub, body, 1); return r; } int smtp_post(char *sub, char *body) { int r=-1; smtp_handle_t *h=&smHandle; if(!h->inited) { return -1; } r = post_data(sub, body); return r; } int smtp_post_alarm(int type, int subtype, int alarm_type, char *content) { int r=0; smtp_pkt_t pkt; char temp[128]; smtp_handle_t *h=&smHandle; if(!h->inited) { return -1; } int prod_id=h->dm->_globalDevInfo.product_id; int langID=h->dm->_globalDevInfo.product_language; switch(type) { case ALARM_TYPE_POWER: { if(subtype) { switch(alarm_type) { case ENABLE_TAC_V_UP: case ENABLE_TAC_V_DOWN: snprintf(temp, sizeof(temp), "PDU %d %s %s %s", prod_id, language_alarm_V[langID], language_exceed[langID], language_alarm[langID]); break; case ENABLE_TAC_A_UP: snprintf(temp, sizeof(temp), "PDU %d %s %s %s", prod_id, language_alarm_A[langID], language_exceed[langID], language_alarm[langID]); break; case ENABLE_TAC_W_UP: snprintf(temp, sizeof(temp), "PDU %d %s %s %s", prod_id, language_alarm_W[langID], language_exceed[langID], language_alarm[langID]); break; case ENABLE_TAC_P_UP: snprintf(temp, sizeof(temp), "PDU %d %s %s %s", prod_id, language_alarm_P[langID], language_exceed[langID], language_alarm[langID]); break; case ENABLE_TAC_STIME: case ENABLE_TAC_ETIME: snprintf(temp, sizeof(temp), "PDU %d %s %s", prod_id, language_alarm_phase_lose[langID], language_alarm[langID]); break; default: return -1; } } else { switch(alarm_type) { case WARNING_V_UP: case WARNING_V_DOWN: snprintf(temp, sizeof(temp), "PDU %d %s %s %s", prod_id, language_alarm_V[langID], language_exceed[langID], language_alarm[langID]); break; case WARNING_A_UP: snprintf(temp, sizeof(temp), "PDU %d %s %s %s", prod_id, language_alarm_A[langID], language_exceed[langID], language_alarm[langID]); break; case WARNING_W_UP: snprintf(temp, sizeof(temp), "PDU %d %s %s %s", prod_id, language_alarm_W[langID], language_exceed[langID], language_alarm[langID]); break; case WARNING_P_UP: snprintf(temp, sizeof(temp), "PDU %d %s %s %s", prod_id, language_alarm_P[langID], language_exceed[langID], language_alarm[langID]); break; case 0 ... 2: snprintf(temp, sizeof(temp), "PDU %d %s %s", prod_id, language_alarm_phase_lose[langID], language_alarm[langID]); break; default: return -1; } } } break; case ALARM_TYPE_SENSOR: { switch(subtype) { case SENSOR_TYPE_DoubleT: case SENSOR_TYPE_TEMPERATURE: snprintf(temp, sizeof(temp), "PDU %d %s %s", prod_id, language_temperature[langID], language_alarm[langID]); break; case SENSOR_TYPE_SMOKE: snprintf(temp, sizeof(temp), "PDU %d %s %s", prod_id, language_sensor_smoke[langID], language_alarm[langID]); break; case SENSOR_TYPE_WATER: snprintf(temp, sizeof(temp), "PDU %d %s %s", prod_id, language_humidity[langID], language_alarm[langID]); break; case SENSOR_TYPE_DOOR: snprintf(temp, sizeof(temp), "PDU %d %s %s", prod_id, language_sensor_access[langID], language_alarm[langID]); break; case SENSOR_TYPE_GAS: snprintf(temp, sizeof(temp), "PDU %d %s %s", prod_id, language_atmosphere_gas[langID], language_alarm[langID]); break; case SENSOR_TYPE_AIR_PRESS: snprintf(temp, sizeof(temp), "PDU %d %s %s", prod_id, language_atmosphere_pressure[langID], language_alarm[langID]); break; default: return -1; } } break; case ALARM_TYPE_NETWORK: { snprintf(temp, sizeof(temp), "PDU %d %s", prod_id, language_alarm_Type_Network[langID]); } break; default: return -1; } replace_sequences(content, h->dm->alarmTab); r = post_data(temp, content); return r; } int smtp_test(void) { int r=0,cnt=0; #ifdef USE_SMTP char temp[200]; smtp_handle_t *h=&smHandle; smtp_info_t info={ .send = { .id = 0, .mode = 1, .account = "rcp064867@163.com", .password = "UODRIFWDFBTJTVLW", .server = "smtp.163.com", .port = "25", //.port = "465", //.port = "587", .auth = "PLAIN", //.auth = "LOGIN", //.auth = "CRAM-MD5", }, .recv = { {0, 1, "1556907721@qq.com"}, {1, 1, "rcp064867@163.com"}, {2, 1, "cijiu0219@sina.com"}, {3, 1, "jiuci4902@gmail.com"}, {4, 1, "cijiu0219@hotmail.com"}, {5, 1, "cijiu990219@outlook.com"}, } }; *h->info = info; char *sub="123"; char *body="hello everyone"; while(cnt++<5) { sprintf(temp, "%s %d", body, cnt); r = smtp_send(sub, temp); sleep(2); } #endif return r; }