/**
 * @file
 * SMTP client module
 * 
 * Author: Simon Goldschmidt
 *
 * @defgroup smtp SMTP client
 * @ingroup apps
 * 
 * This is simple SMTP client for raw API.
 * It is a minimal implementation of SMTP as specified in RFC 5321.
 *
 * Example usage:
@code{.c}
 void my_smtp_result_fn(void *arg, u8_t smtp_result, u16_t srv_err, err_t err)
 {
   printf("mail (%p) sent with results: 0x%02x, 0x%04x, 0x%08x\n", arg,
          smtp_result, srv_err, err);
 }
 static void my_smtp_test(void)
 {
   smtp_set_server_addr("mymailserver.org");
   -> set both username and password as NULL if no auth needed
   smtp_set_auth("username", "password");
   smtp_send_mail("sender", "recipient", "subject", "body", my_smtp_result_fn,
                  some_argument);
 }
@endcode

 * When using from any other thread than the tcpip_thread (for NO_SYS==0), use
 * smtp_send_mail_int()!
 * 
 * SMTP_BODYDH usage:
@code{.c}
 int my_smtp_bodydh_fn(void *arg, struct smtp_bodydh *bdh)
 {
    if(bdh->state >= 10) {
       return BDH_DONE;
    }
    sprintf(bdh->buffer,"Line #%2d\r\n",bdh->state);
    bdh->length = strlen(bdh->buffer);
    ++bdh->state;
    return BDH_WORKING;
 }
 
 smtp_send_mail_bodycback("sender", "recipient", "subject", 
                my_smtp_bodydh_fn, my_smtp_result_fn, some_argument);
@endcode
 * 
 * @todo:
 * - attachments (the main difficulty here is streaming base64-encoding to
 *   prevent having to allocate a buffer for the whole encoded file at once)
 * - test with more mail servers...
 *
 */

#include "lwip/apps/smtp.h"

#if LWIP_TCP && LWIP_CALLBACK_API
#include "lwip/sys.h"
#include "lwip/sockets.h"
#include "lwip/altcp.h"
#include "lwip/dns.h"
#include "lwip/mem.h"
#include "lwip/altcp_tcp.h"
#include "lwip/altcp_tls.h"

#include <string.h> /* strlen, memcpy */
#include <stdlib.h>

/** TCP poll interval. Unit is 0.5 sec. */
#define SMTP_POLL_INTERVAL      4
/** TCP poll timeout while sending message body, reset after every
 * successful write. 3 minutes */
#define SMTP_TIMEOUT_DATABLOCK  ( 3 * 60 * SMTP_POLL_INTERVAL / 2)
/** TCP poll timeout while waiting for confirmation after sending the body.
 * 10 minutes */
#define SMTP_TIMEOUT_DATATERM   (10 * 60 * SMTP_POLL_INTERVAL / 2)
/** TCP poll timeout while not sending the body.
 * This is somewhat lower than the RFC states (5 minutes for initial, MAIL
 * and RCPT) but still OK for us here.
 * 2 minutes */
#define SMTP_TIMEOUT            ( 2 * 60 * SMTP_POLL_INTERVAL / 2)

/* the various debug levels for this file */
#define SMTP_DEBUG_TRACE        (SMTP_DEBUG | LWIP_DBG_TRACE)
#define SMTP_DEBUG_STATE        (SMTP_DEBUG | LWIP_DBG_STATE)
#define SMTP_DEBUG_WARN         (SMTP_DEBUG | LWIP_DBG_LEVEL_WARNING)
#define SMTP_DEBUG_WARN_STATE   (SMTP_DEBUG | LWIP_DBG_LEVEL_WARNING | LWIP_DBG_STATE)
#define SMTP_DEBUG_SERIOUS      (SMTP_DEBUG | LWIP_DBG_LEVEL_SERIOUS)


#define SMTP_RX_BUF_LEN         255
#define SMTP_TX_BUF_LEN         255
#define SMTP_CRLF               "\r\n"
#define SMTP_CRLF_LEN           2

#define SMTP_RESP_220           "220"
#define SMTP_RESP_235           "235"
#define SMTP_RESP_250           "250"
#define SMTP_RESP_334           "334"
#define SMTP_RESP_354           "354"
#define SMTP_RESP_LOGIN_UNAME   "VXNlcm5hbWU6"
#define SMTP_RESP_LOGIN_PASS    "UGFzc3dvcmQ6"

#define SMTP_KEYWORD_AUTH_SP    "AUTH "
#define SMTP_KEYWORD_AUTH_EQ    "AUTH="
#define SMTP_KEYWORD_AUTH_LEN   5
#define SMTP_AUTH_PARAM_PLAIN   "PLAIN"
#define SMTP_AUTH_PARAM_LOGIN   "LOGIN"

#define SMTP_CMD_EHLO_1           "EHLO ["
#define SMTP_CMD_EHLO_1_LEN       6
#define SMTP_CMD_EHLO_2           "]\r\n"
#define SMTP_CMD_EHLO_2_LEN       3
#define SMTP_CMD_AUTHPLAIN_1      "AUTH PLAIN "
#define SMTP_CMD_AUTHPLAIN_1_LEN  11
#define SMTP_CMD_AUTHPLAIN_2      "\r\n"
#define SMTP_CMD_AUTHPLAIN_2_LEN  2
#define SMTP_CMD_AUTHLOGIN        "AUTH LOGIN\r\n"
#define SMTP_CMD_AUTHLOGIN_LEN    12
#define SMTP_CMD_MAIL_1           "MAIL FROM: <"
#define SMTP_CMD_MAIL_1_LEN       12
#define SMTP_CMD_MAIL_2           ">\r\n"
#define SMTP_CMD_MAIL_2_LEN       3
#define SMTP_CMD_RCPT_1           "RCPT TO: <"
#define SMTP_CMD_RCPT_1_LEN       10
#define SMTP_CMD_RCPT_2           ">\r\n"
#define SMTP_CMD_RCPT_2_LEN       3
#define SMTP_CMD_DATA             "DATA\r\n"
#define SMTP_CMD_DATA_LEN         6
#define SMTP_CMD_HEADER_1         "From: <"
#define SMTP_CMD_HEADER_1_LEN     7
#define SMTP_CMD_HEADER_2         ">\r\nTo: <"
#define SMTP_CMD_HEADER_2_LEN     8
#define SMTP_CMD_HEADER_3         ">\r\nSubject: "
#define SMTP_CMD_HEADER_3_LEN     12
#define SMTP_CMD_HEADER_4         "\r\n\r\n"
#define SMTP_CMD_HEADER_4_LEN     4
#define SMTP_CMD_BODY_FINISHED    "\r\n.\r\n"
#define SMTP_CMD_BODY_FINISHED_LEN 5
#define SMTP_CMD_QUIT             "QUIT\r\n"
#define SMTP_CMD_QUIT_LEN         6

#if defined(SMTP_STAT_TX_BUF_MAX) && SMTP_STAT_TX_BUF_MAX
#define SMTP_TX_BUF_MAX(len) LWIP_MACRO(if((len) > smtp_tx_buf_len_max) smtp_tx_buf_len_max = (len);)
#else /* SMTP_STAT_TX_BUF_MAX */
#define SMTP_TX_BUF_MAX(len)
#endif /* SMTP_STAT_TX_BUF_MAX */

#if SMTP_COPY_AUTHDATA
#define SMTP_USERNAME(session)        (session)->username
#define SMTP_PASS(session)            (session)->pass
#define SMTP_AUTH_PLAIN_DATA(session) (session)->auth_plain
#define SMTP_AUTH_PLAIN_LEN(session)  (session)->auth_plain_len
#else /* SMTP_COPY_AUTHDATA */
#define SMTP_USERNAME(session)        smtp_username
#define SMTP_PASS(session)            smtp_pass
#define SMTP_AUTH_PLAIN_DATA(session) smtp_auth_plain
#define SMTP_AUTH_PLAIN_LEN(session)  smtp_auth_plain_len
#endif /* SMTP_COPY_AUTHDATA */

#if SMTP_BODYDH
#ifndef SMTP_BODYDH_MALLOC
#define SMTP_BODYDH_MALLOC(size)      mem_malloc(size)
#define SMTP_BODYDH_FREE(ptr)         mem_free(ptr)
#endif

/* Some internal state return values */
#define BDHALLDATASENT                2
#define BDHSOMEDATASENT               1

enum bdh_handler_state {
  BDH_SENDING,         /* Serving the user function generating body content */
  BDH_STOP             /* User function stopped, closing */
};
#endif

/** State for SMTP client state machine */
enum smtp_session_state {
  SMTP_NULL,
  SMTP_HELO,
  SMTP_AUTH_PLAIN,
  SMTP_AUTH_LOGIN_UNAME,
  SMTP_AUTH_LOGIN_PASS,
  SMTP_AUTH_LOGIN,
  SMTP_MAIL,
  SMTP_RCPT,
  SMTP_DATA,
  SMTP_BODY,
  SMTP_QUIT,
  SMTP_CLOSED
};

#ifdef LWIP_DEBUG
/** State-to-string table for debugging */
static const char *smtp_state_str[] = {
  "SMTP_NULL",
  "SMTP_HELO",
  "SMTP_AUTH_PLAIN",
  "SMTP_AUTH_LOGIN_UNAME",
  "SMTP_AUTH_LOGIN_PASS",
  "SMTP_AUTH_LOGIN",
  "SMTP_MAIL",
  "SMTP_RCPT",
  "SMTP_DATA",
  "SMTP_BODY",
  "SMTP_QUIT",
  "SMTP_CLOSED",
};

static const char *smtp_result_strs[] = {
  "SMTP_RESULT_OK",
  "SMTP_RESULT_ERR_UNKNOWN",
  "SMTP_RESULT_ERR_CONNECT",
  "SMTP_RESULT_ERR_HOSTNAME",
  "SMTP_RESULT_ERR_CLOSED",
  "SMTP_RESULT_ERR_TIMEOUT",
  "SMTP_RESULT_ERR_SVR_RESP",
  "SMTP_RESULT_ERR_MEM"
};
#endif /* LWIP_DEBUG */

#if SMTP_BODYDH
struct smtp_bodydh_state {
  smtp_bodycback_fn callback_fn;  /* The function to call (again) */
  u16_t state;
  struct smtp_bodydh exposed;     /* the user function structure */
};
#endif /* SMTP_BODYDH */

/** struct keeping the body and state of an smtp session */
struct smtp_session {
  /** keeping the state of the smtp session */
  enum smtp_session_state state;
  /** timeout handling, if this reaches 0, the connection is closed */
  u16_t timer;
  /** helper buffer for transmit, not used for sending body */
  char tx_buf[SMTP_TX_BUF_LEN + 1];
  struct pbuf* p;
  /** source email address */
  const char* from;
  /** size of the sourceemail address */
  u16_t from_len;
  /** target email address */
  const char* to;
  /** size of the target email address */
  u16_t to_len;
  /** subject of the email */
  const char *subject;
  /** length of the subject string */
  u16_t subject_len;
  /** this is the body of the mail to be sent */
  const char* body;
  /** this is the length of the body to be sent */
  u16_t body_len;
  /** amount of data from body already sent */
  u16_t body_sent;
  /** callback function to call when closed */
  smtp_result_fn callback_fn;
  /** argument for callback function */
  void *callback_arg;
#if SMTP_COPY_AUTHDATA
  /** Username to use for this request */
  char *username;
  /** Password to use for this request */
  char *pass;
  /** Username and password combined as necessary for PLAIN authentication */
  char auth_plain[SMTP_MAX_USERNAME_LEN + SMTP_MAX_PASS_LEN + 3];
  /** Length of smtp_auth_plain string (cannot use strlen since it includes \0) */
  size_t auth_plain_len;
#endif /* SMTP_COPY_AUTHDATA */
#if SMTP_BODYDH
  struct smtp_bodydh_state *bodydh;
#endif /* SMTP_BODYDH */
};

/** IP address or DNS name of the server to use for next SMTP request */
static char smtp_server[SMTP_MAX_SERVERNAME_LEN + 1];
/** TCP port of the server to use for next SMTP request */
static u16_t smtp_server_port = SMTP_DEFAULT_PORT;
#if LWIP_ALTCP && LWIP_ALTCP_TLS
/** If this is set, mail is sent using SMTPS */
static struct altcp_tls_config *smtp_server_tls_config;
#endif
/** Username to use for the next SMTP request */
static char *smtp_username;
/** Password to use for the next SMTP request */
static char *smtp_pass;
/** Username and password combined as necessary for PLAIN authentication */
static char smtp_auth_plain[SMTP_MAX_USERNAME_LEN + SMTP_MAX_PASS_LEN + 3];
/** Length of smtp_auth_plain string (cannot use strlen since it includes \0) */
static size_t smtp_auth_plain_len;

#if SMTP_CHECK_DATA
static err_t  smtp_verify(const char *data, size_t data_len, u8_t linebreaks_allowed);
#endif /* SMTP_CHECK_DATA */
static err_t  smtp_tcp_recv(void *arg, struct altcp_pcb *pcb, struct pbuf *p, err_t err);
static void   smtp_tcp_err(void *arg, err_t err);
static err_t  smtp_tcp_poll(void *arg, struct altcp_pcb *pcb);
static err_t  smtp_tcp_sent(void *arg, struct altcp_pcb *pcb, u16_t len);
static err_t  smtp_tcp_connected(void *arg, struct altcp_pcb *pcb, err_t err);
#if LWIP_DNS
static void   smtp_dns_found(const char* hostname, const ip_addr_t *ipaddr, void *arg);
#endif /* LWIP_DNS */
#if SMTP_SUPPORT_AUTH_PLAIN || SMTP_SUPPORT_AUTH_LOGIN
static size_t smtp_base64_encode(char* target, size_t target_len, const char* source, size_t source_len);
#endif /* SMTP_SUPPORT_AUTH_PLAIN || SMTP_SUPPORT_AUTH_LOGIN */
static enum   smtp_session_state smtp_prepare_mail(struct smtp_session *s, u16_t *tx_buf_len);
static void   smtp_send_body(struct smtp_session *s, struct altcp_pcb *pcb);
static void   smtp_process(void *arg, struct altcp_pcb *pcb, struct pbuf *p);
#if SMTP_BODYDH
static void   smtp_send_body_data_handler(struct smtp_session *s, struct altcp_pcb *pcb);
#endif /* SMTP_BODYDH */


#ifdef LWIP_DEBUG
/** Convert an smtp result to a string */
const char*
smtp_result_str(u8_t smtp_result)
{
  if (smtp_result >= LWIP_ARRAYSIZE(smtp_result_strs)) {
    return "UNKNOWN";
  }
  return smtp_result_strs[smtp_result];
}

/** Null-terminates the payload of p for printing out messages.
 * WARNING: use this only if p is not needed any more as the last byte of
 *          payload is deleted!
 */
static const char*
smtp_pbuf_str(struct pbuf* p)
{
  if ((p == NULL) || (p->len == 0)) {
    return "";
  }
  ((char*)p->payload)[p->len] = 0;
  return (const char*)p->payload;
}
#endif /* LWIP_DEBUG */

/** @ingroup smtp
 * Set IP address or DNS name for next SMTP connection
 *
 * @param server IP address (in ASCII representation) or DNS name of the server
 */
err_t
smtp_set_server_addr(const char* server)
{
  size_t len = 0;

  LWIP_ASSERT_CORE_LOCKED();

  if (server != NULL) {
    /* strlen: returns length WITHOUT terminating 0 byte */
    len = strlen(server);
  }
  if (len > SMTP_MAX_SERVERNAME_LEN) {
    return ERR_MEM;
  }
  if (len != 0) {
    MEMCPY(smtp_server, server, len);
  }
  smtp_server[len] = 0; /* always OK because of smtp_server[SMTP_MAX_SERVERNAME_LEN + 1] */
  return ERR_OK;
}

/** @ingroup smtp
 * Set TCP port for next SMTP connection
 *
 * @param port TCP port
 */
void
smtp_set_server_port(u16_t port)
{
  LWIP_ASSERT_CORE_LOCKED();
  smtp_server_port = port;
}

#if LWIP_ALTCP && LWIP_ALTCP_TLS
/** @ingroup smtp
 * Set TLS configuration for next SMTP connection
 *
 * @param tls_config TLS configuration
 */
void
smtp_set_tls_config(struct altcp_tls_config *tls_config)
{
  LWIP_ASSERT_CORE_LOCKED();
  smtp_server_tls_config = tls_config;
}
#endif

/** @ingroup smtp
 * Set authentication parameters for next SMTP connection
 *
 * @param username login name as passed to the server
 * @param pass password passed to the server together with username
 */
err_t
smtp_set_auth(const char* username, const char* pass)
{
  size_t uname_len = 0;
  size_t pass_len = 0;

  LWIP_ASSERT_CORE_LOCKED();

  memset(smtp_auth_plain, 0xfa, 64);
  if (username != NULL) {
    uname_len = strlen(username);
    if (uname_len > SMTP_MAX_USERNAME_LEN) {
      LWIP_DEBUGF(SMTP_DEBUG_SERIOUS, ("Username is too long, %d instead of %d\n",
        (int)uname_len, SMTP_MAX_USERNAME_LEN));
      return ERR_ARG;
    }
  }
  if (pass != NULL) {
#if SMTP_SUPPORT_AUTH_LOGIN || SMTP_SUPPORT_AUTH_PLAIN
    pass_len = strlen(pass);
    if (pass_len > SMTP_MAX_PASS_LEN) {
      LWIP_DEBUGF(SMTP_DEBUG_SERIOUS, ("Password is too long, %d instead of %d\n",
        (int)uname_len, SMTP_MAX_USERNAME_LEN));
      return ERR_ARG;
    }
#else /* SMTP_SUPPORT_AUTH_LOGIN || SMTP_SUPPORT_AUTH_PLAIN */
    LWIP_DEBUGF(SMTP_DEBUG_WARN, ("Password not supported as no authentication methods are activated\n"));
#endif /* SMTP_SUPPORT_AUTH_LOGIN || SMTP_SUPPORT_AUTH_PLAIN */
  }
  *smtp_auth_plain = 0;
  if (username != NULL) {
    smtp_username = smtp_auth_plain + 1;
    strcpy(smtp_username, username);
  }
  if (pass != NULL) {
    smtp_pass = smtp_auth_plain + uname_len + 2;
    strcpy(smtp_pass, pass);
  }
  smtp_auth_plain_len = uname_len + pass_len + 2;

  return ERR_OK;
}

#if SMTP_BODYDH
static void smtp_free_struct(struct smtp_session *s)
{
  if (s->bodydh != NULL) {
    SMTP_BODYDH_FREE(s->bodydh);
  }
  SMTP_STATE_FREE(s);
}
#else /* SMTP_BODYDH */
#define smtp_free_struct(x) SMTP_STATE_FREE(x)
#endif /* SMTP_BODYDH */

static struct altcp_pcb*
smtp_setup_pcb(struct smtp_session *s, const ip_addr_t* remote_ip)
{
  struct altcp_pcb* pcb;
  LWIP_UNUSED_ARG(remote_ip);

#if LWIP_ALTCP && LWIP_ALTCP_TLS
  if (smtp_server_tls_config) {
    pcb = altcp_tls_new(smtp_server_tls_config, IP_GET_TYPE(remote_ip));
  } else
#endif
  {
    pcb = altcp_tcp_new_ip_type(IP_GET_TYPE(remote_ip));
  }
  if (pcb != NULL) {
    altcp_arg(pcb, s);
    altcp_recv(pcb, smtp_tcp_recv);
    altcp_err(pcb, smtp_tcp_err);
    altcp_poll(pcb, smtp_tcp_poll, SMTP_POLL_INTERVAL);
    altcp_sent(pcb, smtp_tcp_sent);
  }
  return pcb;
}

/** The actual mail-sending function, called by smtp_send_mail and
 * smtp_send_mail_static after setting up the struct smtp_session.
 */
static err_t
smtp_send_mail_alloced(struct smtp_session *s)
{
  err_t err;
  struct altcp_pcb* pcb = NULL;
  ip_addr_t addr;

  LWIP_ASSERT("no smtp_session supplied", s != NULL);

#if SMTP_CHECK_DATA
  /* check that body conforms to RFC:
   * - convert all single-CR or -LF in body to CRLF
   * - only 7-bit ASCII is allowed
   */
  if (smtp_verify(s->to, s->to_len, 0) != ERR_OK) {
    err = ERR_ARG;
    goto leave;
  }
  if (smtp_verify(s->from, s->from_len, 0) != ERR_OK) {
    err = ERR_ARG;
    goto leave;
  }
  if (smtp_verify(s->subject, s->subject_len, 0) != ERR_OK) {
    err = ERR_ARG;
    goto leave;
  }
#if SMTP_BODYDH
  if (s->bodydh == NULL)
#endif /* SMTP_BODYDH */
  {
    if (smtp_verify(s->body, s->body_len, 0) != ERR_OK) {
      err = ERR_ARG;
      goto leave;
    }
  }
#endif /* SMTP_CHECK_DATA */

#if SMTP_COPY_AUTHDATA
  /* copy auth data, ensuring the first byte is always zero */
  MEMCPY(s->auth_plain + 1, smtp_auth_plain + 1, smtp_auth_plain_len - 1);
  s->auth_plain_len = smtp_auth_plain_len;
  /* default username and pass is empty string */
  s->username = s->auth_plain;
  s->pass = s->auth_plain;
  if (smtp_username != NULL) {
    s->username += smtp_username - smtp_auth_plain;
  }
  if (smtp_pass != NULL) {
    s->pass += smtp_pass - smtp_auth_plain;
  }
#endif /* SMTP_COPY_AUTHDATA */

  s->state = SMTP_NULL;
  s->timer = SMTP_TIMEOUT;

#if LWIP_DNS
  err = dns_gethostbyname(smtp_server, &addr, smtp_dns_found, s);
#else /* LWIP_DNS */
  err = ipaddr_aton(smtp_server, &addr) ? ERR_OK : ERR_ARG;
#endif /* LWIP_DNS */
  if (err == ERR_OK) {
    pcb = smtp_setup_pcb(s, &addr);
    if (pcb == NULL) {
      err = ERR_MEM;
      goto leave;
    }
    err = altcp_connect(pcb, &addr, smtp_server_port, smtp_tcp_connected);
    if (err != ERR_OK) {
      LWIP_DEBUGF(SMTP_DEBUG_WARN_STATE, ("tcp_connect failed: %d\n", (int)err));
      goto deallocate_and_leave;
    }
  } else if (err != ERR_INPROGRESS) {
    LWIP_DEBUGF(SMTP_DEBUG_WARN_STATE, ("dns_gethostbyname failed: %d\n", (int)err));
    goto deallocate_and_leave;
  }
  return ERR_OK;

deallocate_and_leave:
  if (pcb != NULL) {
    altcp_arg(pcb, NULL);
    altcp_close(pcb);
  }
leave:
  smtp_free_struct(s);
  /* no need to call the callback here since we return != ERR_OK */
  return err;
}

/** @ingroup smtp
 *  Send an email via the currently selected server, username and password.
 *
 * @param from source email address (must be NULL-terminated)
 * @param to target email address (must be NULL-terminated)
 * @param subject email subject (must be NULL-terminated)
 * @param body email body (must be NULL-terminated)
 * @param callback_fn callback function
 * @param callback_arg user argument to callback_fn
 * @returns - ERR_OK if structures were allocated and no error occured starting the connection
 *            (this does not mean the email has been successfully sent!)
 *          - another err_t on error.
 */
err_t
smtp_send_mail(const char* from, const char* to, const char* subject, const char* body,
               smtp_result_fn callback_fn, void* callback_arg)
{
  struct smtp_session* s;
  size_t from_len = strlen(from);
  size_t to_len = strlen(to);
  size_t subject_len = strlen(subject);
  size_t body_len = strlen(body);
  size_t mem_len = sizeof(struct smtp_session);
  char *sfrom, *sto, *ssubject, *sbody;

  LWIP_ASSERT_CORE_LOCKED();

  mem_len += from_len + to_len + subject_len + body_len + 4;
  if (mem_len > 0xffff) {
    /* too long! */
    return ERR_MEM;
  }

  /* Allocate memory to keep this email's session state */
  s = (struct smtp_session *)SMTP_STATE_MALLOC((mem_size_t)mem_len);
  if (s == NULL) {
    return ERR_MEM;
  }
  /* initialize the structure */
  memset(s, 0, mem_len);
  s->from = sfrom = (char*)s + sizeof(struct smtp_session);
  s->from_len = (u16_t)from_len;
  s->to = sto = sfrom + from_len + 1;
  s->to_len = (u16_t)to_len;
  s->subject = ssubject = sto + to_len + 1;
  s->subject_len = (u16_t)subject_len;
  s->body = sbody = ssubject + subject_len + 1;
  s->body_len = (u16_t)body_len;
  /* copy source and target email address */
  /* cast to size_t is a hack to cast away constness */
  MEMCPY(sfrom, from, from_len + 1);
  MEMCPY(sto, to, to_len + 1);
  MEMCPY(ssubject, subject, subject_len + 1);
  MEMCPY(sbody, body, body_len + 1);

  s->callback_fn = callback_fn;
  s->callback_arg = callback_arg;

  /* call the actual implementation of this function */
  return smtp_send_mail_alloced(s);
}

/** @ingroup smtp
 * Same as smtp_send_mail, but doesn't copy from, to, subject and body into
 * an internal buffer to save memory.
 * WARNING: the above data must stay untouched until the callback function is
 *          called (unless the function returns != ERR_OK)
 */
err_t
smtp_send_mail_static(const char *from, const char* to, const char* subject,
  const char* body, smtp_result_fn callback_fn, void* callback_arg)
{
  struct smtp_session* s;
  size_t len;

  LWIP_ASSERT_CORE_LOCKED();

  s = (struct smtp_session*)SMTP_STATE_MALLOC(sizeof(struct smtp_session));
  if (s == NULL) {
    return ERR_MEM;
  }
  memset(s, 0, sizeof(struct smtp_session));
  /* initialize the structure */
  s->from = from;
  len = strlen(from);
  LWIP_ASSERT("string is too long", len <= 0xffff);
  s->from_len = (u16_t)len;
  s->to = to;
  len = strlen(to);
  LWIP_ASSERT("string is too long", len <= 0xffff);
  s->to_len = (u16_t)len;
  s->subject = subject;
  len = strlen(subject);
  LWIP_ASSERT("string is too long", len <= 0xffff);
  s->subject_len = (u16_t)len;
  s->body = body;
  len = strlen(body);
  LWIP_ASSERT("string is too long", len <= 0xffff);
  s->body_len = (u16_t)len;
  s->callback_fn = callback_fn;
  s->callback_arg = callback_arg;
  /* call the actual implementation of this function */
  return smtp_send_mail_alloced(s);
}


/** @ingroup smtp
 * Same as smtp_send_mail but takes a struct smtp_send_request as single
 * parameter which contains all the other parameters.
 * To be used with tcpip_callback to send mail from interrupt context or from
 * another thread.
 *
 * WARNING: server and authentication must stay untouched until this function has run!
 *
 * Usage example:
 * - allocate a struct smtp_send_request (in a way that is allowed in interrupt context)
 * - fill the members of the struct as if calling smtp_send_mail
 * - specify a callback_function
 * - set callback_arg to the structure itself
 * - call this function
 * - wait for the callback function to be called
 * - in the callback function, deallocate the structure (passed as arg)
 */
void
smtp_send_mail_int(void *arg)
{
  struct smtp_send_request *req = (struct smtp_send_request*)arg;
  err_t err;

  LWIP_ASSERT_CORE_LOCKED();
  LWIP_ASSERT("smtp_send_mail_int: no argument given", arg != NULL);

  if (req->static_data) {
    err = smtp_send_mail_static(req->from, req->to, req->subject, req->body,
      req->callback_fn, req->callback_arg);
  } else {
    err = smtp_send_mail(req->from, req->to, req->subject, req->body,
      req->callback_fn, req->callback_arg);
  }
  if ((err != ERR_OK) && (req->callback_fn != NULL)) {
    req->callback_fn(req->callback_arg, SMTP_RESULT_ERR_UNKNOWN, 0, err);
  }
}

#if SMTP_CHECK_DATA
/** Verify that a given string conforms to the SMTP rules
 * (7-bit only, no single CR or LF,
 *  @todo: no line consisting of a single dot only)
 */
static err_t
smtp_verify(const char *data, size_t data_len, u8_t linebreaks_allowed)
{
  size_t i;
  u8_t last_was_cr = 0;
  for (i = 0; i < data_len; i++) {
    char current = data[i];
    if ((current & 0x80) != 0) {
      LWIP_DEBUGF(SMTP_DEBUG_WARN, ("smtp_verify: no 8-bit data supported: %s\n", data));
      return ERR_ARG;
    }
    if (current == '\r') {
      if (!linebreaks_allowed) {
        LWIP_DEBUGF(SMTP_DEBUG_WARN, ("smtp_verify: found CR where no linebreaks allowed: %s\n", data));
        return ERR_ARG;
      }
      if (last_was_cr) {
        LWIP_DEBUGF(SMTP_DEBUG_WARN, ("smtp_verify: found double CR: %s\n", data));
        return ERR_ARG;
      }
      last_was_cr = 1;
    } else {
      if (current == '\n') {
        if (!last_was_cr) {
          LWIP_DEBUGF(SMTP_DEBUG_WARN, ("smtp_verify: found LF without CR before: %s\n", data));
          return ERR_ARG;
        }
      }
      last_was_cr = 0;
    }
  }
  return ERR_OK;
}
#endif /* SMTP_CHECK_DATA */

/** Frees the smtp_session and calls the callback function */
static void
smtp_free(struct smtp_session *s, u8_t result, u16_t srv_err, err_t err)
{
  smtp_result_fn fn = s->callback_fn;
  void *arg = s->callback_arg;
  if (s->p != NULL) {
    pbuf_free(s->p);
  }
  smtp_free_struct(s);
  if (fn != NULL) {
    fn(arg, result, srv_err, err);
  }
}

/** Try to close a pcb and free the arg if successful */
static void
smtp_close(struct smtp_session *s, struct altcp_pcb *pcb, u8_t result,
           u16_t srv_err, err_t err)
{
  if (pcb != NULL) {
     altcp_arg(pcb, NULL);
     if (altcp_close(pcb) == ERR_OK) {
       if (s != NULL) {
         smtp_free(s, result, srv_err, err);
       }
     } else {
       /* close failed, set back arg */
       altcp_arg(pcb, s);
     }
  } else {
    if (s != NULL) {
      smtp_free(s, result, srv_err, err);
    }
  }
}

/** Raw API TCP err callback: pcb is already deallocated */
static void
smtp_tcp_err(void *arg, err_t err)
{
  LWIP_UNUSED_ARG(err);
  if (arg != NULL) {
    LWIP_DEBUGF(SMTP_DEBUG_WARN_STATE, ("smtp_tcp_err: connection reset by remote host\n"));
    smtp_free((struct smtp_session*)arg, SMTP_RESULT_ERR_CLOSED, 0, err);
  }
}

/** Raw API TCP poll callback */
static err_t
smtp_tcp_poll(void *arg, struct altcp_pcb *pcb)
{
  if (arg != NULL) {
    struct smtp_session *s = (struct smtp_session*)arg;
    if (s->timer != 0) {
      s->timer--;
    }
  }
  smtp_process(arg, pcb, NULL);
  return ERR_OK;
}

/** Raw API TCP sent callback */
static err_t
smtp_tcp_sent(void *arg, struct altcp_pcb *pcb, u16_t len)
{
  LWIP_UNUSED_ARG(len);

  smtp_process(arg, pcb, NULL);

  return ERR_OK;
}

/** Raw API TCP recv callback */
static err_t
smtp_tcp_recv(void *arg, struct altcp_pcb *pcb, struct pbuf *p, err_t err)
{
  LWIP_UNUSED_ARG(err);
  if (p != NULL) {
    altcp_recved(pcb, p->tot_len);
    smtp_process(arg, pcb, p);
  } else {
    LWIP_DEBUGF(SMTP_DEBUG_WARN_STATE, ("smtp_tcp_recv: connection closed by remote host\n"));
    smtp_close((struct smtp_session*)arg, pcb, SMTP_RESULT_ERR_CLOSED, 0, err);
  }
  return ERR_OK;
}

static err_t
smtp_tcp_connected(void *arg, struct altcp_pcb *pcb, err_t err)
{
  LWIP_UNUSED_ARG(arg);

  if (err == ERR_OK) {
    LWIP_DEBUGF(SMTP_DEBUG_STATE, ("smtp_connected: Waiting for 220\n"));
  } else {
    /* shouldn't happen, but we still check 'err', only to be sure */
    LWIP_DEBUGF(SMTP_DEBUG_WARN, ("smtp_connected: %d\n", (int)err));
    smtp_close((struct smtp_session*)arg, pcb, SMTP_RESULT_ERR_CONNECT, 0, err);
  }
  return ERR_OK;
}

#if LWIP_DNS
/** DNS callback
 * If ipaddr is non-NULL, resolving succeeded, otherwise it failed.
 */
static void
smtp_dns_found(const char* hostname, const ip_addr_t *ipaddr, void *arg)
{
  struct smtp_session *s = (struct smtp_session*)arg;
  struct altcp_pcb *pcb;
  err_t err;
  u8_t result;

  LWIP_UNUSED_ARG(hostname);

  if (ipaddr != NULL) {
    pcb = smtp_setup_pcb(s, ipaddr);
    if (pcb != NULL) {
      LWIP_DEBUGF(SMTP_DEBUG_STATE, ("smtp_dns_found: hostname resolved, connecting\n"));
      err = altcp_connect(pcb, ipaddr, smtp_server_port, smtp_tcp_connected);
      if (err == ERR_OK) {
        return;
      }
      LWIP_DEBUGF(SMTP_DEBUG_WARN_STATE, ("tcp_connect failed: %d\n", (int)err));
      result = SMTP_RESULT_ERR_CONNECT;
    } else {
      LWIP_DEBUGF(SMTP_DEBUG_STATE, ("smtp_dns_found: failed to allocate tcp pcb\n"));
      result = SMTP_RESULT_ERR_MEM;
      err = ERR_MEM;
    }
  } else {
    LWIP_DEBUGF(SMTP_DEBUG_WARN_STATE, ("smtp_dns_found: failed to resolve hostname: %s\n",
      hostname));
    pcb = NULL;
    result = SMTP_RESULT_ERR_HOSTNAME;
    err = ERR_ARG;
  }
  smtp_close(s, pcb, result, 0, err);
}
#endif /* LWIP_DNS */

#if SMTP_SUPPORT_AUTH_PLAIN || SMTP_SUPPORT_AUTH_LOGIN

/** Table 6-bit-index-to-ASCII used for base64-encoding */
static const char base64_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',
  '+', '/'
};

/** Base64 encoding */
static size_t
smtp_base64_encode(char* target, size_t target_len, const char* source, size_t source_len)
{
  size_t i;
  s8_t j;
  size_t target_idx = 0;
  size_t longer = (source_len % 3) ? (3 - (source_len % 3)) : 0;
  size_t source_len_b64 = source_len + longer;
  size_t len = (((source_len_b64) * 4) / 3);
  u8_t x = 5;
  u8_t current = 0;
  LWIP_UNUSED_ARG(target_len);

  LWIP_ASSERT("target_len is too short", target_len >= len);

  for (i = 0; i < source_len_b64; i++) {
    u8_t b = (i < source_len ? (u8_t)source[i] : 0);
    for (j = 7; j >= 0; j--, x--) {
      if ((b & (1 << j)) != 0) {
        current = (u8_t)(current | (1U << x));
      }
      if (x == 0) {
        target[target_idx++] = base64_table[current];
        x = 6;
        current = 0;
      }
    }
  }
  for (i = len - longer; i < len; i++) {
    target[i] = '=';
  }
  return len;
}
#endif /* SMTP_SUPPORT_AUTH_PLAIN || SMTP_SUPPORT_AUTH_LOGIN */

/** Parse pbuf to see if it contains the beginning of an answer.
 * If so, it returns the contained response code as number between 1 and 999.
 * If not, zero is returned.
 *
 * @param s smtp session struct
 */
static u16_t
smtp_is_response(struct smtp_session *s)
{
  char digits[4];
  long num;

  if (s->p == NULL) {
    return 0;
  }
  /* copy three digits and convert them to int */
  if (pbuf_copy_partial(s->p, digits, 3, 0) != 3) {
    /* pbuf was too short */
    return 0;
  }
  digits[3] = 0;
  num = strtol(digits, NULL, 10);
  if ((num <= 0) || (num >= 1000)) {
    /* failed to find response code at start of line */
    return 0;
  }
  return (u16_t)num;
}

/** Parse pbuf to see if it contains a fully received answer.
 * If one is found, ERR_OK is returned.
 * If none is found, ERR_VAL is returned.
 *
 * A fully received answer is a 3-digit number followed by a space,
 * some string and a CRLF as line ending.
 *
 * @param s smtp session struct
 */
static err_t
smtp_is_response_finished(struct smtp_session *s)
{
  u8_t sp;
  u16_t crlf;
  u16_t offset;

  if (s->p == NULL) {
    return ERR_VAL;
  }
  offset = 0;
again:
  /* We could check the response number here, but we trust the
   * protocol definition which says the client can rely on it being
   * the same on every line. */

  /* find CRLF */
  if (offset > 0xFFFF - 4) {
    /* would overflow */
    return ERR_VAL;
  }
  crlf = pbuf_memfind(s->p, SMTP_CRLF, SMTP_CRLF_LEN, (u16_t)(offset + 4));
  if (crlf == 0xFFFF) {
    /* no CRLF found */
    return ERR_VAL;
  }
  sp = pbuf_get_at(s->p, (u16_t)(offset + 3));
  if (sp == '-') {
    /* no space after response code -> try next line */
    offset = (u16_t)(crlf + 2);
    if (offset < crlf) {
      /* overflow */
      return ERR_VAL;
    }
    goto again;
  } else if (sp == ' ') {
    /* CRLF found after response code + space -> valid response */
    return ERR_OK;
  }
  /* sp contains invalid character */
  return ERR_VAL;
}

/** Prepare HELO/EHLO message */
static enum smtp_session_state
smtp_prepare_helo(struct smtp_session *s, u16_t *tx_buf_len, struct altcp_pcb *pcb)
{
  size_t ipa_len;
  const char *ipa = ipaddr_ntoa(altcp_get_ip(pcb, 1));
  LWIP_ASSERT("ipaddr_ntoa returned NULL", ipa != NULL);
  ipa_len = strlen(ipa);
  LWIP_ASSERT("string too long", ipa_len <= (SMTP_TX_BUF_LEN-SMTP_CMD_EHLO_1_LEN-SMTP_CMD_EHLO_2_LEN));

  *tx_buf_len = (u16_t)(SMTP_CMD_EHLO_1_LEN + (u16_t)ipa_len + SMTP_CMD_EHLO_2_LEN);
  LWIP_ASSERT("tx_buf overflow detected", *tx_buf_len <= SMTP_TX_BUF_LEN);

  SMEMCPY(s->tx_buf, SMTP_CMD_EHLO_1, SMTP_CMD_EHLO_1_LEN);
  MEMCPY(&s->tx_buf[SMTP_CMD_EHLO_1_LEN], ipa, ipa_len);
  SMEMCPY(&s->tx_buf[SMTP_CMD_EHLO_1_LEN + ipa_len], SMTP_CMD_EHLO_2, SMTP_CMD_EHLO_2_LEN);
  return SMTP_HELO;
}

#if SMTP_SUPPORT_AUTH_PLAIN || SMTP_SUPPORT_AUTH_LOGIN
/** Parse last server response (in rx_buf) for supported authentication method,
 * create data to send out (to tx_buf), set tx_data_len correctly
 * and return the next state.
 */
static enum smtp_session_state
smtp_prepare_auth_or_mail(struct smtp_session *s, u16_t *tx_buf_len)
{
  /* check response for supported authentication method */
  u16_t auth = pbuf_strstr(s->p, SMTP_KEYWORD_AUTH_SP);
  if (auth == 0xFFFF) {
    auth = pbuf_strstr(s->p, SMTP_KEYWORD_AUTH_EQ);
  }
  if (auth != 0xFFFF) {
    u16_t crlf = pbuf_memfind(s->p, SMTP_CRLF, SMTP_CRLF_LEN, auth);
    if ((crlf != 0xFFFF) && (crlf > auth)) {
      /* use tx_buf temporarily */
      u16_t copied = pbuf_copy_partial(s->p, s->tx_buf, (u16_t)(crlf - auth), auth);
      if (copied != 0) {
        char *sep = s->tx_buf + SMTP_KEYWORD_AUTH_LEN;
        s->tx_buf[copied] = 0;
#if SMTP_SUPPORT_AUTH_PLAIN
        /* favour PLAIN over LOGIN since it involves less requests */
        if (strstr(sep, SMTP_AUTH_PARAM_PLAIN) != NULL) {
          size_t auth_len;
          /* server supports AUTH PLAIN */
          SMEMCPY(s->tx_buf, SMTP_CMD_AUTHPLAIN_1, SMTP_CMD_AUTHPLAIN_1_LEN);

          /* add base64-encoded string "\0username\0password" */
          auth_len = smtp_base64_encode(&s->tx_buf[SMTP_CMD_AUTHPLAIN_1_LEN],
            SMTP_TX_BUF_LEN - SMTP_CMD_AUTHPLAIN_1_LEN, SMTP_AUTH_PLAIN_DATA(s),
            SMTP_AUTH_PLAIN_LEN(s));
          LWIP_ASSERT("string too long", auth_len <= (SMTP_TX_BUF_LEN-SMTP_CMD_AUTHPLAIN_1_LEN-SMTP_CMD_AUTHPLAIN_2_LEN));
          *tx_buf_len = (u16_t)(SMTP_CMD_AUTHPLAIN_1_LEN + SMTP_CMD_AUTHPLAIN_2_LEN + (u16_t)auth_len);
          SMEMCPY(&s->tx_buf[SMTP_CMD_AUTHPLAIN_1_LEN + auth_len], SMTP_CMD_AUTHPLAIN_2,
            SMTP_CMD_AUTHPLAIN_2_LEN);
          return SMTP_AUTH_PLAIN;
        } else
#endif /* SMTP_SUPPORT_AUTH_PLAIN */
        {
#if SMTP_SUPPORT_AUTH_LOGIN
          if (strstr(sep, SMTP_AUTH_PARAM_LOGIN) != NULL) {
            /* server supports AUTH LOGIN */
            *tx_buf_len = SMTP_CMD_AUTHLOGIN_LEN;
            SMEMCPY(s->tx_buf, SMTP_CMD_AUTHLOGIN, SMTP_CMD_AUTHLOGIN_LEN);
            return SMTP_AUTH_LOGIN_UNAME;
          }
#endif /* SMTP_SUPPORT_AUTH_LOGIN */
        }
      }
    }
  }
  /* server didnt's send correct keywords for AUTH, try sending directly */
  return smtp_prepare_mail(s, tx_buf_len);
}
#endif /* SMTP_SUPPORT_AUTH_PLAIN || SMTP_SUPPORT_AUTH_LOGIN */

#if SMTP_SUPPORT_AUTH_LOGIN
/** Send base64-encoded username */
static enum smtp_session_state
smtp_prepare_auth_login_uname(struct smtp_session *s, u16_t *tx_buf_len)
{
  size_t base64_len = smtp_base64_encode(s->tx_buf, SMTP_TX_BUF_LEN,
    SMTP_USERNAME(s), strlen(SMTP_USERNAME(s)));
  /* @todo: support base64-encoded longer than 64k */
  LWIP_ASSERT("string too long", base64_len <= 0xffff);
  LWIP_ASSERT("tx_buf overflow detected", base64_len <= SMTP_TX_BUF_LEN - SMTP_CRLF_LEN);
  *tx_buf_len = (u16_t)(base64_len + SMTP_CRLF_LEN);

  SMEMCPY(&s->tx_buf[base64_len], SMTP_CRLF, SMTP_CRLF_LEN);
  s->tx_buf[*tx_buf_len] = 0;
  return SMTP_AUTH_LOGIN_PASS;
}

/** Send base64-encoded password */
static enum smtp_session_state
smtp_prepare_auth_login_pass(struct smtp_session *s, u16_t *tx_buf_len)
{
  size_t base64_len = smtp_base64_encode(s->tx_buf, SMTP_TX_BUF_LEN,
    SMTP_PASS(s), strlen(SMTP_PASS(s)));
  /* @todo: support base64-encoded longer than 64k */
  LWIP_ASSERT("string too long", base64_len <= 0xffff);
  LWIP_ASSERT("tx_buf overflow detected", base64_len <= SMTP_TX_BUF_LEN - SMTP_CRLF_LEN);
  *tx_buf_len = (u16_t)(base64_len + SMTP_CRLF_LEN);

  SMEMCPY(&s->tx_buf[base64_len], SMTP_CRLF, SMTP_CRLF_LEN);
  s->tx_buf[*tx_buf_len] = 0;
  return SMTP_AUTH_LOGIN;
}
#endif /* SMTP_SUPPORT_AUTH_LOGIN */

/** Prepare MAIL message */
static enum smtp_session_state
smtp_prepare_mail(struct smtp_session *s, u16_t *tx_buf_len)
{
  char *target = s->tx_buf;
  LWIP_ASSERT("tx_buf overflow detected", s->from_len <= (SMTP_TX_BUF_LEN - SMTP_CMD_MAIL_1_LEN - SMTP_CMD_MAIL_2_LEN));
  *tx_buf_len = (u16_t)(SMTP_CMD_MAIL_1_LEN + SMTP_CMD_MAIL_2_LEN + s->from_len);
  target[*tx_buf_len] = 0;

  SMEMCPY(target, SMTP_CMD_MAIL_1, SMTP_CMD_MAIL_1_LEN);
  target += SMTP_CMD_MAIL_1_LEN;
  MEMCPY(target, s->from, s->from_len);
  target += s->from_len;
  SMEMCPY(target, SMTP_CMD_MAIL_2, SMTP_CMD_MAIL_2_LEN);
  return SMTP_MAIL;
}

/** Prepare RCPT message */
static enum smtp_session_state
smtp_prepare_rcpt(struct smtp_session *s, u16_t *tx_buf_len)
{
  char *target = s->tx_buf;
  LWIP_ASSERT("tx_buf overflow detected", s->to_len <= (SMTP_TX_BUF_LEN - SMTP_CMD_RCPT_1_LEN - SMTP_CMD_RCPT_2_LEN));
  *tx_buf_len = (u16_t)(SMTP_CMD_RCPT_1_LEN + SMTP_CMD_RCPT_2_LEN + s->to_len);
  target[*tx_buf_len] = 0;

  SMEMCPY(target, SMTP_CMD_RCPT_1, SMTP_CMD_RCPT_1_LEN);
  target += SMTP_CMD_RCPT_1_LEN;
  MEMCPY(target, s->to, s->to_len);
  target += s->to_len;
  SMEMCPY(target, SMTP_CMD_RCPT_2, SMTP_CMD_RCPT_2_LEN);
  return SMTP_RCPT;
}

/** Prepare header of body */
static enum smtp_session_state
smtp_prepare_header(struct smtp_session *s, u16_t *tx_buf_len)
{
  char *target = s->tx_buf;
  int len = SMTP_CMD_HEADER_1_LEN + SMTP_CMD_HEADER_2_LEN +
    SMTP_CMD_HEADER_3_LEN + SMTP_CMD_HEADER_4_LEN + s->from_len + s->to_len +
    s->subject_len;
  LWIP_ASSERT("tx_buf overflow detected", len > 0 && len <= SMTP_TX_BUF_LEN);
  *tx_buf_len = (u16_t)len;
  target[*tx_buf_len] = 0;

  SMEMCPY(target, SMTP_CMD_HEADER_1, SMTP_CMD_HEADER_1_LEN);
  target += SMTP_CMD_HEADER_1_LEN;
  MEMCPY(target, s->from, s->from_len);
  target += s->from_len;
  SMEMCPY(target, SMTP_CMD_HEADER_2, SMTP_CMD_HEADER_2_LEN);
  target += SMTP_CMD_HEADER_2_LEN;
  MEMCPY(target, s->to, s->to_len);
  target += s->to_len;
  SMEMCPY(target, SMTP_CMD_HEADER_3, SMTP_CMD_HEADER_3_LEN);
  target += SMTP_CMD_HEADER_3_LEN;
  MEMCPY(target, s->subject, s->subject_len);
  target += s->subject_len;
  SMEMCPY(target, SMTP_CMD_HEADER_4, SMTP_CMD_HEADER_4_LEN);

  return SMTP_BODY;
}

/** Prepare QUIT message */
static enum smtp_session_state
smtp_prepare_quit(struct smtp_session *s, u16_t *tx_buf_len)
{
  *tx_buf_len = SMTP_CMD_QUIT_LEN;
  s->tx_buf[*tx_buf_len] = 0;
  SMEMCPY(s->tx_buf, SMTP_CMD_QUIT, SMTP_CMD_QUIT_LEN);
  LWIP_ASSERT("tx_buf overflow detected", *tx_buf_len <= SMTP_TX_BUF_LEN);
  return SMTP_CLOSED;
}

/** If in state SMTP_BODY, try to send more body data */
static void
smtp_send_body(struct smtp_session *s, struct altcp_pcb *pcb)
{
  err_t err;

  if (s->state == SMTP_BODY) {
#if SMTP_BODYDH
    if (s->bodydh) {
      smtp_send_body_data_handler(s, pcb);
    } else
#endif /* SMTP_BODYDH */
    {
      u16_t send_len = (u16_t)(s->body_len - s->body_sent);
      if (send_len > 0) {
        u16_t snd_buf = altcp_sndbuf(pcb);
        if (send_len > snd_buf) {
          send_len = snd_buf;
        }
        if (send_len > 0) {
          /* try to send something out */
          err = altcp_write(pcb, &s->body[s->body_sent], (u16_t)send_len, TCP_WRITE_FLAG_COPY);
          if (err == ERR_OK) {
            s->timer = SMTP_TIMEOUT_DATABLOCK;
            s->body_sent = (u16_t)(s->body_sent + send_len);
            if (s->body_sent < s->body_len) {
              LWIP_DEBUGF(SMTP_DEBUG_STATE, ("smtp_send_body: %d of %d bytes written\n",
                s->body_sent, s->body_len));
            }
          }
        }
      }
    }
    if (s->body_sent == s->body_len) {
      /* the whole body has been written, write last line */
      LWIP_DEBUGF(SMTP_DEBUG_STATE, ("smtp_send_body: body completely written (%d bytes), appending end-of-body\n",
        s->body_len));
      err = altcp_write(pcb, SMTP_CMD_BODY_FINISHED, SMTP_CMD_BODY_FINISHED_LEN, 0);
      if (err == ERR_OK) {
        s->timer = SMTP_TIMEOUT_DATATERM;
        LWIP_DEBUGF(SMTP_DEBUG_STATE, ("smtp_send_body: end-of-body written, changing state to %s\n",
          smtp_state_str[SMTP_QUIT]));
        /* last line written, change state, wait for confirmation */
        s->state = SMTP_QUIT;
      }
    }
  }
}

/** State machine-like implementation of an SMTP client.
 */
static void
smtp_process(void *arg, struct altcp_pcb *pcb, struct pbuf *p)
{
  struct smtp_session* s = (struct smtp_session*)arg;
  u16_t response_code = 0;
  u16_t tx_buf_len = 0;
  enum smtp_session_state next_state;

  if (arg == NULL) {
    /* already closed SMTP connection */
    if (p != NULL) {
      LWIP_DEBUGF(SMTP_DEBUG_TRACE, ("Received %d bytes after closing: %s\n",
        p->tot_len, smtp_pbuf_str(p)));
      pbuf_free(p);
    }
    return;
  }

  next_state = s->state;

  if (p != NULL) {
    /* received data */
    if (s->p == NULL) {
      s->p = p;
    } else {
      pbuf_cat(s->p, p);
    }
  } else {
    /* idle timer, close connection if timed out */
    if (s->timer == 0) {
      LWIP_DEBUGF(SMTP_DEBUG_WARN_STATE, ("smtp_process: connection timed out, closing\n"));
      smtp_close(s, pcb, SMTP_RESULT_ERR_TIMEOUT, 0, ERR_TIMEOUT);
      return;
    }
    if (s->state == SMTP_BODY) {
      smtp_send_body(s, pcb);
      return;
    }
  }
  response_code = smtp_is_response(s);
  if (response_code) {
    LWIP_DEBUGF(SMTP_DEBUG_TRACE, ("smtp_process: received response code: %d\n", response_code));
    if (smtp_is_response_finished(s) != ERR_OK) {
      LWIP_DEBUGF(SMTP_DEBUG_TRACE, ("smtp_process: partly received response code: %d\n", response_code));
      /* wait for next packet to complete the respone */
      return;
    }
  } else {
    if (s->p != NULL) {
      LWIP_DEBUGF(SMTP_DEBUG_WARN, ("smtp_process: unknown data received (%s)\n",
        smtp_pbuf_str(s->p)));
      pbuf_free(s->p);
      s->p = NULL;
    }
    return;
  }

  switch(s->state)
  {
  case(SMTP_NULL):
    /* wait for 220 */
    if (response_code == 220) {
      /* then send EHLO */
      next_state = smtp_prepare_helo(s, &tx_buf_len, pcb);
    }
    break;
  case(SMTP_HELO):
    /* wait for 250 */
    if (response_code == 250) {
#if SMTP_SUPPORT_AUTH_PLAIN || SMTP_SUPPORT_AUTH_LOGIN
      /* then send AUTH or MAIL */
      next_state = smtp_prepare_auth_or_mail(s, &tx_buf_len);
    }
    break;
  case(SMTP_AUTH_LOGIN):
  case(SMTP_AUTH_PLAIN):
    /* wait for 235 */
    if (response_code == 235) {
#endif /* SMTP_SUPPORT_AUTH_PLAIN || SMTP_SUPPORT_AUTH_LOGIN */
      /* send MAIL */
      next_state = smtp_prepare_mail(s, &tx_buf_len);
    }
    break;
#if SMTP_SUPPORT_AUTH_LOGIN
  case(SMTP_AUTH_LOGIN_UNAME):
    /* wait for 334 Username */
    if (response_code == 334) {
      if (pbuf_strstr(s->p, SMTP_RESP_LOGIN_UNAME) != 0xFFFF) {
        /* send username */
        next_state = smtp_prepare_auth_login_uname(s, &tx_buf_len);
      }
    }
    break;
  case(SMTP_AUTH_LOGIN_PASS):
    /* wait for 334 Password */
    if (response_code == 334) {
      if (pbuf_strstr(s->p, SMTP_RESP_LOGIN_PASS) != 0xFFFF) {
        /* send username */
        next_state = smtp_prepare_auth_login_pass(s, &tx_buf_len);
      }
    }
    break;
#endif /* SMTP_SUPPORT_AUTH_LOGIN */
  case(SMTP_MAIL):
    /* wait for 250 */
    if (response_code == 250) {
      /* send RCPT */
      next_state = smtp_prepare_rcpt(s, &tx_buf_len);
    }
    break;
  case(SMTP_RCPT):
    /* wait for 250 */
    if (response_code == 250) {
      /* send DATA */
      SMEMCPY(s->tx_buf, SMTP_CMD_DATA, SMTP_CMD_DATA_LEN);
      tx_buf_len = SMTP_CMD_DATA_LEN;
      next_state = SMTP_DATA;
    }
    break;
  case(SMTP_DATA):
    /* wait for 354 */
    if (response_code == 354) {
      /* send email header */
      next_state = smtp_prepare_header(s, &tx_buf_len);
    }
    break;
  case(SMTP_BODY):
    /* nothing to be done here, handled somewhere else */
    break;
  case(SMTP_QUIT):
    /* wait for 250 */
    if (response_code == 250) {
      /* send QUIT */
      next_state = smtp_prepare_quit(s, &tx_buf_len);
    }
    break;
  case(SMTP_CLOSED):
    /* nothing to do, wait for connection closed from server */
    return;
  default:
    LWIP_DEBUGF(SMTP_DEBUG_SERIOUS, ("Invalid state: %d/%s\n", (int)s->state,
      smtp_state_str[s->state]));
    break;
  }
  if (s->state == next_state) {
    LWIP_DEBUGF(SMTP_DEBUG_WARN_STATE, ("smtp_process[%s]: unexpected response_code, closing: %d (%s)\n",
      smtp_state_str[s->state], response_code, smtp_pbuf_str(s->p)));
    /* close connection */
    smtp_close(s, pcb, SMTP_RESULT_ERR_SVR_RESP, response_code, ERR_OK);
    return;
  }
  if (tx_buf_len > 0) {
    SMTP_TX_BUF_MAX(tx_buf_len);
    if (altcp_write(pcb, s->tx_buf, tx_buf_len, TCP_WRITE_FLAG_COPY) == ERR_OK) {
      LWIP_DEBUGF(SMTP_DEBUG_TRACE, ("smtp_process[%s]: received command %d (%s)\n",
        smtp_state_str[s->state], response_code, smtp_pbuf_str(s->p)));
      LWIP_DEBUGF(SMTP_DEBUG_TRACE, ("smtp_process[%s]: sent %"U16_F" bytes: \"%s\"\n",
        smtp_state_str[s->state], tx_buf_len, s->tx_buf));
      s->timer = SMTP_TIMEOUT;
      pbuf_free(s->p);
      s->p = NULL;
      LWIP_DEBUGF(SMTP_DEBUG_STATE, ("smtp_process: changing state from %s to %s\n",
        smtp_state_str[s->state], smtp_state_str[next_state]));
      s->state = next_state;
      if (next_state == SMTP_BODY) {
        /* try to stream-send body data right now */
        smtp_send_body(s, pcb);
      } else if (next_state == SMTP_CLOSED) {
        /* sent out all data, delete structure */
        altcp_arg(pcb, NULL);
        smtp_free(s, SMTP_RESULT_OK, 0, ERR_OK);
      }
    }
  }
}

#if SMTP_BODYDH
/** Elementary sub-function to send data
 *
 * @returns: BDHALLDATASENT all data has been written
 *           BDHSOMEDATASENT some data has been written
 *           0 no data has been written
 */
static int
smtp_send_bodyh_data(struct altcp_pcb *pcb, const char **from, u16_t *howmany)
{
  err_t err;
  u16_t len = *howmany;

  len = (u16_t)LWIP_MIN(len, altcp_sndbuf(pcb));
  err = altcp_write(pcb, *from, len, TCP_WRITE_FLAG_COPY);
  if (err == ERR_OK) {
    *from += len;
    if ((*howmany -= len) > 0) {
      return BDHSOMEDATASENT;
    }
    return BDHALLDATASENT;
  }
  return 0;
}

/** Same as smtp_send_mail_static, but uses a callback function to send body data
 */
err_t
smtp_send_mail_bodycback(const char *from, const char* to, const char* subject,
  smtp_bodycback_fn bodycback_fn, smtp_result_fn callback_fn, void* callback_arg)
{
  struct smtp_session* s;
  size_t len;

  LWIP_ASSERT_CORE_LOCKED();

  s = (struct smtp_session*)SMTP_STATE_MALLOC(sizeof(struct smtp_session));
  if (s == NULL) {
    return ERR_MEM;
  }
  memset(s, 0, sizeof(struct smtp_session));
  s->bodydh = (struct smtp_bodydh_state*)SMTP_BODYDH_MALLOC(sizeof(struct smtp_bodydh_state));
  if (s->bodydh == NULL) {
    SMTP_STATE_FREE(s);
    return ERR_MEM;
  }
  memset(s->bodydh, 0, sizeof(struct smtp_bodydh_state));
  /* initialize the structure */
  s->from = from;
  len = strlen(from);
  LWIP_ASSERT("string is too long", len <= 0xffff);
  s->from_len = (u16_t)len;
  s->to = to;
  len = strlen(to);
  LWIP_ASSERT("string is too long", len <= 0xffff);
  s->to_len = (u16_t)len;
  s->subject = subject;
  len = strlen(subject);
  LWIP_ASSERT("string is too long", len <= 0xffff);
  s->subject_len = (u16_t)len;
  s->body = NULL;
  LWIP_ASSERT("string is too long", len <= 0xffff);
  s->callback_fn = callback_fn;
  s->callback_arg = callback_arg;
  s->bodydh->callback_fn = bodycback_fn;
  s->bodydh->state = BDH_SENDING;
  /* call the actual implementation of this function */
  return smtp_send_mail_alloced(s);
}

static void
smtp_send_body_data_handler(struct smtp_session *s, struct altcp_pcb *pcb)
{
  struct smtp_bodydh_state *bdh;
  int res = 0, ret;
  LWIP_ASSERT("s != NULL", s != NULL);
  bdh = s->bodydh;
  LWIP_ASSERT("bodydh != NULL", bdh != NULL);

  /* resume any leftovers from prior memory constraints */
  if (s->body_len) {
    LWIP_DEBUGF(SMTP_DEBUG_TRACE, ("smtp_send_body_data_handler: resume\n"));
    if((res = smtp_send_bodyh_data(pcb, (const char **)&s->body, &s->body_len))
        != BDHALLDATASENT) {
      s->body_sent = s->body_len - 1;
      return;
    }
  }
  ret = res;
  /* all data on buffer has been queued, resume execution */
  if (bdh->state == BDH_SENDING) {
    LWIP_DEBUGF(SMTP_DEBUG_TRACE, ("smtp_send_body_data_handler: run\n"));
    do {
      ret |= res; /* remember if we once queued something to send */
      bdh->exposed.length = 0;
      if (bdh->callback_fn(s->callback_arg, &bdh->exposed) == BDH_DONE) {
        bdh->state = BDH_STOP;
      }
      s->body = bdh->exposed.buffer;
      s->body_len = bdh->exposed.length;
      LWIP_DEBUGF(SMTP_DEBUG_TRACE, ("smtp_send_body_data_handler: trying to send %u bytes\n", (unsigned int)s->body_len));
    } while (s->body_len &&
            ((res = smtp_send_bodyh_data(pcb, (const char **)&s->body, &s->body_len)) == BDHALLDATASENT)
            && (bdh->state != BDH_STOP));
  }
  if ((bdh->state != BDH_SENDING) && (ret != BDHSOMEDATASENT)) {
    LWIP_DEBUGF(SMTP_DEBUG_TRACE, ("smtp_send_body_data_handler: stop\n"));
    s->body_sent = s->body_len;
  } else {
    LWIP_DEBUGF(SMTP_DEBUG_TRACE, ("smtp_send_body_data_handler: pause\n"));
    s->body_sent = s->body_len - 1;
  }
}
#endif /* SMTP_BODYDH */

#endif /* LWIP_TCP && LWIP_CALLBACK_API */
