Branch :
/**
* @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
* <a href='https://www.somnisoft.com/smtp-client'>
* www.somnisoft.com/smtp-client
* </a>.
*/
#if defined(_WIN32) || defined(WIN32) || defined(WIN64)
# define SMTP_IS_WINDOWS
#endif /* SMTP_IS_WINDOWS */
#ifdef SMTP_IS_WINDOWS
# include <winsock2.h>
# include <ws2tcpip.h>
#else /* POSIX */
# include <netinet/in.h>
# include <sys/select.h>
# include <sys/socket.h>
# include <netdb.h>
# include <unistd.h>
#endif /* SMTP_IS_WINDOWS */
#include <errno.h>
#include <limits.h>
#include <signal.h>
#include <stdarg.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>
#ifdef SMTP_OPENSSL
# include <openssl/bio.h>
# include <openssl/err.h>
# include <openssl/ssl.h>
# include <openssl/x509.h>
# include <openssl/x509v3.h>
#endif /* SMTP_OPENSSL */
/**
* Get access to the @ref smtp_result_code and @ref smtp_command definitions.
*/
#define SMTP_INTERNAL_DEFINE
#include "smtp.h"
/**
* 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.
*/
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.
*/
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.
*/
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((unsigned long) 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.
*/
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.
*/
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.
*/
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.
*/
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.
*/
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.
*/
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.
*/
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.
*/
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.
*/
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.
*/
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.
*/
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.
*/
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.
*/
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.
*/
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.
*/
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.
*/
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.
*/
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.
*/
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.
*/
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.
*/
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();
#if OPENSSL_VERSION_NUMBER < 0x30000000L || defined(LIBRESSL_VERSION_NUMBER)
ERR_load_BIO_strings();
#endif
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);
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 ((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<user>\0<password>",
* 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 <b64><CR><NL>".
*
* @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 <b64_username><CR><NL>".
* 3. Base64 encode the password and send that by itself on a separate
* line: "<b64_password><CR><NL>".
*
* @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<CR><NL>" 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: "<user> <(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.
*/
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:<mail\@example.com>
* RCPT TO:<mail\@example.com>
*
* @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.
*/
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.
*/
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.
*/
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.
*/
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.
*/
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;
}