/**
 * \file smtp.c -- code for speaking SMTP, ESMTP or LMTP to a listener port
 *
 * Concept due to Harry Hochheiser.  Implementation by ESR.  Cleanup and
 * strict RFC821 compliance by Cameron MacPherson.
 *
 * Relevant standards: RFC-5321, for SMTP AUTH: RFC-4954, RFC-4422.
 *
 * Copyright 1997 Eric S. Raymond
 * Copyright 2009 - 2025 Matthias Andree
 * Contribution 2004 by Phil Endecott (by way of Rob Funk)
 * Contributions 2005, 2011 by Sunil Shetye
 * Contributions 2012, 2021 by Earl Chew
 * For license terms, see the file COPYING in this directory.
 */

#include "config.h"
#include "fetchmail.h"

#include <ctype.h>
#include <errno.h>
#include <limits.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <strings.h>
#include <signal.h>

// for inet_pton, inet_ntop, inet_aton (if provided)
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include "socket.h"
#include "smtp.h"
#include "i18n.h"

/** Structure definition to hold pairs of an option \æ name and the ESMTP_* flags as integer \a value. */
struct opt
{
    const char *name; /**< option's \a name in ESMTP */
    int value;        /**< bitmask with single bit set to mark an option */
};

/** {NULL,0}-terminated list of known ESMTP extensions. Only for \ref SMTP_ehlo. */
static struct opt extensions[] =
{
    {"8BITMIME",	ESMTP_8BITMIME},
    {"SIZE",    	ESMTP_SIZE},
    {"ETRN",		ESMTP_ETRN},
    {"AUTH ",		ESMTP_AUTH},
#ifdef ODMR_ENABLE
    {"ATRN",		ESMTP_ATRN},
#endif /* ODMR_ENABLE */
#ifdef SSL_ENABLE
    {"STARTTLS",        ESMTP_STARTTLS},
#endif
    {(char *)NULL, 0},
};

/** Buffer to hold the SMTP server's responses.

This can be referenced after \ref SMTP_ok if more detailed
information from the reply is required, such as the list of
extensions obtained via EHLO.

Multiline responses are concatenated, with interior CRLF. No trailing CRLF.
*/
char smtp_response[MSGBUFSIZE];

/* XXX: this must not be used for LMTP! */
int SMTP_helo(int sock /** socket to converse with */,
        char smtp_mode /** only for logging */,
        const char *host /** host to identify as */)
/** send a "HELO" message to the SMTP listener. Does not support LMTP (which needs LHLO instead). */
{
  int ok;

  ASSERT(("This function is only for SMTP, not for LMTP or ESMTP" && smtp_mode != 'L'));

  SockPrintf(sock,"HELO %s\r\n", host);
  if (outlevel >= O_MONITOR)
      report(stdout, "%cMTP> HELO %s\n", smtp_mode, host);
  ok = SMTP_ok(sock, smtp_mode, TIMEOUT_HELO);
  return ok;
}

/** Send a * alone on a CRLF terminated line, to abort SASL authentication, to socket \a  sock.
In verbose mode, prints/logs \a msg without adding LF. After and excluding 6.5.5, \a msg may be NULL. */
static void SMTP_auth_error(int sock, const char *msg /** Message to print/log in verbose mode to stdout */)
{
    SockPrintf(sock, "*\r\n");
    SockRead(sock, smtp_response, sizeof(smtp_response) - 1);
    if (outlevel >= O_MONITOR && msg && *msg) report(stdout, "%s", msg);
}

/** This function checks if the \a smtp_reply starts with "334 " and reports either "Server rejected the AUTH command" (if the first part isn't 334)
or "Malformed server reply" if the blank is missing.
\returns
  - true if the reply starts with "334 "
  - false if the reply does not start with "334 ". */
static bool SMTP_check_AUTH_reply(const char *smtp_reply /** input: full reply line received from SMTP server */)
{
  if (strncmp(smtp_reply, "334", 3)) { /* Server rejects AUTH */
    report(stderr, "\"%s\" <- %s", visbuf(smtp_reply),
           GT_("Server rejected the AUTH command.\n"));
    return false;
  }
  if (smtp_reply[3] != ' ') {
    report(stderr, "\"%s\" <- %s",
		    visbuf(smtp_reply),
		    // GT_("Malformed server reply\n"),
		    GT_("protocol error\n"));
    return false;
  }
  return true;
}

/** Authenticate \a sock via ESMTP, currently supported mechanisms: CRAM-MD5, LOGIN, PLAIN.
 * Original ESMTP Authentication support for fetchmail by Wojciech Polak. */
static int SMTP_auth(int sock, char smtp_mode, const char *username, const char *password,
        const char *ehlo_advertised_auth)
{
	int c;
	const char *p = 0;
	char b64buf[512];
	char tmp[512];

	if (!username || !password) {
                report(stderr, "SMTP AUTH: %s\n", GT_("Invalid userid or passphrase"));
                return SM_UNRECOVERABLE;
        }

        if (strlen(username) > INT_MAX) return SM_UNRECOVERABLE;
        const int unlen = (int)strlen(username);
        if (strlen(password) > INT_MAX) return SM_UNRECOVERABLE;
        const int pwlen = (int)strlen(password);

	memset(b64buf, 0, sizeof(b64buf));
	memset(tmp, 0, sizeof(tmp));

	if (strstr(ehlo_advertised_auth, "CRAM-MD5")) {
                // IETF RFC 4952, 2195
		unsigned char digest[16];
		memset(digest, 0, sizeof(digest));

		if (outlevel >= O_MONITOR)
			report(stdout, GT_("ESMTP CRAM-MD5 Authentication...\n"));
		SockPrintf(sock, "AUTH CRAM-MD5\r\n");
		SockRead(sock, smtp_response, sizeof(smtp_response) - 1);
		strlcpy(tmp, smtp_response, sizeof(tmp));

                // could use SMTP_check_AUTH_reply() here instead, but we need the pointer "after the blank" also

		if (strncmp(tmp, "334", 3)) { /* Server rejects AUTH */
			report(stderr, "\"%s\" <- %s", visbuf(tmp), GT_("Server rejected the AUTH command.\n"));
                        // No need to cancel SMTP AUTH here.
			return SM_ERROR;
		}

		p = strchr(tmp, ' ');
		if (!p) {
			report(stderr, "%s: \"%s\"\n", GT_("Malformed server reply"), visbuf(tmp));
			SMTP_auth_error(sock, "");
			return SM_ERROR;
		}
		p++;
		/* (hmh) from64tobits will not NULL-terminate strings! */
                // The challenge cannot be empty, so we use "<= 0"
		if (from64tobits(b64buf, p, sizeof(b64buf) - 1) <= 0) {
			report(stderr, "\"%s\" <- %s", visbuf(tmp), GT_("Bad base64 reply from server.\n"));
			SMTP_auth_error(sock, "");
			return SM_ERROR;
		}
		if (outlevel >= O_DEBUG)
			report(stdout, GT_("Challenge decoded: %s\n"), b64buf);
		hmac_md5((unsigned const char *)password, pwlen,
			 (unsigned char *)b64buf, strlen(b64buf), digest, sizeof(digest));
		int plen = snprintf(tmp, sizeof(tmp),
			"%s %02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x",
			username,  digest[0], digest[1], digest[2], digest[3],
			digest[4], digest[5], digest[6], digest[7], digest[8],
			digest[9], digest[10], digest[11], digest[12], digest[13],
			digest[14], digest[15]);
		if (plen < 0 || (size_t)plen >= sizeof(tmp))
                {
                        // username pretty long so we truncated the output - this is pointless to send, abort authentication instead
                        report(stderr, GT_("Digest text buffer too small!\n"));
                        SMTP_auth_error(sock, "");
                        return SM_ERROR;
                }

		to64frombits(b64buf, tmp, strlen(tmp), sizeof b64buf); // NOSONAR: tmp is our own buffer, so strlen() shan't exceed its size
		SockPrintf(sock, "%s\r\n", b64buf);
		return SMTP_ok(sock, smtp_mode, TIMEOUT_DEFAULT);
	}
	else if (strstr(ehlo_advertised_auth, "PLAIN")) {
                // IETF RFC 4952, 4616.
		int len;
		if (outlevel >= O_MONITOR)
			report(stdout, GT_("ESMTP PLAIN Authentication...\n"));
		snprintf(tmp, sizeof(tmp), "^%s^%s", username, password);

		len = strlen(tmp); // NOSONAR: tmp is our own buffer, so strlen() shan't exceed its size

		/* Take care not to overflow the buffer */
		c = 0;
		tmp[c] = '\0';
		c += 1 + unlen;
		if (c < len)
			tmp[c] = '\0';

		to64frombits(b64buf, tmp, len, sizeof b64buf);
		fm_safe_clearmem(tmp, sizeof(tmp));
		SockPrintfClear(sock, "AUTH PLAIN %s\r\n", b64buf);
		fm_safe_clearmem(b64buf, sizeof(b64buf));
		return SMTP_ok(sock, smtp_mode, TIMEOUT_DEFAULT);
	}
	else if (strstr(ehlo_advertised_auth, "LOGIN")) {
                // NONSTANDARD and also inefficient because it takes too many round trips
                // compared to RFC-4616 PLAIN authentication, see above
                // https://datatracker.ietf.org/doc/html/draft-murchison-sasl-login-00
		// has expired and never became even a RFC, let alone a standard.
		if (outlevel >= O_MONITOR)
			report(stdout, GT_("ESMTP LOGIN Authentication...\n"));

		SockPrintf(sock, "AUTH LOGIN\r\n");
		SockRead(sock, smtp_response, sizeof(smtp_response) - 1);
		strlcpy(tmp, smtp_response, sizeof(tmp));

                /* we used to check the base64-encoded username and password prompts.
                   We don't do that any more, although RFC 4954 requires us to.

                   1. If I understand recent bug fixes to mutt 2.2.15 properly, then
                   some versions of Microsoft Exchange would send garbage here
                   such as "GSSAPI supported" that isn't base64.
                   <https://gitlab.com/muttmua/mutt/-/commit/b363b602e17906f63f3177f9899cd6c150e746ab>
                   Quote "Another workaround for broken SMTP servers.  Instead of an empty challenge,
                   some MS servers return a meaningless non-BASE64 encoded response in the initial reply,
                   e.g. "334 GSSAPI supported".
"
                   2. Servers that comply with RFC-4952 must just send "334 \r\n" which our base64 decoder
                   decodes to 0 bytes, and the former smtp.c client code required a non-zero length.
                   If the server's reply starts with "334 ", we're good and continue.
                 */
                if (!SMTP_check_AUTH_reply(smtp_response)) {
			// No need to cancel SMTP AUTH here.
                        return SM_ERROR;
                }

                // send base64(username)
                to64frombits(b64buf, username, unlen, sizeof b64buf);
		SockPrintf(sock, "%s\r\n", b64buf);
		SockRead(sock, smtp_response, sizeof(smtp_response) - 1);

                if (!SMTP_check_AUTH_reply(smtp_response)) {
                        return SM_ERROR;
                }

                /* again, RFC-4952 section 4 suggests we should decode the server
                 * challenge and abort authentication if we can't decode it; again,
                 * we just ignore anything after "334 ".
                 */

                // send base64(password)
		to64frombits(b64buf, password, pwlen, sizeof b64buf);
		SockPrintfClear(sock, "%s\r\n", b64buf);
		fm_safe_clearmem(b64buf, sizeof(b64buf));

		return SMTP_ok(sock, smtp_mode, TIMEOUT_DEFAULT);
	}
	return SM_UNRECOVERABLE;
}

int SMTP_ehlo(int sock, char smtp_mode /**< 'S' for ESMTP or 'L' for LMTP */,
        const char *host, const char *name, const char *password, int *opt)
/** send a "EHLO" or "LHLO" message to the SMTP or LMTP listener (depending on \a smtp_mode) via \a sock identifying ourselves as \a host, potentially log in with \a name and \a password through SMTP AUTH, and return extension status bits in \a *opt */
{
  char auth_response[511];
  SIGHANDLERTYPE alrmsave;
  const int tmout = (mytimeout >= TIMEOUT_HELO ? mytimeout : TIMEOUT_HELO);

  SockPrintf(sock,"%cHLO %s\r\n", (smtp_mode == 'S') ? 'E' : smtp_mode, host);
  if (outlevel >= O_MONITOR)
      report(stdout, "%cMTP> %cHLO %s\n",
	    smtp_mode, (smtp_mode == 'S') ? 'E' : smtp_mode, host);

  alrmsave = set_signal_handler(SIGALRM, null_signal_handler);
  set_timeout(tmout);

  *opt = 0;
  while ((SockRead(sock, smtp_response, sizeof(smtp_response)-1)) != -1)
  {
      set_timeout(0);
      (void)set_signal_handler(SIGALRM, alrmsave);

      size_t n = strlen(smtp_response);
      if (n > 0 && smtp_response[n-1] == '\n')
	  smtp_response[--n] = '\0';
      if (n > 0 && smtp_response[n-1] == '\r')
	  smtp_response[--n] = '\0';
      if (n < 4)
	  return SM_ERROR;
      smtp_response[n] = '\0';
      if (outlevel >= O_MONITOR)
	  report(stdout, "%cMTP< %s\n", smtp_mode, smtp_response);
      for (const struct opt *hp = extensions; hp->name; hp++)
	  if (!strncasecmp(hp->name, smtp_response+4, strlen(hp->name))) {
	      *opt |= hp->value;
	      if (strncmp(hp->name, "AUTH ", 5) == 0)
		strlcpy(auth_response, smtp_response, sizeof(auth_response));
	  }
      if ((smtp_response[0] == '1' || smtp_response[0] == '2' || smtp_response[0] == '3') && smtp_response[3] == ' ') {
          if (name && password) {
            if (*opt & ESMTP_AUTH) {
		if (SM_OK != SMTP_auth(sock, smtp_mode, name, password, auth_response)) {
                        report_build(stderr, GT_("fetchmail authentication failed on %s@%s"), name, host);
                        report_complete(stderr, "\n");
                        return SM_UNRECOVERABLE;
                }
            } else {
                report(stderr, "%cMTP: %s\n", smtp_mode, GT_("authentication requested, but not offered by server."));
                return SM_UNRECOVERABLE;
            }
          }
	  return SM_OK;
      }
      else if (smtp_response[3] != '-')
	  return SM_ERROR;

      alrmsave = set_signal_handler(SIGALRM, null_signal_handler);
      set_timeout(tmout);
  }
  return SM_UNRECOVERABLE;
}

static char *SMTP_fixup_address_literal(const char *addr /** input address (user@domain) */);

int SMTP_from(int sock,
        char smtp_mode /** only for logging */,
        const char *_from /** envelope sender */,
        const char *opts /** options for MAIL FROM:<>, needs to start with a blank, will be concatenated to the command. NULL for no addition. */)
/**< send a "MAIL FROM:<\a from>" message to the SMTP listener */
{
    int ok;
    char buf[MSGBUFSIZE];
    char *from = SMTP_fixup_address_literal(_from);
    if (!from) return SM_UNRECOVERABLE;

    if (from[0]=='<')
	snprintf(buf, sizeof(buf), "MAIL FROM:%s", from);
    else
	snprintf(buf, sizeof(buf), "MAIL FROM:<%s>", from);
    if (opts)
	snprintf(buf+strlen(buf), sizeof(buf)-strlen(buf), "%s", opts);
    SockPrintf(sock,"%s\r\n", buf);
    if (outlevel >= O_MONITOR)
	report(stdout, "%cMTP> %s\n", smtp_mode, buf);
    ok = SMTP_ok(sock, smtp_mode, TIMEOUT_MAIL);
    xfree(from);
    return ok;
}

/** This function returns a copy of \a addr with the domain part
 * reformatted as an IPv4 or IPv6 address literal, or NULL on failure.
 * \returns NULL or a buffer that the caller must \ref free(). */
static char *SMTP_fixup_address_literal(const char *addr /** input address (user@domain) */)
{
    const char *pfx = NULL;
    const char *pos = NULL;
    char *allocd = NULL;

    char newlit[INET_ADDRSTRLEN + 1] = "";

    if (1 != regex_ere_search("@[0-9a-fA-Fx:.]+[[:space:]]*$", addr))
        return xstrdup(addr);

    pos = strrchr(addr, '@');
    ASSERT(pos);
    pos++;
    union {
      struct in_addr  in4;
#ifdef AF_INET6
      struct in6_addr in6;
#endif
    } in;

#ifdef AF_INET6
    if (1 == inet_pton(AF_INET6, pos, &in)) {
      pfx = "[IPv6:";
    }
#endif

    if (!pfx && 1 == inet_pton(AF_INET, pos, &in)) {
      pfx = "[";
    }

    // inet_pton requires dotted-quad form and does
    // not understand a.b.c, a.b or a short forms
#ifdef HAVE_INET_ATON
    if (!pfx && -1 != inet_aton(pos, &in.in4)) {
	    // this will barf on 255.255.255.255 but that shan't be used as
	    // smtphost anyways
      pfx = "[";
      inet_ntop(AF_INET, &in.in4, newlit, INET_ADDRSTRLEN);
    }
#endif

    if (!pfx) return xstrdup(addr);

    if (pos < addr || pos - addr > INT_MAX) {
        return NULL;
    }

    size_t needsize =
        pos - addr + strlen(pfx) + 2 + strlen(*newlit ? newlit : pos);
    // what's past the @ is a literal IPv4 or IPv6 and needs [] around
    allocd = (char *)xmalloc(needsize);
    int havesize = snprintf(allocd, needsize, "%-.*s%s%s]", (int)(pos - addr), addr,
                            pfx, *newlit ? newlit : pos);
    if (havesize <= 0 || (unsigned)havesize >= needsize) {
        xfree(allocd);
        return NULL;
    }
    return allocd;
}

/** Send a "RCPT TO:" message with address \a to to the SMTP listener on socket \a sock */
int SMTP_rcpt(int sock /** connected SMTP socket */,
        char smtp_mode /** only for logging */,
        const char *to /** destination address - bare address literals after the last @ on the line will be reformatted for RFC-5321 compliance*/)
{
    int ok;
    char *newto = SMTP_fixup_address_literal(to);
    if (!newto)
      return SM_UNRECOVERABLE;

    SockPrintf(sock, "RCPT TO:<%s>\r\n", newto);
    if (outlevel >= O_MONITOR)
      report(stdout, "%cMTP> RCPT TO:<%s>\n", smtp_mode, newto);
    xfree(newto);
    ok = SMTP_ok(sock, smtp_mode, TIMEOUT_RCPT);
    return ok;
}

int SMTP_data(int sock, char smtp_mode /** only for logging */)
/** send a "DATA" message to the SMTP listener */
{
  int ok;

  SockPrintf(sock,"DATA\r\n");
  if (outlevel >= O_MONITOR)
      report(stdout, "%cMTP> DATA\n", smtp_mode);
  ok = SMTP_ok(sock, smtp_mode, TIMEOUT_DATA);
  return ok;
}

int SMTP_rset(int sock, char smtp_mode /** only for logging */)
/** send a "RSET" message to the SMTP listener */
{
  int ok;

  SockPrintf(sock,"RSET\r\n");
  if (outlevel >= O_MONITOR)
      report(stdout, "%cMTP> RSET\n", smtp_mode);
  ok = SMTP_ok(sock, smtp_mode, TIMEOUT_DEFAULT);
  return ok;
}

int SMTP_quit(int sock, char smtp_mode /** only for logging */)
/** send a "QUIT" message to the SMTP listener */
{
  int ok;

  SockPrintf(sock,"QUIT\r\n");
  if (outlevel >= O_MONITOR)
      report(stdout, "%cMTP> QUIT\n", smtp_mode);
  ok = SMTP_ok(sock, smtp_mode, TIMEOUT_DEFAULT);
  return ok;
}

int SMTP_eom(int sock, char smtp_mode /** 'S' for (E)SMTP, 'L' for LMTP */)
/** Send a message data terminator to the SMTP listener.
 * In (E)SMTP mode, collect the response and handle it.
 * \note that in LMTP mode, this function does \b not collect responses
 * and it's the caller's responsibility to handle them to assess the delivery status! */
{
  ssize_t res = SockPrintf(sock,".\r\n");
  if (res < 0) { /* write error is fatal, we can't be sure what state the session is in */
        return SM_UNRECOVERABLE;
  }
  if (outlevel >= O_MONITOR)
      report(stdout, "%cMTP>. (EOM)\n", smtp_mode);

  /*
   * When doing LMTP, must process many of these at the outer level.
   */
  if (smtp_mode == 'S')
      return SMTP_ok(sock, smtp_mode, TIMEOUT_EOM);
  else
      return SM_OK;
}

time_t last_smtp_ok = 0; /**< exported time_t timestamp when we last received a well-formatted SMTP response. Set by \ref SMTP_ok, never reset by \ref smtp.c */

int SMTP_ok(int sock /** socket to read from */,
        char smtp_mode /** used for logging only */,
        int mintimeout /** minimum bound for wait timeout */)
/**< Obtains status (possibly multi-line) of (E)SMTP/LMTP connection and saves the message in
 * \a smtp_response, without trailing [CR]LF, but with normalized CRLF
 * between multiple lines of multi-line replies.
 *
 * \note for multi-line replies, if the codes are inconsistent in violation of standards,
 * the code on the final (last) line of the multi-line reply prevails.
 *
 * \returns
 * + SM_OK for response codes 100...399
 * + SM_ERROR for response code with other numbers (including 400...599)
 * + SM_UNRECOVERABLE on protocol errors (malformatted answer, read error)
 */
{
    SIGHANDLERTYPE alrmsave;
    char reply[MSGBUFSIZE];

    /* set an alarm for smtp ok */
    alrmsave = set_signal_handler(SIGALRM, null_signal_handler);
    set_timeout(mytimeout >= mintimeout ? mytimeout : mintimeout);

    smtp_response[0] = '\0';

    while ((SockRead(sock, reply, sizeof(reply)-1)) != -1)
    {
	size_t n;

	/* restore alarm */
	set_timeout(0);
	set_signal_handler(SIGALRM, alrmsave);

	n = strlen(reply);
	if (n > 0 && reply[n-1] == '\n')
	    reply[--n] = '\0';
	if (n > 0 && reply[n-1] == '\r')
	    reply[--n] = '\0';

	/* stomp over control characters */
	for (char *i = reply; *i; i++)
	    if (iscntrl((unsigned char)*i))
		*i = '?';

	if (outlevel >= O_MONITOR)
	    report(stdout, "%cMTP< %s\n", smtp_mode, reply);
	/* note that \0 is part of the strchr search string and the
	 * blank after the reply code is optional (RFC 5321 4.2.1) */
	if (n < 3 || !strchr(" -", reply[3]))
	{
	    if (outlevel >= O_MONITOR)
		report(stderr, GT_("smtp listener protocol error\n"));
	    return SM_UNRECOVERABLE;
	}

	last_smtp_ok = time((time_t *) NULL);

	strlcat(smtp_response, reply,  sizeof(smtp_response));

	if (strchr("123", reply[0])
		&& isdigit((unsigned char)reply[1])
		&& isdigit((unsigned char)reply[2])
		&& strchr(" ", reply[3])) /* matches space and \0 */ {
	    return SM_OK;
	} else if (reply[3] != '-')
	    return SM_ERROR;

	strlcat(smtp_response, "\r\n", sizeof(smtp_response));

	/* set an alarm for smtp ok */
	set_signal_handler(SIGALRM, null_signal_handler);
	set_timeout(mytimeout);
    }

    /* restore alarm */
    set_timeout(0);
    set_signal_handler(SIGALRM, alrmsave);

    if (outlevel >= O_MONITOR)
	report(stderr, GT_("smtp listener protocol error\n"));
    return SM_UNRECOVERABLE;
}

/* smtp.c ends here */
