From 5e624647d2f2750cf1fe7f97d8c89718d187879d Mon Sep 17 00:00:00 2001 From: Anders Betts Date: Sun, 20 Sep 2026 15:50:24 +0200 Subject: smtp: send messages with a PDF attachment --- src/smtp.c | 951 +++++++++++++++++++++++++++++++++++++++++++++++++++++ src/smtp.h | 24 ++ tests/smtp_check.c | 663 +++++++++++++++++++++++++++++++++++++ 3 files changed, 1638 insertions(+) create mode 100644 src/smtp.c create mode 100644 src/smtp.h create mode 100644 tests/smtp_check.c diff --git a/src/smtp.c b/src/smtp.c new file mode 100644 index 0000000..ed70519 --- /dev/null +++ b/src/smtp.c @@ -0,0 +1,951 @@ +#include "smtp.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "util.h" + +#define SMTP_TIMEOUT_SEC 30 +#define SMTP_LINE_MAX 1024 +#define SMTP_REPLY_TEXT_MAX 2048 +#define SMTP_READ_BUF 4096 + +struct smtp_conn { + int fd; + SSL_CTX *ctx; + SSL *ssl; + unsigned char in[SMTP_READ_BUF]; + size_t in_len; + size_t in_pos; +}; + +struct smtp_reply { + int code; + char text[SMTP_REPLY_TEXT_MAX]; +}; + +static void set_err(char *err, size_t errlen, const char *fmt, ...) +{ + if (!err || errlen == 0) + return; + va_list ap; + va_start(ap, fmt); + vsnprintf(err, errlen, fmt, ap); + va_end(ap); +} + +static void buf_append_str(struct buf *b, const char *s) +{ + buf_append(b, s, strlen(s)); +} + +static void buf_appendf(struct buf *b, const char *fmt, ...) +{ + va_list ap; + va_start(ap, fmt); + char tmp[512]; + int n = vsnprintf(tmp, sizeof tmp, fmt, ap); + va_end(ap); + if (n < 0) + return; + if ((size_t)n < sizeof tmp) { + buf_append(b, tmp, (size_t)n); + return; + } + char *big = xmalloc((size_t)n + 1); + va_start(ap, fmt); + vsnprintf(big, (size_t)n + 1, fmt, ap); + va_end(ap); + buf_append(b, big, (size_t)n); + free(big); +} + +static void conn_close(struct smtp_conn *c) +{ + if (c->ssl) { + SSL_shutdown(c->ssl); + SSL_free(c->ssl); + c->ssl = NULL; + } + if (c->ctx) { + SSL_CTX_free(c->ctx); + c->ctx = NULL; + } + if (c->fd >= 0) { + close(c->fd); + c->fd = -1; + } +} + +static int conn_write(struct smtp_conn *c, const void *data, size_t n, + char *err, size_t errlen) +{ + const unsigned char *p = data; + while (n > 0) { + ssize_t w; + if (c->ssl) { + int chunk = n > (size_t)INT_MAX ? INT_MAX : (int)n; + w = SSL_write(c->ssl, p, chunk); + if (w <= 0) { + int e = SSL_get_error(c->ssl, (int)w); + if (e == SSL_ERROR_WANT_READ || e == SSL_ERROR_WANT_WRITE) + continue; + set_err(err, errlen, "smtp: TLS write failed"); + return -1; + } + } else { + w = send(c->fd, p, n, MSG_NOSIGNAL); + if (w < 0) { + if (errno == EINTR) + continue; + if (errno == EAGAIN || errno == EWOULDBLOCK) { + set_err(err, errlen, "smtp: send timed out"); + return -1; + } + set_err(err, errlen, "smtp: send failed: %s", + strerror(errno)); + return -1; + } + } + p += (size_t)w; + n -= (size_t)w; + } + return 0; +} + +static int conn_line(struct smtp_conn *c, const char *line, char *err, + size_t errlen) +{ + if (conn_write(c, line, strlen(line), err, errlen) != 0) + return -1; + return conn_write(c, "\r\n", 2, err, errlen); +} + +static int conn_buf_line(struct smtp_conn *c, const struct buf *b, char *err, + size_t errlen) +{ + if (conn_write(c, b->p, b->len, err, errlen) != 0) + return -1; + return conn_write(c, "\r\n", 2, err, errlen); +} + +static int conn_fill(struct smtp_conn *c, char *err, size_t errlen) +{ + for (;;) { + if (c->ssl) { + int n = SSL_read(c->ssl, c->in, (int)sizeof c->in); + if (n > 0) { + c->in_len = (size_t)n; + c->in_pos = 0; + return 0; + } + int e = SSL_get_error(c->ssl, n); + if (e == SSL_ERROR_WANT_READ || e == SSL_ERROR_WANT_WRITE) + continue; + if (e == SSL_ERROR_ZERO_RETURN) { + set_err(err, errlen, "smtp: connection closed by server"); + return -1; + } + if (e == SSL_ERROR_SYSCALL && n == 0) { + set_err(err, errlen, "smtp: connection closed by server"); + return -1; + } + if (e == SSL_ERROR_SYSCALL && + (errno == EAGAIN || errno == EWOULDBLOCK)) { + set_err(err, errlen, "smtp: receive timed out"); + return -1; + } + unsigned long ec = ERR_get_error(); + if (ec) + set_err(err, errlen, "smtp: TLS read failed: %s", + ERR_error_string(ec, NULL)); + else + set_err(err, errlen, "smtp: TLS read failed"); + return -1; + } + ssize_t n = recv(c->fd, c->in, sizeof c->in, 0); + if (n > 0) { + c->in_len = (size_t)n; + c->in_pos = 0; + return 0; + } + if (n == 0) { + set_err(err, errlen, "smtp: connection closed by server"); + return -1; + } + if (errno == EINTR) + continue; + if (errno == EAGAIN || errno == EWOULDBLOCK) { + set_err(err, errlen, "smtp: receive timed out"); + return -1; + } + set_err(err, errlen, "smtp: receive failed: %s", strerror(errno)); + return -1; + } +} + +static int conn_read_line(struct smtp_conn *c, char *out, size_t outsz, + char *err, size_t errlen) +{ + size_t o = 0; + for (;;) { + if (c->in_pos >= c->in_len) { + if (conn_fill(c, err, errlen) != 0) + return -1; + } + unsigned char ch = c->in[c->in_pos++]; + if (ch == '\n') { + while (o > 0 && out[o - 1] == '\r') + o--; + out[o] = '\0'; + return 0; + } + if (o + 1 < outsz) + out[o++] = (char)ch; + } +} + +static int parse_code(const char *line, int *code) +{ + if (strlen(line) < 3) + return -1; + for (int i = 0; i < 3; i++) + if (!isdigit((unsigned char)line[i])) + return -1; + *code = (line[0] - '0') * 100 + (line[1] - '0') * 10 + (line[2] - '0'); + return 0; +} + +static int conn_read_reply(struct smtp_conn *c, struct smtp_reply *r, + char *err, size_t errlen) +{ + char line[SMTP_LINE_MAX]; + size_t tlen = 0; + r->code = 0; + r->text[0] = '\0'; + for (;;) { + if (conn_read_line(c, line, sizeof line, err, errlen) != 0) + return -1; + int code; + if (parse_code(line, &code) != 0) { + set_err(err, errlen, "smtp: malformed server reply"); + return -1; + } + r->code = code; + const char *text = line + 3; + while (*text == ' ' || *text == '-') + text++; + if (*text) { + size_t n = strlen(text); + if (tlen) { + if (tlen + 1 < sizeof r->text) + r->text[tlen++] = '\n'; + else + n = 0; + } + if (tlen + n >= sizeof r->text) + n = sizeof r->text - 1 - tlen; + memcpy(r->text + tlen, text, n); + tlen += n; + r->text[tlen] = '\0'; + } + if (line[3] != '-') + break; + } + return 0; +} + +static int conn_expect(struct smtp_conn *c, int want, const char *what, + char *err, size_t errlen) +{ + struct smtp_reply r; + if (conn_read_reply(c, &r, err, errlen) != 0) + return -1; + if (r.code != want) { + if (r.text[0]) + set_err(err, errlen, "%s: server said %d %s", what, r.code, + r.text); + else + set_err(err, errlen, "%s: server said %d", what, r.code); + return -1; + } + return 0; +} + +static int tcp_connect(struct smtp_conn *c, const char *host, int port, + char *err, size_t errlen) +{ + char portstr[16]; + snprintf(portstr, sizeof portstr, "%d", port); + struct addrinfo hints; + memset(&hints, 0, sizeof hints); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + struct addrinfo *res = NULL; + int gai = getaddrinfo(host, portstr, &hints, &res); + if (gai != 0) { + set_err(err, errlen, "smtp: cannot resolve %s: %s", host, + gai_strerror(gai)); + return -1; + } + int last = 0; + for (struct addrinfo *ai = res; ai; ai = ai->ai_next) { + int fd = socket(ai->ai_family, ai->ai_socktype | SOCK_CLOEXEC, + ai->ai_protocol); + if (fd < 0) { + last = errno; + continue; + } + struct timeval tv; + tv.tv_sec = SMTP_TIMEOUT_SEC; + tv.tv_usec = 0; + (void)setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv); + (void)setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof tv); + if (connect(fd, ai->ai_addr, ai->ai_addrlen) == 0) { + c->fd = fd; + freeaddrinfo(res); + return 0; + } + last = errno; + close(fd); + } + freeaddrinfo(res); + if (last) + set_err(err, errlen, "smtp: cannot connect to %s:%d: %s", host, port, + strerror(last)); + else + set_err(err, errlen, "smtp: cannot connect to %s:%d", host, port); + return -1; +} + +static int tls_start(struct smtp_conn *c, const char *host, char *err, + size_t errlen) +{ + c->ctx = SSL_CTX_new(TLS_client_method()); + if (!c->ctx) { + set_err(err, errlen, "smtp: cannot create TLS context"); + return -1; + } + SSL_CTX_set_min_proto_version(c->ctx, TLS1_2_VERSION); + SSL_CTX_set_options(c->ctx, SSL_OP_NO_COMPRESSION | + SSL_OP_NO_RENEGOTIATION); + SSL_CTX_set_verify(c->ctx, SSL_VERIFY_PEER, NULL); + if (SSL_CTX_set_default_verify_paths(c->ctx) != 1) { + set_err(err, errlen, "smtp: cannot load system CA certificates"); + return -1; + } + c->ssl = SSL_new(c->ctx); + if (!c->ssl) { + set_err(err, errlen, "smtp: cannot create TLS connection"); + return -1; + } + if (SSL_set_fd(c->ssl, c->fd) != 1 || + SSL_set_tlsext_host_name(c->ssl, host) != 1 || + SSL_set1_host(c->ssl, host) != 1) { + set_err(err, errlen, "smtp: cannot set up TLS connection"); + return -1; + } + if (SSL_connect(c->ssl) != 1) { + long vr = SSL_get_verify_result(c->ssl); + unsigned long ec = ERR_get_error(); + if (vr != X509_V_OK) + set_err(err, errlen, + "smtp: TLS certificate verification failed: %s", + X509_verify_cert_error_string(vr)); + else if (ec) + set_err(err, errlen, "smtp: TLS handshake failed: %s", + ERR_error_string(ec, NULL)); + else + set_err(err, errlen, "smtp: TLS handshake failed"); + return -1; + } + if (SSL_get_verify_result(c->ssl) != X509_V_OK) { + set_err(err, errlen, "smtp: TLS certificate verification failed: %s", + X509_verify_cert_error_string(SSL_get_verify_result(c->ssl))); + return -1; + } + return 0; +} + +static int mech_offered(const char *caps, const char *mech) +{ + if (!caps) + return 0; + size_t mlen = strlen(mech); + for (const char *p = caps; *p;) { + const char *line_end = strchr(p, '\n'); + size_t llen = line_end ? (size_t)(line_end - p) : strlen(p); + if (llen >= 4 && strncasecmp(p, "AUTH", 4) == 0) { + const char *q = p + 4; + const char *end = p + llen; + while (q < end) { + while (q < end && (*q == ' ' || *q == '=' || *q == '\t')) + q++; + const char *w = q; + while (q < end && *q != ' ' && *q != '=' && *q != '\t') + q++; + size_t wl = (size_t)(q - w); + if (wl == mlen && strncasecmp(w, mech, mlen) == 0) + return 1; + } + } + if (!line_end) + break; + p = line_end + 1; + } + return 0; +} + +static void ehlo_name(char *out, size_t n) +{ + if (gethostname(out, n) != 0 || out[0] == '\0' || !strchr(out, '.')) + snprintf(out, n, "localhost"); + out[n - 1] = '\0'; +} + +static int smtp_ehlo(struct smtp_conn *c, const char *name, struct buf *caps, + char *err, size_t errlen) +{ + struct buf cmd; + buf_init(&cmd); + buf_appendf(&cmd, "EHLO %s", name); + int rc = conn_buf_line(c, &cmd, err, errlen); + buf_free(&cmd); + if (rc != 0) + return -1; + struct smtp_reply r; + if (conn_read_reply(c, &r, err, errlen) != 0) + return -1; + if (r.code != 250) { + if (r.text[0]) + set_err(err, errlen, "EHLO: server said %d %s", r.code, r.text); + else + set_err(err, errlen, "EHLO: server said %d", r.code); + return -1; + } + caps->len = 0; + buf_append(caps, r.text, strlen(r.text) + 1); + return 0; +} + +static int auth_plain(struct smtp_conn *c, const char *user, const char *pass, + char *err, size_t errlen) +{ + size_t ulen = strlen(user); + size_t plen = strlen(pass); + unsigned char *raw = xmalloc(ulen + plen + 2); + raw[0] = 0; + memcpy(raw + 1, user, ulen); + raw[1 + ulen] = 0; + memcpy(raw + 2 + ulen, pass, plen); + char *b64 = util_b64(raw, ulen + plen + 2); + free(raw); + + struct buf cmd; + buf_init(&cmd); + buf_appendf(&cmd, "AUTH PLAIN %s", b64); + int rc = conn_buf_line(c, &cmd, err, errlen); + buf_free(&cmd); + if (rc != 0) { + free(b64); + return -1; + } + struct smtp_reply r; + if (conn_read_reply(c, &r, err, errlen) != 0) { + free(b64); + return -1; + } + if (r.code == 334) { + rc = conn_line(c, b64, err, errlen); + if (rc == 0 && conn_read_reply(c, &r, err, errlen) != 0) + rc = -1; + } + free(b64); + if (rc != 0) + return -1; + if (r.code != 235) { + if (r.text[0]) + set_err(err, errlen, "AUTH PLAIN rejected: %d %s", r.code, + r.text); + else + set_err(err, errlen, "AUTH PLAIN rejected: %d", r.code); + return -1; + } + return 0; +} + +static int auth_login(struct smtp_conn *c, const char *user, const char *pass, + char *err, size_t errlen) +{ + struct smtp_reply r; + if (conn_line(c, "AUTH LOGIN", err, errlen) != 0) + return -1; + if (conn_read_reply(c, &r, err, errlen) != 0) + return -1; + if (r.code != 334) { + set_err(err, errlen, "AUTH LOGIN rejected: %d", r.code); + return -1; + } + char *ub = util_b64((const unsigned char *)user, strlen(user)); + int rc = conn_line(c, ub, err, errlen); + free(ub); + if (rc != 0) + return -1; + if (conn_read_reply(c, &r, err, errlen) != 0) + return -1; + if (r.code != 334) { + set_err(err, errlen, "AUTH LOGIN rejected: %d", r.code); + return -1; + } + char *pb = util_b64((const unsigned char *)pass, strlen(pass)); + rc = conn_line(c, pb, err, errlen); + free(pb); + if (rc != 0) + return -1; + if (conn_read_reply(c, &r, err, errlen) != 0) + return -1; + if (r.code != 235) { + if (r.text[0]) + set_err(err, errlen, "AUTH LOGIN rejected: %d %s", r.code, + r.text); + else + set_err(err, errlen, "AUTH LOGIN rejected: %d", r.code); + return -1; + } + return 0; +} + +static int smtp_auth(struct smtp_conn *c, const struct smtp_message *m, + const char *caps, char *err, size_t errlen) +{ + if (!m->user || !m->user[0]) + return 0; + const char *pass = m->password ? m->password : ""; + if (mech_offered(caps, "PLAIN")) + return auth_plain(c, m->user, pass, err, errlen); + if (mech_offered(caps, "LOGIN")) + return auth_login(c, m->user, pass, err, errlen); + set_err(err, errlen, "smtp: server offers no supported AUTH mechanism"); + return -1; +} + +static int normalize_addr(const char *in, char *out, size_t outsz) +{ + while (*in == ' ' || *in == '\t') + in++; + size_t n = strlen(in); + while (n > 0 && (in[n - 1] == ' ' || in[n - 1] == '\t')) + n--; + if (n >= 2 && in[0] == '<' && in[n - 1] == '>') { + in++; + n -= 2; + } + if (n == 0 || n >= outsz) + return -1; + for (size_t i = 0; i < n; i++) { + unsigned char ch = (unsigned char)in[i]; + if (ch <= 0x20 || ch == 0x7f || ch == '<' || ch == '>') + return -1; + } + memcpy(out, in, n); + out[n] = '\0'; + return 0; +} + +static int ascii_printable(const char *s) +{ + for (; *s; s++) { + unsigned char ch = (unsigned char)*s; + if (ch < 0x20 || ch >= 0x7f) + return 0; + } + return 1; +} + +static int needs_quoting(const char *s) +{ + if (!*s) + return 1; + size_t n = strlen(s); + if (*s == ' ' || *s == '\t' || s[n - 1] == ' ' || s[n - 1] == '\t') + return 1; + for (const char *p = s; *p; p++) + if (strchr("()<>[]:;@\\,.\"", *p)) + return 1; + return 0; +} + +static size_t last_line_len(const struct buf *b) +{ + size_t i = b->len; + while (i > 0 && b->p[i - 1] != '\n') + i--; + return b->len - i; +} + +static void append_encoded_words(struct buf *b, const char *s) +{ + size_t i = 0; + while (s[i]) { + size_t start = i; + size_t chunk = 0; + while (s[i] && chunk < 45) { + unsigned char ch = (unsigned char)s[i]; + size_t clen; + if (ch < 0x80) + clen = 1; + else if ((ch & 0xE0) == 0xC0) + clen = 2; + else if ((ch & 0xF0) == 0xE0) + clen = 3; + else if ((ch & 0xF8) == 0xF0) + clen = 4; + else + clen = 1; + if (chunk + clen > 45) + break; + chunk += clen; + i += clen; + } + if (chunk == 0) { + chunk = 1; + i++; + } + char *b64 = util_b64((const unsigned char *)(s + start), chunk); + if (start > 0) { + if (last_line_len(b) > 60) + buf_append_str(b, "\r\n "); + else + buf_append_str(b, " "); + } + buf_append(b, "=?UTF-8?B?", 10); + buf_append(b, b64, strlen(b64)); + buf_append(b, "?=", 2); + free(b64); + } +} + +static char *sanitize_dup(const char *s) +{ + size_t n = strlen(s); + char *out = xmalloc(n + 1); + for (size_t i = 0; i < n; i++) { + unsigned char ch = (unsigned char)s[i]; + out[i] = (ch == '\r' || ch == '\n') ? ' ' : (char)ch; + } + out[n] = '\0'; + return out; +} + +static void append_phrase(struct buf *b, const char *s) +{ + if (ascii_printable(s) && !needs_quoting(s)) { + buf_append(b, s, strlen(s)); + return; + } + if (ascii_printable(s)) { + buf_append(b, "\"", 1); + for (const char *p = s; *p; p++) { + if (*p == '"' || *p == '\\') + buf_append(b, "\\", 1); + buf_append(b, p, 1); + } + buf_append(b, "\"", 1); + return; + } + append_encoded_words(b, s); +} + +static void append_b64_wrapped(struct buf *out, const unsigned char *data, + size_t n) +{ + if (n == 0) + return; + char *b64 = util_b64(data, n); + size_t len = strlen(b64); + for (size_t i = 0; i < len; i += 76) { + size_t chunk = len - i < 76 ? len - i : 76; + buf_append(out, b64 + i, chunk); + buf_append(out, "\r\n", 2); + } + free(b64); +} + +static void append_quoted_param(struct buf *b, const char *s) +{ + for (; *s; s++) { + if (*s == '"' || *s == '\\') + buf_append(b, "\\", 1); + buf_append(b, s, 1); + } +} + +static void copy_str(char *dst, size_t n, const char *src) +{ + size_t len = strlen(src); + if (len >= n) + len = n - 1; + memcpy(dst, src, len); + dst[len] = '\0'; +} + +static void message_domain(const char *from, char *out, size_t outsz) +{ + const char *at = strrchr(from, '@'); + if (at && at[1]) + copy_str(out, outsz, at + 1); + else + copy_str(out, outsz, "localhost"); + for (char *p = out; *p; p++) { + unsigned char ch = (unsigned char)*p; + if (ch < 0x21 || ch > 0x7e || ch == '<' || ch == '>' || ch == '[' || + ch == ']' || ch == '\\') + *p = '_'; + } +} + +static int build_message(const struct smtp_message *m, const char *from, + const char *to, struct buf *out, char *err, + size_t errlen) +{ + unsigned char rnd[12]; + if (util_random(rnd, sizeof rnd) != 0) { + set_err(err, errlen, "smtp: no entropy source"); + return -1; + } + char rhex[25]; + util_hex(rnd, sizeof rnd, rhex); + + char domain[256]; + message_domain(from, domain, sizeof domain); + + char date[64]; + time_t now = time(NULL); + struct tm tm; + if (gmtime_r(&now, &tm)) + strftime(date, sizeof date, "%a, %d %b %Y %H:%M:%S +0000", &tm); + else + snprintf(date, sizeof date, "Thu, 01 Jan 1970 00:00:00 +0000"); + + buf_append_str(out, "From: "); + if (m->from_name && m->from_name[0]) { + char *name = sanitize_dup(m->from_name); + append_phrase(out, name); + free(name); + buf_append_str(out, " "); + } + buf_appendf(out, "<%s>\r\n", from); + buf_appendf(out, "To: <%s>\r\n", to); + + char *subject = sanitize_dup(m->subject ? m->subject : ""); + buf_append_str(out, "Subject: "); + if (ascii_printable(subject)) + buf_append(out, subject, strlen(subject)); + else + append_encoded_words(out, subject); + free(subject); + buf_append_str(out, "\r\n"); + + buf_appendf(out, "Date: %s\r\n", date); + buf_appendf(out, "Message-ID: <%s.%ld@%s>\r\n", rhex, (long)getpid(), + domain); + buf_append_str(out, "MIME-Version: 1.0\r\n"); + + int has_attach = m->attach_name && m->attach_name[0] && m->attach && + m->attach_len > 0; + if (has_attach) { + char boundary[64]; + snprintf(boundary, sizeof boundary, "=_bokf_%s", rhex); + char *fname = sanitize_dup(m->attach_name); + const char *body = m->body ? m->body : ""; + size_t body_len = strlen(body); + + buf_appendf(out, "Content-Type: multipart/mixed; boundary=\"%s\"\r\n", + boundary); + buf_appendf(out, "\r\n--%s\r\n", boundary); + buf_append_str(out, "Content-Type: text/plain; charset=utf-8\r\n"); + buf_append_str(out, "Content-Transfer-Encoding: base64\r\n"); + buf_append(out, "\r\n", 2); + append_b64_wrapped(out, (const unsigned char *)body, body_len); + buf_appendf(out, "--%s\r\n", boundary); + buf_append_str(out, "Content-Type: application/pdf; name=\""); + append_quoted_param(out, fname); + buf_append_str(out, "\"\r\n"); + buf_append_str(out, "Content-Transfer-Encoding: base64\r\n"); + buf_append_str(out, "Content-Disposition: attachment; filename=\""); + append_quoted_param(out, fname); + buf_append_str(out, "\"\r\n\r\n"); + append_b64_wrapped(out, m->attach, m->attach_len); + buf_appendf(out, "--%s--\r\n", boundary); + free(fname); + } else { + const char *body = m->body ? m->body : ""; + size_t body_len = strlen(body); + buf_append_str(out, "Content-Type: text/plain; charset=utf-8\r\n"); + buf_append_str(out, "Content-Transfer-Encoding: base64\r\n"); + buf_append(out, "\r\n", 2); + append_b64_wrapped(out, (const unsigned char *)body, body_len); + } + return 0; +} + +static int conn_write_dotstuffed(struct smtp_conn *c, const unsigned char *data, + size_t n, char *err, size_t errlen) +{ + size_t start = 0; + while (start < n) { + const unsigned char *nl = memchr(data + start, '\n', n - start); + size_t end = nl ? (size_t)(nl - data) + 1 : n; + if (data[start] == '.') { + if (conn_write(c, ".", 1, err, errlen) != 0) + return -1; + } + if (conn_write(c, data + start, end - start, err, errlen) != 0) + return -1; + start = end; + } + return 0; +} + +int smtp_send(const struct smtp_message *m, char *err, size_t errlen) +{ + if (err && errlen) + err[0] = '\0'; + if (!m) { + set_err(err, errlen, "smtp: no message"); + return -1; + } + if (!m->host || !m->host[0]) { + set_err(err, errlen, "smtp: no host"); + return -1; + } + if (!m->from || !m->from[0]) { + set_err(err, errlen, "smtp: no from address"); + return -1; + } + if (!m->to || !m->to[0]) { + set_err(err, errlen, "smtp: no to address"); + return -1; + } + if (m->port <= 0 || m->port > 65535) { + set_err(err, errlen, "smtp: invalid port"); + return -1; + } + const char *security = m->security ? m->security : "plain"; + int use_tls = strcmp(security, "tls") == 0; + int use_starttls = strcmp(security, "starttls") == 0; + int use_plain = strcmp(security, "plain") == 0; + if (!use_tls && !use_starttls && !use_plain) { + set_err(err, errlen, "smtp: unknown security mode \"%s\"", security); + return -1; + } + + char from[512]; + char to[512]; + if (normalize_addr(m->from, from, sizeof from) != 0 || + normalize_addr(m->to, to, sizeof to) != 0) { + set_err(err, errlen, "smtp: invalid address"); + return -1; + } + + int ret = -1; + struct smtp_conn c; + memset(&c, 0, sizeof c); + c.fd = -1; + struct buf message; + buf_init(&message); + struct buf caps; + buf_init(&caps); + + if (tcp_connect(&c, m->host, m->port, err, errlen) != 0) + goto out; + if (use_tls && tls_start(&c, m->host, err, errlen) != 0) + goto out; + if (conn_expect(&c, 220, "greeting", err, errlen) != 0) + goto out; + + char hostname[256]; + ehlo_name(hostname, sizeof hostname); + if (smtp_ehlo(&c, hostname, &caps, err, errlen) != 0) + goto out; + + if (use_starttls) { + if (conn_line(&c, "STARTTLS", err, errlen) != 0 || + conn_expect(&c, 220, "STARTTLS", err, errlen) != 0) + goto out; + if (tls_start(&c, m->host, err, errlen) != 0) + goto out; + if (smtp_ehlo(&c, hostname, &caps, err, errlen) != 0) + goto out; + } + + if (smtp_auth(&c, m, (const char *)caps.p, err, errlen) != 0) + goto out; + + char cmd[1200]; + int n = snprintf(cmd, sizeof cmd, "MAIL FROM:<%s>", from); + if (n < 0 || (size_t)n >= sizeof cmd) { + set_err(err, errlen, "smtp: from address too long"); + goto out; + } + if (conn_line(&c, cmd, err, errlen) != 0 || + conn_expect(&c, 250, "MAIL FROM", err, errlen) != 0) + goto out; + + n = snprintf(cmd, sizeof cmd, "RCPT TO:<%s>", to); + if (n < 0 || (size_t)n >= sizeof cmd) { + set_err(err, errlen, "smtp: to address too long"); + goto out; + } + if (conn_line(&c, cmd, err, errlen) != 0) + goto out; + struct smtp_reply r; + if (conn_read_reply(&c, &r, err, errlen) != 0) + goto out; + if (r.code != 250 && r.code != 251) { + if (r.text[0]) + set_err(err, errlen, "RCPT TO: server said %d %s", r.code, + r.text); + else + set_err(err, errlen, "RCPT TO: server said %d", r.code); + goto out; + } + + if (conn_line(&c, "DATA", err, errlen) != 0 || + conn_expect(&c, 354, "DATA", err, errlen) != 0) + goto out; + + if (build_message(m, from, to, &message, err, errlen) != 0) + goto out; + if (conn_write_dotstuffed(&c, message.p, message.len, err, errlen) != 0) + goto out; + if (conn_write(&c, ".\r\n", 3, err, errlen) != 0 || + conn_expect(&c, 250, "message", err, errlen) != 0) + goto out; + + if (conn_line(&c, "QUIT", NULL, 0) == 0) + (void)conn_read_reply(&c, &r, NULL, 0); + + ret = 0; + +out: + buf_free(&caps); + buf_free(&message); + conn_close(&c); + return ret; +} diff --git a/src/smtp.h b/src/smtp.h new file mode 100644 index 0000000..a6c4a34 --- /dev/null +++ b/src/smtp.h @@ -0,0 +1,24 @@ +#ifndef BOKF_SMTP_H +#define BOKF_SMTP_H + +#include + +struct smtp_message { + const char *host; + int port; + const char *security; /* "tls" (implicit), "starttls", "plain" */ + const char *user; /* NULL/empty = no AUTH */ + const char *password; + const char *from; /* envelope + From address */ + const char *from_name; /* display name, may be NULL */ + const char *to; + const char *subject; + const char *body; /* UTF-8 text body */ + const char *attach_name; /* NULL -> text-only */ + const unsigned char *attach; + size_t attach_len; +}; + +int smtp_send(const struct smtp_message *m, char *err, size_t errlen); + +#endif diff --git a/tests/smtp_check.c b/tests/smtp_check.c new file mode 100644 index 0000000..331f4a9 --- /dev/null +++ b/tests/smtp_check.c @@ -0,0 +1,663 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "smtp.h" +#include "util.h" + +enum { + SRV_NOAUTH = 0, + SRV_AUTH_PLAIN, + SRV_AUTH_LOGIN, + SRV_RCPT_REJECT, + SRV_TLS, + SRV_STARTTLS +}; + +struct srv_result { + int saw_ehlo; + int saw_auth; + int saw_auth_plain; + int saw_auth_login; + int saw_starttls; + size_t data_len; +}; + +static int failures; + +static void check(int cond, const char *what) +{ + if (!cond) { + printf("FAIL %s\n", what); + failures++; + } else { + printf("ok %s\n", what); + } +} + +static int contains(const unsigned char *data, size_t len, const char *needle) +{ + size_t n = strlen(needle); + if (n > len) + return 0; + for (size_t i = 0; i + n <= len; i++) + if (memcmp(data + i, needle, n) == 0) + return 1; + return 0; +} + +static int count_occ(const unsigned char *data, size_t len, const char *needle) +{ + size_t n = strlen(needle); + int c = 0; + if (n == 0 || n > len) + return 0; + for (size_t i = 0; i + n <= len; i++) + if (memcmp(data + i, needle, n) == 0) + c++; + return c; +} + +static ssize_t read_full(int fd, void *buf, size_t n) +{ + unsigned char *p = buf; + size_t got = 0; + while (got < n) { + ssize_t r = read(fd, p + got, n - got); + if (r < 0) { + if (errno == EINTR) + continue; + return -1; + } + if (r == 0) + break; + got += (size_t)r; + } + return (ssize_t)got; +} + +static int write_full(int fd, const void *buf, size_t n) +{ + const unsigned char *p = buf; + size_t done = 0; + while (done < n) { + ssize_t w = write(fd, p + done, n - done); + if (w < 0) { + if (errno == EINTR) + continue; + return -1; + } + done += (size_t)w; + } + return 0; +} + +struct srv_io { + int fd; + SSL *ssl; + unsigned char buf[4096]; + size_t len; + size_t pos; +}; + +static int srv_write_all(struct srv_io *io, const char *s) +{ + size_t n = strlen(s); + size_t o = 0; + while (o < n) { + ssize_t w = io->ssl ? SSL_write(io->ssl, s + o, (int)(n - o)) + : write(io->fd, s + o, n - o); + if (w <= 0) + return -1; + o += (size_t)w; + } + return 0; +} + +static ssize_t srv_line(struct srv_io *io, char *out, size_t outsz) +{ + size_t o = 0; + for (;;) { + if (io->pos >= io->len) { + ssize_t n = io->ssl ? SSL_read(io->ssl, io->buf, (int)sizeof io->buf) + : read(io->fd, io->buf, sizeof io->buf); + if (n <= 0) + return -1; + io->len = (size_t)n; + io->pos = 0; + } + unsigned char ch = io->buf[io->pos++]; + if (ch == '\n') { + while (o > 0 && out[o - 1] == '\r') + o--; + out[o] = '\0'; + return (ssize_t)o; + } + if (o + 1 < outsz) + out[o++] = (char)ch; + } +} + +static void srv_tls_accept(struct srv_io *io) +{ + SSL_CTX *ctx = SSL_CTX_new(TLS_server_method()); + if (!ctx || + SSL_CTX_use_certificate_chain_file(ctx, + "tests/tls_test_cert.pem") != 1 || + SSL_CTX_use_PrivateKey_file(ctx, "tests/tls_test_key.pem", + SSL_FILETYPE_PEM) != 1) + _exit(1); + SSL *ssl = SSL_new(ctx); + if (!ssl || SSL_set_fd(ssl, io->fd) != 1 || SSL_accept(ssl) != 1) + _exit(1); + io->ssl = ssl; +} + +static void srv_ehlo(struct srv_io *io, int mode, int after_tls) +{ + srv_write_all(io, "250-test.local hello\r\n"); + if (mode == SRV_AUTH_PLAIN) + srv_write_all(io, "250-AUTH PLAIN LOGIN\r\n"); + else if (mode == SRV_AUTH_LOGIN) + srv_write_all(io, "250-AUTH LOGIN\r\n"); + if (mode == SRV_STARTTLS && !after_tls) + srv_write_all(io, "250-STARTTLS\r\n"); + srv_write_all(io, "250 SIZE 10485760\r\n"); +} + +static int plain_ok(const char *b64) +{ + static const unsigned char want[] = { 0, 'm', 'a', 'i', 'l', + 'e', 'r', 0, 's', '3', + 'c', 'r', 'e', 't' }; + unsigned char *raw = NULL; + size_t len = 0; + if (util_b64_decode(b64, strlen(b64), &raw, &len) != 0) { + if (getenv("SMTP_CHECK_DUMP")) + fprintf(stderr, "server: AUTH PLAIN decode failed [%s]\n", b64); + return 0; + } + int ok = len == sizeof want && memcmp(raw, want, len) == 0; + if (!ok && getenv("SMTP_CHECK_DUMP")) + fprintf(stderr, "server: AUTH PLAIN mismatch len=%zu [%s]\n", len, + b64); + free(raw); + return ok; +} + +static void server_main(int wfd, int mode) +{ + int sfd = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0); + if (sfd < 0) + _exit(1); + struct sockaddr_in sa; + memset(&sa, 0, sizeof sa); + sa.sin_family = AF_INET; + sa.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + sa.sin_port = 0; + if (bind(sfd, (struct sockaddr *)&sa, sizeof sa) != 0 || + listen(sfd, 1) != 0) + _exit(1); + socklen_t slen = sizeof sa; + if (getsockname(sfd, (struct sockaddr *)&sa, &slen) != 0) + _exit(1); + int port = ntohs(sa.sin_port); + if (write_full(wfd, &port, sizeof port) != 0) + _exit(1); + int cfd = accept(sfd, NULL, NULL); + if (cfd < 0) + _exit(1); + close(sfd); + + struct srv_io io; + memset(&io, 0, sizeof io); + io.fd = cfd; + struct srv_result res; + memset(&res, 0, sizeof res); + struct buf data; + buf_init(&data); + + char *b64_user = util_b64((const unsigned char *)"mailer", 6); + char *b64_pass = util_b64((const unsigned char *)"s3cret", 6); + + if (mode == SRV_TLS) + srv_tls_accept(&io); + srv_write_all(&io, "220 test.local ESMTP\r\n"); + + int in_data = 0; + char line[2048]; + for (;;) { + ssize_t n = srv_line(&io, line, sizeof line); + if (n < 0) + break; + if (in_data) { + if (strcmp(line, ".") == 0) { + res.data_len = data.len; + in_data = 0; + if (srv_write_all(&io, "250 2.0.0 queued\r\n") != 0) + break; + continue; + } + const char *payload = line[0] == '.' ? line + 1 : line; + buf_append(&data, payload, strlen(payload)); + buf_append(&data, "\r\n", 2); + continue; + } + if (strncmp(line, "EHLO", 4) == 0) { + res.saw_ehlo++; + srv_ehlo(&io, mode, mode == SRV_TLS || io.ssl != NULL); + } else if (strcmp(line, "STARTTLS") == 0) { + res.saw_starttls = 1; + srv_write_all(&io, "220 2.0.0 ready\r\n"); + srv_tls_accept(&io); + } else if (strncmp(line, "AUTH PLAIN", 10) == 0) { + res.saw_auth = 1; + res.saw_auth_plain = 1; + const char *arg = line + 10; + while (*arg == ' ') + arg++; + srv_write_all(&io, plain_ok(arg) + ? "235 2.7.0 authenticated\r\n" + : "535 5.7.8 bad credentials\r\n"); + } else if (strcmp(line, "AUTH LOGIN") == 0) { + res.saw_auth = 1; + res.saw_auth_login = 1; + srv_write_all(&io, "334 VXNlcm5hbWU6\r\n"); + if (srv_line(&io, line, sizeof line) < 0) + _exit(1); + if (strcmp(line, b64_user) != 0) { + srv_write_all(&io, "535 5.7.8 bad credentials\r\n"); + continue; + } + srv_write_all(&io, "334 UGFzc3dvcmQ6\r\n"); + if (srv_line(&io, line, sizeof line) < 0) + _exit(1); + srv_write_all(&io, strcmp(line, b64_pass) == 0 + ? "235 2.7.0 authenticated\r\n" + : "535 5.7.8 bad credentials\r\n"); + } else if (strncmp(line, "MAIL FROM:", 10) == 0) { + srv_write_all(&io, "250 2.1.0 ok\r\n"); + } else if (strncmp(line, "RCPT TO:", 8) == 0) { + srv_write_all(&io, mode == SRV_RCPT_REJECT + ? "550 5.1.1 no such user\r\n" + : "250 2.1.5 ok\r\n"); + } else if (strcmp(line, "DATA") == 0) { + if (mode == SRV_RCPT_REJECT) { + srv_write_all(&io, "503 5.5.1 need RCPT\r\n"); + } else { + srv_write_all(&io, "354 end with .\r\n"); + in_data = 1; + } + } else if (strcmp(line, "QUIT") == 0) { + srv_write_all(&io, "221 2.0.0 bye\r\n"); + break; + } else { + srv_write_all(&io, "500 5.5.1 unknown command\r\n"); + } + } + + (void)write_full(wfd, &res, sizeof res); + if (data.len) + (void)write_full(wfd, data.p, data.len); + close(wfd); + close(cfd); + buf_free(&data); + free(b64_user); + free(b64_pass); + _exit(0); +} + +struct case_result { + int rc; + char err[512]; + struct srv_result res; + unsigned char data[131072]; + size_t data_len; + int server_ok; +}; + +static void run_case(int mode, struct smtp_message m, struct case_result *out) +{ + signal(SIGPIPE, SIG_IGN); + int pfd[2]; + memset(out, 0, sizeof *out); + if (pipe(pfd) != 0) { + printf("FAIL pipe\n"); + failures++; + return; + } + pid_t pid = fork(); + if (pid == 0) { + close(pfd[0]); + server_main(pfd[1], mode); + } + close(pfd[1]); + int port = 0; + if (read_full(pfd[0], &port, sizeof port) != (ssize_t)sizeof port) + port = 0; + m.port = port; + out->rc = smtp_send(&m, out->err, sizeof out->err); + + struct srv_result res; + memset(&res, 0, sizeof res); + if (read_full(pfd[0], &res, sizeof res) == (ssize_t)sizeof res) { + out->res = res; + size_t cap = res.data_len < sizeof out->data ? res.data_len + : sizeof out->data; + if (cap && read_full(pfd[0], out->data, cap) == (ssize_t)cap) + out->data_len = cap; + } + close(pfd[0]); + int status = 0; + waitpid(pid, &status, 0); + out->server_ok = WIFEXITED(status) && WEXITSTATUS(status) == 0; +} + +static void check_delivered(const char *what, const struct case_result *c) +{ + check(c->server_ok && c->rc == 0, what); + if (c->server_ok && c->rc == 0) + return; + if (!c->server_ok) + printf(" server aborted\n"); + if (c->rc != 0) + printf(" err: %s\n", c->err); +} + +static struct smtp_message base_message(void) +{ + struct smtp_message m; + memset(&m, 0, sizeof m); + m.host = "localhost"; + m.security = "plain"; + m.from = "invoice@example.test"; + m.from_name = "Bokf AB"; + m.to = "kund@example.test"; + m.subject = "Faktura 1001"; + m.body = "Hej!\nBifogad faktura.\n"; + return m; +} + +static const unsigned char pdf[] = + "%PDF-1.4\n1 0 obj\n<< /Type /Catalog >>\nendobj\ntrailer\n" + "<< /Root 1 0 R >>\n%%EOF\n"; + +static void flatten(const unsigned char *data, size_t len, + unsigned char **out, size_t *out_len) +{ + unsigned char *flat = malloc(len + 1); + size_t o = 0; + for (size_t i = 0; i < len; i++) + if (data[i] != '\r' && data[i] != '\n') + flat[o++] = data[i]; + flat[o] = '\0'; + *out = flat; + *out_len = o; +} + +static void check_crlf(const unsigned char *data, size_t len) +{ + int crlf = 1; + int max = 0; + size_t start = 0; + for (size_t i = 0; i < len; i++) { + if (data[i] != '\n') + continue; + size_t l = i - start; + if (l > 0 && data[i - 1] == '\r') + l--; + else + crlf = 0; + if (l > (size_t)max) + max = (int)l; + start = i + 1; + } + check(crlf, "message uses CRLF only"); + check(max > 0 && max < 998, "line lengths under 998"); +} + +static void test_multipart(void) +{ + struct smtp_message m = base_message(); + m.from_name = "Bokf \xc3\x84rende"; + m.subject = "Faktura p\xc3\xa5 svenska"; + m.attach_name = "faktura.pdf"; + m.attach = pdf; + m.attach_len = sizeof pdf - 1; + + struct case_result c; + run_case(SRV_NOAUTH, m, &c); + check_delivered("multipart: delivered", &c); + check(c.res.saw_ehlo == 1, "multipart: one EHLO"); + check(!c.res.saw_auth, "multipart: no AUTH when user empty"); + + const unsigned char *d = c.data; + size_t n = c.data_len; + if (getenv("SMTP_CHECK_DUMP")) + printf("--- message ---\n%.*s\n---\n", (int)n, d); + char *name_word = NULL; + char *subj_word = NULL; + { + char *b = util_b64((const unsigned char *)m.from_name, + strlen(m.from_name)); + size_t l = strlen(b) + 13; + name_word = malloc(l); + snprintf(name_word, l, "=?UTF-8?B?%s?=", b); + free(b); + b = util_b64((const unsigned char *)m.subject, strlen(m.subject)); + l = strlen(b) + 13; + subj_word = malloc(l); + snprintf(subj_word, l, "=?UTF-8?B?%s?=", b); + free(b); + } + char from_hdr[256]; + snprintf(from_hdr, sizeof from_hdr, "From: %s ", + name_word); + char subj_hdr[256]; + snprintf(subj_hdr, sizeof subj_hdr, "Subject: %s", subj_word); + + check(contains(d, n, "MIME-Version: 1.0\r\n"), "multipart: MIME-Version"); + check(contains(d, n, from_hdr), "multipart: From with B-encoded name"); + check(contains(d, n, "To: \r\n"), "multipart: To"); + check(contains(d, n, subj_hdr), "multipart: B-encoded subject"); + check(contains(d, n, "Date: ") && contains(d, n, "+0000\r\n"), + "multipart: UTC Date"); + check(contains(d, n, "Message-ID: <"), "multipart: Message-ID"); + check(contains(d, n, "Content-Type: multipart/mixed; boundary=\""), + "multipart: multipart content type"); + check(count_occ(d, n, "Content-Type: text/plain; charset=utf-8") == 1, + "multipart: text part"); + check(contains(d, n, "Content-Type: application/pdf; name=\"faktura.pdf\""), + "multipart: attachment type and name"); + check(contains(d, n, + "Content-Disposition: attachment; filename=\"faktura.pdf\""), + "multipart: attachment disposition"); + check(count_occ(d, n, "Content-Transfer-Encoding: base64") == 2, + "multipart: two base64 parts"); + + unsigned char *flat = NULL; + size_t flat_len = 0; + flatten(d, n, &flat, &flat_len); + char *b64 = util_b64(pdf, sizeof pdf - 1); + check(contains(flat, flat_len, b64), "multipart: attachment base64"); + free(b64); + b64 = util_b64((const unsigned char *)m.body, strlen(m.body)); + check(contains(flat, flat_len, b64), "multipart: body base64"); + free(b64); + free(flat); + + check(count_occ(d, n, "--=_bokf_") == 3, "multipart: boundary delimiters"); + check_crlf(d, n); + free(name_word); + free(subj_word); +} + +static void test_text_only(void) +{ + struct smtp_message m = base_message(); + struct case_result c; + run_case(SRV_NOAUTH, m, &c); + check_delivered("text-only: delivered", &c); + const unsigned char *d = c.data; + size_t n = c.data_len; + check(contains(d, n, "From: Bokf AB \r\n"), + "text-only: plain display name"); + check(contains(d, n, "Subject: Faktura 1001\r\n"), + "text-only: plain subject"); + check(contains(d, n, "Content-Type: text/plain; charset=utf-8\r\n"), + "text-only: text content type"); + check(!contains(d, n, "multipart/mixed"), "text-only: not multipart"); + check_crlf(d, n); +} + +static void test_rcpt_reject(void) +{ + struct smtp_message m = base_message(); + m.attach_name = "faktura.pdf"; + m.attach = pdf; + m.attach_len = sizeof pdf - 1; + struct case_result c; + run_case(SRV_RCPT_REJECT, m, &c); + check(c.server_ok, "reject: server completed session"); + check(c.rc == -1, "reject: smtp_send failed"); + check(strstr(c.err, "550") != NULL, "reject: err has 550"); + check(strstr(c.err, "no such user") != NULL, "reject: err has server text"); + check(!contains(c.data, c.data_len, "faktura"), + "reject: no DATA delivered"); +} + +static void test_auth_plain(void) +{ + struct smtp_message m = base_message(); + m.user = "mailer"; + m.password = "s3cret"; + struct case_result c; + run_case(SRV_AUTH_PLAIN, m, &c); + check_delivered("auth plain: delivered", &c); + check(c.res.saw_auth_plain == 1, "auth plain: PLAIN used"); + check(c.res.saw_auth_login == 0, "auth plain: LOGIN not used"); + check(!contains(c.data, c.data_len, "s3cret"), + "auth plain: no secret in message"); +} + +static void test_auth_login(void) +{ + struct smtp_message m = base_message(); + m.user = "mailer"; + m.password = "s3cret"; + struct case_result c; + run_case(SRV_AUTH_LOGIN, m, &c); + check_delivered("auth login: delivered", &c); + check(c.res.saw_auth_login == 1, "auth login: LOGIN used"); + check(c.res.saw_auth_plain == 0, "auth login: PLAIN not used"); +} + +static void test_auth_reject(void) +{ + struct smtp_message m = base_message(); + m.user = "mailer"; + m.password = "wrong"; + struct case_result c; + run_case(SRV_AUTH_PLAIN, m, &c); + check(c.rc == -1, "auth reject: smtp_send failed"); + check(strstr(c.err, "535") != NULL, "auth reject: err has 535"); + check(strstr(c.err, "s3cret") == NULL, "auth reject: no secret in err"); +} + +static void test_validation(void) +{ + char err[256]; + check(smtp_send(NULL, err, sizeof err) == -1, "validation: NULL message"); + + struct smtp_message m = base_message(); + m.port = 25; + m.security = "bogus"; + check(smtp_send(&m, err, sizeof err) == -1, "validation: bad security"); + check(strstr(err, "security") != NULL, "validation: bad security text"); + + m = base_message(); + m.from = ""; + check(smtp_send(&m, err, sizeof err) == -1, "validation: empty from"); + + m = base_message(); + m.host = NULL; + check(smtp_send(&m, err, sizeof err) == -1, "validation: NULL host"); +} + +static int tls_available(void) +{ + return access("tests/tls_test_cert.pem", R_OK) == 0 && + access("tests/tls_test_key.pem", R_OK) == 0; +} + +static void with_test_ca(void (*fn)(struct smtp_message *, struct case_result *)) +{ + char *saved = getenv("SSL_CERT_FILE"); + char *copy = saved ? strdup(saved) : NULL; + setenv("SSL_CERT_FILE", "tests/tls_test_cert.pem", 1); + struct smtp_message m = base_message(); + struct case_result c; + fn(&m, &c); + check_delivered("TLS case delivered", &c); + if (saved) + setenv("SSL_CERT_FILE", copy, 1); + else + unsetenv("SSL_CERT_FILE"); + free(copy); +} + +static void tls_case(struct smtp_message *m, struct case_result *c) +{ + m->security = "tls"; + m->attach_name = "faktura.pdf"; + m->attach = pdf; + m->attach_len = sizeof pdf - 1; + run_case(SRV_TLS, *m, c); +} + +static void starttls_case(struct smtp_message *m, struct case_result *c) +{ + m->security = "starttls"; + m->attach_name = "faktura.pdf"; + m->attach = pdf; + m->attach_len = sizeof pdf - 1; + run_case(SRV_STARTTLS, *m, c); + check(c->res.saw_starttls == 1, "starttls: STARTTLS sent"); + check(c->res.saw_ehlo == 2, "starttls: EHLO before and after"); +} + +int main(void) +{ + signal(SIGPIPE, SIG_IGN); + alarm(120); + + test_multipart(); + test_text_only(); + test_rcpt_reject(); + test_auth_plain(); + test_auth_login(); + test_auth_reject(); + test_validation(); + if (tls_available()) { + with_test_ca(tls_case); + with_test_ca(starttls_case); + } else { + printf("skip TLS cases: tests/tls_test_cert.pem missing\n"); + } + + if (failures) { + printf("%d failures\n", failures); + return 1; + } + puts("all ok"); + return 0; +} -- cgit v1.3