aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGilles Chehade <gilles@poolp.org>2019-03-11 18:19:41 +0100
committerGilles Chehade <gilles@poolp.org>2019-03-11 18:19:41 +0100
commit36a45c827713a2a5fa7c3ad1513cd011ebfa49a4 (patch)
tree458960fd59b6ca36072f17aa1d985afdbf232e1d
parentremove deprecated info (diff)
downloadOpenSMTPD-36a45c827713a2a5fa7c3ad1513cd011ebfa49a4.tar.xz
OpenSMTPD-36a45c827713a2a5fa7c3ad1513cd011ebfa49a4.zip
move smtpscript/ to OpenSMTPD
-rw-r--r--smtpscript/LICENSE15
-rw-r--r--smtpscript/Makefile3
-rw-r--r--smtpscript/Makefile.inc3
-rw-r--r--smtpscript/README.md40
-rw-r--r--smtpscript/iobuf.c466
-rw-r--r--smtpscript/iobuf.h71
-rw-r--r--smtpscript/parse.y925
-rw-r--r--smtpscript/smtpscript.c1009
-rw-r--r--smtpscript/smtpscript.h79
-rw-r--r--smtpscript/smtpscript/Makefile12
-rw-r--r--smtpscript/ssl.c167
11 files changed, 2790 insertions, 0 deletions
diff --git a/smtpscript/LICENSE b/smtpscript/LICENSE
new file mode 100644
index 00000000..92859014
--- /dev/null
+++ b/smtpscript/LICENSE
@@ -0,0 +1,15 @@
+/*
+ * Copyright (c) 2013-2014 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.
+ */
diff --git a/smtpscript/Makefile b/smtpscript/Makefile
new file mode 100644
index 00000000..6a628f2a
--- /dev/null
+++ b/smtpscript/Makefile
@@ -0,0 +1,3 @@
+SUBDIR+= smtpscript
+
+.include <bsd.subdir.mk>
diff --git a/smtpscript/Makefile.inc b/smtpscript/Makefile.inc
new file mode 100644
index 00000000..be93057b
--- /dev/null
+++ b/smtpscript/Makefile.inc
@@ -0,0 +1,3 @@
+CFLAGS= -Wall -W
+
+BINDIR?= /usr/bin
diff --git a/smtpscript/README.md b/smtpscript/README.md
new file mode 100644
index 00000000..18f5fe9e
--- /dev/null
+++ b/smtpscript/README.md
@@ -0,0 +1,40 @@
+smtpscript
+==========
+
+smtpscript is a tool to write SMTP scenarios and easily implement regression tests for SMTP server-side implementations.
+
+A smtpscript will look like:
+
+
+ # this is a function init-helo that we want to call in all our regress tests
+ proc init-helo {
+ expect smtp ok
+ writeln "HELO regress"
+ expect smtp helo
+ }
+
+ # each of the test-case will be called sequentially
+ test-case name "mailfrom.empty" {
+ call init-helo
+ writeln "MAIL FROM:<>"
+ expect smtp ok
+ }
+
+ test-case name "mailfrom.broken" {
+ call init-helo
+ writeln "MAIL FROM:< @bleh>"
+ expect smtp permfail
+ }
+
+
+which once executed, produces the output:
+
+ $ smtpscript foo
+ ===> running test-case "mailfrom.empty" ok
+ ===> running test-case "mailfrom.broken" ok
+ ===> all run
+ passed: 2/2 (skipped: 0, failed: 0, error: 0)
+ $
+
+
+The scripting language also supports TLS, randomization and loops, so fairly complex scenarios can be achieved.
diff --git a/smtpscript/iobuf.c b/smtpscript/iobuf.c
new file mode 100644
index 00000000..7dcd45b9
--- /dev/null
+++ b/smtpscript/iobuf.c
@@ -0,0 +1,466 @@
+/* $OpenBSD$ */
+/*
+ * Copyright (c) 2012 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 <sys/uio.h>
+
+#include <errno.h>
+#include <limits.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#ifdef IO_SSL
+#include <openssl/err.h>
+#include <openssl/ssl.h>
+#endif
+
+#include "iobuf.h"
+
+#define IOBUF_MAX 65536
+#define IOBUFQ_MIN 4096
+
+struct ioqbuf *ioqbuf_alloc(struct iobuf *, size_t);
+void iobuf_drain(struct iobuf *, size_t);
+
+int
+iobuf_init(struct iobuf *io, size_t size, size_t max)
+{
+ memset(io, 0, sizeof *io);
+
+ if (max == 0)
+ max = IOBUF_MAX;
+
+ if (size == 0)
+ size = max;
+
+ if (size > max)
+ return (-1);
+
+ if ((io->buf = malloc(size)) == NULL)
+ return (-1);
+
+ io->size = size;
+ io->max = max;
+
+ return (0);
+}
+
+void
+iobuf_clear(struct iobuf *io)
+{
+ struct ioqbuf *q;
+
+ if (io->buf)
+ free(io->buf);
+
+ while ((q = io->outq)) {
+ io->outq = q->next;
+ free(q);
+ }
+
+ memset(io, 0, sizeof (*io));
+}
+
+void
+iobuf_drain(struct iobuf *io, size_t n)
+{
+ struct ioqbuf *q;
+ size_t left = n;
+
+ while ((q = io->outq) && left) {
+ if ((q->wpos - q->rpos) > left) {
+ q->rpos += left;
+ left = 0;
+ } else {
+ left -= q->wpos - q->rpos;
+ io->outq = q->next;
+ free(q);
+ }
+ }
+
+ io->queued -= (n - left);
+ if (io->outq == NULL)
+ io->outqlast = NULL;
+}
+
+int
+iobuf_extend(struct iobuf *io, size_t n)
+{
+ char *t;
+
+ if (n > io->max)
+ return (-1);
+
+ if (io->max - io->size < n)
+ return (-1);
+
+ t = realloc(io->buf, io->size + n);
+ if (t == NULL)
+ return (-1);
+
+ io->size += n;
+ io->buf = t;
+
+ return (0);
+}
+
+size_t
+iobuf_left(struct iobuf *io)
+{
+ return io->size - io->wpos;
+}
+
+size_t
+iobuf_space(struct iobuf *io)
+{
+ return io->size - (io->wpos - io->rpos);
+}
+
+size_t
+iobuf_len(struct iobuf *io)
+{
+ return io->wpos - io->rpos;
+}
+
+char *
+iobuf_data(struct iobuf *io)
+{
+ return io->buf + io->rpos;
+}
+
+void
+iobuf_drop(struct iobuf *io, size_t n)
+{
+ if (n >= iobuf_len(io)) {
+ io->rpos = io->wpos = 0;
+ return;
+ }
+
+ io->rpos += n;
+}
+
+char *
+iobuf_getline(struct iobuf *iobuf, size_t *rlen)
+{
+ char *buf;
+ size_t len, i;
+
+ buf = iobuf_data(iobuf);
+ len = iobuf_len(iobuf);
+
+ for (i = 0; i + 1 <= len; i++)
+ if (buf[i] == '\n') {
+ /* Note: the returned address points into the iobuf
+ * buffer. We NUL-end it for convenience, and discard
+ * the data from the iobuf, so that the caller doesn't
+ * have to do it. The data remains "valid" as long
+ * as the iobuf does not overwrite it, that is until
+ * the next call to iobuf_normalize() or iobuf_extend().
+ */
+ iobuf_drop(iobuf, i + 1);
+ len = (i && buf[i - 1] == '\r') ? i - 1 : i;
+ buf[len] = '\0';
+ if (rlen)
+ *rlen = len;
+ return (buf);
+ }
+
+ return (NULL);
+}
+
+void
+iobuf_normalize(struct iobuf *io)
+{
+ if (io->rpos == 0)
+ return;
+
+ if (io->rpos == io->wpos) {
+ io->rpos = io->wpos = 0;
+ return;
+ }
+
+ memmove(io->buf, io->buf + io->rpos, io->wpos - io->rpos);
+ io->wpos -= io->rpos;
+ io->rpos = 0;
+}
+
+ssize_t
+iobuf_read(struct iobuf *io, int fd)
+{
+ ssize_t n;
+
+ n = read(fd, io->buf + io->wpos, iobuf_left(io));
+ if (n == -1) {
+ /* XXX is this really what we want? */
+ if (errno == EAGAIN || errno == EINTR)
+ return (IOBUF_WANT_READ);
+ return (IOBUF_ERROR);
+ }
+ if (n == 0)
+ return (IOBUF_CLOSED);
+
+ io->wpos += n;
+
+ return (n);
+}
+
+struct ioqbuf *
+ioqbuf_alloc(struct iobuf *io, size_t len)
+{
+ struct ioqbuf *q;
+
+ if (len < IOBUFQ_MIN)
+ len = IOBUFQ_MIN;
+
+ if ((q = malloc(sizeof(*q) + len)) == NULL)
+ return (NULL);
+
+ q->rpos = 0;
+ q->wpos = 0;
+ q->size = len;
+ q->next = NULL;
+ q->buf = (char *)(q) + sizeof(*q);
+
+ if (io->outqlast == NULL)
+ io->outq = q;
+ else
+ io->outqlast->next = q;
+ io->outqlast = q;
+
+ return (q);
+}
+
+size_t
+iobuf_queued(struct iobuf *io)
+{
+ return io->queued;
+}
+
+void *
+iobuf_reserve(struct iobuf *io, size_t len)
+{
+ struct ioqbuf *q;
+ void *r;
+
+ if (len == 0)
+ return (NULL);
+
+ if (((q = io->outqlast) == NULL) || q->size - q->wpos <= len) {
+ if ((q = ioqbuf_alloc(io, len)) == NULL)
+ return (NULL);
+ }
+
+ r = q->buf + q->wpos;
+ q->wpos += len;
+ io->queued += len;
+
+ return (r);
+}
+
+int
+iobuf_queue(struct iobuf *io, const void *data, size_t len)
+{
+ void *buf;
+
+ if (len == 0)
+ return (0);
+
+ if ((buf = iobuf_reserve(io, len)) == NULL)
+ return (-1);
+
+ memmove(buf, data, len);
+
+ return (0);
+}
+
+int
+iobuf_queuev(struct iobuf *io, const struct iovec *iov, int iovcnt)
+{
+ int i;
+ size_t len = 0;
+ char *buf;
+
+ for (i = 0; i < iovcnt; i++)
+ len += iov[i].iov_len;
+
+ if ((buf = iobuf_reserve(io, len)) == NULL)
+ return (-1);
+
+ for (i = 0; i < iovcnt; i++) {
+ if (iov[i].iov_len == 0)
+ continue;
+ memmove(buf, iov[i].iov_base, iov[i].iov_len);
+ buf += iov[i].iov_len;
+ }
+
+ return (0);
+
+}
+
+int
+iobuf_fqueue(struct iobuf *io, const char *fmt, ...)
+{
+ va_list ap;
+ int len;
+
+ va_start(ap, fmt);
+ len = iobuf_vfqueue(io, fmt, ap);
+ va_end(ap);
+
+ return (len);
+}
+
+int
+iobuf_vfqueue(struct iobuf *io, const char *fmt, va_list ap)
+{
+ char *buf;
+ int len;
+
+ len = vasprintf(&buf, fmt, ap);
+
+ if (len == -1)
+ return (-1);
+
+ len = iobuf_queue(io, buf, len);
+ free(buf);
+
+ return (len);
+}
+
+ssize_t
+iobuf_write(struct iobuf *io, int fd)
+{
+ struct iovec iov[IOV_MAX];
+ struct ioqbuf *q;
+ int i;
+ ssize_t n;
+
+ i = 0;
+ for (q = io->outq; q ; q = q->next) {
+ if (i >= IOV_MAX)
+ break;
+ iov[i].iov_base = q->buf + q->rpos;
+ iov[i].iov_len = q->wpos - q->rpos;
+ i++;
+ }
+
+ n = writev(fd, iov, i);
+ if (n == -1) {
+ if (errno == EAGAIN || errno == EINTR)
+ return (IOBUF_WANT_WRITE);
+ if (errno == EPIPE)
+ return (IOBUF_CLOSED);
+ return (IOBUF_ERROR);
+ }
+
+ iobuf_drain(io, n);
+
+ return (n);
+}
+
+int
+iobuf_flush(struct iobuf *io, int fd)
+{
+ ssize_t s;
+
+ while (io->queued)
+ if ((s = iobuf_write(io, fd)) < 0)
+ return (s);
+
+ return (0);
+}
+
+#ifdef IO_SSL
+
+int
+iobuf_flush_ssl(struct iobuf *io, void *ssl)
+{
+ ssize_t s;
+
+ while (io->queued)
+ if ((s = iobuf_write_ssl(io, ssl) < 0))
+ return (s);
+
+ return (0);
+}
+
+ssize_t
+iobuf_write_ssl(struct iobuf *io, void *ssl)
+{
+ struct ioqbuf *q;
+ int r;
+ ssize_t n;
+
+ q = io->outq;
+ n = SSL_write(ssl, q->buf + q->rpos, q->wpos - q->rpos);
+ if (n <= 0) {
+ switch ((r = SSL_get_error(ssl, n))) {
+ case SSL_ERROR_WANT_READ:
+ return (IOBUF_WANT_READ);
+ case SSL_ERROR_WANT_WRITE:
+ return (IOBUF_WANT_WRITE);
+ case SSL_ERROR_ZERO_RETURN: /* connection closed */
+ return (IOBUF_CLOSED);
+ case SSL_ERROR_SYSCALL:
+ if (ERR_peek_last_error())
+ return (IOBUF_SSLERROR);
+ if (r == 0)
+ errno = EPIPE;
+ return (IOBUF_ERROR);
+ default:
+ return (IOBUF_SSLERROR);
+ }
+ }
+ iobuf_drain(io, n);
+
+ return (n);
+}
+
+ssize_t
+iobuf_read_ssl(struct iobuf *io, void *ssl)
+{
+ ssize_t n;
+ int r;
+
+ n = SSL_read(ssl, io->buf + io->wpos, iobuf_left(io));
+ if (n < 0) {
+ switch ((r = SSL_get_error(ssl, n))) {
+ case SSL_ERROR_WANT_READ:
+ return (IOBUF_WANT_READ);
+ case SSL_ERROR_WANT_WRITE:
+ return (IOBUF_WANT_WRITE);
+ case SSL_ERROR_SYSCALL:
+ if (ERR_peek_last_error())
+ return (IOBUF_SSLERROR);
+ if (r == 0)
+ errno = EPIPE;
+ return (IOBUF_ERROR);
+ default:
+ return (IOBUF_SSLERROR);
+ }
+ } else if (n == 0)
+ return (IOBUF_CLOSED);
+
+ io->wpos += n;
+
+ return (n);
+}
+
+#endif /* IO_SSL */
diff --git a/smtpscript/iobuf.h b/smtpscript/iobuf.h
new file mode 100644
index 00000000..ee4690c8
--- /dev/null
+++ b/smtpscript/iobuf.h
@@ -0,0 +1,71 @@
+/* $OpenBSD$ */
+/*
+ * Copyright (c) 2012 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 <stdarg.h>
+
+struct ioqbuf {
+ struct ioqbuf *next;
+ char *buf;
+ size_t size;
+ size_t wpos;
+ size_t rpos;
+};
+
+struct iobuf {
+ char *buf;
+ size_t max;
+ size_t size;
+ size_t wpos;
+ size_t rpos;
+
+ size_t queued;
+ struct ioqbuf *outq;
+ struct ioqbuf *outqlast;
+};
+
+#define IOBUF_WANT_READ -1
+#define IOBUF_WANT_WRITE -2
+#define IOBUF_CLOSED -3
+#define IOBUF_ERROR -4
+#define IOBUF_SSLERROR -5
+
+int iobuf_init(struct iobuf *, size_t, size_t);
+void iobuf_clear(struct iobuf *);
+
+int iobuf_extend(struct iobuf *, size_t);
+void iobuf_normalize(struct iobuf *);
+void iobuf_drop(struct iobuf *, size_t);
+size_t iobuf_space(struct iobuf *);
+size_t iobuf_len(struct iobuf *);
+size_t iobuf_left(struct iobuf *);
+char *iobuf_data(struct iobuf *);
+char *iobuf_getline(struct iobuf *, size_t *);
+ssize_t iobuf_read(struct iobuf *, int);
+ssize_t iobuf_read_ssl(struct iobuf *, void *);
+
+size_t iobuf_queued(struct iobuf *);
+void* iobuf_reserve(struct iobuf *, size_t);
+int iobuf_queue(struct iobuf *, const void*, size_t);
+int iobuf_queuev(struct iobuf *, const struct iovec *, int);
+int iobuf_fqueue(struct iobuf *, const char *, ...);
+int iobuf_vfqueue(struct iobuf *, const char *, va_list);
+int iobuf_flush(struct iobuf *, int);
+int iobuf_flush_ssl(struct iobuf *, void *);
+ssize_t iobuf_write(struct iobuf *, int);
+ssize_t iobuf_write_ssl(struct iobuf *, void *);
diff --git a/smtpscript/parse.y b/smtpscript/parse.y
new file mode 100644
index 00000000..10ddb90e
--- /dev/null
+++ b/smtpscript/parse.y
@@ -0,0 +1,925 @@
+/* $OpenBSD: parse.y,v 1.109 2012/10/14 11:58:23 gilles Exp $ */
+
+/*
+ * Copyright (c) 2012 Eric Faurot <eric@openbsd.org>
+ * Copyright (c) 2008 Gilles Chehade <gilles@openbsd.org>
+ * Copyright (c) 2008 Pierre-Yves Ritschard <pyr@openbsd.org>
+ * Copyright (c) 2002, 2003, 2004 Henning Brauer <henning@openbsd.org>
+ * Copyright (c) 2001 Markus Friedl. All rights reserved.
+ * Copyright (c) 2001 Daniel Hartmeier. All rights reserved.
+ * Copyright (c) 2001 Theo de Raadt. All rights reserved.
+ *
+ * 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/queue.h>
+#include <sys/tree.h>
+#include <sys/param.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/ioctl.h>
+
+#include <ctype.h>
+#include <err.h>
+#include <errno.h>
+#include <event.h>
+#include <inttypes.h>
+#include <netdb.h>
+#include <paths.h>
+#include <pwd.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <util.h>
+
+#include "smtpscript.h"
+
+TAILQ_HEAD(files, file) files = TAILQ_HEAD_INITIALIZER(files);
+static struct file {
+ TAILQ_ENTRY(file) entry;
+ FILE *stream;
+ char *name;
+ int lineno;
+ int errors;
+} *file, *topfile;
+struct file *pushfile(const char *, int);
+int popfile(void);
+int check_file_secrecy(int, const char *);
+int yyparse(void);
+int yylex(void);
+int kw_cmp(const void *, const void *);
+int lookup(char *);
+int lgetc(int);
+int lungetc(int);
+int findeol(void);
+int yyerror(const char *, ...)
+ __attribute__ ((format (printf, 1, 2)));
+
+TAILQ_HEAD(symhead, sym) symhead = TAILQ_HEAD_INITIALIZER(symhead);
+struct sym {
+ TAILQ_ENTRY(sym) entry;
+ int used;
+ int persist;
+ char *nam;
+ char *val;
+};
+int symset(const char *, const char *, int);
+char *symget(const char *);
+
+void push_op(struct op *);
+struct op *peek_op(void);
+struct op *pop_op(void);
+
+#define MAXDEPTH 50
+
+static struct op * opstack[MAXDEPTH];
+static int opstackidx;
+
+static int errors = 0;
+
+static struct script *currscript;
+static struct procedure *currproc;
+
+int delaytonum(char *);
+
+typedef struct {
+ union {
+ int64_t number;
+ char *string;
+ struct op *op;
+ } v;
+ int lineno;
+} YYSTYPE;
+
+%}
+
+%token INCLUDE PORT REPEAT RANDOM NOOP
+%token PROC TESTCASE NAME NO_AUTOCONNECT EXPECT FAIL SKIP
+%token CALL CONNECT DISCONNECT STARTTLS SLEEP WRITE WRITELN
+%token SMTP OK TEMPFAIL PERMFAIL HELO
+%token ERROR ARROW
+%token <v.string> STRING
+%token <v.number> NUMBER
+%type <v.number> quantifier port duration size
+%type <v.op> statement block
+%%
+
+grammar : /* empty */
+ | grammar '\n'
+ | grammar include '\n'
+ | grammar varset '\n'
+ | grammar proc '\n'
+ | grammar testcase '\n'
+ | grammar error '\n' { file->errors++; }
+ ;
+
+include : INCLUDE STRING {
+ struct file *nfile;
+
+ if ((nfile = pushfile($2, 0)) == NULL) {
+ yyerror("failed to include file %s", $2);
+ free($2);
+ YYERROR;
+ }
+ free($2);
+
+ file = nfile;
+ lungetc('\n');
+ }
+ ;
+
+varset : STRING '=' STRING {
+ if (symset($1, $3, 0) == -1)
+ errx(1, "cannot store variable");
+ free($1);
+ free($3);
+ }
+ ;
+
+optnl : '\n' optnl
+ |
+ ;
+
+nl : '\n' optnl
+ ;
+
+quantifier : /* empty */ { $$ = 1; }
+ | 's' { $$ = 1000; }
+ | 'm' { $$ = 60 * 1000; }
+ | 'h' { $$ = 3600 * 1000; }
+ ;
+
+duration : NUMBER quantifier {
+ if ($1 < 0) {
+ yyerror("invalid duration: %" PRId64, $1);
+ YYERROR;
+ }
+ $$ = $1 * $2;
+ }
+ ;
+
+size : NUMBER {
+ if ($1 < 0) {
+ yyerror("invalid size: %" PRId64, $1);
+ YYERROR;
+ }
+ $$ = $1;
+ }
+ | STRING {
+ long long result;
+
+ if (scan_scaled($1, &result) == -1 || result < 0) {
+ yyerror("invalid size: %s", $1);
+ YYERROR;
+ }
+ free($1);
+
+ $$ = result;
+ }
+ ;
+
+port : PORT STRING {
+ struct servent *servent;
+
+ servent = getservbyname($2, "tcp");
+ if (servent == NULL) {
+ yyerror("port %s is invalid", $2);
+ free($2);
+ YYERROR;
+ }
+ $$ = ntohs(servent->s_port);
+ free($2);
+ }
+ | PORT NUMBER {
+ if ($2 <= 0 || $2 >= (int)USHRT_MAX) {
+ yyerror("invalid port: %" PRId64, $2);
+ YYERROR;
+ }
+ $$ = $2;
+ }
+ | /* empty */ {
+ $$ = 25;
+ }
+ ;
+
+statement : block
+ | REPEAT NUMBER { push_op(NULL); } statement {
+ pop_op();
+ $$ = op_repeat(peek_op(), $2, $4);
+ }
+ | RANDOM { push_op(NULL); } block {
+ pop_op();
+ $$ = op_random(peek_op(), $3);
+ }
+ | CALL STRING {
+ struct procedure *p;
+ p = procedure_get_by_name(currscript, $2);
+ if (p == NULL) {
+ yyerror("call to undefined proc \"%s\"", $2);
+ file->errors++;
+ } else if (p == currproc) {
+ yyerror("recursive call to proc \"%s\"", $2);
+ file->errors++;
+ } else {
+ $$ = op_call(peek_op(), p);
+ }
+ free($2);
+ }
+ | NOOP {
+ $$ = op_noop(peek_op());
+ }
+ | SLEEP duration {
+ $$ = op_sleep(peek_op(), $2);
+ }
+ | FAIL STRING {
+ $$ = op_fail(peek_op(), $2);
+ }
+ | CONNECT STRING port {
+ $$ = op_connect(peek_op(), $2, $3);
+ }
+ | DISCONNECT {
+ $$ = op_disconnect(peek_op());
+ }
+ | STARTTLS {
+ $$ = op_starttls(peek_op());
+ }
+ | WRITE STRING {
+ $$ = op_write(peek_op(), $2, strlen($2));
+ }
+ | WRITELN STRING {
+ $$ = op_printf(peek_op(), "%s\r\n", $2);
+ free($2);
+ }
+ | EXPECT DISCONNECT {
+ $$ = op_expect_disconnect(peek_op());
+ }
+ | EXPECT SMTP {
+ $$ = op_expect_smtp_response(peek_op(),
+ RESP_SMTP_ANY | RESP_SMTP_MULTILINE);
+ }
+ | EXPECT SMTP OK {
+ $$ = op_expect_smtp_response(peek_op(),
+ RESP_SMTP_OK);
+ }
+ | EXPECT SMTP HELO {
+ $$ = op_expect_smtp_response(peek_op(),
+ RESP_SMTP_OK | RESP_SMTP_MULTILINE);
+ }
+ | EXPECT SMTP TEMPFAIL {
+ $$ = op_expect_smtp_response(peek_op(),
+ RESP_SMTP_TEMPFAIL);
+ }
+ | EXPECT SMTP PERMFAIL {
+ $$ = op_expect_smtp_response(peek_op(),
+ RESP_SMTP_PERMFAIL);
+ }
+ ;
+
+statement_list : statement nl statement_list
+ | statement
+ | /* EMPTY */
+ ;
+
+block : '{' {
+ push_op(op_block(peek_op()));
+ } optnl statement_list '}' {
+ $$ = pop_op();
+ }
+ ;
+
+procparam : '%' STRING {
+ if (proc_addvar(currproc, $2) == -1) {
+ yyerror("cannot add parameter %s", $2);
+ file->errors++;
+ }
+ }
+ ;
+
+procparams : procparam procparams
+ | /* EMPTY */
+ ;
+
+proc : PROC STRING {
+ printf("# proc %s\n", $2);
+ currproc = procedure_create(currscript, $2);
+ if (currproc == NULL)
+ file->errors++;
+ } procparams block {
+ if (currproc)
+ currproc->root = $5;
+ }
+ ;
+
+testopt_name : NAME STRING {
+ if (procedure_get_by_name(currscript, $2)) {
+ file->errors++;
+ } else {
+ free(currproc->name);
+ currproc->name = ($2);
+ }
+ }
+ | /* EMPTY */
+ ;
+
+testopt_cnx : NO_AUTOCONNECT {
+ currproc->flags |= PROC_NOCONNECT;
+ }
+ | /* EMPTY */
+ ;
+testopt_fail : EXPECT FAIL {
+ currproc->flags |= PROC_EXPECTFAIL;
+ }
+ | /* EMPTY */
+ ;
+
+testopt_skip : SKIP {
+ currproc->flags |= PROC_SKIP;
+ }
+ | /* EMPTY */
+ ;
+
+testcaseopts : testopt_name testopt_cnx testopt_fail testopt_skip;
+
+testcase : TESTCASE {
+ char buf[1024];
+ snprintf(buf, sizeof buf, "<%s:%i>",
+ file->name, file->lineno);
+ currproc = procedure_create(currscript, strdup(buf));
+ if (currproc) {
+ currproc->flags |= PROC_TESTCASE;
+ } else {
+ file->errors++;
+ }
+ } testcaseopts block {
+ currproc->root = $4;
+ }
+ ;
+%%
+
+struct keywords {
+ const char *k_name;
+ int k_val;
+};
+
+int
+yyerror(const char *fmt, ...)
+{
+ va_list ap;
+
+ file->errors++;
+ va_start(ap, fmt);
+ fprintf(stderr, "%s:%d: ", file->name, yylval.lineno);
+ vfprintf(stderr, fmt, ap);
+ fprintf(stderr, "\n");
+ va_end(ap);
+ return (0);
+}
+
+int
+kw_cmp(const void *k, const void *e)
+{
+ return (strcmp(k, ((const struct keywords *)e)->k_name));
+}
+
+int
+lookup(char *s)
+{
+ /* this has to be sorted always */
+ static const struct keywords keywords[] = {
+ { "call", CALL },
+ { "connect", CONNECT },
+ { "disconnect", DISCONNECT },
+ { "expect", EXPECT },
+ { "fail", FAIL },
+ { "helo", HELO },
+ { "name", NAME },
+ { "no-autoconnect", NO_AUTOCONNECT },
+ { "noop", NOOP },
+ { "ok", OK },
+ { "permfail", PERMFAIL },
+ { "port", PORT },
+ { "proc", PROC },
+ { "random", RANDOM },
+ { "repeat", REPEAT },
+ { "skip", SKIP },
+ { "sleep", SLEEP },
+ { "smtp", SMTP },
+ { "starttls", STARTTLS },
+ { "tempfail", TEMPFAIL },
+ { "test-case", TESTCASE },
+ { "write", WRITE },
+ { "writeln", WRITELN },
+ };
+ const struct keywords *p;
+
+ p = bsearch(s, keywords, sizeof(keywords)/sizeof(keywords[0]),
+ sizeof(keywords[0]), kw_cmp);
+
+ if (p)
+ return (p->k_val);
+ else
+ return (STRING);
+}
+
+#define MAXPUSHBACK 128
+
+char *parsebuf;
+int parseindex;
+char pushback_buffer[MAXPUSHBACK];
+int pushback_index = 0;
+
+int
+lgetc(int quotec)
+{
+ int c, next;
+
+ if (parsebuf) {
+ /* Read character from the parsebuffer instead of input. */
+ if (parseindex >= 0) {
+ c = parsebuf[parseindex++];
+ if (c != '\0')
+ return (c);
+ parsebuf = NULL;
+ } else
+ parseindex++;
+ }
+
+ if (pushback_index)
+ return (pushback_buffer[--pushback_index]);
+
+ if (quotec) {
+ if ((c = getc(file->stream)) == EOF) {
+ yyerror("reached end of file while parsing "
+ "quoted string");
+ if (file == topfile || popfile() == EOF)
+ return (EOF);
+ return (quotec);
+ }
+ return (c);
+ }
+
+ while ((c = getc(file->stream)) == '\\') {
+ next = getc(file->stream);
+ if (next != '\n') {
+ c = next;
+ break;
+ }
+ yylval.lineno = file->lineno;
+ file->lineno++;
+ }
+
+ while (c == EOF) {
+ if (file == topfile || popfile() == EOF)
+ return (EOF);
+ c = getc(file->stream);
+ }
+ return (c);
+}
+
+int
+lungetc(int c)
+{
+ if (c == EOF)
+ return (EOF);
+ if (parsebuf) {
+ parseindex--;
+ if (parseindex >= 0)
+ return (c);
+ }
+ if (pushback_index < MAXPUSHBACK-1)
+ return (pushback_buffer[pushback_index++] = c);
+ else
+ return (EOF);
+}
+
+int
+findeol(void)
+{
+ int c;
+
+ parsebuf = NULL;
+ pushback_index = 0;
+
+ /* skip to either EOF or the first real EOL */
+ while (1) {
+ c = lgetc(0);
+ if (c == '\n') {
+ file->lineno++;
+ break;
+ }
+ if (c == EOF)
+ break;
+ }
+ return (ERROR);
+}
+
+int
+yylex(void)
+{
+ char buf[8096];
+ char *p, *val;
+ int quotec, next, c;
+ int token;
+
+top:
+ p = buf;
+ while ((c = lgetc(0)) == ' ' || c == '\t')
+ ; /* nothing */
+
+ yylval.lineno = file->lineno;
+ if (c == '#')
+ while ((c = lgetc(0)) != '\n' && c != EOF)
+ ; /* nothing */
+ if (c == '$' && parsebuf == NULL) {
+ while (1) {
+ if ((c = lgetc(0)) == EOF)
+ return (0);
+
+ if (p + 1 >= buf + sizeof(buf) - 1) {
+ yyerror("string too long");
+ return (findeol());
+ }
+ if (isalnum(c) || c == '_') {
+ *p++ = (char)c;
+ continue;
+ }
+ *p = '\0';
+ lungetc(c);
+ break;
+ }
+ val = symget(buf);
+ if (val == NULL) {
+ yyerror("macro '%s' not defined", buf);
+ return (findeol());
+ }
+ parsebuf = val;
+ parseindex = 0;
+ goto top;
+ }
+
+ switch (c) {
+ case '\'':
+ case '"':
+ quotec = c;
+ while (1) {
+ if ((c = lgetc(quotec)) == EOF)
+ return (0);
+ if (c == '\n') {
+ file->lineno++;
+ continue;
+ } else if (c == '\\') {
+ if ((next = lgetc(quotec)) == EOF)
+ return (0);
+ if (next == quotec || c == ' ' || c == '\t')
+ c = next;
+ else if (next == '\n') {
+ file->lineno++;
+ continue;
+ } else
+ lungetc(next);
+ } else if (c == quotec) {
+ *p = '\0';
+ break;
+ }
+ if (p + 1 >= buf + sizeof(buf) - 1) {
+ yyerror("string too long");
+ return (findeol());
+ }
+ *p++ = (char)c;
+ }
+ yylval.v.string = strdup(buf);
+ if (yylval.v.string == NULL)
+ err(1, "yylex: strdup");
+ return (STRING);
+ }
+
+#define allowed_to_end_number(x) \
+ (isspace(x) || x == ')' || x ==',' || x == '/' || x == '}' || x == '=')
+
+ if (c == '-' || isdigit(c)) {
+ do {
+ *p++ = c;
+ if ((unsigned)(p-buf) >= sizeof(buf)) {
+ yyerror("string too long");
+ return (findeol());
+ }
+ } while ((c = lgetc(0)) != EOF && isdigit(c));
+ lungetc(c);
+ if (p == buf + 1 && buf[0] == '-')
+ goto nodigits;
+ if (c == EOF || allowed_to_end_number(c)) {
+ const char *errstr = NULL;
+
+ *p = '\0';
+ yylval.v.number = strtonum(buf, LLONG_MIN,
+ LLONG_MAX, &errstr);
+ if (errstr) {
+ yyerror("\"%s\" invalid number: %s",
+ buf, errstr);
+ return (findeol());
+ }
+ return (NUMBER);
+ } else {
+nodigits:
+ while (p > buf + 1)
+ lungetc(*--p);
+ c = *--p;
+ if (c == '-')
+ return (c);
+ }
+ }
+
+ if (c == '=') {
+ if ((c = lgetc(0)) != EOF && c == '>')
+ return (ARROW);
+ lungetc(c);
+ c = '=';
+ }
+
+#define allowed_in_string(x) \
+ (isalnum(x) || (ispunct(x) && x != '(' && x != ')' && \
+ x != '{' && x != '}' && x != '<' && x != '>' && \
+ x != '!' && x != '=' && x != '#' && \
+ x != ','))
+
+ if (isalnum(c) || c == ':' || c == '_') {
+ do {
+ *p++ = c;
+ if ((unsigned)(p-buf) >= sizeof(buf)) {
+ yyerror("string too long");
+ return (findeol());
+ }
+ } while ((c = lgetc(0)) != EOF && (allowed_in_string(c)));
+ lungetc(c);
+ *p = '\0';
+ if ((token = lookup(buf)) == STRING)
+ if ((yylval.v.string = strdup(buf)) == NULL)
+ err(1, "yylex: strdup");
+ return (token);
+ }
+ if (c == '\n') {
+ yylval.lineno = file->lineno;
+ file->lineno++;
+ }
+ if (c == EOF)
+ return (0);
+ return (c);
+}
+
+int
+check_file_secrecy(int fd, const char *fname)
+{
+ struct stat st;
+
+ if (fstat(fd, &st)) {
+ warn("cannot stat %s", fname);
+ return (-1);
+ }
+ if (st.st_uid != 0 && st.st_uid != getuid()) {
+ warnx("%s: owner not root or current user", fname);
+ return (-1);
+ }
+ if (st.st_mode & (S_IRWXG | S_IRWXO)) {
+ warnx("%s: group/world readable/writeable", fname);
+ return (-1);
+ }
+ return (0);
+}
+
+struct file *
+pushfile(const char *name, int secret)
+{
+ struct file *nfile;
+
+ if ((nfile = calloc(1, sizeof(struct file))) == NULL) {
+ warn("malloc");
+ return (NULL);
+ }
+ if ((nfile->name = strdup(name)) == NULL) {
+ warn("malloc");
+ free(nfile);
+ return (NULL);
+ }
+ if ((nfile->stream = fopen(nfile->name, "r")) == NULL) {
+ warn("%s", nfile->name);
+ free(nfile->name);
+ free(nfile);
+ return (NULL);
+ } else if (secret &&
+ check_file_secrecy(fileno(nfile->stream), nfile->name)) {
+ fclose(nfile->stream);
+ free(nfile->name);
+ free(nfile);
+ return (NULL);
+ }
+ nfile->lineno = 1;
+ TAILQ_INSERT_TAIL(&files, nfile, entry);
+ return (nfile);
+}
+
+int
+popfile(void)
+{
+ struct file *prev;
+
+ if ((prev = TAILQ_PREV(file, files, entry)) != NULL)
+ prev->errors += file->errors;
+
+ TAILQ_REMOVE(&files, file, entry);
+ fclose(file->stream);
+ free(file->name);
+ free(file);
+ file = prev;
+ return (file ? 0 : EOF);
+}
+
+struct script *
+parse_script(const char *filename)
+{
+ errors = 0;
+
+ currscript = calloc(1, sizeof *currscript);
+ TAILQ_INIT(&currscript->procs);
+ currproc = NULL;
+
+ opstackidx = 0;
+
+ if ((file = pushfile(filename, 0)) == NULL)
+ return (NULL);
+
+ topfile = file;
+
+ /*
+ * parse configuration
+ */
+ setservent(1);
+ yyparse();
+ errors = file->errors;
+ popfile();
+ endservent();
+
+ if (errors)
+ return (NULL);
+
+ return (currscript);
+}
+
+int
+symset(const char *nam, const char *val, int persist)
+{
+ struct sym *sym;
+
+ for (sym = TAILQ_FIRST(&symhead); sym && strcmp(nam, sym->nam);
+ sym = TAILQ_NEXT(sym, entry))
+ ; /* nothing */
+
+ if (sym != NULL) {
+ if (sym->persist == 1)
+ return (0);
+ else {
+ free(sym->nam);
+ free(sym->val);
+ TAILQ_REMOVE(&symhead, sym, entry);
+ free(sym);
+ }
+ }
+ if ((sym = calloc(1, sizeof(*sym))) == NULL)
+ return (-1);
+
+ sym->nam = strdup(nam);
+ if (sym->nam == NULL) {
+ free(sym);
+ return (-1);
+ }
+ sym->val = strdup(val);
+ if (sym->val == NULL) {
+ free(sym->nam);
+ free(sym);
+ return (-1);
+ }
+ sym->used = 0;
+ sym->persist = persist;
+ TAILQ_INSERT_TAIL(&symhead, sym, entry);
+ return (0);
+}
+
+int
+cmdline_symset(char *s)
+{
+ char *sym, *val;
+ int ret;
+ size_t len;
+
+ if ((val = strrchr(s, '=')) == NULL)
+ return (-1);
+
+ len = strlen(s) - strlen(val) + 1;
+ if ((sym = malloc(len)) == NULL)
+ errx(1, "cmdline_symset: malloc");
+
+ (void)strlcpy(sym, s, len);
+
+ ret = symset(sym, val + 1, 1);
+ free(sym);
+
+ return (ret);
+}
+
+char *
+symget(const char *nam)
+{
+ struct sym *sym;
+
+ TAILQ_FOREACH(sym, &symhead, entry)
+ if (strcmp(nam, sym->nam) == 0) {
+ sym->used = 1;
+ return (sym->val);
+ }
+ return (NULL);
+}
+
+int
+delaytonum(char *str)
+{
+ unsigned int factor;
+ size_t len;
+ const char *errstr = NULL;
+ int delay;
+
+ /* we need at least 1 digit and 1 unit */
+ len = strlen(str);
+ if (len < 2)
+ goto bad;
+
+ switch(str[len - 1]) {
+
+ case 's':
+ factor = 1;
+ break;
+
+ case 'm':
+ factor = 60;
+ break;
+
+ case 'h':
+ factor = 60 * 60;
+ break;
+
+ case 'd':
+ factor = 24 * 60 * 60;
+ break;
+
+ default:
+ goto bad;
+ }
+
+ str[len - 1] = '\0';
+ delay = strtonum(str, 1, INT_MAX / factor, &errstr);
+ if (errstr)
+ goto bad;
+
+ return (delay * factor);
+
+bad:
+ return (-1);
+}
+
+
+void
+push_op(struct op *op)
+{
+ if (opstackidx == MAXDEPTH) {
+ yyerror("too deep");
+ return;
+ }
+ opstack[opstackidx++] = op;
+}
+
+struct op *
+pop_op(void)
+{
+ if (opstackidx == 0)
+ return (NULL);
+ return (opstack[--opstackidx]);
+}
+
+struct op *
+peek_op(void)
+{
+ if (opstackidx == 0)
+ return (NULL);
+ return (opstack[opstackidx - 1]);
+}
diff --git a/smtpscript/smtpscript.c b/smtpscript/smtpscript.c
new file mode 100644
index 00000000..a75c4249
--- /dev/null
+++ b/smtpscript/smtpscript.c
@@ -0,0 +1,1009 @@
+/* $OpenBSD: iobuf.h,v 1.1 2012/01/29 00:32:51 eric Exp $ */
+/*
+ * Copyright (c) 2012 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 <sys/queue.h>
+
+#include <ctype.h>
+#include <err.h>
+#include <errno.h>
+#include <getopt.h>
+#include <netdb.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <vis.h>
+
+#include "iobuf.h"
+
+#include "smtpscript.h"
+
+void *ssl_connect(int);
+void ssl_close(void *);
+
+/* XXX */
+#define SMTP_LINE_MAX 4096
+
+enum {
+ OP_BLOCK,
+ OP_REPEAT,
+ OP_RANDOM,
+
+ OP_NOOP,
+
+ OP_FAIL,
+ OP_CALL,
+ OP_CONNECT,
+ OP_DISCONNECT,
+ OP_STARTTLS,
+ OP_SLEEP,
+ OP_WRITE,
+
+ OP_EXPECT_DISCONNECT,
+ OP_EXPECT_SMTP_RESPONSE,
+};
+
+struct op {
+ struct op *next;
+ int type;
+ union {
+ struct {
+ int count;
+ struct op *start;
+ struct op *last;
+ } block;
+ struct {
+ struct op *op;
+ int count;
+ } repeat;
+ struct {
+ struct op *block;
+ } random;
+ struct {
+ char *reason;
+ } fail;
+ struct {
+ struct procedure *proc;
+ } call;
+ struct {
+ char *hostname;
+ int portno;
+ } connect;
+ struct {
+ unsigned int ms;
+ } sleep;
+ struct {
+ const void *buf;
+ size_t len;
+ } write;
+ struct {
+ int flags;
+ } expect_smtp;
+ } u;
+};
+
+#define RES_OK 0
+#define RES_SKIP 1
+#define RES_FAIL 2
+#define RES_ERROR 3
+
+struct ctx {
+ int sock;
+ void *ssl;
+ struct iobuf iobuf;
+ int lvl;
+
+ int result;
+ char *reason;
+};
+
+static struct op * _op_connect;
+
+int verbose;
+int randomdelay; /* between each testcase */
+int tapout;
+size_t rundelay; /* between each testcase */
+
+static size_t test_pass;
+static size_t test_skip;
+static size_t test_fail;
+static size_t test_error;
+static size_t test_total = 0;
+
+static struct op *op_add_child(struct op *, const struct op *);
+static void run_testcase(struct procedure *);
+static void print_testcase(char *status, char *name, char *reason, char *directive, size_t number);
+static void process_op(struct ctx *, struct op *);
+static const char * parse_smtp_response(char *, size_t, char **, int *);
+
+struct procedure *
+procedure_create(struct script *scr, char *name)
+{
+ struct procedure *p;
+
+ if (procedure_get_by_name(scr, name)) {
+ warnx("procedure \"%s\" already exists", name);
+ return (NULL);
+ }
+
+ p = calloc(1, sizeof *p);
+ TAILQ_INIT(&p->vars);
+ p->name = strdup(name);
+
+ TAILQ_INSERT_TAIL(&scr->procs, p, entry);
+
+ return (p);
+}
+
+struct procedure *
+procedure_get_by_name(struct script *scr, const char *name)
+{
+ struct procedure *p;
+
+ TAILQ_FOREACH(p, &scr->procs, entry)
+ if (!strcmp(name, p->name))
+ return (p);
+
+ return (NULL);
+}
+
+int
+proc_getvaridx(struct procedure *proc, char *name)
+{
+ struct variable *v;
+ int n;
+
+ n = 0;
+ TAILQ_FOREACH(v, &proc->vars, entry) {
+ if (!strcmp(name, v->name))
+ return (n);
+ n++;
+ }
+
+ return (-1);
+}
+
+int
+proc_addvar(struct procedure *proc, char *name)
+{
+ struct variable *v;
+
+ printf("adding variable \"%s\"\n", name);
+
+ if (proc_getvaridx(proc, name) != -1)
+ return (-1);
+ v = calloc(1, sizeof *v);
+ v->name = name;
+ TAILQ_INSERT_TAIL(&proc->vars, v, entry);
+
+ return (proc->varcount++);
+}
+
+struct op *
+op_block(struct op *parent)
+{
+ struct op o;
+
+ bzero(&o, sizeof o);
+ o.type = OP_BLOCK;
+
+ return (op_add_child(parent, &o));
+}
+
+struct op *
+op_repeat(struct op *parent, int count, struct op *op)
+{
+ struct op o;
+
+ bzero(&o, sizeof o);
+ o.type = OP_REPEAT;
+ o.u.repeat.count = count;
+ o.u.repeat.op = op;
+
+ return (op_add_child(parent, &o));
+}
+
+struct op *
+op_random(struct op *parent, struct op *op)
+{
+ struct op o;
+
+ bzero(&o, sizeof o);
+ o.type = OP_RANDOM;
+ o.u.random.block = op;
+
+ return (op_add_child(parent, &o));
+}
+
+struct op *
+op_noop(struct op *parent)
+{
+ struct op o;
+
+ bzero(&o, sizeof o);
+ o.type = OP_NOOP;
+
+ return (op_add_child(parent, &o));
+}
+
+struct op *
+op_call(struct op *parent, struct procedure *proc)
+{
+ struct op o;
+
+ bzero(&o, sizeof o);
+ o.type = OP_CALL;
+ o.u.call.proc = proc;
+
+ return (op_add_child(parent, &o));
+}
+
+struct op *
+op_fail(struct op *parent, char *reason)
+{
+ struct op o;
+
+ bzero(&o, sizeof o);
+ o.type = OP_FAIL;
+ o.u.fail.reason = reason;
+
+ return (op_add_child(parent, &o));
+}
+
+struct op *
+op_connect(struct op *parent, const char *hostname, int portno)
+{
+ struct op o;
+
+ bzero(&o, sizeof o);
+ o.type = OP_CONNECT;
+ o.u.connect.hostname = strdup(hostname);
+ o.u.connect.portno = portno;
+ return (op_add_child(parent, &o));
+}
+
+struct op *
+op_disconnect(struct op *parent)
+{
+ struct op o;
+
+ bzero(&o, sizeof o);
+ o.type = OP_DISCONNECT;
+ return (op_add_child(parent, &o));
+}
+
+struct op *
+op_starttls(struct op *parent)
+{
+ struct op o;
+
+ bzero(&o, sizeof o);
+ o.type = OP_STARTTLS;
+ return (op_add_child(parent, &o));
+}
+
+struct op *
+op_sleep(struct op *parent, unsigned int ms)
+{
+ struct op o;
+
+ bzero(&o, sizeof o);
+ o.type = OP_SLEEP;
+ o.u.sleep.ms = ms;
+ return (op_add_child(parent, &o));
+}
+
+struct op *
+op_write(struct op *parent, const void *buf, size_t len)
+{
+ struct op o;
+
+ bzero(&o, sizeof o);
+ o.type = OP_WRITE;
+ o.u.write.buf = buf;
+ o.u.write.len = len;
+ return (op_add_child(parent, &o));
+}
+
+struct op *
+op_printf(struct op *parent, const char *fmt, ...)
+{
+ va_list ap;
+ char *buf;
+ int len;
+
+ va_start(ap, fmt);
+ if ((len = vasprintf(&buf, fmt, ap)) == -1)
+ err(1, "vasprintf");
+ va_end(ap);
+
+ return op_write(parent, buf, len);
+}
+
+struct op *
+op_expect_disconnect(struct op *parent)
+{
+ struct op o;
+
+ bzero(&o, sizeof o);
+ o.type = OP_EXPECT_DISCONNECT;
+ return (op_add_child(parent, &o));
+}
+
+struct op *
+op_expect_smtp_response(struct op *parent, int flags)
+{
+ struct op o;
+
+ bzero(&o, sizeof o);
+ o.type = OP_EXPECT_SMTP_RESPONSE;
+ o.u.expect_smtp.flags = flags;
+ return (op_add_child(parent, &o));
+}
+
+static void
+usage(void)
+{
+ extern const char *__progname;
+ errx(1, "Usage: %s [-rvt] [-d delay] script", __progname);
+}
+
+int
+main(int argc, char **argv)
+{
+ struct script *s;
+ struct procedure *p;
+ int ch;
+
+ while ((ch = getopt(argc, argv, "d:rvt")) != -1) {
+ switch(ch) {
+ case 'v':
+ verbose += 1;
+ break;
+ case 'd':
+ rundelay = atoi(optarg) * 1000;
+ break;
+ case 'r':
+ randomdelay = 1;
+ break;
+ case 't':
+ tapout = 1;
+ break;
+ default:
+ usage();
+ /* NOTREACHED */
+ }
+ }
+ argc -= optind;
+ argv += optind;
+
+ if (argc != 1)
+ usage();
+
+ s = parse_script(argv[0]);
+ if (s == NULL)
+ errx(1, "error reading script file");
+
+ _op_connect = op_connect(NULL, "127.0.0.1", 25);
+
+ if (tapout) {
+ printf("# smtpscript is an SMTP testing framework\n\n");
+ printf("TAP version 13\n");
+ }
+
+ TAILQ_FOREACH(p, &s->procs, entry)
+ if (p->flags & PROC_TESTCASE)
+ run_testcase(p);
+
+ if (tapout)
+ printf("1..%zu\n", test_total);
+ else {
+ printf("passed: %zu/%zu (skipped: %zu, failed: %zu, error: %zu)\n",
+ test_pass,
+ test_total,
+ test_skip,
+ test_fail,
+ test_error);
+ }
+ return (0);
+}
+
+static struct op *
+op_add_child(struct op *parent, const struct op *op)
+{
+ struct op *n;
+
+ n = malloc(sizeof *n);
+ if (n == NULL)
+ err(1, "malloc");
+
+ memmove(n, op, sizeof *n);
+ n->next = NULL;
+
+ /* printf("op:%p type:%i parent: %p\n", n, n->type, parent); */
+
+ if (parent) {
+ if (parent->u.block.start == NULL)
+ parent->u.block.start = n;
+ if (parent->u.block.last)
+ parent->u.block.last->next = n;
+ parent->u.block.last = n;
+ parent->u.block.count += 1;
+ }
+
+ return (n);
+}
+
+static void
+run_testcase(struct procedure *proc)
+{
+ struct ctx c;
+ uint32_t rdelay;
+
+ bzero(&c, sizeof c);
+ c.sock = -1;
+ c.lvl = 1;
+
+ if (rundelay) {
+ if (randomdelay)
+ rdelay = arc4random_uniform(rundelay);
+ else
+ rdelay = rundelay;
+ usleep(rdelay);
+ }
+
+ fflush(stdout);
+
+ if (verbose > 1)
+ printf("\n");
+
+ if (!(proc->flags & PROC_NOCONNECT))
+ process_op(&c, _op_connect);
+ process_op(&c, proc->root);
+
+ if (c.sock != -1)
+ close(c.sock);
+ if (c.ssl)
+ ssl_close(c.ssl);
+ iobuf_clear(&c.iobuf);
+
+ if (verbose > 1) {
+ printf("# Done with test-case \"%s\": ", proc->name);
+ }
+
+ switch (c.result) {
+ case RES_OK:
+ test_total += 1;
+ if (proc->flags & PROC_EXPECTFAIL) {
+ print_testcase("not ok", proc->name, c.reason, "TODO", test_total); // XPass
+ test_fail += 1;
+ } else if (proc->flags & PROC_SKIP) {
+ test_skip += 1;
+ print_testcase("ok", proc->name, c.reason, "SKIP", test_total);
+ }
+ else {
+ print_testcase("ok", proc->name, c.reason, NULL, test_total);
+ test_pass += 1;
+ }
+
+ break;
+
+ case RES_SKIP:
+ test_skip += 1;
+ test_total += 1;
+ print_testcase("not ok", proc->name, c.reason, "SKIP", test_total);
+ break;
+
+ case RES_FAIL:
+ test_total += 1;
+ if (proc->flags & PROC_EXPECTFAIL) {
+ print_testcase("not ok", proc->name, c.reason, "TODO", test_total); // XFail
+ test_pass += 1;
+ } else if (proc->flags & PROC_SKIP) {
+ test_skip += 1;
+ print_testcase("ok", proc->name, c.reason, "SKIP", test_total);
+ }
+ else {
+ print_testcase("not ok", proc->name, c.reason, NULL, test_total);
+ test_fail += 1;
+ }
+
+ break;
+
+ case RES_ERROR:
+ test_error += 1;
+ test_total += 1;
+ print_testcase("not ok", proc->name, c.reason, NULL, test_total);
+ break;
+ }
+
+ if (verbose > 1) {
+ printf("\n");
+ }
+
+}
+
+void print_testcase(char *status, char *name, char *reason, char *directive, size_t number)
+{
+ printf("%s %zu", status, number);
+ if (directive)
+ printf(" - %s # %s\n", name, directive);
+ else
+ if (reason)
+ printf(" - %s # %s\n", name, reason);
+ else
+ printf(" - %s\n", name);
+}
+
+static size_t
+strvisx2(char *dst, const char *src, size_t srclen, int flag)
+{
+ size_t n, r, i;
+
+ n = strvisx(dst, src, srclen, flag);
+ if (n == 0)
+ return (0);
+
+ r = n;
+ for (i = n - 1; i; i--) {
+ if (dst[i] == '\r') {
+ memmove(dst + i + 2, dst + i + 1, n + 1 - i);
+ dst[i+1] = 'r';
+ dst[i] = '\\';
+ r++;
+ } else if (dst[i] == '"') {
+ memmove(dst + i + 2, dst + i + 1, n + 1 - i);
+ dst[i+1] = '"';
+ dst[i] = '\\';
+ r++;
+ }
+ }
+
+ return (r);
+}
+
+static const char *
+show_data(const char *src, size_t len, size_t max)
+{
+ static char buf[8192 + 3];
+ char tmp[256];
+ size_t l, n;
+
+ l = len;
+ if (len > 2048)
+ l = 2048;
+
+ buf[0] = '"';
+ n = strvisx2(&buf[1], src, l, VIS_SAFE | VIS_NL | VIS_TAB | VIS_CSTYLE);
+ if (n >= max) {
+ snprintf(tmp, sizeof tmp, "...\" [%zu]", l);
+ buf[max - strlen(tmp)] = '\0';
+ strlcat(buf, tmp, sizeof(buf));
+ } else {
+ strlcat(buf, "\"", sizeof(buf));
+ }
+
+ return (buf);
+}
+
+static void
+print_op(struct op *op, int lvl)
+{
+
+
+ if (op->type == OP_BLOCK)
+ return;
+
+ while (lvl--)
+ printf(" ");
+
+ switch(op->type) {
+
+ case OP_REPEAT:
+ printf("=> repeat: %i\n", op->u.repeat.count);
+ break;
+
+ case OP_RANDOM:
+ printf("=> random: %i\n", op->u.random.block->u.block.count);
+ break;
+
+ case OP_NOOP:
+ printf("=> noop\n");
+ break;
+
+ case OP_FAIL:
+ printf("=> fail: %s\n", op->u.fail.reason);
+ break;
+
+ case OP_CALL:
+ printf("=> call: %s\n", op->u.call.proc->name);
+ break;
+
+ case OP_CONNECT:
+ printf("=> connect %s:%i\n",
+ op->u.connect.hostname,
+ op->u.connect.portno);
+ break;
+
+ case OP_DISCONNECT:
+ printf("=> disconnect\n");
+ break;
+
+ case OP_STARTTLS:
+ printf("=> starttls\n");
+ break;
+
+ case OP_SLEEP:
+ printf("=> sleep %ims\n", op->u.sleep.ms);
+ break;
+
+ case OP_WRITE:
+ printf("=> write %s\n",
+ show_data(op->u.write.buf, op->u.write.len, 70));
+ break;
+
+ case OP_EXPECT_DISCONNECT:
+ printf("<= disconnect\n");
+ break;
+
+ case OP_EXPECT_SMTP_RESPONSE:
+ printf("<= smtp-response 0x%04x\n", op->u.expect_smtp.flags);
+ break;
+
+ default:
+ printf("<> ??? %i;\n", op->type);
+ break;
+ }
+}
+
+
+static void
+set_failure(struct ctx *ctx, int res, const char *fmt, ...)
+{
+ va_list ap;
+ int len;
+
+ ctx->result = res;
+ va_start(ap, fmt);
+ if ((len = vasprintf(&ctx->reason, fmt, ap)) == -1)
+ err(1, "vasprintf");
+ va_end(ap);
+}
+
+static void
+process_op(struct ctx *ctx, struct op *op)
+{
+ struct addrinfo hints, *a, *ai;
+ struct op *o;
+ struct iobuf *iobuf;
+ int i, r, s, save_errno, cont;
+ const char *cause;
+ char buf[16], *servname, *line;
+ ssize_t n;
+ size_t len;
+ const char *e;
+
+ if (verbose > 1)
+ print_op(op, ctx->lvl);
+
+ iobuf = &ctx->iobuf;
+
+ switch(op->type) {
+
+ case OP_BLOCK:
+ ctx->lvl += 1;
+ for (o = op->u.block.start; o; o = o->next) {
+ process_op(ctx, o);
+ if (ctx->result)
+ break;
+ }
+ ctx->lvl -= 1;
+ break;
+
+ case OP_REPEAT:
+ ctx->lvl += 1;
+ for (i = 0; i < op->u.repeat.count; i++) {
+ process_op(ctx, op->u.repeat.op);
+ if (ctx->result)
+ break;
+ }
+ ctx->lvl -= 1;
+ break;
+
+ case OP_RANDOM:
+
+ if (op->u.random.block->u.block.count == 0)
+ return;
+
+ ctx->lvl += 1;
+
+ i = arc4random_uniform(op->u.random.block->u.block.count);
+ for (o = op->u.random.block->u.block.start; i; i--, o = o->next)
+ ;
+ process_op(ctx, o);
+ if (ctx->result)
+ break;
+ ctx->lvl -= 1;
+ break;
+
+ case OP_NOOP:
+ break;
+
+ case OP_FAIL:
+ set_failure(ctx, RES_FAIL, op->u.fail.reason);
+ break;
+
+ case OP_CALL:
+ process_op(ctx, op->u.call.proc->root);
+ break;
+
+ case OP_CONNECT:
+ if (ctx->sock != -1)
+ close(ctx->sock);
+ ctx->sock = -1;
+ iobuf_clear(iobuf);
+
+ servname = NULL;
+ if (op->u.connect.portno) {
+ snprintf(buf, sizeof buf, "%i", op->u.connect.portno);
+ servname = buf;
+ }
+ bzero(&hints, sizeof hints);
+ hints.ai_socktype = SOCK_STREAM;
+ hints.ai_protocol = IPPROTO_TCP;
+ r = getaddrinfo(op->u.connect.hostname, servname, &hints, &ai);
+ if (r) {
+ set_failure(ctx, RES_ERROR,
+ "failed to connect to %s:%s: %s",
+ op->u.connect.hostname, servname, gai_strerror(r));
+ return;
+ }
+
+ s = -1;
+ for(a = ai; a; a = a->ai_next) {
+ s = socket(a->ai_family, a->ai_socktype, a->ai_protocol);
+ if (s == -1) {
+ cause = "socket";
+ continue;
+ }
+ if (connect(s, a->ai_addr, a->ai_addrlen) == -1) {
+ cause = "connect";
+ save_errno = errno;
+ close(s);
+ errno = save_errno;
+ s = -1;
+ continue;
+ }
+ break; /* okay we got one */
+ }
+ freeaddrinfo(ai);
+ if (s == -1) {
+ set_failure(ctx, RES_ERROR,
+ "failed to connect to %s:%s: %s",
+ op->u.connect.hostname, servname, cause);
+ } else {
+ ctx->sock = s;
+ iobuf_init(iobuf, 0, 0);
+ }
+ break;
+
+ case OP_DISCONNECT:
+ if (ctx->sock != -1)
+ close(ctx->sock);
+ ctx->sock = -1;
+ iobuf_clear(iobuf);
+ break;
+
+ case OP_STARTTLS:
+ if (ctx->ssl)
+ set_failure(ctx, RES_ERROR, "SSL context already here");
+ else if ((ctx->ssl = ssl_connect(ctx->sock)) == NULL)
+ set_failure(ctx, RES_ERROR, "SSL connection failed");
+ break;
+
+ case OP_SLEEP:
+ usleep(op->u.sleep.ms * 1000);
+ break;
+
+ case OP_WRITE:
+ iobuf_queue(iobuf, op->u.write.buf, op->u.write.len);
+ if (ctx->ssl)
+ r = iobuf_flush_ssl(iobuf, ctx->ssl);
+ else
+ r = iobuf_flush(iobuf, ctx->sock);
+ switch (r) {
+ case 0:
+ break;
+ case IOBUF_CLOSED:
+ set_failure(ctx, RES_FAIL, "connection closed");
+ break;
+ case IOBUF_WANT_WRITE:
+ set_failure(ctx, RES_ERROR, "iobuf_write(): WANT_WRITE");
+ break;
+ case IOBUF_ERROR:
+ set_failure(ctx, RES_ERROR, "IO error");
+ break;
+ case IOBUF_SSLERROR:
+ set_failure(ctx, RES_ERROR, "SSL error");
+ break;
+ default:
+ set_failure(ctx, RES_ERROR, "iobuf_write(): bad value");
+ break;
+ }
+ break;
+
+ case OP_EXPECT_DISCONNECT:
+ if (iobuf_len(iobuf)) {
+ set_failure(ctx, RES_ERROR, "%zu bytes of input left",
+ iobuf_len(iobuf));
+ break;
+ }
+ if (ctx->ssl)
+ n = iobuf_read_ssl(iobuf, ctx->ssl);
+ else
+ n = iobuf_read(iobuf, ctx->sock);
+ switch (n) {
+ case IOBUF_CLOSED:
+ close(ctx->sock);
+ ctx->sock = -1;
+ if (ctx->ssl)
+ ssl_close(ctx->ssl);
+ break;
+ case IOBUF_WANT_READ:
+ set_failure(ctx, RES_ERROR, "iobuf_read(): WANT_READ");
+ break;
+ case IOBUF_ERROR:
+ set_failure(ctx, RES_ERROR, "IO error");
+ break;
+ case IOBUF_SSLERROR:
+ set_failure(ctx, RES_ERROR, "SSL error");
+ break;
+ default:
+ set_failure(ctx, RES_FAIL, "data read: %s",
+ show_data(iobuf_data(iobuf), iobuf_len(iobuf), 70));
+ break;
+ }
+ break;
+
+ case OP_EXPECT_SMTP_RESPONSE:
+ line = NULL;
+ while (1) {
+ line = iobuf_getline(iobuf, &len);
+ if (line) {
+ e = parse_smtp_response(line, len, NULL, &cont);
+ if (e) {
+ set_failure(ctx, RES_FAIL, e);
+ return;
+ }
+ if (!cont) {
+ iobuf_normalize(iobuf);
+ break;
+ }
+ if (!(op->u.expect_smtp.flags
+ & RESP_SMTP_MULTILINE)) {
+ set_failure(ctx, RES_FAIL,
+ "single line response expected");
+ return;
+ }
+ continue;
+ }
+
+ if (iobuf_len(iobuf) >= SMTP_LINE_MAX) {
+ set_failure(ctx, RES_FAIL, "line too long");
+ return;
+ }
+
+ iobuf_normalize(iobuf);
+
+ again:
+ if (ctx->ssl)
+ n = iobuf_read_ssl(iobuf, ctx->ssl);
+ else
+ n = iobuf_read(iobuf, ctx->sock);
+ switch (n) {
+ case IOBUF_CLOSED:
+ set_failure(ctx, RES_FAIL, "connection closed");
+ return;
+ case IOBUF_WANT_READ:
+ goto again;
+ case IOBUF_ERROR:
+ set_failure(ctx, RES_ERROR, "io error");
+ return;
+ case IOBUF_SSLERROR:
+ set_failure(ctx, RES_ERROR, "SSL error");
+ return;
+ default:
+ break;
+ }
+ }
+
+ /* got our response */
+
+ if (verbose > 1) {
+ len = ctx->lvl;
+ while (len--)
+ printf(" ");
+ printf(" >>> %s\n", show_data(line, strlen(line), 70));
+ }
+
+ switch (line[0]) {
+ case '2':
+ case '3':
+ if (!(op->u.expect_smtp.flags & RESP_SMTP_OK))
+ set_failure(ctx, RES_FAIL,
+ "unexpected response code0: %s", line);
+ break;
+ case '4':
+ if (!(op->u.expect_smtp.flags & RESP_SMTP_TEMPFAIL))
+ set_failure(ctx, RES_FAIL,
+ "unexpected response code1: %s", line);
+ break;
+ case '5':
+ if (!(op->u.expect_smtp.flags & RESP_SMTP_PERMFAIL))
+ set_failure(ctx, RES_FAIL,
+ "unexpected response code2: %s", line);
+ break;
+ default:
+ set_failure(ctx, RES_FAIL,
+ "unexpected response code???: %s", line);
+ break;
+ }
+ break;
+
+ default:
+ ctx->result = RES_ERROR;
+ ctx->reason = "invalid operator";
+ break;
+ }
+}
+
+static const char *
+parse_smtp_response(char *line, size_t len, char **msg, int *cont)
+{
+ size_t i;
+
+ if (len >= SMTP_LINE_MAX)
+ return "line too long";
+
+ if (len > 3) {
+ if (msg)
+ *msg = line + 4;
+ if (cont)
+ *cont = (line[3] == '-');
+ } else if (len == 3) {
+ if (msg)
+ *msg = line + 3;
+ if (cont)
+ *cont = 0;
+ } else
+ return "line too short";
+
+ /* validate reply code */
+ if (line[0] < '2' || line[0] > '5' || !isdigit(line[1]) ||
+ !isdigit(line[2]))
+ return "reply code out of range";
+
+ /* validate reply message */
+ for (i = 0; i < len; i++)
+ if (!isprint(line[i]))
+ return "non-printable character in reply";
+
+ return NULL;
+}
diff --git a/smtpscript/smtpscript.h b/smtpscript/smtpscript.h
new file mode 100644
index 00000000..ba90b240
--- /dev/null
+++ b/smtpscript/smtpscript.h
@@ -0,0 +1,79 @@
+/* $OpenBSD: iobuf.h,v 1.1 2012/01/29 00:32:51 eric Exp $ */
+/*
+ * Copyright (c) 2012 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.
+ */
+
+struct op;
+
+#define PROC_TESTCASE 0x0001
+#define PROC_SKIP 0x0002
+#define PROC_EXPECTFAIL 0x0004
+#define PROC_NOCONNECT 0x0008
+
+
+struct variable {
+ TAILQ_ENTRY(variable) entry;
+ char *name;
+};
+
+struct procedure {
+ int flags;
+
+ TAILQ_ENTRY(procedure) entry;
+ char *name;
+
+ TAILQ_HEAD(, variable) vars;
+ int varcount;
+
+ struct op *root;
+
+ int skip;
+ int expect_fail;
+};
+
+#define RESP_SMTP_OK 0x0001
+#define RESP_SMTP_TEMPFAIL 0x0002
+#define RESP_SMTP_PERMFAIL 0x0004
+#define RESP_SMTP_ANY 0x0007
+
+#define RESP_SMTP_MULTILINE 0x0100
+
+struct script {
+ TAILQ_HEAD(, procedure) procs;
+};
+
+int proc_addvar(struct procedure *, char *name);
+int proc_getvaridx(struct procedure *, char *name);
+
+struct op *op_block(struct op *);
+struct op *op_repeat(struct op *, int, struct op *);
+struct op *op_random(struct op *, struct op *);
+struct op *op_noop(struct op *);
+struct op *op_fail(struct op *, char *);
+struct op *op_call(struct op *, struct procedure *);
+struct op *op_connect(struct op *, const char *, int);
+struct op *op_disconnect(struct op *);
+struct op *op_starttls(struct op *);
+struct op *op_sleep(struct op *, unsigned int);
+struct op *op_write(struct op *, const void *, size_t);
+struct op *op_printf(struct op *, const char *, ...);
+
+struct op *op_expect_disconnect(struct op *);
+struct op *op_expect_smtp_response(struct op *, int);
+
+struct procedure *procedure_create(struct script *, char *);
+struct procedure *procedure_get_by_name(struct script *, const char *);
+
+struct script * parse_script(const char *);
diff --git a/smtpscript/smtpscript/Makefile b/smtpscript/smtpscript/Makefile
new file mode 100644
index 00000000..3679481c
--- /dev/null
+++ b/smtpscript/smtpscript/Makefile
@@ -0,0 +1,12 @@
+.PATH: ${.CURDIR}/..
+
+PROG= smtpscript
+SRCS= smtpscript.c iobuf.c parse.y ssl.c
+NOMAN= noman
+
+LDADD+= -lutil -lssl -lcrypto
+DPADD+= ${LIBEVENT} ${LIBUTIL} ${LIBSSL} ${LIBCRYPTO}
+CPPFLAGS+= -I${.CURDIR}/..
+CPPFLAGS+= -DIO_SSL
+
+.include <bsd.prog.mk>
diff --git a/smtpscript/ssl.c b/smtpscript/ssl.c
new file mode 100644
index 00000000..54f0993d
--- /dev/null
+++ b/smtpscript/ssl.c
@@ -0,0 +1,167 @@
+/* $OpenBSD: ssl.c,v 1.50 2012/11/12 14:58:53 eric Exp $ */
+
+/*
+ * Copyright (c) 2008 Pierre-Yves Ritschard <pyr@openbsd.org>
+ * Copyright (c) 2008 Reyk Floeter <reyk@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/queue.h>
+#include <sys/tree.h>
+#include <sys/param.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+
+#include <ctype.h>
+#include <err.h>
+#include <event.h>
+#include <fcntl.h>
+#include <imsg.h>
+#include <pwd.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <openssl/ssl.h>
+#include <openssl/engine.h>
+#include <openssl/err.h>
+
+#define SSL_CIPHERS "HIGH"
+
+void ssl_error(const char *);
+
+static void ssl_init(void);
+static SSL_CTX *ssl_ctx_create(void);
+static void *ssl_client_ctx(void);
+
+static void
+ssl_init(void)
+{
+ static int init = 0;
+
+ if (init)
+ return;
+
+ init = 1;
+
+ SSL_library_init();
+ SSL_load_error_strings();
+
+ OpenSSL_add_all_algorithms();
+
+ /* Init hardware crypto engines. */
+ ENGINE_load_builtin_engines();
+ ENGINE_register_all_complete();
+}
+
+void
+ssl_error(const char *where)
+{
+ unsigned long code;
+ char errbuf[128];
+
+ for (; (code = ERR_get_error()) != 0 ;) {
+ ERR_error_string_n(code, errbuf, sizeof(errbuf));
+ fprintf(stderr, "debug: SSL library error: %s: %s",
+ where, errbuf);
+ }
+}
+
+void *
+ssl_connect(int sock)
+{
+ SSL *ssl;
+
+ ssl = ssl_client_ctx();
+
+ if (SSL_set_fd(ssl, sock) == 0) {
+ ssl_error("ssl_connect:SSL_set_fd");
+ SSL_free(ssl);
+ return (NULL);
+ }
+
+ if (SSL_connect(ssl) != 1) {
+ ssl_error("ssl_connect:SSL_connect");
+ SSL_free(ssl);
+ return (NULL);
+ }
+
+ return ((void*)ssl);
+}
+
+void
+ssl_close(void *a)
+{
+ SSL *ssl = a;
+
+ SSL_free(ssl);
+}
+
+static SSL_CTX *
+ssl_ctx_create(void)
+{
+ SSL_CTX *ctx;
+
+ ssl_init();
+
+ ctx = SSL_CTX_new(SSLv23_method());
+ if (ctx == NULL) {
+ ssl_error("ssl_ctx_create");
+ errx(1, "ssl_ctx_create: could not create SSL context");
+ }
+
+ SSL_CTX_set_session_cache_mode(ctx, SSL_SESS_CACHE_OFF);
+ SSL_CTX_set_timeout(ctx, 30);
+ SSL_CTX_set_options(ctx,
+ SSL_OP_ALL | SSL_OP_NO_SSLv2 | SSL_OP_NO_TICKET);
+ SSL_CTX_set_options(ctx,
+ SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION);
+
+ if (!SSL_CTX_set_cipher_list(ctx, SSL_CIPHERS)) {
+ ssl_error("ssl_ctx_create");
+ errx(1, "ssl_ctx_create: could not set cipher list");
+ }
+
+ return (ctx);
+}
+
+static void *
+ssl_client_ctx(void)
+{
+ SSL_CTX *ctx;
+ SSL *ssl = NULL;
+ int rv = -1;
+
+ ctx = ssl_ctx_create();
+
+ if ((ssl = SSL_new(ctx)) == NULL)
+ goto done;
+ SSL_CTX_free(ctx);
+
+ if (!SSL_set_ssl_method(ssl, SSLv23_client_method()))
+ goto done;
+
+ rv = 0;
+done:
+ if (rv) {
+ if (ssl)
+ SSL_free(ssl);
+ else if (ctx)
+ SSL_CTX_free(ctx);
+ ssl = NULL;
+ }
+ return (void*)(ssl);
+}