aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSergey Poznyakoff <gray@gnu.org>2021-06-12 22:28:29 +0300
committerSergey Poznyakoff <gray@gnu.org>2021-06-12 22:28:29 +0300
commita016e5ce3c2ac119e3d38a91cc338715b5e3a479 (patch)
tree033c74a6490762abccabb9b635a4c49532c1b918
downloadmockmta-a016e5ce3c2ac119e3d38a91cc338715b5e3a479.tar.gz
mockmta-a016e5ce3c2ac119e3d38a91cc338715b5e3a479.tar.bz2
Initial commit
-rw-r--r--mockmta.c1585
1 files changed, 1585 insertions, 0 deletions
diff --git a/mockmta.c b/mockmta.c
new file mode 100644
index 0000000..8c276d6
--- /dev/null
+++ b/mockmta.c
@@ -0,0 +1,1585 @@
+/*
+ NAME
+ mockmta - mock MTA server for use in test suites
+
+ SYNOPSIS
+ mockmta [-d] [-c CERT] [-f CA] [-k KEY] [-p PORT] [-t SEC] MAILBOX
+
+ DESCRIPTION
+ Starts a mock MTA, which behaves almost identically to the real one,
+ except that it delivers all messages to the given MAILBOX file.
+
+ No attempts are made to interpret the data supplied during the STMP
+ transaction, such as domain names, email addresses, etc, neither is
+ the material supplied in the DATA command verified to be a valid
+ email message. Except for being written to MAILBOX, these data are
+ ignored.
+
+ Mockmta can work both as a foreground process and as a standalone
+ daemon. The foreground mode can be used with GNU pies as follows:
+
+ component sm {
+ mode inetd;
+ socket inet://0.0.0.0:25;
+ command "/usr/bin/mockmta /var/spool/mail/dropmail";
+ }
+
+ When run as a daemon, mockmta starts listening on localhost port
+ PORT (default 25). Incoming SMTP sessions are processed sequentially.
+ Listening for incoming requests is blocked while an SMTP session is
+ active.
+
+ To support TLS, the program must be compiled with the GnuTLS library.
+ To do so, make sure the library and its headers are properly installed
+ and:
+
+ cc -omockmta -DWITH_TLS mockmta.c -lgnutls
+
+ To enable the STARTTLS ESMTP command, supply the names of the certificate
+ (-c CERT) and certificate key (-k KEY) files.
+
+ OPTIONS
+ -c CERT Name of the certificate file.
+ -d Daemon mode
+ -f CA Name of certificate authority file.
+ -k KEY Name of the certificate key file.
+ -p PORT Listen on this port.
+ -t SEC Reserved for future use.
+
+ EXIT CODES
+ 0 Success.
+ 1 Failure (see stderr for details).
+ 2 Command line usage error.
+
+ BUGS
+ At most 32 RCPT commands are allowed.
+
+ AUTHOR
+ Sergey Poznyakoff <gray@gnu.org>
+
+ LICENSE
+ Copyright (C) 2020-2021 Free Software Foundation, Inc.
+
+ Mockmta is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 3, or (at your option)
+ any later version.
+
+ Mockmta is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with GNU Mailutils. If not, see <http://www.gnu.org/licenses/>.
+
+ */
+#ifdef HAVE_CONFIG_H
+# include <config.h>
+#endif
+#include <stdlib.h>
+#include <stdio.h>
+#include <stdarg.h>
+#include <string.h>
+#include <unistd.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <time.h>
+#include <syslog.h>
+#include <sys/time.h>
+#include <sys/socket.h>
+#include <netinet/in.h>
+
+char *progname;
+char *mailbox_name;
+int daemon_opt;
+int idle_timeout;
+int port = 25;
+int msgid = 1;
+
+enum
+ {
+ EX_OK,
+ EX_FAILURE,
+ EX_USAGE
+ };
+
+static void
+terror_stderr (char const *fmt, ...)
+{
+ va_list ap;
+ int m;
+ static char *fmtbuf = NULL;
+ static size_t fmtsize = 0;
+ int ec = errno;
+ char const *es = NULL;
+ size_t len;
+
+ for (m = 0; fmt[m += strcspn (fmt + m, "%")]; )
+ {
+ m++;
+ if (fmt[m] == 'm')
+ break;
+ }
+
+ len = strlen (fmt) + 1;
+ if (fmt[m])
+ {
+ es = strerror (ec);
+ len += strlen (es) - 2;
+ }
+ if (len > fmtsize)
+ {
+ fmtsize = len;
+ fmtbuf = realloc (fmtbuf, fmtsize);
+ if (!fmtbuf)
+ {
+ perror ("realloc");
+ exit (EX_FAILURE);
+ }
+ }
+
+ if (es)
+ {
+ memcpy (fmtbuf, fmt, m - 1);
+ memcpy (fmtbuf + m - 1, es, strlen (es) + 1);
+ strcat (fmtbuf, fmt + m + 1);
+ }
+ else
+ strcpy (fmtbuf, fmt);
+
+ va_start (ap, fmt);
+ fprintf (stderr, "%s: ", progname);
+ vfprintf (stderr, fmtbuf, ap);
+ fputc ('\n', stderr);
+ va_end (ap);
+}
+
+static void
+terror_syslog (char const *fmt, ...)
+{
+ va_list ap;
+ va_start (ap, fmt);
+ vsyslog (LOG_ERR, fmt, ap);
+ va_end (ap);
+}
+
+static void (*terror) (char const *fmt, ...) = terror_stderr;
+
+static void
+nomemory (void)
+{
+ terror ("out of memory");
+ exit (EX_FAILURE);
+}
+
+static volatile int io_timed_out;
+
+static void
+sigalrm (int sig)
+{
+ io_timed_out = 1;
+}
+
+struct iodrv
+{
+ int (*drv_read) (void *, char *, size_t, size_t *);
+ int (*drv_write) (void *, char *, size_t, size_t *);
+ void (*drv_close) (void *);
+ const char *(*drv_strerror) (void *, int);
+};
+
+#define IOBUFSIZE 1024
+
+struct iobase
+{
+ struct iodrv *iob_drv;
+ char iob_buf[IOBUFSIZE];
+ size_t iob_start;
+ size_t iob_level;
+ int iob_errno;
+ int iob_eof;
+};
+
+static struct iobase *
+iobase_create (struct iodrv *drv, size_t size)
+{
+ struct iobase *bp;
+
+ bp = calloc (1, size);
+ if (!bp)
+ nomemory ();
+
+ bp->iob_drv = drv;
+ bp->iob_start = 0;
+ bp->iob_level = 0;
+ bp->iob_errno = 0;
+ bp->iob_eof = 0;
+
+ return bp;
+}
+
+/* Number of data bytes in buffer */
+static inline size_t
+iobase_data_bytes (struct iobase *bp)
+{
+ return bp->iob_level - bp->iob_start;
+}
+
+/* Pointer to the first data byte */
+static inline char *
+iobase_data_start (struct iobase *bp)
+{
+ return bp->iob_buf + bp->iob_start;
+}
+
+static inline void
+iobase_data_less (struct iobase *bp, size_t n)
+{
+ bp->iob_start += n;
+ if (bp->iob_start == bp->iob_level)
+ {
+ bp->iob_start = 0;
+ bp->iob_level = 0;
+ }
+}
+
+static inline int
+iobase_data_getc (struct iobase *bp)
+{
+ char *p;
+ if (iobase_data_bytes (bp) == 0)
+ return -1;
+ p = iobase_data_start (bp);
+ iobase_data_less (bp, 1);
+ return *p;
+}
+
+static inline int
+iobase_data_ungetc (struct iobase *bp, int c)
+{
+ if (bp->iob_start > 0)
+ bp->iob_buf[--bp->iob_start] = c;
+ else if (bp->iob_level == 0)
+ bp->iob_buf[bp->iob_level++] = c;
+ else
+ return -1;
+ return 0;
+}
+
+/* Number of bytes available for writing in buffer */
+static inline size_t
+iobase_avail_bytes (struct iobase *bp)
+{
+ return IOBUFSIZE - bp->iob_level;
+}
+
+/* Pointer to the first byte available for writing */
+static inline char *
+iobase_avail_start (struct iobase *bp)
+{
+ return bp->iob_buf + bp->iob_level;
+}
+
+static inline void
+iobase_avail_less (struct iobase *bp, size_t n)
+{
+ bp->iob_level += n;
+}
+
+/* Fill the buffer */
+static inline int
+iobase_fill (struct iobase *bp)
+{
+ int rc;
+ size_t n;
+
+ rc = bp->iob_drv->drv_read (bp, iobase_avail_start (bp),
+ iobase_avail_bytes (bp), &n);
+ if (rc == 0)
+ {
+ if (n == 0)
+ bp->iob_eof = 1;
+ else
+ iobase_avail_less (bp, n);
+ }
+ bp->iob_errno = rc;
+ return rc;
+}
+
+/* Flush the data available in buffer to external storage */
+static inline int
+iobase_flush (struct iobase *bp)
+{
+ int rc;
+ size_t n;
+
+ rc = bp->iob_drv->drv_write (bp, iobase_data_start (bp),
+ iobase_data_bytes (bp), &n);
+ if (rc == 0)
+ iobase_data_less (bp, n);
+ bp->iob_errno = rc;
+ return rc;
+}
+
+static inline char const *
+iobase_strerror (struct iobase *bp)
+{
+ return bp->iob_drv->drv_strerror (bp, bp->iob_errno);
+}
+
+static inline int
+iobase_eof (struct iobase *bp)
+{
+ return bp->iob_eof && iobase_data_bytes (bp) == 0;
+}
+
+#if 0
+/* Not actually used. Provided for completeness sake. */
+static ssize_t
+iobase_read (struct iobase *bp, char *buf, size_t size)
+{
+ size_t len = 0;
+
+ while (size)
+ {
+ size_t n = iobase_data_bytes (bp);
+ if (n == 0)
+ {
+ if (bp->iob_eof)
+ break;
+ if (iobase_fill (bp))
+ break;
+ continue;
+ }
+ if (n > size)
+ n = size;
+ memcpy (buf, iobase_data_start (bp), n);
+ iobase_data_less (bp, n);
+ len += n;
+ buf += n;
+ size -= n;
+ }
+
+ if (len == 0 && bp->iob_errno)
+ return -1;
+ return len;
+}
+#endif
+
+static ssize_t
+iobase_readln (struct iobase *bp, char *buf, size_t size)
+{
+ size_t len = 0;
+ int cr_seen = 0;
+
+ size--;
+ if (idle_timeout)
+ alarm (idle_timeout);
+ while (len < size)
+ {
+ int c = iobase_data_getc (bp);
+ if (c < 0)
+ {
+ if (bp->iob_eof)
+ break;
+ if (iobase_fill (bp) || io_timed_out)
+ break;
+ continue;
+ }
+ if (c == '\r')
+ {
+ cr_seen = 1;
+ continue;
+ }
+ if (c != '\n' && cr_seen)
+ {
+ buf[len++] = '\r';
+ if (len == size)
+ {
+ if (iobase_data_ungetc (bp, c))
+ abort ();
+ break;
+ }
+ }
+ cr_seen = 0;
+ buf[len++] = c;
+ if (c == '\n')
+ break;
+ }
+ if (idle_timeout)
+ alarm (0);
+ if (io_timed_out)
+ {
+ bp->iob_errno = ETIMEDOUT;
+ return -1;
+ }
+ if (len == 0 && bp->iob_errno)
+ return -1;
+ buf[len] = 0;
+ return len;
+}
+
+static ssize_t
+iobase_write (struct iobase *bp, char *buf, size_t size)
+{
+ size_t len = 0;
+
+ while (size)
+ {
+ size_t n = iobase_avail_bytes (bp);
+ if (n == 0)
+ {
+ if (iobase_flush (bp))
+ break;
+ continue;
+ }
+ if (n > size)
+ n = size;
+ memcpy (iobase_avail_start (bp), buf + len, n);
+ iobase_avail_less (bp, n);
+ len += n;
+ size -= n;
+ }
+ if (len == 0 && bp->iob_errno)
+ return -1;
+ return len;
+}
+
+static ssize_t
+iobase_writeln (struct iobase *bp, char *buf, size_t size)
+{
+ size_t len = 0;
+
+ while (size)
+ {
+ char *p = memchr (buf, '\n', size);
+ size_t n = p ? p - buf + 1 : size;
+ ssize_t rc = iobase_write (bp, buf, n);
+ if (rc <= 0)
+ break;
+ if (p && iobase_flush (bp))
+ break;
+ buf = p;
+ len += rc;
+ size -= rc;
+ }
+ if (len == 0 && bp->iob_errno)
+ return -1;
+ return len;
+}
+
+static void
+iobase_close (struct iobase *bp)
+{
+ bp->iob_drv->drv_close (bp);
+ free (bp);
+}
+
+/* File-descriptor I/O streams */
+struct iofile
+{
+ struct iobase base;
+ int fd;
+};
+
+static int
+iofile_read (void *sd, char *data, size_t size, size_t *nbytes)
+{
+ struct iofile *bp = sd;
+ ssize_t n = read (bp->fd, data, size);
+ if (n == -1)
+ return errno;
+ if (nbytes)
+ *nbytes = n;
+ return 0;
+}
+
+static int
+iofile_write (void *sd, char *data, size_t size, size_t *nbytes)
+{
+ struct iofile *bp = sd;
+ ssize_t n = write (bp->fd, data, size);
+ if (n == -1)
+ return errno;
+ if (nbytes)
+ *nbytes = n;
+ return 0;
+}
+
+static const char *
+iofile_strerror (void *sd, int rc)
+{
+ return strerror (rc);
+}
+
+static void
+iofile_close (void *sd)
+{
+ struct iofile *bp = sd;
+ close (bp->fd);
+}
+
+static struct iodrv iofile_drv = {
+ iofile_read,
+ iofile_write,
+ iofile_close,
+ iofile_strerror
+};
+
+struct iobase *
+iofile_create (int fd)
+{
+ struct iofile *bp = (struct iofile *) iobase_create (&iofile_drv, sizeof (*bp));
+ bp->fd = fd;
+ return (struct iobase*) bp;
+}
+
+enum
+ {
+ IO2_RD,
+ IO2_WR
+ };
+
+static void disable_starttls (void);
+
+#ifdef WITH_TLS
+# include <gnutls/gnutls.h>
+
+/* TLS support */
+char *tls_cert; /* TLS sertificate */
+char *tls_key; /* TLS key */
+char *tls_cafile;
+
+static inline int
+set_tls_opt (int c)
+{
+ switch (c)
+ {
+ case 'c':
+ tls_cert = optarg;
+ break;
+
+ case 'k':
+ tls_key = optarg;
+ break;
+
+ case 'f':
+ tls_cafile = optarg;
+ break;
+
+ default:
+ return 1;
+ }
+ return 0;
+}
+
+static inline int
+enable_tls (void)
+{
+ return tls_cert != NULL && tls_key != NULL;
+}
+
+/* TLS streams */
+struct iotls
+{
+ struct iobase base;
+ gnutls_session_t sess;
+ int fd[2];
+};
+
+static int
+iotls_read (void *sd, char *data, size_t size, size_t *nbytes)
+{
+ struct iotls *iob = sd;
+ int rc;
+
+ do
+ rc = gnutls_record_recv (iob->sess, data, size);
+ while (rc == GNUTLS_E_AGAIN || rc == GNUTLS_E_INTERRUPTED);
+ if (rc >= 0)
+ {
+ if (nbytes)
+ *nbytes = rc;
+ return 0;
+ }
+ return rc;
+}
+
+static int
+iotls_write (void *sd, char *data, size_t size, size_t *nbytes)
+{
+ struct iotls *iob = sd;
+ int rc;
+
+ do
+ rc = gnutls_record_send (iob->sess, data, size);
+ while (rc == GNUTLS_E_INTERRUPTED || rc == GNUTLS_E_AGAIN);
+ if (rc >= 0)
+ {
+ if (nbytes)
+ *nbytes = rc;
+ return 0;
+ }
+ return rc;
+}
+
+static const char *
+iotls_strerror (void *sd, int rc)
+{
+ // struct iotls *iob = sd;
+ return gnutls_strerror (rc);
+}
+
+static void
+iotls_close (void *sd)
+{
+ struct iotls *iob = sd;
+ gnutls_bye (iob->sess, GNUTLS_SHUT_RDWR);
+ gnutls_deinit (iob->sess);
+}
+
+static struct iodrv iotls_drv = {
+ iotls_read,
+ iotls_write,
+ iotls_close,
+ iotls_strerror
+};
+
+static gnutls_dh_params_t dh_params;
+static gnutls_certificate_server_credentials x509_cred;
+
+static void
+_tls_cleanup_x509 (void)
+{
+ if (x509_cred)
+ gnutls_certificate_free_credentials (x509_cred);
+}
+
+#define DH_BITS 512
+static void
+generate_dh_params (void)
+{
+ gnutls_dh_params_init (&dh_params);
+ gnutls_dh_params_generate2 (dh_params, DH_BITS);
+}
+
+static int
+tls_init (void)
+{
+ int rc;
+
+ if (!enable_tls())
+ return -1;
+
+ gnutls_global_init ();
+ atexit (gnutls_global_deinit);
+ gnutls_certificate_allocate_credentials (&x509_cred);
+ atexit (_tls_cleanup_x509);
+ if (tls_cafile)
+ {
+ rc = gnutls_certificate_set_x509_trust_file (x509_cred,
+ tls_cafile,
+ GNUTLS_X509_FMT_PEM);
+ if (rc < 0)
+ {
+ terror ("%s: %s", tls_cafile, gnutls_strerror (rc));
+ return -1;
+ }
+ }
+
+ rc = gnutls_certificate_set_x509_key_file (x509_cred,
+ tls_cert, tls_key,
+ GNUTLS_X509_FMT_PEM);
+ if (rc < 0)
+ {
+ terror ("error reading certificate files: %s", gnutls_strerror (rc));
+ return -1;
+ }
+
+ generate_dh_params ();
+ gnutls_certificate_set_dh_params (x509_cred, dh_params);
+
+ return 0;
+}
+
+static ssize_t
+_tls_fd_pull (gnutls_transport_ptr_t fd, void *buf, size_t size)
+{
+ struct iotls *bp = fd;
+ int rc;
+ do
+ {
+ rc = read (bp->fd[IO2_RD], buf, size);
+ }
+ while (rc == -1 && errno == EAGAIN);
+ return rc;
+}
+
+static ssize_t
+_tls_fd_push (gnutls_transport_ptr_t fd, const void *buf, size_t size)
+{
+ struct iotls *bp = fd;
+ int rc;
+ do
+ {
+ rc = write (bp->fd[IO2_WR], buf, size);
+ }
+ while (rc == -1 && errno == EAGAIN);
+ return rc;
+}
+
+struct iobase *
+iotls_create (int in, int out)
+{
+ struct iotls *bp = (struct iotls *) iobase_create (&iotls_drv, sizeof (*bp));
+ int rc;
+
+ bp->fd[IO2_RD] = in;
+ bp->fd[IO2_WR] = out;
+ gnutls_init (&bp->sess, GNUTLS_SERVER);
+ gnutls_set_default_priority (bp->sess);
+ gnutls_credentials_set (bp->sess, GNUTLS_CRD_CERTIFICATE, x509_cred);
+ gnutls_certificate_server_set_request (bp->sess, GNUTLS_CERT_REQUEST);
+ gnutls_dh_set_prime_bits (bp->sess, DH_BITS);
+
+ gnutls_transport_set_pull_function (bp->sess, _tls_fd_pull);
+ gnutls_transport_set_push_function (bp->sess, _tls_fd_push);
+
+ gnutls_transport_set_ptr2 (bp->sess,
+ (gnutls_transport_ptr_t) bp,
+ (gnutls_transport_ptr_t) bp);
+ rc = gnutls_handshake (bp->sess);
+ if (rc < 0)
+ {
+ gnutls_deinit (bp->sess);
+ gnutls_perror (rc);
+ free (bp);
+ return NULL;
+ }
+
+ return (struct iobase *)bp;
+}
+#else
+static inline int set_tls_opt (int c) {
+ terror ("option -%c not supported: program compiled without support for TLS",
+ c);
+ return 1;
+}
+static inline int enable_tls(void) { return 0; }
+static inline int tls_init (void) { return -1; }
+static inline struct iobase *iotls_create (int in, int out) { return NULL; }
+#endif
+
+/* Two-way I/O */
+struct io2
+{
+ struct iobase base;
+ struct iobase *iob[2];
+};
+
+static int
+io2_read (void *sd, char *data, size_t size, size_t *nbytes)
+{
+ struct io2 *iob = sd;
+ ssize_t n = iobase_readln (iob->iob[IO2_RD], data, size);
+ if (n < 0)
+ return -(1 + IO2_RD);
+ *nbytes = n;
+ return 0;
+}
+
+static int
+io2_write (void *sd, char *data, size_t size, size_t *nbytes)
+{
+ struct io2 *iob = sd;
+ ssize_t n = iobase_writeln (iob->iob[IO2_WR], data, size);
+ if (n < 0)
+ return -(1 + IO2_WR);
+ *nbytes = n;
+ return 0;
+}
+
+static char const *
+io2_strerror (void *sd, int rc)
+{
+ struct io2 *iob = sd;
+ int n = -rc - 1;
+ switch (n)
+ {
+ case IO2_RD:
+ case IO2_WR:
+ return iobase_strerror (iob->iob[n]);
+
+ default:
+ return "undefined error";
+ }
+}
+
+static void
+io2_close (void *sd)
+{
+ struct io2 *iob = sd;
+ iobase_close (iob->iob[IO2_RD]);
+ iobase_close (iob->iob[IO2_WR]);
+}
+
+static struct iodrv io2_drv = {
+ io2_read,
+ io2_write,
+ io2_close,
+ io2_strerror
+};
+
+struct iobase *
+io2_create (struct iobase *in, struct iobase *out)
+{
+ struct io2 *bp = (struct io2 *) iobase_create (&io2_drv, sizeof (*bp));
+ bp->iob[IO2_RD] = in;
+ bp->iob[IO2_WR] = out;
+ return (struct iobase*) bp;
+}
+
+/* SMTP implementation */
+enum smtp_state
+ {
+ STATE_ERR, // Reserved
+ STATE_INIT,
+ STATE_EHLO,
+ STATE_MAIL,
+ STATE_RCPT,
+ STATE_DATA,
+ STATE_QUIT,
+ MAX_STATE
+ };
+
+#define MAX_RCPT 32
+
+struct smtp
+{
+ enum smtp_state state;
+ struct iobase *iob;
+ char buf[IOBUFSIZE];
+ char *arg;
+ int capa_mask;
+ char *helo;
+ char *sender;
+ char *rcpt[MAX_RCPT];
+ int nrcpt;
+ char *data_buf;
+ size_t data_len;
+ size_t data_size;
+};
+
+static void
+smtp_io_send (struct iobase *iob, int code, char *fmt, ...)
+{
+ va_list ap;
+ char buf[IOBUFSIZE];
+ int n;
+
+ snprintf (buf, sizeof buf, "%3d ", code);
+ va_start (ap, fmt);
+ n = vsnprintf (buf + 4, sizeof buf - 6, fmt, ap);
+ va_end (ap);
+ n += 4;
+ buf[n++] = '\r';
+ buf[n++] = '\n';
+ if (iobase_writeln (iob, buf, n) < 0)
+ {
+ terror ("iobase_writeln: %s", iobase_strerror (iob));
+ exit (EX_FAILURE);
+ }
+}
+
+static void
+smtp_io_mlsend (struct iobase *iob, int code, char const **av)
+{
+ char buf[IOBUFSIZE];
+ size_t n;
+ int i;
+
+ snprintf (buf, sizeof buf, "%3d", code);
+ for (i = 0; av[i]; i++)
+ {
+ n = snprintf (buf, sizeof(buf), "%3d%c%s\r\n",
+ code, av[i+1] ? '-' : ' ', av[i]);
+ if (iobase_writeln (iob, buf, n) < 0)
+ {
+ terror ("iobase_writeln: %s", iobase_strerror (iob));
+ exit (EX_FAILURE);
+ }
+ }
+}
+
+static void
+smtp_reset (struct smtp *smtp, int state)
+{
+ switch (state)
+ {
+ case STATE_INIT:
+ free (smtp->helo);
+ smtp->helo = NULL;
+ /* FALL THROUGH */
+ case STATE_MAIL:
+ free (smtp->sender);
+ smtp->sender = NULL;
+ /* FALL THROUGH */
+ case STATE_RCPT:
+ {
+ int i;
+ for (i = 0; i < smtp->nrcpt; i++)
+ free (smtp->rcpt[i]);
+ smtp->nrcpt = 0;
+ }
+ /* FALL THROUGH */
+ case STATE_DATA:
+ free (smtp->data_buf);
+ smtp->data_buf = NULL;
+ smtp->data_len = 0;
+ smtp->data_size = 0;
+ }
+}
+
+void
+smtp_end (struct smtp *smtp)
+{
+ smtp_io_send (smtp->iob, 221, "Bye");
+ iobase_close (smtp->iob);
+ smtp_reset (smtp, STATE_INIT);
+ smtp->iob = NULL;
+}
+
+enum smtp_keyword
+ {
+ KW_HELP,
+ KW_RSET,
+ KW_EHLO,
+ KW_HELO,
+ KW_MAIL,
+ KW_RCPT,
+ KW_DATA,
+ KW_STARTTLS,
+ KW_QUIT,
+ MAX_KW
+ };
+
+static char *smtp_keyword_trans[MAX_KW] = {
+ [KW_HELP] = "HELP",
+ [KW_RSET] = "RSET",
+ [KW_EHLO] = "EHLO",
+ [KW_HELO] = "HELO",
+ [KW_MAIL] = "MAIL",
+ [KW_RCPT] = "RCPT",
+ [KW_DATA] = "DATA",
+ [KW_STARTTLS] = "STARTTLS",
+ [KW_QUIT] = "QUIT"
+};
+
+static int
+smtp_keyword_find (char const *kw)
+{
+ int i;
+ for (i = 0; i < MAX_KW; i++)
+ if (strcasecmp (kw, smtp_keyword_trans[i]) == 0)
+ return i;
+ return -1;
+}
+
+static int
+smtp_help (struct smtp *smtp)
+{
+ smtp_io_send (smtp->iob, 214, "http://www.ietf.org/rfc/rfc2821.txt");
+ return 0;
+}
+
+static int
+smtp_rset (struct smtp *smtp)
+{
+ if (smtp->arg)
+ {
+ smtp_io_send (smtp->iob, 501, "rset does not take arguments");
+ return -1;
+ }
+ smtp_io_send (smtp->iob, 250, "Reset state");
+
+ smtp_reset (smtp, STATE_INIT);
+
+ return 0;
+}
+
+enum
+ {
+ CAPA_PIPELINING,
+ CAPA_STARTTLS,
+ CAPA_HELP,
+ MAX_CAPA
+ };
+
+static char const *capa_str[] = {
+ "PIPELINING",
+ "STARTTLS",
+ "HELP"
+};
+
+#define CAPA_MASK(n) (1<<(n))
+
+static int
+smtp_ehlo (struct smtp *smtp)
+{
+ char const *capa[MAX_CAPA+2];
+ int i, j;
+
+ if (!smtp->arg)
+ {
+ smtp_io_send (smtp->iob, 501, "ehlo requires domain address");
+ return -1;
+ }
+
+ capa[0] = "localhost Mock MTA pleased to meet you";
+ for (i = 0, j = 1; i < MAX_CAPA; i++)
+ if (!(smtp->capa_mask & CAPA_MASK (i)))
+ capa[j++] = capa_str[i];
+ capa[j] = NULL;
+ smtp_io_mlsend (smtp->iob, 250, capa);
+
+ smtp_reset (smtp, STATE_INIT);
+ if ((smtp->helo = strdup (smtp->arg)) == NULL)
+ nomemory ();
+ return 0;
+}
+
+static int
+smtp_starttls (struct smtp *smtp)
+{
+ struct io2 *orig_iob = (struct io2 *) smtp->iob;
+ struct iofile *inb = (struct iofile *)orig_iob->iob[IO2_RD];
+ struct iofile *outb = (struct iofile *)orig_iob->iob[IO2_WR];
+ struct iobase *iob;
+
+ if (smtp->arg)
+ {
+ smtp_io_send (smtp->iob, 501, "Syntax error (no parameters allowed)");
+ return -1;
+ }
+
+ smtp_io_send (smtp->iob, 220, "Ready to start TLS");
+
+ iob = iotls_create (inb->fd, outb->fd);
+
+ if (iob)
+ {
+ free (inb);
+ free (outb);
+ free (orig_iob);
+ smtp->iob = iob;
+ disable_starttls ();
+ smtp->capa_mask |= CAPA_MASK (CAPA_STARTTLS);
+ }
+ else
+ {
+ free (iob);
+ exit (EX_FAILURE);
+ }
+ return 0;
+}
+
+static int
+smtp_quit (struct smtp *smtp)
+{
+ return 0;
+}
+
+static int
+smtp_helo (struct smtp *smtp)
+{
+ if (!smtp->arg)
+ {
+ smtp_io_send (smtp->iob, 501, "helo requires domain address");
+ return -1;
+ }
+ smtp_io_send (smtp->iob, 250, "localhost Mock MTA pleased to meet you");
+
+ smtp_reset (smtp, STATE_INIT);
+ if ((smtp->helo = strdup (smtp->arg)) == NULL)
+ nomemory ();
+ return 0;
+}
+
+static int
+smtp_mail (struct smtp *smtp)
+{
+ static char from_str[] = "FROM:";
+ static size_t from_len = sizeof(from_str) - 1;
+ char *p;
+
+ if (!smtp->arg)
+ {
+ smtp_io_send (smtp->iob, 501, "mail requires email address");
+ return -1;
+ }
+ if (strncasecmp (smtp->arg, from_str, from_len))
+ {
+ smtp_io_send (smtp->iob, 501, "syntax error");
+ return -1;
+ }
+ p = smtp->arg + from_len;
+ while (*p && (*p == ' ' || *p == '\t'))
+ p++;
+ if (!*p)
+ {
+ smtp_io_send (smtp->iob, 501, "mail requires email address");
+ return -1;
+ }
+ smtp_reset (smtp, STATE_MAIL);
+ if ((smtp->sender = strdup (p)) == NULL)
+ nomemory ();
+ smtp_io_send (smtp->iob, 250, "Sender ok");
+ return 0;
+}
+
+static int
+smtp_rcpt (struct smtp *smtp)
+{
+ static char to_str[] = "TO:";
+ static size_t to_len = sizeof(to_str) - 1;
+ char *p;
+
+ if (!smtp->arg)
+ {
+ smtp_io_send (smtp->iob, 501, "rcpt requires email address");
+ return -1;
+ }
+ if (strncasecmp (smtp->arg, to_str, to_len))
+ {
+ smtp_io_send (smtp->iob, 501, "syntax error");
+ return -1;
+ }
+ p = smtp->arg + to_len;
+ while (*p && (*p == ' ' || *p == '\t'))
+ p++;
+ if (!*p)
+ {
+ smtp_io_send (smtp->iob, 501, "to requires email address");
+ return -1;
+ }
+ if (smtp->nrcpt == MAX_RCPT)
+ {
+ smtp_io_send (smtp->iob, 501, "too many recipients");
+ return -1;
+ }
+ if ((smtp->rcpt[smtp->nrcpt] = strdup (p)) == NULL)
+ nomemory ();
+ smtp->nrcpt++;
+ smtp_io_send (smtp->iob, 250, "Recipient ok");
+ return 0;
+}
+
+static void
+smtp_data_save (struct smtp *smtp)
+{
+ size_t len = strlen (smtp->buf);
+ while (smtp->data_len + len > smtp->data_size)
+ {
+ char *p;
+ size_t n = smtp->data_size;
+ if (!smtp->data_buf)
+ {
+ n = smtp->data_len + len;
+ }
+ else
+ {
+ if ((size_t)-1 / 3 * 2 <= n)
+ nomemory ();
+ n += (n + 1) / 2;
+ }
+ p = realloc (smtp->data_buf, n);
+ if (!p)
+ nomemory ();
+ smtp->data_buf = p;
+ smtp->data_size = n;
+ }
+ memcpy (smtp->data_buf + smtp->data_len, smtp->buf, len);
+ smtp->data_len += len;
+}
+
+static int
+begins_with_from (char const *p)
+{
+ while (*p == '>')
+ p++;
+ return strncmp (p, "From", 4) == 0;
+}
+
+static int
+smtp_data (struct smtp *smtp)
+{
+ ssize_t n;
+ int i;
+ int fd;
+ FILE *fp;
+ struct flock lk;
+ time_t t;
+ int in_body = 0;
+ off_t length;
+ int res = 0;
+
+ /* Open the mailbox */
+ fd = open (mailbox_name, O_CREAT|O_WRONLY|O_APPEND, 0600);
+ if (fd == -1)
+ {
+ terror ("can't open %s: %s", mailbox_name, strerror (errno));
+ smtp_io_send (smtp->iob, 451, "Local filesystem error");
+ return -1;
+ }
+ lk.l_type = F_WRLCK;
+ lk.l_whence = SEEK_END;
+ lk.l_start = 0;
+ lk.l_len = 0;
+ if (fcntl (fd, F_SETLKW, &lk)) /* FIXME: ttl */
+ {
+ terror ("can't lock %s: %s", mailbox_name, strerror (errno));
+ smtp_io_send (smtp->iob, 451, "Local filesystem error");
+ close (fd);
+ return -1;
+ }
+
+ length = lseek (fd, 0, SEEK_END);
+
+ fp = fdopen (fd, "a");
+ if (!fp)
+ {
+ terror ("fdopen: %s", strerror (errno));
+ smtp_io_send (smtp->iob, 451, "Local filesystem error");
+ close (fd);
+ return -1;
+ }
+
+ smtp_io_send (smtp->iob, 354,
+ "Enter mail, end with \".\" on a line by itself");
+
+ t = time (NULL);
+ fprintf (fp, "From %s %s", smtp->sender, asctime (gmtime (&t)));
+
+ while (1)
+ {
+ char *p;
+ n = iobase_readln (smtp->iob, smtp->buf, sizeof (smtp->buf));
+ if (n <= 0)
+ {
+ smtp->state = STATE_QUIT;
+ if (smtp->iob->iob_eof)
+ terror ("unexpected end of file");
+ else
+ terror ("read error: %s", strerror (smtp->iob->iob_errno));
+ res = 1;
+ break;
+ }
+
+ if (smtp->buf[n-1] == '\n')
+ {
+ if (n > 1 && smtp->buf[n-2] == '\r')
+ {
+ smtp->buf[n-2] = '\n';
+ smtp->buf[n-1] = 0;
+ n--;
+ }
+ }
+ else
+ {
+ terror ("line too long");
+ break;
+ }
+
+ if (n == 1 && !in_body)
+ in_body = 1;
+
+ p = smtp->buf;
+ if (in_body)
+ {
+ if (*p == '.')
+ {
+ if (p[1] == '\n')
+ break;
+ if (p[1] == '.')
+ {
+ p++;
+ n--;
+ }
+ }
+ else if (begins_with_from (p))
+ fputc ('>', fp);
+ }
+ fwrite (p, n, 1, fp);
+ }
+ fputc ('\n', fp);
+
+ if (res)
+ {
+ fflush (fp);
+ ftruncate (fd, length);
+ }
+
+ lk.l_type = F_UNLCK;
+ lk.l_whence = SEEK_SET;
+ lk.l_start = 0;
+ lk.l_len = 0;
+ if (fcntl (fd, F_SETLK, &lk))
+ terror ("can't unlock %s: %s", mailbox_name, strerror (errno));
+ fclose (fp);
+
+ smtp_io_send (smtp->iob, 250, "%04d Message accepted for delivery", msgid);
+ msgid++;
+ return res;
+}
+
+
+struct smtp_transition
+{
+ int new_state;
+ int (*handler) (struct smtp *);
+};
+
+static struct smtp_transition smtp_transition_table[MAX_STATE][MAX_KW] = {
+ [STATE_INIT] = {
+ [KW_HELP] = { STATE_INIT, smtp_help },
+ [KW_RSET] = { STATE_INIT, smtp_rset },
+ [KW_HELO] = { STATE_EHLO, smtp_helo },
+ [KW_EHLO] = { STATE_EHLO, smtp_ehlo },
+ [KW_QUIT] = { STATE_QUIT, smtp_quit }
+ },
+ [STATE_EHLO] = {
+ [KW_HELP] = { STATE_EHLO, smtp_help },
+ [KW_RSET] = { STATE_INIT, smtp_rset },
+ [KW_HELO] = { STATE_EHLO, smtp_helo },
+ [KW_EHLO] = { STATE_EHLO, smtp_ehlo },
+ [KW_MAIL] = { STATE_MAIL, smtp_mail },
+ [KW_STARTTLS] = { STATE_EHLO, smtp_starttls },
+ [KW_QUIT] = { STATE_QUIT, smtp_quit }
+ },
+ [STATE_MAIL] = {
+ [KW_HELP] = { STATE_MAIL, smtp_help },
+ [KW_RSET] = { STATE_INIT, smtp_rset },
+ [KW_RCPT] = { STATE_RCPT, smtp_rcpt },
+ [KW_HELO] = { STATE_EHLO, smtp_helo },
+ [KW_EHLO] = { STATE_EHLO, smtp_ehlo },
+ [KW_QUIT] = { STATE_QUIT, smtp_quit }
+ },
+ [STATE_RCPT] = {
+ [KW_HELP] = { STATE_RCPT, smtp_help },
+ [KW_RSET] = { STATE_INIT, smtp_rset },
+ [KW_RCPT] = { STATE_RCPT, smtp_rcpt },
+ [KW_HELO] = { STATE_EHLO, smtp_helo },
+ [KW_EHLO] = { STATE_EHLO, smtp_