diff options
| author | Anders Betts <anders.betts@gmail.com> | 2026-09-20 15:50:24 +0200 |
|---|---|---|
| committer | Anders Betts <anders.betts@gmail.com> | 2026-09-20 15:50:42 +0200 |
| commit | 5e624647d2f2750cf1fe7f97d8c89718d187879d (patch) | |
| tree | 6909b8ddc7ee6a1843ecab064fbf8b96e662eaa0 /src | |
| parent | fb2addedce94bea29c9be5eab2786e621a5d0e65 (diff) | |
| download | bokf-5e624647d2f2750cf1fe7f97d8c89718d187879d.tar.gz bokf-5e624647d2f2750cf1fe7f97d8c89718d187879d.zip | |
smtp: send messages with a PDF attachment
Diffstat (limited to 'src')
| -rw-r--r-- | src/smtp.c | 951 | ||||
| -rw-r--r-- | src/smtp.h | 24 |
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 |
