/* Module to send mail using the SMTP protocol. * * IRC Services is copyright (c) 1996-2007 Andrew Church. * E-mail: * 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; } /*************************************************************************/