aboutsummaryrefslogtreecommitdiffstats
path: root/smtpd/smtp_client.c
diff options
context:
space:
mode:
Diffstat (limited to 'smtpd/smtp_client.c')
-rw-r--r--smtpd/smtp_client.c923
1 files changed, 923 insertions, 0 deletions
diff --git a/smtpd/smtp_client.c b/smtpd/smtp_client.c
new file mode 100644
index 00000000..8e146e1b
--- /dev/null
+++ b/smtpd/smtp_client.c
@@ -0,0 +1,923 @@
+/* $OpenBSD: smtp_client.c,v 1.14 2020/04/24 11:34:07 eric Exp $ */
+
+/*
+ * Copyright (c) 2018 Eric Faurot <eric@openbsd.org>
+ *
+ * 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 <sys/types.h>
+#include <sys/socket.h>
+
+#include <netinet/in.h>
+
+#include <ctype.h>
+#include <errno.h>
+#include <limits.h>
+#include <resolv.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#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);
+}