/* Module to send mail using the SMTP protocol.
*
* IRC Services is copyright (c) 1996-2007 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;
}
/*************************************************************************/
syntax highlighted by Code2HTML, v. 0.9.1