DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
smtp.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file smtp.cpp
6 * @brief Outbound SMTP client (RFC 5321) - implementation. See smtp.h for the model.
7 *
8 * smtp_run() is the pure dialogue engine (host-testable via the send/recv seam);
9 * smtp_send() binds it to the real transport on Arduino (det_client, +det_tls csess).
10 */
11
12#include "services/smtp/smtp.h"
13#include "ServerConfig.h"
14
15#if DETWS_ENABLE_SMTP
16
18#include <stdio.h> // snprintf
19#include <string.h> // strlen, memcmp
20
21namespace
22{
23// Send an entire C string; returns true only if every byte went out.
24bool send_str(SmtpSendFn send, void *ctx, const char *s)
25{
26 size_t n = strnlen(s, DETWS_SMTP_LINE_MAX + 1);
27 return n == 0 || send(ctx, (const uint8_t *)s, n) == (int)n;
28}
29
30// Is buf[0..len) a complete SMTP reply? A reply is one or more CRLF lines that share a
31// 3-digit code; the FINAL line has a space (or nothing) after the code, continuation
32// lines have '-'. On a complete reply, set *code to the 3-digit value and return true.
33bool reply_complete(const char *buf, size_t len, int *code)
34{
35 size_t start = 0;
36 for (size_t i = 0; i + 1 < len; i++)
37 {
38 if (buf[i] != '\r' || buf[i + 1] != '\n')
39 continue;
40 size_t line_len = i - start; // excludes the CRLF
41 if (line_len >= 3 && buf[start] >= '0' && buf[start] <= '9' && buf[start + 1] >= '0' && buf[start + 1] <= '9' &&
42 buf[start + 2] >= '0' && buf[start + 2] <= '9')
43 {
44 bool final_line = (line_len == 3) || buf[start + 3] == ' ';
45 if (final_line)
46 {
47 *code = (buf[start] - '0') * 100 + (buf[start + 1] - '0') * 10 + (buf[start + 2] - '0');
48 return true;
49 }
50 }
51 start = i + 2; // next line begins after the CRLF
52 }
53 return false; // no final line yet - need more bytes
54}
55
56// Read one (possibly multi-line) reply into a stack buffer and return its code.
57SmtpResult read_reply(SmtpRecvFn recv, void *ctx, int *code)
58{
59 char buf[DETWS_SMTP_REPLY_MAX];
60 size_t len = 0;
61 for (;;)
62 {
63 if (reply_complete(buf, len, code))
65 if (len >= sizeof(buf))
67 int n = recv(ctx, (uint8_t *)buf + len, sizeof(buf) - len);
68 if (n <= 0)
70 len += (size_t)n;
71 }
72}
73
74// Send one command line (already CRLF-terminated) and return the reply code, or a
75// negative ::SmtpResult on an I/O failure.
76int command(SmtpSendFn send, SmtpRecvFn recv, void *ctx, const char *line)
77{
78 if (!send_str(send, ctx, line))
79 return (int)SmtpResult::SMTP_ERR_IO;
80 int code = 0;
81 SmtpResult r = read_reply(recv, ctx, &code);
82 return (r == SmtpResult::SMTP_OK) ? code : (int)r;
83}
84
85// AUTH LOGIN leg: send @p secret base64-encoded + CRLF, return the reply code.
86int auth_send_b64(SmtpSendFn send, SmtpRecvFn recv, void *ctx, const char *secret)
87{
88 char line[DETWS_SMTP_LINE_MAX];
89 char b64[DETWS_SMTP_LINE_MAX];
90 size_t slen = strnlen(secret, sizeof(b64));
91 if (((slen + 2) / 3) * 4 + 3 >= sizeof(b64)) // b64 + CRLF must fit
93 base64_encode((const uint8_t *)secret, slen, b64);
94 int n = snprintf(line, sizeof(line), "%s\r\n", b64);
95 if (n < 0 || (size_t)n >= sizeof(line))
96 return (int)SmtpResult::SMTP_ERR_OVERFLOW; // GCOVR_EXCL_LINE b64+CRLF was just checked to fit
97 // sizeof(b64)==sizeof(line); can't overflow
98 return command(send, recv, ctx, line);
99}
100
101// Assemble the DATA payload (headers + body + terminating dot) into @p out, applying
102// CRLF normalization and RFC 5321 sec 4.5.2 dot-stuffing. Returns the length, or <0.
103int build_message(char *out, size_t cap, const SmtpConfig *cfg, const SmtpMessage *msg)
104{
105 int hn = snprintf(out, cap,
106 "From: <%s>\r\n"
107 "To: <%s>\r\n"
108 "Subject: %s\r\n"
109 "MIME-Version: 1.0\r\n"
110 "Content-Type: text/plain; charset=UTF-8\r\n"
111 "\r\n",
112 cfg->from, msg->to, msg->subject ? msg->subject : "");
113 if (hn < 0 || (size_t)hn >= cap)
115 size_t n = (size_t)hn;
116
117 const char *b = msg->body ? msg->body : "";
118 bool at_line_start = true;
119 for (size_t i = 0; b[i]; i++)
120 {
121 char c = b[i];
122 if (c == '\r')
123 continue; // normalize: CR is dropped, LF becomes CRLF
124 if (c == '\n')
125 {
126 if (n + 2 > cap)
128 out[n++] = '\r';
129 out[n++] = '\n';
130 at_line_start = true;
131 continue;
132 }
133 if (at_line_start && c == '.')
134 {
135 if (n + 1 > cap) // dot-stuff: a body line starting with '.' gets an extra '.'
137 out[n++] = '.';
138 }
139 if (n + 1 > cap)
141 out[n++] = c;
142 at_line_start = false;
143 }
144 // Body must end with CRLF before the terminator.
145 if (!(n >= 2 && out[n - 2] == '\r' && out[n - 1] == '\n'))
146 {
147 if (n + 2 > cap)
149 out[n++] = '\r';
150 out[n++] = '\n';
151 }
152 if (n + 3 > cap) // terminating "."CRLF
154 out[n++] = '.';
155 out[n++] = '\r';
156 out[n++] = '\n';
157 return (int)n;
158}
159} // namespace
160
161SmtpResult smtp_run(const SmtpConfig *cfg, const SmtpMessage *msg, SmtpSendFn send, SmtpRecvFn recv, void *ctx)
162{
163 if (!cfg || !msg || !send || !recv || !cfg->host || !cfg->from || !cfg->from[0] || !msg->to || !msg->to[0])
165
166 char line[DETWS_SMTP_LINE_MAX];
167 int code;
168
169 // Greeting.
170 if (read_reply(recv, ctx, &code) != SmtpResult::SMTP_OK)
172 if (code != 220)
174
175 // EHLO.
176 int n = snprintf(line, sizeof(line), "EHLO %s\r\n", (cfg->helo && cfg->helo[0]) ? cfg->helo : "esp32");
177 if (n < 0 || (size_t)n >= sizeof(line))
179 code = command(send, recv, ctx, line);
180 if (code < 0)
181 return (SmtpResult)code;
182 if (code != 250)
184
185 // AUTH LOGIN (only when a username is configured).
186 if (cfg->user && cfg->user[0])
187 {
188 code = command(send, recv, ctx, "AUTH LOGIN\r\n");
189 if (code < 0)
190 return (SmtpResult)code;
191 if (code != 334)
193 code = auth_send_b64(send, recv, ctx, cfg->user);
194 if (code < 0)
195 return (SmtpResult)code;
196 if (code != 334)
198 code = auth_send_b64(send, recv, ctx, cfg->pass ? cfg->pass : "");
199 if (code < 0)
200 return (SmtpResult)code;
201 if (code != 235)
203 }
204
205 // MAIL FROM.
206 n = snprintf(line, sizeof(line), "MAIL FROM:<%s>\r\n", cfg->from);
207 if (n < 0 || (size_t)n >= sizeof(line))
209 code = command(send, recv, ctx, line);
210 if (code < 0)
211 return (SmtpResult)code;
212 if (code != 250)
214
215 // RCPT TO.
216 n = snprintf(line, sizeof(line), "RCPT TO:<%s>\r\n", msg->to);
217 if (n < 0 || (size_t)n >= sizeof(line))
219 code = command(send, recv, ctx, line);
220 if (code < 0)
221 return (SmtpResult)code;
222 if (code != 250 && code != 251) // 251 = user not local; will forward
224
225 // DATA.
226 code = command(send, recv, ctx, "DATA\r\n");
227 if (code < 0)
228 return (SmtpResult)code;
229 if (code != 354)
231
232 // The message itself, then wait for acceptance.
233 char body[DETWS_SMTP_MSG_MAX];
234 int mlen = build_message(body, sizeof(body), cfg, msg);
235 if (mlen < 0)
236 return (SmtpResult)mlen;
237 if (send(ctx, (const uint8_t *)body, (size_t)mlen) != mlen)
239 if (read_reply(recv, ctx, &code) != SmtpResult::SMTP_OK)
241 if (code != 250)
243
244 // QUIT is best-effort - the message is already accepted.
245 (void)command(send, recv, ctx, "QUIT\r\n");
246 return SmtpResult::SMTP_OK;
247}
248
249// ---------------------------------------------------------------------------
250// Real-transport binding (Arduino): det_client, plus a det_tls csess for SMTPS.
251// ---------------------------------------------------------------------------
252
253#if defined(ARDUINO)
254
256#include <Arduino.h> // millis, delay
257#if DETWS_ENABLE_TLS
259#endif
260
261namespace
262{
263struct SmtpXport
264{
265 int cid;
266 uint32_t deadline;
267};
268
269// Plaintext seam over det_client.
270int cl_send(void *ctx, const uint8_t *data, size_t len)
271{
272 SmtpXport *x = (SmtpXport *)ctx;
273 size_t sent = 0;
274 while (sent < len)
275 {
276 size_t chunk = len - sent;
277 if (chunk > 0xFFFF)
278 chunk = 0xFFFF;
279 if (!det_client_send(x->cid, data + sent, chunk))
280 return -1;
281 sent += chunk;
282 }
283 return (int)len;
284}
285int cl_recv(void *ctx, uint8_t *buf, size_t cap)
286{
287 SmtpXport *x = (SmtpXport *)ctx;
288 while ((int32_t)(x->deadline - millis()) > 0)
289 {
290 size_t n = det_client_read(x->cid, buf, cap);
291 if (n > 0)
292 return (int)n;
293 if (det_client_is_closed(x->cid) && det_client_available(x->cid) == 0)
294 return -1;
295 delay(5);
296 }
297 return -1; // timeout
298}
299
300#if DETWS_ENABLE_TLS
301// TLS ciphertext BIO: the csess handshake/records read/write the wire via det_client.
302int tls_bio_send(void *ctx, const unsigned char *buf, size_t len)
303{
304 SmtpXport *x = (SmtpXport *)ctx;
305 return det_client_send(x->cid, buf, len) ? (int)len : MBEDTLS_ERR_SSL_WANT_WRITE;
306}
307int tls_bio_recv(void *ctx, unsigned char *buf, size_t len)
308{
309 SmtpXport *x = (SmtpXport *)ctx;
310 size_t n = det_client_read(x->cid, buf, len);
311 if (n == 0)
312 return det_client_is_closed(x->cid) ? 0 : MBEDTLS_ERR_SSL_WANT_READ;
313 return (int)n;
314}
315// Application seam over the established TLS session.
316int tls_send(void *ctx, const uint8_t *data, size_t len)
317{
318 (void)ctx;
319 return det_tls_csess_write(data, len) == (int)len ? (int)len : -1;
320}
321int tls_recv(void *ctx, uint8_t *buf, size_t cap)
322{
323 SmtpXport *x = (SmtpXport *)ctx;
324 while ((int32_t)(x->deadline - millis()) > 0)
325 {
326 int n = det_tls_csess_read(buf, cap);
327 if (n > 0)
328 return n;
329 if (n < 0 && n != MBEDTLS_ERR_SSL_WANT_READ && n != MBEDTLS_ERR_SSL_WANT_WRITE)
330 return -1;
331 delay(5);
332 }
333 return -1; // timeout
334}
335#endif // DETWS_ENABLE_TLS
336} // namespace
337
338SmtpResult smtp_send(const SmtpConfig *cfg, const SmtpMessage *msg)
339{
340 if (!cfg || !cfg->host)
342
343 SmtpXport x;
344 x.cid = det_client_open(cfg->host, cfg->port, DETWS_SMTP_TIMEOUT_MS);
345 if (x.cid < 0)
347 x.deadline = millis() + DETWS_SMTP_TIMEOUT_MS;
348
349 SmtpResult rc;
350 if (cfg->tls)
351 {
352#if DETWS_ENABLE_TLS
353 if (!det_tls_csess_begin(cfg->host, tls_bio_send, tls_bio_recv))
354 {
355 det_client_close(x.cid);
357 }
358 int h;
359 while ((h = det_tls_csess_handshake()) == 0 && (int32_t)(x.deadline - millis()) > 0)
360 delay(5);
361 if (h != 1) // 1 = established; 0 = still pending at timeout; <0 = fatal
362 {
363 det_tls_csess_end();
364 det_client_close(x.cid);
366 }
367 rc = smtp_run(cfg, msg, tls_send, tls_recv, &x);
368 det_tls_csess_end();
369#else
370 det_client_close(x.cid);
371 return SmtpResult::SMTP_ERR_TLS; // SMTPS requested but TLS not built in
372#endif
373 }
374 else
375 {
376 rc = smtp_run(cfg, msg, cl_send, cl_recv, &x);
377 }
378
379 det_client_close(x.cid);
380 return rc;
381}
382
383#else // host build: no lwIP. smtp_run() above is host-testable; smtp_send() is a stub.
384
386{
388}
389
390#endif // ARDUINO
391
392#endif // DETWS_ENABLE_SMTP
User-facing configuration for DeterministicESPAsyncWebServer.
#define DETWS_SMTP_TIMEOUT_MS
SMTP connect / per-reply timeout in milliseconds.
#define DETWS_SMTP_REPLY_MAX
Max size of one (possibly multi-line) server reply held while parsing, bytes.
#define DETWS_SMTP_LINE_MAX
Max length of one SMTP command / address line (bytes, incl. CRLF).
#define DETWS_SMTP_MSG_MAX
Max size of the assembled DATA payload (headers + dot-stuffed body), bytes.
void base64_encode(const uint8_t *src, size_t src_len, char *dst)
Encode src_len bytes of src as Base64.
Definition base64.cpp:26
Base64 encoder/decoder.
size_t det_client_available(int)
Wire bytes currently buffered and ready to read.
Definition client.cpp:326
bool det_client_is_closed(int)
True once the peer closed (FIN) or the connection errored.
Definition client.cpp:318
int det_client_open(const char *, uint16_t, uint32_t)
Resolve host (dotted-quad fast path, else DNS) and connect to host : port, blocking up to timeout_ms.
Definition client.cpp:310
size_t det_client_read(int, uint8_t *, size_t)
Drain up to cap buffered wire bytes into buf; returns the count.
Definition client.cpp:330
void det_client_close(int)
Tear down the connection (marshaled) and return the slot to the pool.
Definition client.cpp:334
bool det_client_send(int, const void *, size_t)
Queue len wire bytes for transmission (marshaled tcp_write + output).
Definition client.cpp:322
Layer 4 outbound TCP client transport - the client-side peer of the (server) transport in tcp....
Outbound SMTP client (RFC 5321) - send a device email alert.
SmtpResult smtp_send(const SmtpConfig *cfg, const SmtpMessage *msg)
Blocking one-shot send over the real transport (det_client, plus TLS when cfg->tls)....
int(* SmtpSendFn)(void *ctx, const uint8_t *data, size_t len)
Transport seam for smtp_run(): the engine sends and receives raw bytes only through these,...
Definition smtp.h:51
SmtpResult
Result of an SMTP send. 0 is success; every failure is a distinct negative code.
Definition smtp.h:33
@ SMTP_ERR_IO
a send/recv failed or the reply timed out
@ SMTP_ERR_ARG
a required field (host / from / to) was null or empty
@ SMTP_ERR_AUTH
AUTH was rejected (bad user/password)
@ SMTP_ERR_OVERFLOW
a command line or the message exceeded its fixed buffer
@ SMTP_ERR_TLS
the TLS handshake failed (SMTPS)
@ SMTP_ERR_PROTOCOL
the server returned an unexpected reply code
@ SMTP_ERR_CONNECT
could not open the transport (DNS / connect)
int(* SmtpRecvFn)(void *ctx, uint8_t *buf, size_t cap)
Definition smtp.h:52
SmtpResult smtp_run(const SmtpConfig *cfg, const SmtpMessage *msg, SmtpSendFn send, SmtpRecvFn recv, void *ctx)
Drive the full SMTP exchange over send / recv. Pure - no lwIP or TLS - so it is host-testable with a ...
Server address + credentials for one send. Addresses are bare (no angle brackets).
Definition smtp.h:56
const char * user
AUTH LOGIN username (null or empty => skip AUTH)
Definition smtp.h:60
bool tls
true = implicit TLS on connect (SMTPS); false = plaintext
Definition smtp.h:59
const char * pass
AUTH LOGIN password.
Definition smtp.h:61
const char * from
envelope sender + From: header address
Definition smtp.h:62
uint16_t port
25 / 587 / 465
Definition smtp.h:58
const char * host
server hostname (also the TLS SNI name)
Definition smtp.h:57
const char * helo
EHLO domain to announce (null => "esp32")
Definition smtp.h:63
One plain-text message.
Definition smtp.h:68
const char * body
plain-text UTF-8 body; LF or CRLF line ends, dot-stuffed for you
Definition smtp.h:71
const char * to
single recipient address (envelope + To: header)
Definition smtp.h:69
const char * subject
Subject: header (null => empty)
Definition smtp.h:70
Deterministic TLS engine: mbedTLS over a static memory pool (DETWS_ENABLE_TLS).