/* $OpenBSD: smtp_client.c,v 1.14 2020/04/24 11:34:07 eric Exp $ */ /* * Copyright (c) 2018 Eric Faurot * * 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 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 #include #include #include #include #include #include #include #include #include #include #include "log.h" #include "ioev.h" #include "smtp.h" #define TRACE_SMTPCLT 2 #define TRACE_IO 3 enum { STATE_INIT = 0, STATE_BANNER, STATE_EHLO, STATE_HELO, STATE_LHLO, STATE_STARTTLS, STATE_AUTH, STATE_AUTH_PLAIN, STATE_AUTH_LOGIN, STATE_AUTH_LOGIN_USER, STATE_AUTH_LOGIN_PASS, STATE_READY, STATE_MAIL, STATE_RCPT, STATE_DATA, STATE_BODY, STATE_EOM, STATE_RSET, STATE_QUIT, STATE_LAST }; #define base64_encode __b64_ntop #define base64_decode __b64_pton #define FLAG_TLS 0x01 #define FLAG_TLS_VERIFIED 0x02 #define SMTP_EXT_STARTTLS 0x01 #define SMTP_EXT_PIPELINING 0x02 #define SMTP_EXT_AUTH 0x04 #define SMTP_EXT_AUTH_PLAIN 0x08 #define SMTP_EXT_AUTH_LOGIN 0x10 #define SMTP_EXT_DSN 0x20 #define SMTP_EXT_SIZE 0x40 struct smtp_client { void *tag; struct smtp_params params; int state; int flags; int ext; size_t ext_size; struct io *io; char *reply; size_t replysz; struct smtp_mail *mail; int rcptidx; int rcptok; }; void log_trace_verbose(int); void log_trace(int, const char *, ...) __attribute__((format (printf, 2, 3))); static void smtp_client_io(struct io *, int, void *); static void smtp_client_free(struct smtp_client *); static void smtp_client_state(struct smtp_client *, int); static void smtp_client_abort(struct smtp_client *, int, const char *); static void smtp_client_cancel(struct smtp_client *, int, const char *); static void smtp_client_sendcmd(struct smtp_client *, char *, ...); static void smtp_client_sendbody(struct smtp_client *); static int smtp_client_readline(struct smtp_client *); static int smtp_client_replycat(struct smtp_client *, const char *); static void smtp_client_response(struct smtp_client *, const char *); static void smtp_client_mail_abort(struct smtp_client *); static void smtp_client_mail_status(struct smtp_client *, const char *); static void smtp_client_rcpt_status(struct smtp_client *, struct smtp_rcpt *, const char *); static const char *strstate[STATE_LAST] = { "INIT", "BANNER", "EHLO", "HELO", "LHLO", "STARTTLS", "AUTH", "AUTH_PLAIN", "AUTH_LOGIN", "AUTH_LOGIN_USER", "AUTH_LOGIN_PASS", "READY", "MAIL", "RCPT", "DATA", "BODY", "EOM", "RSET", "QUIT", }; struct smtp_client * smtp_connect(const struct smtp_params *params, void *tag) { struct smtp_client *proto; proto = calloc(1, sizeof *proto); if (proto == NULL) return NULL; memmove(&proto->params, params, sizeof(*params)); proto->tag = tag; proto->io = io_new(); if (proto->io == NULL) { free(proto); return NULL; } io_set_callback(proto->io, smtp_client_io, proto); io_set_timeout(proto->io, proto->params.timeout); if (io_connect(proto->io, proto->params.dst, proto->params.src) == -1) { smtp_client_abort(proto, FAIL_CONN, io_error(proto->io)); return NULL; } return proto; } void smtp_cert_verified(struct smtp_client *proto, int verified) { if (verified == CERT_OK) proto->flags |= FLAG_TLS_VERIFIED; else if (proto->params.tls_verify) { errno = EAUTH; smtp_client_abort(proto, FAIL_CONN, "Invalid server certificate"); return; } io_resume(proto->io, IO_IN); if (proto->state == STATE_INIT) smtp_client_state(proto, STATE_BANNER); else { /* Clear extensions before re-issueing an EHLO command. */ proto->ext = 0; smtp_client_state(proto, STATE_EHLO); } } void smtp_set_tls(struct smtp_client *proto, void *ctx) { io_start_tls(proto->io, ctx); } void smtp_quit(struct smtp_client *proto) { if (proto->state != STATE_READY) fatalx("connection is not ready"); smtp_client_state(proto, STATE_QUIT); } void smtp_sendmail(struct smtp_client *proto, struct smtp_mail *mail) { if (proto->state != STATE_READY) fatalx("connection is not ready"); proto->mail = mail; smtp_client_state(proto, STATE_MAIL); } static void smtp_client_free(struct smtp_client *proto) { if (proto->mail) fatalx("current task should have been deleted already"); smtp_closed(proto->tag, proto); if (proto->io) io_free(proto->io); free(proto->reply); free(proto); } /* * End the session immediatly. */ static void smtp_client_abort(struct smtp_client *proto, int err, const char *reason) { smtp_failed(proto->tag, proto, err, reason); if (proto->mail) smtp_client_mail_abort(proto); smtp_client_free(proto); } /* * Properly close the session. */ static void smtp_client_cancel(struct smtp_client *proto, int err, const char *reason) { if (proto->mail) fatal("not supposed to have a mail"); smtp_failed(proto->tag, proto, err, reason); smtp_client_state(proto, STATE_QUIT); } static void smtp_client_state(struct smtp_client *proto, int newstate) { struct smtp_rcpt *rcpt; char ibuf[LINE_MAX], obuf[LINE_MAX]; size_t n; int oldstate; if (proto->reply) proto->reply[0] = '\0'; again: oldstate = proto->state; proto->state = newstate; log_trace(TRACE_SMTPCLT, "%p: %s -> %s", proto, strstate[oldstate], strstate[newstate]); /* don't try this at home! */ #define smtp_client_state(_s, _st) do { newstate = _st; goto again; } while (0) switch (proto->state) { case STATE_BANNER: io_set_read(proto->io); break; case STATE_EHLO: smtp_client_sendcmd(proto, "EHLO %s", proto->params.helo); break; case STATE_HELO: smtp_client_sendcmd(proto, "HELO %s", proto->params.helo); break; case STATE_LHLO: smtp_client_sendcmd(proto, "LHLO %s", proto->params.helo); break; case STATE_STARTTLS: if (proto->params.tls_req == TLS_NO || proto->flags & FLAG_TLS) smtp_client_state(proto, STATE_AUTH); else if (proto->ext & SMTP_EXT_STARTTLS) smtp_client_sendcmd(proto, "STARTTLS"); else if (proto->params.tls_req == TLS_FORCE) smtp_client_cancel(proto, FAIL_IMPL, "TLS not supported by remote host"); else smtp_client_state(proto, STATE_AUTH); break; case STATE_AUTH: if (!proto->params.auth_user) smtp_client_state(proto, STATE_READY); else if ((proto->flags & FLAG_TLS) == 0) smtp_client_cancel(proto, FAIL_IMPL, "Authentication requires TLS"); else if ((proto->ext & SMTP_EXT_AUTH) == 0) smtp_client_cancel(proto, FAIL_IMPL, "AUTH not supported by remote host"); else if (proto->ext & SMTP_EXT_AUTH_PLAIN) smtp_client_state(proto, STATE_AUTH_PLAIN); else if (proto->ext & SMTP_EXT_AUTH_LOGIN) smtp_client_state(proto, STATE_AUTH_LOGIN); else smtp_client_cancel(proto, FAIL_IMPL, "No supported AUTH method"); break; case STATE_AUTH_PLAIN: (void)strlcpy(ibuf, "-", sizeof(ibuf)); (void)strlcat(ibuf, proto->params.auth_user, sizeof(ibuf)); if (strlcat(ibuf, ":", sizeof(ibuf)) >= sizeof(ibuf)) { errno = EMSGSIZE; smtp_client_cancel(proto, FAIL_INTERNAL, "credentials too large"); break; } n = strlcat(ibuf, proto->params.auth_pass, sizeof(ibuf)); if (n >= sizeof(ibuf)) { errno = EMSGSIZE; smtp_client_cancel(proto, FAIL_INTERNAL, "credentials too large"); break; } *strchr(ibuf, ':') = '\0'; ibuf[0] = '\0'; if (base64_encode(ibuf, n, obuf, sizeof(obuf)) == -1) { errno = EMSGSIZE; smtp_client_cancel(proto, FAIL_INTERNAL, "credentials too large"); break; } smtp_client_sendcmd(proto, "AUTH PLAIN %s", obuf); explicit_bzero(ibuf, sizeof ibuf); explicit_bzero(obuf, sizeof obuf); break; case STATE_AUTH_LOGIN: smtp_client_sendcmd(proto, "AUTH LOGIN"); break; case STATE_AUTH_LOGIN_USER: if (base64_encode(proto->params.auth_user, strlen(proto->params.auth_user), obuf, sizeof(obuf)) == -1) { errno = EMSGSIZE; smtp_client_cancel(proto, FAIL_INTERNAL, "credentials too large"); break; } smtp_client_sendcmd(proto, "%s", obuf); explicit_bzero(obuf, sizeof obuf); break; case STATE_AUTH_LOGIN_PASS: if (base64_encode(proto->params.auth_pass, strlen(proto->params.auth_pass), obuf, sizeof(obuf)) == -1) { errno = EMSGSIZE; smtp_client_cancel(proto, FAIL_INTERNAL, "credentials too large"); break; } smtp_client_sendcmd(proto, "%s", obuf); explicit_bzero(obuf, sizeof obuf); break; case STATE_READY: smtp_ready(proto->tag, proto); break; case STATE_MAIL: if (proto->ext & SMTP_EXT_DSN) smtp_client_sendcmd(proto, "MAIL FROM:<%s>%s%s%s%s", proto->mail->from, proto->mail->dsn_ret ? " RET=" : "", proto->mail->dsn_ret ? proto->mail->dsn_ret : "", proto->mail->dsn_envid ? " ENVID=" : "", proto->mail->dsn_envid ? proto->mail->dsn_envid : ""); else smtp_client_sendcmd(proto, "MAIL FROM:<%s>", proto->mail->from); break; case STATE_RCPT: if (proto->rcptidx == proto->mail->rcptcount) { smtp_client_state(proto, STATE_DATA); break; } rcpt = &proto->mail->rcpt[proto->rcptidx]; if (proto->ext & SMTP_EXT_DSN) smtp_client_sendcmd(proto, "RCPT TO:<%s>%s%s%s%s", rcpt->to, rcpt->dsn_notify ? " NOTIFY=" : "", rcpt->dsn_notify ? rcpt->dsn_notify : "", rcpt->dsn_orcpt ? " ORCPT=" : "", rcpt->dsn_orcpt ? rcpt->dsn_orcpt : ""); else smtp_client_sendcmd(proto, "RCPT TO:<%s>", rcpt->to); break; case STATE_DATA: if (proto->rcptok == 0) { smtp_client_mail_abort(proto); smtp_client_state(proto, STATE_RSET); } else smtp_client_sendcmd(proto, "DATA"); break; case STATE_BODY: fseek(proto->mail->fp, 0, SEEK_SET); smtp_client_sendbody(proto); break; case STATE_EOM: smtp_client_sendcmd(proto, "."); break; case STATE_RSET: smtp_client_sendcmd(proto, "RSET"); break; case STATE_QUIT: smtp_client_sendcmd(proto, "QUIT"); break; default: fatalx("%s: bad state %d", __func__, proto->state); } #undef smtp_client_state } /* * Handle a response to an SMTP command */ static void smtp_client_response(struct smtp_client *proto, const char *line) { struct smtp_rcpt *rcpt; int i, seen; switch (proto->state) { case STATE_BANNER: if (line[0] != '2') smtp_client_abort(proto, FAIL_RESP, line); else if (proto->params.lmtp) smtp_client_state(proto, STATE_LHLO); else smtp_client_state(proto, STATE_EHLO); break; case STATE_EHLO: if (line[0] != '2') { /* * Either rejected or not implemented. If we want to * use EHLO extensions, report an SMTP error. * Otherwise, fallback to using HELO. */ if ((proto->params.tls_req == TLS_FORCE) || (proto->params.auth_user)) smtp_client_cancel(proto, FAIL_RESP, line); else smtp_client_state(proto, STATE_HELO); break; } smtp_client_state(proto, STATE_STARTTLS); break; case STATE_HELO: if (line[0] != '2') smtp_client_cancel(proto, FAIL_RESP, line); else smtp_client_state(proto, STATE_READY); break; case STATE_LHLO: if (line[0] != '2') smtp_client_cancel(proto, FAIL_RESP, line); else smtp_client_state(proto, STATE_READY); break; case STATE_STARTTLS: if (line[0] != '2') { if ((proto->params.tls_req == TLS_FORCE) || (proto->params.auth_user)) { smtp_client_cancel(proto, FAIL_RESP, line); break; } smtp_client_state(proto, STATE_AUTH); } else smtp_require_tls(proto->tag, proto); break; case STATE_AUTH_PLAIN: if (line[0] != '2') smtp_client_cancel(proto, FAIL_RESP, line); else smtp_client_state(proto, STATE_READY); break; case STATE_AUTH_LOGIN: if (strncmp(line, "334 ", 4)) smtp_client_cancel(proto, FAIL_RESP, line); else smtp_client_state(proto, STATE_AUTH_LOGIN_USER); break; case STATE_AUTH_LOGIN_USER: if (strncmp(line, "334 ", 4)) smtp_client_cancel(proto, FAIL_RESP, line); else smtp_client_state(proto, STATE_AUTH_LOGIN_PASS); break; case STATE_AUTH_LOGIN_PASS: if (line[0] != '2') smtp_client_cancel(proto, FAIL_RESP, line); else smtp_client_state(proto, STATE_READY); break; case STATE_MAIL: if (line[0] != '2') { smtp_client_mail_status(proto, line); smtp_client_state(proto, STATE_RSET); } else smtp_client_state(proto, STATE_RCPT); break; case STATE_RCPT: rcpt = &proto->mail->rcpt[proto->rcptidx++]; if (line[0] != '2') smtp_client_rcpt_status(proto, rcpt, line); else { proto->rcptok++; smtp_client_state(proto, STATE_RCPT); } break; case STATE_DATA: if (line[0] != '2' && line[0] != '3') { smtp_client_mail_status(proto, line); smtp_client_state(proto, STATE_RSET); } else smtp_client_state(proto, STATE_BODY); break; case STATE_EOM: if (proto->params.lmtp) { /* * LMTP reports a status of each accepted RCPT. * Report status for the first pending RCPT and read * more lines if another rcpt needs a status. */ for (i = 0, seen = 0; i < proto->mail->rcptcount; i++) { rcpt = &proto->mail->rcpt[i]; if (rcpt->done) continue; if (seen) { io_set_read(proto->io); return; } smtp_client_rcpt_status(proto, &proto->mail->rcpt[i], line); seen = 1; } } smtp_client_mail_status(proto, line); smtp_client_state(proto, STATE_READY); break; case STATE_RSET: if (line[0] != '2') smtp_client_cancel(proto, FAIL_RESP, line); else smtp_client_state(proto, STATE_READY); break; case STATE_QUIT: smtp_client_free(proto); break; default: fatalx("%s: bad state %d", __func__, proto->state); } } static void smtp_client_io(struct io *io, int evt, void *arg) { struct smtp_client *proto = arg; log_trace(TRACE_IO, "%p: %s %s", proto, io_strevent(evt), io_strio(io)); switch (evt) { case IO_CONNECTED: if (proto->params.tls_req == TLS_SMTPS) { io_set_write(io); smtp_require_tls(proto->tag, proto); } else smtp_client_state(proto, STATE_BANNER); break; case IO_TLSREADY: proto->flags |= FLAG_TLS; io_pause(proto->io, IO_IN); smtp_verify_server_cert(proto->tag, proto, io_tls(proto->io)); break; case IO_DATAIN: while (smtp_client_readline(proto)) ; break; case IO_LOWAT: if (proto->state == STATE_BODY) smtp_client_sendbody(proto); else io_set_read(io); break; case IO_TIMEOUT: errno = ETIMEDOUT; smtp_client_abort(proto, FAIL_CONN, "Connection timeout"); break; case IO_ERROR: smtp_client_abort(proto, FAIL_CONN, io_error(io)); break; case IO_TLSERROR: smtp_client_abort(proto, FAIL_CONN, io_error(io)); break; case IO_DISCONNECTED: smtp_client_abort(proto, FAIL_CONN, io_error(io)); break; default: fatalx("%s: bad event %d", __func__, evt); } } /* * return 1 if a new line is expected. */ static int smtp_client_readline(struct smtp_client *proto) { const char *e; size_t len; char *line, *msg, *p; int cont; line = io_getline(proto->io, &len); if (line == NULL) { if (io_datalen(proto->io) >= proto->params.linemax) smtp_client_abort(proto, FAIL_PROTO, "Line too long"); return 0; } /* Strip trailing '\r' */ if (len && line[len - 1] == '\r') line[--len] = '\0'; log_trace(TRACE_SMTPCLT, "%p: <<< %s", proto, line); /* Validate SMTP */ if (len > 3) { msg = line + 4; cont = (line[3] == '-'); } else if (len == 3) { msg = line + 3; cont = 0; } else { smtp_client_abort(proto, FAIL_PROTO, "Response too short"); return 0; } /* Validate reply code. */ if (line[0] < '2' || line[0] > '5' || !isdigit((unsigned char)line[1]) || !isdigit((unsigned char)line[2])) { smtp_client_abort(proto, FAIL_PROTO, "Invalid reply code"); return 0; } /* Validate reply message. */ for (p = msg; *p; p++) if (!isprint((unsigned char)*p)) { smtp_client_abort(proto, FAIL_PROTO, "Non-printable characters in response"); return 0; } /* Read extensions. */ if (proto->state == STATE_EHLO) { if (strcmp(msg, "STARTTLS") == 0) proto->ext |= SMTP_EXT_STARTTLS; else if (strncmp(msg, "AUTH ", 5) == 0) { proto->ext |= SMTP_EXT_AUTH; if ((p = strstr(msg, " PLAIN")) && (*(p+6) == '\0' || *(p+6) == ' ')) proto->ext |= SMTP_EXT_AUTH_PLAIN; if ((p = strstr(msg, " LOGIN")) && (*(p+6) == '\0' || *(p+6) == ' ')) proto->ext |= SMTP_EXT_AUTH_LOGIN; } else if (strcmp(msg, "PIPELINING") == 0) proto->ext |= SMTP_EXT_PIPELINING; else if (strcmp(msg, "DSN") == 0) proto->ext |= SMTP_EXT_DSN; else if (strncmp(msg, "SIZE ", 5) == 0) { proto->ext_size = strtonum(msg + 5, 0, SIZE_T_MAX, &e); if (e == NULL) proto->ext |= SMTP_EXT_SIZE; } } if (smtp_client_replycat(proto, line) == -1) { smtp_client_abort(proto, FAIL_INTERNAL, NULL); return 0; } if (cont) return 1; if (io_datalen(proto->io)) { /* * There should be no pending data after a response is read, * except for the multiple status lines after a LMTP message. * It can also happen with pipelineing, but we don't do that * for now. */ if (!(proto->params.lmtp && proto->state == STATE_EOM)) { smtp_client_abort(proto, FAIL_PROTO, "Trailing data"); return 0; } } io_set_write(proto->io); smtp_client_response(proto, proto->reply); return 0; } /* * Concatenate the given response line. */ static int smtp_client_replycat(struct smtp_client *proto, const char *line) { size_t len; char *tmp; int first; if (proto->reply && proto->reply[0]) { /* * If the line is the continuation of an multi-line response, * skip the status and ESC parts. First, skip the status, then * skip the separator amd ESC if found. */ first = 0; line += 3; if (line[0]) { line += 1; if (isdigit((unsigned char)line[0]) && line[1] == '.' && isdigit((unsigned char)line[2]) && line[3] == '.' && isdigit((unsigned char)line[4]) && isspace((unsigned char)line[5])) line += 5; } } else first = 1; if (proto->reply) { len = strlcat(proto->reply, line, proto->replysz); if (len < proto->replysz) return 0; } else len = strlen(line); if (len > proto->params.ibufmax) { errno = EMSGSIZE; return -1; } /* Allocate by multiples of 2^8 */ len += (len % 256) ? (256 - (len % 256)) : 0; tmp = realloc(proto->reply, len); if (tmp == NULL) return -1; if (proto->reply == NULL) tmp[0] = '\0'; proto->reply = tmp; proto->replysz = len; (void)strlcat(proto->reply, line, proto->replysz); /* Replace the separator with a space for the first line. */ if (first && proto->reply[3]) proto->reply[3] = ' '; return 0; } static void smtp_client_sendbody(struct smtp_client *proto) { ssize_t len; size_t sz = 0, total, w; char *ln = NULL; int n; total = io_queued(proto->io); w = 0; while (total < proto->params.obufmax) { if ((len = getline(&ln, &sz, proto->mail->fp)) == -1) break; if (ln[len - 1] == '\n') ln[len - 1] = '\0'; n = io_printf(proto->io, "%s%s\r\n", *ln == '.'?".":"", ln); if (n == -1) { free(ln); smtp_client_abort(proto, FAIL_INTERNAL, NULL); return; } total += n; w += n; } free(ln); if (ferror(proto->mail->fp)) { smtp_client_abort(proto, FAIL_INTERNAL, "Cannot read message"); return; } log_trace(TRACE_SMTPCLT, "%p: >>> [...%zd bytes...]", proto, w); if (feof(proto->mail->fp)) smtp_client_state(proto, STATE_EOM); } static void smtp_client_sendcmd(struct smtp_client *proto, char *fmt, ...) { va_list ap; char *p; int len; va_start(ap, fmt); len = vasprintf(&p, fmt, ap); va_end(ap); if (len == -1) { smtp_client_abort(proto, FAIL_INTERNAL, NULL); return; } log_trace(TRACE_SMTPCLT, "mta: %p: >>> %s", proto, p); len = io_printf(proto->io, "%s\r\n", p); free(p); if (len == -1) smtp_client_abort(proto, FAIL_INTERNAL, NULL); } static void smtp_client_mail_status(struct smtp_client *proto, const char *status) { int i; for (i = 0; i < proto->mail->rcptcount; i++) smtp_client_rcpt_status(proto, &proto->mail->rcpt[i], status); smtp_done(proto->tag, proto, proto->mail); proto->mail = NULL; } static void smtp_client_mail_abort(struct smtp_client *proto) { smtp_done(proto->tag, proto, proto->mail); proto->mail = NULL; } static void smtp_client_rcpt_status(struct smtp_client *proto, struct smtp_rcpt *rcpt, const char *line) { struct smtp_status status; if (rcpt->done) return; rcpt->done = 1; status.rcpt = rcpt; status.cmd = strstate[proto->state]; status.status = line; smtp_status(proto->tag, proto, &status); }