/* $Id: femail.c,v 1.6 2005/12/20 21:53:09 ca Exp $ */ /* * Copyright (c) 2005 Henning Brauer * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ #include "sm/generic.h" SM_RCSID("@(#)$Id: femail.c,v 1.6 2005/12/20 21:53:09 ca Exp $") #include "sm/error.h" #include "sm/assert.h" #include "sm/types.h" #include "sm/ctype.h" #include "sm/sysexits.h" #include "sm/io.h" #include "sm/signal.h" #include "sm/param.h" #include "sm/socket.h" #include "sm/net.h" #include "sm/pwd.h" #include "sm/string.h" #include "sm/err.h" #include #if !HAVE_FGETLN char * fgetln(FILE *_stream, size_t *_len); #endif #if !HAVE_VSNPRINTF #define vsnprintf sm_vsnprintf #endif void usage(const char *); void sighdlr(int); int main(int, char *[]); void femail_write(const void *, size_t); void femail_put(const char *, ...); void send_cmd(const char *); void build_from(char *, struct passwd *); int parse_message(FILE *, int, int); void parse_addr(char *, size_t, int); void parse_addr_terminal(int); char *qualify_addr(char *); void rcpt_add(char *); void received(void); void hdr_add_env(const char *, const char *); int open_connection(const char *, const char *, sa_family_t); int read_reply(int); void greeting(int); void mailfrom(char *); void rcptto(char *); void start_data(void); void send_message(int); void end_data(void); void parse_config(void); char *next_token(char **, size_t *); enum headerfields { HDR_NONE, HDR_FROM, HDR_TO, HDR_CC, HDR_BCC, HDR_SUBJECT, HDR_DATE, HDR_MSGID }; struct { char *word; enum headerfields type; } keywords[] = { { "From:", HDR_FROM }, { "To:", HDR_TO }, { "Cc:", HDR_CC }, { "Bcc:", HDR_BCC }, { "Subject:", HDR_SUBJECT }, { "Date:", HDR_DATE }, { "Message-Id:", HDR_MSGID } }; enum cftokens { CF_NONE, CF_SMTPHOST, CF_SMTPPORT, CF_MYNAME }; struct { enum cftokens key; const char *word; } cfwords[] = { { CF_SMTPHOST, "smtphost" }, { CF_SMTPPORT, "smtpport" }, { CF_MYNAME, "myname" } }; #define F_VERSION "femail 0.97" #define CONFIGFILE "/etc/mail/msp.conf" #define STATUS_GREETING 220 #define STATUS_HELO 250 #define STATUS_MAILFROM 250 #define STATUS_RCPTTO 250 #define STATUS_DATA 354 #define STATUS_QUEUED 250 #define SMTP_LINELEN 1000 #define SMTP_TIMEOUT 120 #define TIMEOUTMSG "Timeout\n" #if HAVE_GETADDRINFO #define SMTP_HOST "localhost" #else #define SMTP_HOST "127.0.0.1" #endif #define WSP(c) (c == ' ' || c == '\t') int verbose = 0; int verp = 0; int cap_verp = 0; char host[MAXHOSTNAMELEN]; char *user = NULL; char *smtphost = NULL; char *smtpport = NULL; struct { int fd; char *from; char *fromname; char **rcpts; int rcpt_cnt; char *data; size_t len; int saw_date; int saw_msgid; int saw_from; } msg; struct { u_int quote; u_int comment; u_int esc; u_int brackets; size_t wpos; char buf[SMTP_LINELEN]; } pstate; void usage(const char *prg) { fprintf(stderr, "usage: %s [-46tVv] [-f from] [-F name] [to [...]]\n", prg); exit (EX_USAGE); } void sighdlr(int sig) { if (sig == SIGALRM) { write(STDERR_FILENO, TIMEOUTMSG, sizeof(TIMEOUTMSG)); _exit (2); } } int main(int argc, char *argv[]) { int i, ch, tflag = 0, status, noheader; char *fake_from = NULL; char *prg = F_VERSION; sa_family_t af = PF_UNSPEC; struct passwd *pw; bzero(&msg, sizeof(msg)); prg = argv[0]; while ((ch = getopt(argc, argv, "46B:b:E::e:F:f:iJ::mo:p:tVvx")) != -1) { switch (ch) { case '4': af = AF_INET; break; #ifdef AF_INET6 case '6': af = AF_INET6; break; #endif case 'f': fake_from = optarg; break; case 'F': msg.fromname = optarg; break; case 't': tflag = 1; break; case 'V': verp = 1; break; case 'v': verbose = 1; break; /* all remaining: ignored, sendmail compat */ case 'B': case 'b': case 'E': case 'e': case 'i': case 'm': case 'o': case 'p': case 'x': break; default: usage(prg); } } argc -= optind; argv += optind; if (gethostname(host, sizeof(host)) == -1) err(1, "gethostname"); if ((pw = getpwuid(getuid())) == NULL) user = "anonymous"; if (pw != NULL && (user = strdup(pw->pw_name)) == NULL) err(1, "strdup"); parse_config(); build_from(fake_from, pw); while(argc > 0) { rcpt_add(argv[0]); argv++; argc--; } if (smtphost == NULL && (smtphost = getenv("SMTPHOST")) == NULL) smtphost = SMTP_HOST; if (smtpport == NULL && (smtpport = getenv("SMTPPORT")) == NULL) smtpport = "25"; noheader = parse_message(stdin, fake_from == NULL, tflag); if (msg.rcpt_cnt == 0) errx(1, "no recipients"); signal(SIGALRM, sighdlr); alarm(SMTP_TIMEOUT); msg.fd = open_connection(smtphost, smtpport, af); if ((status = read_reply(0)) != STATUS_GREETING) errx(1, "remote host greets us with status %d", status); greeting(1); mailfrom(msg.from); for (i = 0; i < msg.rcpt_cnt; i++) rcptto(msg.rcpts[i]); start_data(); send_message(noheader); end_data(); close(msg.fd); exit (0); } void femail_write(const void *buf, size_t nbytes) { ssize_t n; do { n = write(msg.fd, buf, nbytes); } while (n == -1 && errno == EINTR); if (n == 0) errx(1, "write: connection closed"); if (n == -1) err(1, "write"); if ((size_t)n < nbytes) errx(1, "short write: %ld of %lu bytes written", (long)n, (u_long)nbytes); } void femail_put(const char *fmt, ...) { va_list ap; char buf[SMTP_LINELEN]; va_start(ap, fmt); if (vsnprintf(buf, sizeof(buf), fmt, ap) >= (int)sizeof(buf)) errx(1, "femail_put: line length exceeded"); va_end(ap); femail_write(buf, strlen(buf)); } void send_cmd(const char *cmd) { if (verbose) printf(">>> %s\n", cmd); femail_put("%s\r\n", cmd); } void build_from(char *fake_from, struct passwd *pw) { char *p; if (fake_from == NULL) msg.from = qualify_addr(user); else { if (fake_from[0] == '<') { if (fake_from[strlen(fake_from) - 1] != '>') errx(1, "leading < but no trailing >"); fake_from[strlen(fake_from) - 1] = 0; if ((p = malloc(strlen(fake_from))) == NULL) err(1, "malloc"); strlcpy(p, fake_from + 1, strlen(fake_from)); msg.from = qualify_addr(p); free(p); } else msg.from = qualify_addr(fake_from); } if (msg.fromname == NULL && fake_from == NULL && pw != NULL) { size_t len; len = strcspn(pw->pw_gecos, ","); len++; /* null termination */ if ((msg.fromname = malloc(len)) == NULL) err(1, NULL); strlcpy(msg.fromname, pw->pw_gecos, len); } } int parse_message(FILE *fin, int get_from, int tflag) { char *buf, *twodots = ".."; size_t len, new_len; void *newp; u_int i, cur = HDR_NONE, dotonly; u_int header_seen = 0, header_done = 0; bzero(&pstate, sizeof(pstate)); for (;;) { buf = fgetln(fin, &len); if (buf == NULL && ferror(fin)) err(1, "fgetln"); if (buf == NULL && feof(fin)) break; /* account for \r\n linebreaks */ if (len >= 2 && buf[len - 2] == '\r' && buf[len - 1] == '\n') buf[--len - 1] = '\n'; if (len == 1 && buf[0] == '\n') /* end of header */ header_done = 1; if (buf == NULL || len < 1) err(1, "fgetln weird"); if (!WSP(buf[0])) { /* whitespace -> continuation */ if (cur == HDR_FROM) parse_addr_terminal(1); if (cur == HDR_TO || cur == HDR_CC || cur == HDR_BCC) parse_addr_terminal(0); cur = HDR_NONE; } for (i = 0; !header_done && cur == HDR_NONE && i < (sizeof(keywords) / sizeof(keywords[0])); i++) if (len > strlen(keywords[i].word) && !strncasecmp(buf, keywords[i].word, strlen(keywords[i].word))) cur = keywords[i].type; if (cur != HDR_NONE) header_seen = 1; if (cur != HDR_BCC) { /* save data, \n -> \r\n, . -> .. */ if (buf[len - 1] == '\n') new_len = msg.len + len + 1; else new_len = msg.len + len + 2; if ((len == 1 && buf[0] == '.') || (len > 1 && buf[0] == '.' && buf[1] == '\n')) { dotonly = 1; new_len++; } else dotonly = 0; if ((newp = realloc(msg.data, new_len)) == NULL) err(1, "realloc header"); msg.data = newp; if (dotonly) memcpy(msg.data + msg.len, twodots, 2); else memcpy(msg.data + msg.len, buf, len); msg.len = new_len; msg.data[msg.len - 2] = '\r'; msg.data[msg.len - 1] = '\n'; } /* * using From: as envelope sender is not sendmail compatible, * but I really want it that way - maybe needs a knob */ if (cur == HDR_FROM) { msg.saw_from++; if (get_from) parse_addr(buf, len, 1); } if (tflag && (cur == HDR_TO || cur == HDR_CC || cur == HDR_BCC)) parse_addr(buf, len, 0); if (cur == HDR_DATE) msg.saw_date++; if (cur == HDR_MSGID) msg.saw_msgid++; } return (!header_seen); } void parse_addr(char *s, size_t len, int is_from) { size_t pos = 0; int terminal = 0; /* unless this is a continuation... */ if (!WSP(s[pos]) && s[pos] != ',' && s[pos] != ';') { /* ... skip over everything before the ':' */ for (; pos < len && s[pos] != ':'; pos++) ; /* nothing */ /* ... and check & reset parser state */ parse_addr_terminal(is_from); } /* skip over ':' ',' ';' and whitespace */ for (; pos < len && !pstate.quote && (WSP(s[pos]) || s[pos] == ':' || s[pos] == ',' || s[pos] == ';'); pos++) ; /* nothing */ for (; pos < len; pos++) { if (!pstate.esc && !pstate.quote && s[pos] == '(') pstate.comment++; if (!pstate.comment && !pstate.esc && s[pos] == '"') pstate.quote = !pstate.quote; if (!pstate.comment && !pstate.quote && !pstate.esc) { if (s[pos] == ':') { /* group */ for(pos++; pos < len && WSP(s[pos]); pos++) ; /* nothing */ pstate.wpos = 0; } if (s[pos] == '\n' || s[pos] == '\r') break; if (s[pos] == ',' || s[pos] == ';') { terminal = 1; break; } if (s[pos] == '<') { pstate.brackets = 1; pstate.wpos = 0; } if (pstate.brackets && s[pos] == '>') terminal = 1; } if (!pstate.comment && !terminal && (!(!(pstate.quote || pstate.esc) && (s[pos] == '<' || WSP(s[pos]))))) { if (pstate.wpos >= sizeof(pstate.buf)) errx(1, "address exceeds buffer size"); pstate.buf[pstate.wpos++] = s[pos]; } if (!pstate.quote && pstate.comment && s[pos] == ')') pstate.comment--; if (!pstate.esc && !pstate.comment && !pstate.quote && s[pos] == '\\') pstate.esc = 1; else pstate.esc = 0; } if (terminal) parse_addr_terminal(is_from); for (; pos < len && (s[pos] == '\r' || s[pos] == '\n'); pos++) ; /* nothing */ if (pos < len) parse_addr(s + pos, len - pos, is_from); } void parse_addr_terminal(int is_from) { if (pstate.comment || pstate.quote || pstate.esc) errx(1, "syntax error in address"); if (pstate.wpos) { if (pstate.wpos >= sizeof(pstate.buf)) errx(1, "address exceeds buffer size"); pstate.buf[pstate.wpos] = '\0'; if (is_from) msg.from = qualify_addr(pstate.buf); else rcpt_add(pstate.buf); pstate.wpos = 0; } } char * qualify_addr(char *in) { char *out; if (strchr(in, '@') == NULL) { if (asprintf(&out, "%s@%s", in, host) == -1) err(1, "qualify asprintf"); } else if ((out = strdup(in)) == NULL) err(1, "qualify strdup"); return (out); } void rcpt_add(char *addr) { void *nrcpts; if ((nrcpts = realloc(msg.rcpts, sizeof(char *) * (msg.rcpt_cnt + 1))) == NULL) err(1, "rcpt_add realloc"); msg.rcpts = nrcpts; msg.rcpts[msg.rcpt_cnt++] = qualify_addr(addr); } void received(void) { char datestr[256]; time_t now; now = time(NULL); if (strftime(datestr, sizeof(datestr), "%a, %d %b %Y %T %z", localtime(&now)) == 0) err(1, "strftime"); femail_put( "Received: (from %s@%s, uid %lu)\r\n\tby %s (%s)\r\n\t%s\r\n", user, "localhost", (u_long)getuid(), host, F_VERSION, datestr); } void hdr_add_env(const char *headername, const char *envname) { char *p; if ((p = getenv(envname)) != NULL) femail_put("%s: %s\r\n", headername, p); } int open_connection(const char *server, const char *port, sa_family_t af) { int error, s = -1; #if HAVE_GETADDRINFO struct addrinfo hints, *res, *res0; char *cause = NULL; bzero(&hints, sizeof(hints)); hints.ai_family = af; hints.ai_socktype = SOCK_STREAM; if ((error = getaddrinfo(server, port, &hints, &res0))) errx(1, "%s", gai_strerror(error)); for (res = res0; res; res = res->ai_next) { s = socket(res->ai_family, res->ai_socktype, res->ai_protocol); if (s == -1) { cause = "connect"; continue; } if (connect(s, res->ai_addr, res->ai_addrlen) == -1) { cause = "socket"; close (s); s = -1; continue; } break; /* got one */ } if (s == -1) err(1, "%s", cause); freeaddrinfo(res0); #else /* HAVE_GETADDRINFO */ int port_n; port_n = (int) strtoul(port, NULL, 0); error = net_client_connect(server, port_n, &s); if (sm_is_err(error) || s == -1) err(1, "connect"); #endif /* HAVE_GETADDRINFO */ return (s); } int read_reply(int readcapabilities) { size_t len, pos, spos; long status = 0; char buf[BUFSIZ]; ssize_t rlen; int done = 0; len = pos = spos = 0; while (!done) { if (pos == 0 || (pos > 0 && memchr(buf + pos, '\n', len - pos) == NULL)) { memmove(buf, buf + pos, len - pos); len -= pos; pos = 0; if ((rlen = read(msg.fd, buf + len, sizeof(buf) - len)) == -1) err(1, "read"); len += rlen; } spos = pos; /* status code */ for (; pos < len && buf[pos] >= '0' && buf[pos] <= '9'; pos++) ; /* nothing */ if (pos == len) return (0); if (buf[pos] == ' ') done = 1; else if (buf[pos] != '-') errx(1, "invalid syntax in reply from server"); if (readcapabilities) { if (strncasecmp(buf + pos + 1, "xverp", 5) == 0) cap_verp = 1; } /* skip up to \n */ for (; pos < len && buf[pos - 1] != '\n'; pos++) ; /* nothing */ if (verbose) { char *lbuf = NULL; size_t clen; clen = pos - spos + 1; /* + 1 for trailing \0 */ if (buf[pos - 1] == '\n') clen--; if (buf[pos - 2] == '\r') clen--; if ((lbuf = malloc(clen)) == NULL) err(1, NULL); strlcpy(lbuf, buf + spos, clen); printf("<<< %s\n", lbuf); free(lbuf); } } status = strtol(buf, NULL, 10); if (status < 100 || status > 999) errx(1, "error reading status: out of range"); return (status); } void greeting(int use_ehlo) { int status; char *cmd, *how; if (use_ehlo) how = "EHLO"; else how = "HELO"; if (asprintf(&cmd, "%s %s", how, host) == -1) err(1, "asprintf"); send_cmd(cmd); free(cmd); if ((status = read_reply(use_ehlo)) != STATUS_HELO) { if (use_ehlo) greeting(0); else errx(1, "remote host refuses our greeting"); } } void mailfrom(char *addr) { int status; char *cmd; if (asprintf(&cmd, "MAIL FROM:<%s>%s", addr, (verp && cap_verp) ? " XVERP" : "") == -1) err(1, "asprintf"); send_cmd(cmd); free(cmd); if ((status = read_reply(0)) != STATUS_MAILFROM) errx(1, "mail from %s refused by server", addr); } void rcptto(char *addr) { int status; char *cmd; if (asprintf(&cmd, "RCPT TO:<%s>", addr) == -1) err(1, "asprintf"); send_cmd(cmd); free(cmd); if ((status = read_reply(0)) != STATUS_RCPTTO) errx(1, "rcpt to %s refused by server", addr); } void start_data(void) { int status; send_cmd("DATA"); if ((status = read_reply(0)) != STATUS_DATA) errx(1, "server sends error after DATA"); } void send_message(int noheader) { /* our own headers */ received(); #if ADD_X_HTTP hdr_add_env("X-HTTP-ServerName", "SERVER_NAME"); hdr_add_env("X-HTTP-Host", "HTTP_HOST"); hdr_add_env("X-HTTP-RemoteAddr", "REMOTE_ADDR"); hdr_add_env("X-HTTP-RemotePort", "REMOTE_PORT"); hdr_add_env("X-HTTP-RemoteUser", "REMOTE_USER"); hdr_add_env("X-HTTP-URI", "REQUEST_URI"); #endif if (!msg.saw_from) { if (msg.fromname != NULL) femail_put("From: %s <%s>\r\n", msg.fromname, msg.from); else femail_put("From: %s\r\n", msg.from); } if (!msg.saw_date) { char datestr[256]; time_t now; now = time(NULL); if (strftime(datestr, sizeof(datestr), "%a, %d %b %Y %T %z", localtime(&now)) == 0) err(1, "strftime"); femail_put("Date: %s\r\n", datestr); } if (!msg.saw_msgid) /* leaks pid :( */ femail_put("Message-Id: <%lu.%lu.femail@%s>\r\n", (u_long)time(NULL), (u_long)getpid(), host); if (noheader) femail_write("\r\n", 2); femail_write(msg.data, msg.len); } void end_data(void) { int status; femail_write(".\r\n", 3); if ((status = read_reply(0)) != STATUS_QUEUED) errx(1, "error after sending mail, got status %d", status); send_cmd("QUIT"); } void parse_config(void) { FILE *cfile; size_t len; char *buf, *p, *t; u_int i, key, line = 0; if ((cfile = fopen(CONFIGFILE, "r")) == NULL) { if (errno != ENOENT) warn("%s", CONFIGFILE); return; } while ((buf = fgetln(cfile, &len)) != NULL) { line++; p = buf; key = CF_NONE; if ((t = next_token(&p, &len)) != NULL) { for (i = 0; key == CF_NONE && i < (sizeof(cfwords) / sizeof(cfwords[0])); i++) if (!strcmp(cfwords[i].word, t)) key = cfwords[i].key; free(t); if (key == CF_NONE) errx(1, "%s:%u: syntax error 1\n", CONFIGFILE, line); if ((t = next_token(&p, &len)) == NULL || strcmp(t, "=")) errx(1, "%s:%u: syntax error 2\n", CONFIGFILE, line); free(t); if ((t = next_token(&p, &len)) == NULL) errx(1, "%s:%u: syntax error 3\n", CONFIGFILE, line); switch (key) { case CF_SMTPHOST: smtphost = t; break; case CF_SMTPPORT: smtpport = t; break; case CF_MYNAME: strlcpy(host, t, sizeof(host)); break; default: errx(1, "unhandled token"); } if ((t = next_token(&p, &len)) != NULL) errx(1, "%s:%d: syntax error\n", CONFIGFILE, line); } } fclose(cfile); } char * next_token(char **s, size_t *len) { char *p, *r; size_t tlen = 0; while (*len > 0 && isspace(*s[0])) { (*s)++; (*len)--; } if (*len == 0) return (NULL); if (*s[0] == '#') return (NULL); if (*s[0] == '=') { if ((r = strdup("=")) == NULL) err(1, "next_token strdup"); (*s)++; (*len)--; return (r); } p = *s; while(*len > 0 && !isspace(*s[0]) && *s[0] != '=') { (*s)++; (*len)--; tlen++; } tlen++; /* null termination */ if ((r = malloc(tlen)) == NULL) err(1, "next_token malloc"); strlcpy(r, p, tlen); return (r); }