/* NAME mockmta - mock MTA server for use in test suites SYNOPSIS mockmta [-d] [-c CERT] [-a CA] [-k KEY] [-p PORT] [-t SEC] MAILBOX DESCRIPTION Starts a mock MTA, which behaves almost identically to the real one, except that it listens on localhost only and 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). To support TLS, the program must be compiled with the GnuTLS library. To enable the STARTTLS ESMTP command, supply the names of the certificate (-c CERT) and certificate key (-k KEY) files. OPTIONS -a CA Name of certificate authority file. -c CERT Name of the certificate file. -d Daemon mode. -f Remain in foreground (implies -d). -k KEY Name of the certificate key file. -p PORT Listen on this port. -t SEC Set SMTP timeout. 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 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 . */ #ifdef HAVE_CONFIG_H # include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include char *progname; char *mailbox_name; int daemon_opt; int smtp_timeout = 5*60; int port = 25; int msgid = 1; pthread_mutex_t msgid_mutex = PTHREAD_MUTEX_INITIALIZER; #ifndef _PATH_DEVNULL # define _PATH_DEVNULL "/dev/null" #endif enum { SMTP_IN, SMTP_OUT }; void smtp_timer_enqueue (pthread_t tid, int state); void smtp_timer_dequeue (pthread_t tid); 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); } 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--; while (len < size) { int c = iobase_data_getc (bp); if (c < 0) { if (bp->iob_eof) break; if (iobase_fill (bp)) 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 (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 /* 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 'a': tls_cafile = optarg; break; case 'c': tls_cert = optarg; break; case 'k': tls_key = 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; unsigned sid; char buf[IOBUFSIZE]; char *arg; int capa_mask; char *helo; char *sender; char *rcpt[MAX_RCPT]; int nrcpt; int tempfd; }; enum { CAPA_PIPELINING, CAPA_STARTTLS, CAPA_HELP, MAX_CAPA }; static char const *capa_str[] = { "PIPELINING", "STARTTLS", "HELP" }; #define CAPA_MASK(n) (1<<(n)) struct smtp * smtp_create (int ifd, int ofd) { struct iobase *iob = io2_create (iofile_create (ifd), iofile_create (ofd)); struct smtp *smtp = calloc(1, sizeof (*smtp)); if (!smtp) nomemory (); smtp->state = STATE_INIT; smtp->iob = iob; smtp->capa_mask = 0; if (!enable_tls ()) smtp->capa_mask |= CAPA_MASK (CAPA_STARTTLS); smtp->helo = NULL; smtp->sender = NULL; smtp->nrcpt = 0; pthread_mutex_lock (&msgid_mutex); smtp->sid = msgid++; pthread_mutex_unlock (&msgid_mutex); return smtp; } static ssize_t smtp_io_readln (struct smtp *smtp) { ssize_t n; smtp_timer_enqueue (pthread_self (), SMTP_IN); n = iobase_readln (smtp->iob, smtp->buf, sizeof (smtp->buf)); smtp_timer_dequeue (pthread_self ()); return n; } static void smtp_io_send (struct smtp *smtp, 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'; smtp_timer_enqueue (pthread_self (), SMTP_OUT); if (iobase_writeln (smtp->iob, buf, n) < 0) { terror ("iobase_writeln: %s", iobase_strerror (smtp->iob)); pthread_exit (NULL); } smtp_timer_dequeue (pthread_self ()); } static void smtp_io_mlsend (struct smtp *smtp, 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]); smtp_timer_enqueue (pthread_self (), SMTP_OUT); if (iobase_writeln (smtp->iob, buf, n) < 0) { terror ("iobase_writeln: %s", iobase_strerror (smtp->iob)); pthread_exit (NULL); } smtp_timer_dequeue (pthread_self ()); } } 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: //FIXME: Clean up any collected mail? break; } } void smtp_end (struct smtp *smtp) { smtp_io_send (smtp, 221, "Bye"); smtp_reset (smtp, STATE_INIT); } void smtp_free (struct smtp *smtp) { iobase_close (smtp->iob); free (smtp); } 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, 214, "http://www.ietf.org/rfc/rfc2821.txt"); return 0; } static int smtp_rset (struct smtp *smtp) { if (smtp->arg) { smtp_io_send (smtp, 501, "rset does not take arguments"); return -1; } smtp_io_send (smtp, 250, "Reset state"); smtp_reset (smtp, STATE_INIT); return 0; } static int smtp_ehlo (struct smtp *smtp) { char const *capa[MAX_CAPA+2]; int i, j; if (!smtp->arg) { smtp_io_send (smtp, 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, 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, 501, "Syntax error (no parameters allowed)"); return -1; } smtp_io_send (smtp, 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); pthread_exit (NULL); } 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, 501, "helo requires domain address"); return -1; } smtp_io_send (smtp, 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, 501, "mail requires email address"); return -1; } if (strncasecmp (smtp->arg, from_str, from_len)) { smtp_io_send (smtp, 501, "syntax error"); return -1; } p = smtp->arg + from_len; while (*p && (*p == ' ' || *p == '\t')) p++; if (!*p) { smtp_io_send (smtp, 501, "mail requires email address"); return -1; } smtp_reset (smtp, STATE_MAIL); if ((smtp->sender = strdup (p)) == NULL) nomemory (); smtp_io_send (smtp, 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, 501, "rcpt requires email address"); return -1; } if (strncasecmp (smtp->arg, to_str, to_len)) { smtp_io_send (smtp, 501, "syntax error"); return -1; } p = smtp->arg + to_len; while (*p && (*p == ' ' || *p == '\t')) p++; if (!*p) { smtp_io_send (smtp, 501, "to requires email address"); return -1; } if (smtp->nrcpt == MAX_RCPT) { smtp_io_send (smtp, 501, "too many recipients"); return -1; } if ((smtp->rcpt[smtp->nrcpt] = strdup (p)) == NULL) nomemory (); smtp->nrcpt++; smtp_io_send (smtp, 250, "Recipient ok"); return 0; } static int begins_with_from (char const *p) { while (*p == '>') p++; return strncmp (p, "From", 4) == 0; } static int mailbox_append (FILE *tf) { int fd; FILE *fp; struct flock lk; off_t length; int res = 0; int c; /* 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)); 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)); close (fd); return -1; } length = lseek (fd, 0, SEEK_END); fp = fdopen (fd, "a"); if (!fp) { terror ("fdopen: %s", strerror (errno)); close (fd); return -1; } while ((c = fgetc (tf)) != EOF) fputc (c, fp); if (ferror (fp) || ferror (tf)) { res = -1; 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: %m", mailbox_name); fclose (fp); return res; } static void tempfile_cleanup (void *ptr) { fclose ((FILE*)ptr); } static int smtp_data (struct smtp *smtp) { char template[] = "/tmp/mockmta.XXXXXX"; int fd; FILE *fp; ssize_t n; time_t t; int res; int in_body = 0; fd = mkstemp (template); if (fd == -1) { terror ("can't create temporary: %m"); smtp_io_send (smtp, 451, "Local filesystem error"); return -1; } fp = fdopen (fd, "w+"); if (!fp) { terror ("fdopen: %m"); smtp_io_send (smtp, 451, "Local filesystem error"); close (fd); return -1; } unlink (template); pthread_cleanup_push (tempfile_cleanup, fp); smtp_io_send (smtp, 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 = smtp_io_readln (smtp); 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 == 0) { rewind (fp); res = mailbox_append (fp); } pthread_cleanup_pop (1); if (res) smtp_io_send (smtp, 451, "Local filesystem error"); else smtp_io_send (smtp, 250, "%x Message accepted for delivery", smtp->sid); 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_ehlo }, [KW_DATA] = { STATE_EHLO, smtp_data }, [KW_QUIT] = { STATE_QUIT, smtp_quit } }, }; static void disable_starttls (void) { #ifdef WITH_TLS tls_cert = tls_key = tls_cafile = NULL; #endif smtp_transition_table[STATE_EHLO][KW_STARTTLS].new_state = STATE_ERR; } static void do_smtp (struct smtp *smtp) { struct smtp_transition *trans; smtp_io_send (smtp, 220, "Ready"); while (smtp->state != STATE_QUIT) { size_t i; int kw; int new_state; ssize_t n = smtp_io_readln (smtp); if (n <= 0) break; smtp->buf[--n] = 0; i = strcspn (smtp->buf, " \t"); if (smtp->buf[i]) { smtp->buf[i++] = 0; while (i < n && (smtp->buf[i] == ' ' || smtp->buf[i] == '\t')) i++; if (smtp->buf[i]) smtp->arg = &smtp->buf[i]; else smtp->arg = NULL; } else smtp->arg = NULL; kw = smtp_keyword_find (smtp->buf); if (kw == -1) { smtp_io_send (smtp, 500, "Command unrecognized"); continue; } trans = &smtp_transition_table[smtp->state][kw]; new_state = trans->new_state; if (new_state == STATE_ERR) { smtp_io_send (smtp, 500, "Command not valid"); continue; } if (trans->handler (smtp)) continue; smtp->state = new_state; } smtp_end (smtp); } struct smtp_timer { pthread_t tid; int state; struct timespec wakeup_time; struct smtp_timer *prev, *next; }; struct smtp_timer *smtp_timer_head, *smtp_timer_tail; static pthread_mutex_t smtp_timer_mutex = PTHREAD_MUTEX_INITIALIZER; static pthread_cond_t smtp_timer_cond = PTHREAD_COND_INITIALIZER; static inline int timespec_cmp (struct timespec const *a, struct timespec const *b) { if (a->tv_sec < b->tv_sec) return -1; if (a->tv_sec > b->tv_sec) return 1; if (a->tv_nsec < b->tv_nsec) return -1; if (a->tv_nsec > b->tv_nsec) return 1; return 0; } void smtp_timer_enqueue (pthread_t tid, int state) { struct smtp_timer *timer; timer = malloc (sizeof (timer[0])); if (!timer) nomemory (); timer->tid = tid; timer->state = state; clock_gettime (CLOCK_REALTIME, &timer->wakeup_time); timer->wakeup_time.tv_sec += smtp_timeout; pthread_mutex_lock (&smtp_timer_mutex); timer->next = NULL; timer->prev = smtp_timer_tail; if (smtp_timer_tail) smtp_timer_tail->next = timer; else smtp_timer_head = timer; smtp_timer_tail = timer; if (timer == smtp_timer_head) pthread_cond_broadcast (&smtp_timer_cond); pthread_mutex_unlock (&smtp_timer_mutex); } void smtp_timer_unlink (struct smtp_timer *timer) { if (timer->prev) timer->prev->next = timer->next; else smtp_timer_head = timer->next; if (timer->next) timer->next->prev = timer->prev; else smtp_timer_tail = timer->prev; } void smtp_timer_dequeue (pthread_t tid) { struct smtp_timer *p; pthread_mutex_lock (&smtp_timer_mutex); for (p = smtp_timer_head; p; p = p->next) { if (p->tid == tid) { smtp_timer_unlink (p); free (p); break; } } pthread_mutex_unlock (&smtp_timer_mutex); } void * thr_watcher (void *ptr) { pthread_mutex_lock (&smtp_timer_mutex); while (1) { struct smtp_timer *timer = smtp_timer_head; if (!timer) { pthread_cond_wait (&smtp_timer_cond, &smtp_timer_mutex); continue; } switch (pthread_cond_timedwait (&smtp_timer_cond, &smtp_timer_mutex, &timer->wakeup_time)) { case 0: /* Condition signalled: timer list has been updated. Restart. */ continue; case ETIMEDOUT: /* Wakeup time is reached */ break; default: /* Should not happen */ terror ("unexpected error from pthread_cond_timedwait: %m"); exit (EX_FAILURE); } /* Thread I/O timed out. Terminate the thread. */ pthread_cancel (timer->tid); smtp_timer_unlink (timer); free (timer); } pthread_mutex_unlock (&smtp_timer_mutex); return NULL; } static int mta_open (int port) { int on = 1; struct sockaddr_in address; int fd; fd = socket (PF_INET, SOCK_STREAM, 0); if (fd < 0) { terror ("socket: %m"); exit (EX_FAILURE); } setsockopt (fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof (on)); memset (&address, 0, sizeof (address)); address.sin_family = AF_INET; address.sin_addr.s_addr = htonl (INADDR_LOOPBACK); address.sin_port = htons (port); if (bind (fd, (struct sockaddr *) &address, sizeof (address)) < 0) { close (fd); terror ("bind: %m"); exit (EX_FAILURE); } listen (fd, 5); return fd; } static void smtp_cleanup (void *ptr) { struct smtp *smtp = ptr; smtp_free (smtp); } void * thr_smtp (void *ptr) { pthread_cleanup_push (smtp_cleanup, ptr); do_smtp (ptr); pthread_cleanup_pop (1); return NULL; } void * thr_mta_listener (void *ptr) { int fd = *(int*) ptr; pthread_attr_t attr; pthread_attr_init (&attr); pthread_attr_setdetachstate (&attr, PTHREAD_CREATE_DETACHED); while (1) { int sfd; struct sockaddr_in remote_addr; socklen_t len = sizeof (remote_addr); pthread_t tid; struct smtp *smtp; if ((sfd = accept (fd, (struct sockaddr *) &remote_addr, &len)) < 0) { terror ("accept: %m"); exit (EX_FAILURE); } smtp = smtp_create (sfd, sfd); pthread_create (&tid, &attr, thr_smtp, smtp); } return NULL; } static int fatal_signals[] = { SIGHUP, SIGINT, SIGQUIT, SIGTERM, 0 }; static void signull(int sig) { } int main (int argc, char **argv) { int c; int fd; int foreground = 0; progname = argv[0]; while ((c = getopt (argc, argv, "a:dc:fk:p:t:")) != EOF) { switch (c) { case 'd': daemon_opt = 1; break; case 'f': daemon_opt = 1; foreground = 1; break; case 'p': port = atoi (optarg); break; case 't': smtp_timeout = atoi (optarg); if (smtp_timeout <= 0) { terror ("invalid timeout value"); exit (EX_USAGE); } break; default: if (set_tls_opt (c)) exit (EX_USAGE); } } argc -= optind; argv += optind; if (argc != 1) { terror ("bad number of arguments"); exit (EX_USAGE); } mailbox_name = argv[0]; if (tls_init ()) { disable_starttls (); } if (daemon_opt) { struct sigaction act; sigset_t sigs; int i; pthread_t tid; fd = mta_open (port); if (!foreground) { switch (fork()) { case -1: terror ("daemon: %m"); exit (EX_FAILURE); case 0: break; default: _exit (0); } if (setsid() == -1) { terror ("setsid: %m"); exit (EX_FAILURE); } chdir("/"); close(0); close(1); close(2); open(_PATH_DEVNULL, O_RDONLY); open(_PATH_DEVNULL, O_WRONLY); dup(1); /* Set up logging */ openlog (progname, LOG_PID, LOG_MAIL); terror = terror_syslog; } /* Set up signal handling */ sigemptyset (&sigs); act.sa_flags = 0; sigemptyset (&act.sa_mask); act.sa_handler = signull; for (i = 0; fatal_signals[i]; i++) { sigaddset (&sigs, fatal_signals[i]); sigaction (fatal_signals[i], &act, NULL); } sigaddset (&sigs, SIGPIPE); sigaddset (&sigs, SIGALRM); sigaddset (&sigs, SIGCHLD); pthread_sigmask (SIG_BLOCK, &sigs, NULL); pthread_create (&tid, NULL, thr_mta_listener, &fd); pthread_create (&tid, NULL, thr_watcher, NULL); /* Unblock only the fatal signals */ sigemptyset (&sigs); for (i = 0; fatal_signals[i]; i++) sigaddset (&sigs, fatal_signals[i]); pthread_sigmask (SIG_UNBLOCK, &sigs, NULL); /* Wait for signal to arrive */ sigwait (&sigs, &i); } else { struct smtp *smtp = smtp_create (0, 1); do_smtp (smtp); smtp_free (smtp); } exit (EX_OK); }