/* Module to send mail using the SMTP protocol.
 *
 * IRC Services is copyright (c) 1996-2009 Andrew Church.
 *     E-mail: <achurch@achurch.org>
 * Parts written by Andrew Kempe and others.
 * This program is free but copyrighted software; see the file COPYING for
 * details.
 */

#include "services.h"
#include "modules.h"
#include "conffile.h"
#include "language.h"
#include "mail.h"
#include "mail-local.h"

/*************************************************************************/

static Module *module;

static char **RelayHosts;
int RelayHosts_count;
static char *SMTPName;
static int MaxSockets;

static Module *module_mail_main;
static typeof(low_send) *low_send_p;

/*************************************************************************/

/* Maximum number of garbage lines to accept before disconnecting: */
#define GARBAGE_MAX	10

typedef struct {
    Socket *sock;   /* NULL if unused */
    char *from, *fromname, *to, *subject, *body;
    int relaynum;   /* Index into RelayHosts[], used to try backup servers */
    enum { ST_GREETING, ST_HELO, ST_MAIL, ST_RCPT, ST_DATA, ST_FINISH } state;
    int replycode;  /* nonzero if in the middle of a line (no EOL) */
    char replychar; /* 4th character of reply (space or hyphen) */
    int garbage;    /* number of garbage lines seen so far */
} SocketInfo;
static SocketInfo *connections;

static SocketInfo *get_socketinfo(Socket *sock);
static void free_socketinfo(SocketInfo *si);
static int try_next_relay(SocketInfo *si);
static void smtp_readline(Socket *sock, void *param_unused);
static void smtp_writeline(Socket *sock, const char *fmt, ...)
    FORMAT(printf,2,3);
static void smtp_disconnect(Socket *sock, void *why);

/*************************************************************************/
/***************************** Mail sending ******************************/
/*************************************************************************/

static int send_smtp(const char *from, const char *fromname,
		     const char *to, const char *subject, const char *body)
{
    SocketInfo *si;

    si = get_socketinfo(NULL);
    if (!si) {
	module_log("send_smtp(): no sockets available");
	return 1;
    }
    si->sock = sock_new();
    if (!si->sock) {
	module_log("send_smtp(): sock_new() failed");
	return 1;
    }
    if (debug)
	module_log("debug: SMTP(%p) connecting", si->sock);
    si->from     = strdup(from);
    si->fromname = strdup(fromname);
    si->to       = strdup(to);
    si->subject  = strdup(subject);
    si->body     = strdup(body);
    if (!si->from || !si->fromname || !si->to || !si->subject || !si->body) {
	module_log("send_smtp(): out of memory");
	free_socketinfo(si);
	return 1;
    }
    si->state = ST_GREETING;
    si->replycode = 0;
    si->garbage = 0;
    sock_setcb(si->sock, SCB_READLINE, smtp_readline);
    sock_setcb(si->sock, SCB_DISCONNECT, smtp_disconnect);
    si->relaynum = -1;  /* incremented by try_next_relay() */
    /* Initiate connection and return */
    return try_next_relay(si);
}

/*************************************************************************/
/*************************************************************************/

/* Auxiliary routines: */

/*************************************************************************/

/* Return the SocketInfo corresponding to the given socket, or NULL if none
 * exists.  Note that get_socketinfo(NULL) can be used to find an empty
 * connection slot.
 */

static SocketInfo *get_socketinfo(Socket *sock)
{
    int i;
    for (i = 0; i < MaxSockets; i++) {
	if (connections[i].sock == sock)
	    return &connections[i];
    }
    return NULL;
}

/*************************************************************************/

/* Free/clear all data associated with the given SocketInfo. */

static void free_socketinfo(SocketInfo *si)
{
    free(si->from);
    free(si->fromname);
    free(si->to);
    free(si->subject);
    free(si->body);
    si->from = si->fromname = si->to = si->subject = si->body = NULL;
    if (si->sock) {
	sock_free(si->sock);
	si->sock = NULL;
    }
}

/*************************************************************************/
/*************************************************************************/

/* Try connecting to the next relay in RelayHosts[].  Return 0 if a
 * connection was successfully initiated (but possibly not completed), else
 * free the SocketInfo and return -1. */

static int try_next_relay(SocketInfo *si)
{
    si->relaynum++;
    if (si->relaynum >= RelayHosts_count) {
	module_log("send_smtp(): No relay hosts available");
	free_socketinfo(si);
	return -1;
    }
    if (conn(si->sock, RelayHosts[si->relaynum], 25, NULL, 0) < 0) {
	module_log_perror("send_smtp(): Connection to %s:25 failed",
			  RelayHosts[si->relaynum]);
	return try_next_relay(si);
    }
    return 0;
}

/*************************************************************************/

/* Read a line from an SMTP socket. */

static void smtp_readline(Socket *sock, void *param_unused)
{
    SocketInfo *si = get_socketinfo(sock);
    char buf[BUFSIZE], *s;
    int have_eol = 0;
    int replycode;

#ifdef CLEAN_COMPILE
    param_unused = param_unused;
#endif

    if (!(si = get_socketinfo(sock))) {
	module_log("smtp_readline(): no SocketInfo for socket %p!", sock);
	sock_setcb(sock, SCB_DISCONNECT, NULL);
	disconn(sock);
	return;
    }

    /* Remove any double quotes in the From: name and log a warning */
    if (strchr(si->fromname, '"')) {
	int i;
	module_log("warning: double quotes (\") are not allowed in the"
		   " sender name; will be changed to single quotes (')");
	for (i = 0; si->fromname[i]; i++) {
	    if (si->fromname[i] == '"')
		si->fromname[i] = '\'';
	}
    }

    sgets(buf, sizeof(buf), sock);
    s = buf + strlen(buf);
    if (*--s == '\n')
	have_eol++;
    if (*--s == '\r')
	have_eol++;
    *s = 0;
    if (debug)
	module_log("debug: SMTP(%p) received: %s", sock, buf);
    if (!si->replycode) {
	if (buf[0] < '1' || buf[0] > '5'
	 || buf[1] < '0' || buf[1] > '9'
	 || buf[2] < '0' || buf[2] > '9'
	 || (buf[3] != ' ' && buf[3] != '-')) {
	    module_log("smtp_readline(%p) got garbage line: %s", sock, buf);
	    si->garbage++;
	    if (si->garbage > GARBAGE_MAX) {
		int count = 0;
		module_log("Too many garbage lines, giving up.  Message was:");
		module_log("   From: %s <%s>", si->fromname, si->from);
		module_log("   To: %s", si->to);
		module_log("   Subject: %s", si->subject);
		for (s = strtok(si->body, "\n"); s; s = strtok(NULL, "\n")) {
		    module_log("   %s %s", count ? "     " : "Body:", s);
		    count++;
		}
		free_socketinfo(si);
		return;
	    }
	}
	si->replycode = strtol(buf, &s, 10);
	if (s != buf+3) {
	    module_log("BUG: strtol ate %d characters from reply (should be"
		       " 3)!", (int)(s-buf));
	}
	si->replychar = buf[3];
    }
    if (!have_eol)
	return;
    replycode = si->replycode;
    si->replycode = 0;
    if (si->replychar != ' ')
	return;

    if (replycode >= 400) {
	module_log("Received error reply (%d) for socket %p state %d,"
		   " aborting", replycode, sock, si->state);
	free_socketinfo(si);
	return;
    }
    switch (si->state++) {
      case ST_GREETING:
	smtp_writeline(sock, "HELO %s", SMTPName);
	break;
      case ST_HELO:
	smtp_writeline(sock, "MAIL FROM:<%s>", si->from);
	break;
      case ST_MAIL:
	smtp_writeline(sock, "RCPT TO:<%s>", si->to);
	break;
      case ST_RCPT:
	smtp_writeline(sock, "DATA");
	break;
      case ST_DATA: {
	time_t t;
	time(&t);
	if (!strftime(buf, sizeof(buf), "%a, %d %b %Y %H:%M:%S", gmtime(&t)))
	    strscpy(buf, "Thu, 1 Jan 1970 00:00:00", sizeof(buf));
	smtp_writeline(sock, "From: \"%s\" <%s>", si->fromname, si->from);
	smtp_writeline(sock, "To: <%s>", si->to);
	smtp_writeline(sock, "Subject: %s", si->subject);
	smtp_writeline(sock, "Date: %s +0000", buf);
	/* writeline(sock,"") makes GCC warn about an empty format string */
	smtp_writeline(sock, "%s", "");
	for (s = strtok(si->body, "\r\n"); s; s = strtok(NULL, "\r\n")) {
	    smtp_writeline(sock, "%s%s", *s=='.' ? "." : "", s);
	}
	smtp_writeline(sock, ".");
	break;
      } /* ST_DATA */
      default:
	module_log("BUG: bad state %d for socket %p", si->state-1, sock);
	/* fall through */
      case ST_FINISH:
	smtp_writeline(sock, "QUIT");
	free_socketinfo(si);
	break;
    } /* switch (si->state++) */
}

/*************************************************************************/

static void smtp_writeline(Socket *sock, const char *fmt, ...)
{
    va_list args;
    char buf[4096];

    va_start(args, fmt);
    snprintf(buf, sizeof(buf), "%s\r\n", fmt);
    vsockprintf(sock, buf, args);
    if (debug) {
	char *s = buf;
	s += snprintf(buf, sizeof(buf), "debug: SMTP(%p) sent: ", sock);
	vsnprintf(s, sizeof(buf)-(s-buf), fmt, args);
	module_log("%s", buf);
    }
}

/*************************************************************************/

/* Handle a socket disconnection. */

static void smtp_disconnect(Socket *sock, void *why)
{
    SocketInfo *si;

    if (!(si = get_socketinfo(sock))) {
	module_log("smtp_disconnect(): no SocketInfo for socket %p!", sock);
	return;
    }

    if (debug) {
	module_log("debug: SMTP(%p) closed (%s)", sock,
		   why==DISCONN_LOCAL ? "local" :
		   why==DISCONN_CONNFAIL ? "connfail" : "remote");
    }
    if (why == DISCONN_LOCAL)  /* we explicitly closed the socket */
	return;
    if (why == DISCONN_CONNFAIL) {
	module_log_perror("Connection to server %s failed for socket %p",
			  RelayHosts[si->relaynum], sock);
	try_next_relay(si);
    } else {
	module_log("Connection to server %s broken for socket %p",
		   RelayHosts[si->relaynum], sock);
	free_socketinfo(si);
    }
}

/*************************************************************************/
/***************************** Module stuff ******************************/
/*************************************************************************/

const int32 module_version = MODULE_VERSION_CODE;

static int do_RelayHost(const char *filename, int linenum, char *param);
ConfigDirective module_config[] = {
    { "RelayHost",        { { CD_FUNC, CF_DIRREQ, do_RelayHost } } },
    { "SMTPName",         { { CD_STRING, CF_DIRREQ, &SMTPName } } },
    { "MaxSockets",       { { CD_POSINT, CF_DIRREQ, &MaxSockets } } },
    { NULL }
};

/*************************************************************************/

static int do_RelayHost(const char *filename, int linenum, char *param)
{
    static char **new_RelayHosts = NULL;
    static int new_RelayHosts_count = 0;
    int i;

    if (filename) {
	/* Check parameter for validity and save */
	if (!*param) {
	    /* Empty value */
	    config_error(filename, linenum, "Empty hostname in RelayHost");
	}
	ARRAY_EXTEND(new_RelayHosts);
	new_RelayHosts[new_RelayHosts_count-1] = sstrdup(param);
    } else if (linenum == CDFUNC_INIT) {
	/*Prepare for reading config file: clear out "new" array just in case*/
	ARRAY_FOREACH (i, new_RelayHosts)
	    free(new_RelayHosts[i]);
	free(new_RelayHosts);
	new_RelayHosts = NULL;
	new_RelayHosts_count = 0;
    } else if (linenum == CDFUNC_SET) {
	/* Copy new values to config variables and clear */
	ARRAY_FOREACH (i, RelayHosts)
	    free(RelayHosts[i]);
	free(RelayHosts);
	RelayHosts = new_RelayHosts;
	RelayHosts_count = new_RelayHosts_count;
	new_RelayHosts = NULL;
	new_RelayHosts_count = 0;
    } else if (linenum == CDFUNC_DECONFIG) {
	/* Reset to defaults */
	ARRAY_FOREACH (i, RelayHosts)
	    free(RelayHosts[i]);
	free(RelayHosts);
	RelayHosts = NULL;
	RelayHosts_count = 0;
    }
    return 1;
}

/*************************************************************************/

static int do_load_module(Module *mod, const char *modname)
{
    if (strcmp(modname, "mail/main") == 0) {
	module_mail_main = mod;
	low_send_p = get_module_symbol(mod, "low_send");
	if (low_send_p)
	    *low_send_p = send_smtp;
	else
	    module_log("Unable to find `low_send' symbol, cannot send mail");
    }
    return 0;
}

/*************************************************************************/

static int do_unload_module(Module *mod)
{
    if (mod == module_mail_main) {
	if (low_send_p)
	    *low_send_p = NULL;
	low_send_p = NULL;
	module_mail_main = NULL;
    }
    return 0;
}

/*************************************************************************/

int init_module(Module *module_)
{
    Module *tmpmod;

    module = module_;

    connections = calloc(sizeof(*connections), MaxSockets);
    if (!connections) {
	module_log("No memory for connection data");
	exit_module(0);
	return 0;
    }

    if (!add_callback(NULL, "load module", do_load_module)
     || !add_callback(NULL, "unload module", do_unload_module)
    ) {
	module_log("Unable to add callbacks");
	exit_module(0);
	return 0;
    }

    tmpmod = find_module("mail/main");
    if (tmpmod)
	do_load_module(tmpmod, "mail/main");

    return 1;
}

/*************************************************************************/

int exit_module(int shutdown_unused)
{
#ifdef CLEAN_COMPILE
    shutdown_unused = shutdown_unused;
#endif

    if (module_mail_main)
	do_unload_module(module_mail_main);
    remove_callback(NULL, "unload module", do_unload_module);
    remove_callback(NULL, "load module", do_load_module);
    free(connections);
    connections = NULL;
    return 1;
}

/*************************************************************************/
