aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorAnders Betts <anders.betts@gmail.com>2026-09-20 15:50:24 +0200
committerAnders Betts <anders.betts@gmail.com>2026-09-20 15:50:42 +0200
commit5e624647d2f2750cf1fe7f97d8c89718d187879d (patch)
tree6909b8ddc7ee6a1843ecab064fbf8b96e662eaa0 /src
parentfb2addedce94bea29c9be5eab2786e621a5d0e65 (diff)
downloadbokf-5e624647d2f2750cf1fe7f97d8c89718d187879d.tar.gz
bokf-5e624647d2f2750cf1fe7f97d8c89718d187879d.zip
smtp: send messages with a PDF attachment
Diffstat (limited to 'src')
-rw-r--r--src/smtp.c951
-rw-r--r--src/smtp.h24
2 files changed, 975 insertions, 0 deletions
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 <ctype.h>
+#include <errno.h>
+#include <limits.h>
+#include <netdb.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <strings.h>
+#include <sys/socket.h>
+#include <sys/time.h>
+#include <time.h>
+#include <unistd.h>
+
+#include <openssl/err.h>
+#include <openssl/ssl.h>
+#include <openssl/x509.h>
+
+#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 <stddef.h>
+
+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