ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
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 (pc_client, +pc_tls csess).
10 */
11
13#include "protocore_config.h"
14#include "services/system/clock.h" // pcdelay
15#include "shared_primitives/strbuf.h" // pc_sb frame builder
16
17#if PC_ENABLE_SMTP
18
20#include <stdio.h> // snprintf
21#include <string.h> // strlen, memcmp
22
23#if defined(ARDUINO)
25#include <Arduino.h> // millis, delay
26#endif
27#if defined(ARDUINO) && PC_ENABLE_SMTP_TLS
29#include <mbedtls/ssl.h> // MBEDTLS_ERR_SSL_WANT_* for the BIO callbacks
30#endif
31namespace
32{
33// Send an entire C string; returns true only if every byte went out.
34bool send_str(SmtpSendFn send, void *ctx, const char *s)
35{
36 size_t n = strnlen(s, PC_SMTP_LINE_MAX + 1);
37 // Every caller passes a CRLF-terminated command - a string literal, or a snprintf'd line with a
38 // fixed non-empty prefix - so n is never 0; the check just keeps send_str total for any string.
39 return n == 0 || send(ctx, (const uint8_t *)s, n) == (int)n; // GCOVR_EXCL_LINE n == 0 unreachable
40}
41
42// Is buf[0..len) a complete SMTP reply? A reply is one or more CRLF lines that share a
43// 3-digit code; the FINAL line has a space (or nothing) after the code, continuation
44// lines have '-'. On a complete reply, set *code to the 3-digit value and return true.
45bool reply_complete(const char *buf, size_t len, int *code)
46{
47 size_t start = 0;
48 for (size_t i = 0; i + 1 < len; i++)
49 {
50 if (buf[i] != '\r' || buf[i + 1] != '\n')
51 {
52 continue;
53 }
54 size_t line_len = i - start; // excludes the CRLF
55 if (line_len >= 3 && buf[start] >= '0' && buf[start] <= '9' && buf[start + 1] >= '0' && buf[start + 1] <= '9' &&
56 buf[start + 2] >= '0' && buf[start + 2] <= '9')
57 {
58 bool final_line = (line_len == 3) || buf[start + 3] == ' ';
59 if (final_line)
60 {
61 *code = (buf[start] - '0') * 100 + (buf[start + 1] - '0') * 10 + (buf[start + 2] - '0');
62 return true;
63 }
64 }
65 start = i + 2; // next line begins after the CRLF
66 }
67 return false; // no final line yet - need more bytes
68}
69
70// Case-insensitive compare of @p n bytes. EHLO keywords are case-insensitive (RFC 5321 sec 2.4)
71// and strncasecmp is not portable across every toolchain this builds under.
72bool ieq(const char *a, const char *b, size_t n)
73{
74 for (size_t i = 0; i < n; i++)
75 {
76 char ca = a[i];
77 char cb = b[i];
78 if (ca >= 'A' && ca <= 'Z')
79 {
80 ca = (char)(ca - 'A' + 'a');
81 }
82 // b is always the caller's `want`, and reply_has_cap's only call site passes the literal
83 // "STARTTLS", so cb is always an upper-case letter here. Folding it keeps ieq symmetric.
84 if (cb >= 'A' && cb <= 'Z') // GCOVR_EXCL_LINE cb is always 'A'..'Z' (see above)
85 {
86 cb = (char)(cb - 'A' + 'a');
87 }
88 if (ca != cb)
89 {
90 return false;
91 }
92 }
93 return true;
94}
95
96// Does @p want appear as its own EHLO capability line? Each line is "NNN<sep>KEYWORD[ params]",
97// so the keyword starts at offset 4 and is matched whole - a server advertising "STARTTLSX" must
98// not read as one advertising STARTTLS, since that decides whether credentials go out in clear.
99bool reply_has_cap(const char *buf, size_t len, const char *want)
100{
101 size_t wlen = strnlen(want, len + 1); // a whole capability keyword cannot exceed the reply
102 size_t start = 0;
103 for (size_t i = 0; i + 1 < len; i++)
104 {
105 if (buf[i] != '\r' || buf[i + 1] != '\n')
106 {
107 continue;
108 }
109 size_t line_len = i - start; // excludes the CRLF
110 if (line_len > 4) // "NNN" + separator + at least one keyword character
111 {
112 const char *kw = buf + start + 4;
113 size_t klen = line_len - 4;
114 if (klen >= wlen && ieq(kw, want, wlen) && (klen == wlen || kw[wlen] == ' '))
115 {
116 return true;
117 }
118 }
119 start = i + 2;
120 }
121 return false;
122}
123
124// Read one (possibly multi-line) reply and return its code. When @p want is given, @p found
125// reports whether that capability appeared in the reply.
126SmtpResult read_reply_cap(SmtpRecvFn recv, void *ctx, int *code, const char *want, bool *found)
127{
128 char buf[PC_SMTP_REPLY_MAX];
129 size_t len = 0;
130 for (;;)
131 {
132 if (reply_complete(buf, len, code))
133 {
134 // The two call sites pass want and found together (read_reply passes neither,
135 // greet_ehlo passes both), so the pair is never half-populated.
136 if (want && found) // GCOVR_EXCL_LINE want and found are always both set or both null
137 {
138 *found = reply_has_cap(buf, len, want);
139 }
140 return SmtpResult::SMTP_OK;
141 }
142 if (len >= sizeof(buf))
143 {
145 }
146 int n = recv(ctx, (uint8_t *)buf + len, sizeof(buf) - len);
147 if (n <= 0)
148 {
150 }
151 len += (size_t)n;
152 }
153}
154
155SmtpResult read_reply(SmtpRecvFn recv, void *ctx, int *code)
156{
157 return read_reply_cap(recv, ctx, code, nullptr, nullptr);
158}
159
160// Send one command line (already CRLF-terminated) and return the reply code, or a
161// negative ::SmtpResult on an I/O failure.
162int command(SmtpSendFn send, SmtpRecvFn recv, void *ctx, const char *line)
163{
164 if (!send_str(send, ctx, line))
165 {
166 return (int)SmtpResult::SMTP_ERR_IO;
167 }
168 int code = 0;
169 SmtpResult r = read_reply(recv, ctx, &code);
170 return (r == SmtpResult::SMTP_OK) ? code : (int)r;
171}
172
173// AUTH LOGIN leg: send @p secret base64-encoded + CRLF, return the reply code.
174int auth_send_b64(SmtpSendFn send, SmtpRecvFn recv, void *ctx, const char *secret)
175{
176 char line[PC_SMTP_LINE_MAX];
177 char b64[PC_SMTP_LINE_MAX];
178 size_t slen = strnlen(secret, sizeof(b64));
179 if (((slen + 2) / 3) * 4 + 3 >= sizeof(b64)) // b64 + CRLF must fit
180 {
182 }
183 pc_base64_encode((const uint8_t *)secret, slen, b64);
184 pc_sb sb_line = {line, sizeof(line), 0, true};
185 pc_sb_put(&sb_line, b64);
186 pc_sb_put(&sb_line, "\r\n");
187 pc_sb_finish(&sb_line);
188 // GCOVR_EXCL_BR_START cannot fire: b64+CRLF was checked to fit above and sizeof(b64) == sizeof(line)
189 if (!sb_line.ok)
190 {
192 }
193 // GCOVR_EXCL_BR_STOP
194 return command(send, recv, ctx, line);
195}
196
197// Assemble the DATA payload (headers + body + terminating dot) into @p out, applying
198// CRLF normalization and RFC 5321 sec 4.5.2 dot-stuffing. Returns the length, or <0.
199int build_message(char *out, size_t cap, const SmtpConfig *cfg, const SmtpMessage *msg)
200{
201 pc_sb sb_out = {out, cap, 0, true};
202 pc_sb_put(&sb_out, "From: <");
203 pc_sb_put(&sb_out, cfg->from);
204 pc_sb_put(&sb_out, ">\r\nTo: <");
205 pc_sb_put(&sb_out, msg->to);
206 pc_sb_put(&sb_out, ">\r\nSubject: ");
207 pc_sb_put(&sb_out, msg->subject ? msg->subject : "");
208 pc_sb_put(&sb_out, "\r\nMIME-Version: 1.0\r\nContent-Type: text/plain; charset=UTF-8\r\n\r\n");
209 int hn = (int)pc_sb_finish(&sb_out);
210 // hn < 0 is unreachable: snprintf only reports failure on an output/encoding error, which
211 // formatting %s into a caller buffer cannot produce. The >= cap truncation check is live.
212 if (!sb_out.ok) // GCOVR_EXCL_LINE hn < 0 unreachable (see above)
213 {
215 }
216 size_t n = (size_t)hn;
217
218 const char *b = msg->body ? msg->body : "";
219 bool at_line_start = true;
220 for (size_t i = 0; b[i]; i++)
221 {
222 char c = b[i];
223 if (c == '\r')
224 {
225 continue; // normalize: CR is dropped, LF becomes CRLF
226 }
227 if (c == '\n')
228 {
229 if (n + 2 > cap)
230 {
232 }
233 out[n++] = '\r';
234 out[n++] = '\n';
235 at_line_start = true;
236 continue;
237 }
238 if (at_line_start && c == '.')
239 {
240 if (n + 1 > cap) // dot-stuff: a body line starting with '.' gets an extra '.'
241 {
243 }
244 out[n++] = '.';
245 }
246 if (n + 1 > cap)
247 {
249 }
250 out[n++] = c;
251 at_line_start = false;
252 }
253 // Body must end with CRLF before the terminator. n >= 2 always holds (n starts at the
254 // fixed-header length, well over 2), and the only CR ever written to out is the one the
255 // LF->CRLF rewrite emits immediately before its LF (a body CR is dropped above), so
256 // out[n-2]=='\r' implies out[n-1]=='\n' - both are guards, not reachable states.
257 if (!(n >= 2 && out[n - 2] == '\r' && out[n - 1] == '\n')) // GCOVR_EXCL_LINE see above
258 {
259 if (n + 2 > cap)
260 {
262 }
263 out[n++] = '\r';
264 out[n++] = '\n';
265 }
266 if (n + 3 > cap) // terminating "."CRLF
267 {
269 }
270 out[n++] = '.';
271 out[n++] = '\r';
272 out[n++] = '\n';
273 return (int)n;
274}
275
276// Send @p line and require reply code @p want; @p bad is what to report for any other code.
277// A negative code is an I/O ::SmtpResult and passes straight through.
278SmtpResult cmd_expect(SmtpSendFn send, SmtpRecvFn recv, void *ctx, const char *line, int want, SmtpResult bad)
279{
280 int code = command(send, recv, ctx, line);
281 if (code < 0)
282 {
283 return (SmtpResult)code;
284 }
285 return (code == want) ? SmtpResult::SMTP_OK : bad;
286}
287
288// Greeting + EHLO. @p line keeps the EHLO command, which the STARTTLS path reissues verbatim.
289SmtpResult greet_ehlo(const SmtpConfig *cfg, SmtpSendFn send, SmtpRecvFn recv, void *ctx, char *line, size_t cap,
290 bool *has_starttls)
291{
292 int code = 0;
293 if (read_reply(recv, ctx, &code) != SmtpResult::SMTP_OK)
294 {
296 }
297 if (code != 220)
298 {
300 }
301
302 // The capability list is only trustworthy once the channel is secure, which is why the
303 // STARTTLS path reissues this command after the upgrade.
304 pc_sb sb_line2 = {line, cap, 0, true};
305 pc_sb_put(&sb_line2, "EHLO ");
306 pc_sb_put(&sb_line2, (cfg->helo && cfg->helo[0]) ? cfg->helo : "esp32");
307 pc_sb_put(&sb_line2, "\r\n");
308 int n = (int)pc_sb_finish(&sb_line2);
309 if (!sb_line2.ok) // GCOVR_EXCL_LINE n < 0 unreachable: snprintf of %s into memory cannot fail
310 {
312 }
313 if (!send_str(send, ctx, line))
314 {
316 }
317 if (read_reply_cap(recv, ctx, &code, "STARTTLS", has_starttls) != SmtpResult::SMTP_OK)
318 {
320 }
321 return (code == 250) ? SmtpResult::SMTP_OK : SmtpResult::SMTP_ERR_PROTOCOL;
322}
323
324// STARTTLS (RFC 3207): upgrade in band, then start the session over.
325SmtpResult upgrade_starttls(SmtpSendFn send, SmtpRecvFn recv, SmtpStartTlsFn starttls, void *ctx, const char *ehlo,
326 bool has_starttls)
327{
328 // Fail closed on a stripped advertisement. An attacker who can delete the capability line
329 // would otherwise get the whole exchange - AUTH credentials included - in the clear.
330 if (!has_starttls)
331 {
333 }
334 if (!starttls)
335 {
336 return SmtpResult::SMTP_ERR_ARG; // asked to upgrade with no way to do it
337 }
338 SmtpResult r = cmd_expect(send, recv, ctx, "STARTTLS\r\n", 220, SmtpResult::SMTP_ERR_TLS);
339 if (r != SmtpResult::SMTP_OK)
340 {
341 return r;
342 }
343 if (!starttls(ctx))
344 {
346 }
347 // RFC 3207 sec 4.2: discard everything learned in the clear and reissue EHLO - the real
348 // capability list (AUTH mechanisms especially) is the one the server sends encrypted.
349 return cmd_expect(send, recv, ctx, ehlo, 250, SmtpResult::SMTP_ERR_PROTOCOL);
350}
351
352// AUTH LOGIN: the username then the password, each base64 on its own line.
353SmtpResult auth_login(const SmtpConfig *cfg, SmtpSendFn send, SmtpRecvFn recv, void *ctx)
354{
355 SmtpResult r = cmd_expect(send, recv, ctx, "AUTH LOGIN\r\n", 334, SmtpResult::SMTP_ERR_AUTH);
356 if (r != SmtpResult::SMTP_OK)
357 {
358 return r;
359 }
360 int code = auth_send_b64(send, recv, ctx, cfg->user);
361 if (code < 0)
362 {
363 return (SmtpResult)code;
364 }
365 if (code != 334)
366 {
368 }
369 code = auth_send_b64(send, recv, ctx, cfg->pass ? cfg->pass : "");
370 if (code < 0)
371 {
372 return (SmtpResult)code;
373 }
374 return (code == 235) ? SmtpResult::SMTP_OK : SmtpResult::SMTP_ERR_AUTH;
375}
376
377// MAIL FROM + RCPT TO, both built into @p line.
378SmtpResult send_envelope(const SmtpConfig *cfg, const SmtpMessage *msg, SmtpSendFn send, SmtpRecvFn recv, void *ctx,
379 char *line, size_t cap)
380{
381 pc_sb sb_line3 = {line, cap, 0, true};
382 pc_sb_put(&sb_line3, "MAIL FROM:<");
383 pc_sb_put(&sb_line3, cfg->from);
384 pc_sb_put(&sb_line3, ">\r\n");
385 int n = (int)pc_sb_finish(&sb_line3);
386 if (!sb_line3.ok) // GCOVR_EXCL_LINE n < 0 unreachable: snprintf of %s into memory cannot fail
387 {
389 }
390 SmtpResult r = cmd_expect(send, recv, ctx, line, 250, SmtpResult::SMTP_ERR_PROTOCOL);
391 if (r != SmtpResult::SMTP_OK)
392 {
393 return r;
394 }
395
396 pc_sb sb_line4 = {line, cap, 0, true};
397 pc_sb_put(&sb_line4, "RCPT TO:<");
398 pc_sb_put(&sb_line4, msg->to);
399 pc_sb_put(&sb_line4, ">\r\n");
400 n = (int)pc_sb_finish(&sb_line4);
401 if (!sb_line4.ok) // GCOVR_EXCL_LINE n < 0 unreachable: snprintf of %s into memory cannot fail
402 {
404 }
405 int code = command(send, recv, ctx, line);
406 if (code < 0)
407 {
408 return (SmtpResult)code;
409 }
410 if (code != 250 && code != 251) // 251 = user not local; will forward
411 {
413 }
414 return SmtpResult::SMTP_OK;
415}
416
417// DATA, the assembled message, then the acceptance reply.
418SmtpResult send_data(const SmtpConfig *cfg, const SmtpMessage *msg, SmtpSendFn send, SmtpRecvFn recv, void *ctx)
419{
420 SmtpResult r = cmd_expect(send, recv, ctx, "DATA\r\n", 354, SmtpResult::SMTP_ERR_PROTOCOL);
421 if (r != SmtpResult::SMTP_OK)
422 {
423 return r;
424 }
425 char body[PC_SMTP_MSG_MAX];
426 int mlen = build_message(body, sizeof(body), cfg, msg);
427 if (mlen < 0)
428 {
429 return (SmtpResult)mlen;
430 }
431 if (send(ctx, (const uint8_t *)body, (size_t)mlen) != mlen)
432 {
434 }
435 int code = 0;
436 if (read_reply(recv, ctx, &code) != SmtpResult::SMTP_OK)
437 {
439 }
440 return (code == 250) ? SmtpResult::SMTP_OK : SmtpResult::SMTP_ERR_PROTOCOL;
441}
442} // namespace
443
444SmtpResult smtp_run(const SmtpConfig *cfg, const SmtpMessage *msg, SmtpSendFn send, SmtpRecvFn recv,
445 SmtpStartTlsFn starttls, void *ctx)
446{
447 if (!cfg || !msg || !send || !recv || !cfg->host || !cfg->from || !cfg->from[0] || !msg->to || !msg->to[0])
448 {
450 }
451
452 char line[PC_SMTP_LINE_MAX]; // holds the EHLO command, then each envelope command
453 bool has_starttls = false;
454 SmtpResult r = greet_ehlo(cfg, send, recv, ctx, line, sizeof(line), &has_starttls);
455 if (r != SmtpResult::SMTP_OK)
456 {
457 return r;
458 }
459
461 {
462 r = upgrade_starttls(send, recv, starttls, ctx, line, has_starttls);
463 if (r != SmtpResult::SMTP_OK)
464 {
465 return r;
466 }
467 }
468
469 if (cfg->user && cfg->user[0]) // AUTH LOGIN only when a username is configured
470 {
471 r = auth_login(cfg, send, recv, ctx);
472 if (r != SmtpResult::SMTP_OK)
473 {
474 return r;
475 }
476 }
477
478 r = send_envelope(cfg, msg, send, recv, ctx, line, sizeof(line));
479 if (r != SmtpResult::SMTP_OK)
480 {
481 return r;
482 }
483
484 r = send_data(cfg, msg, send, recv, ctx);
485 if (r != SmtpResult::SMTP_OK)
486 {
487 return r;
488 }
489
490 // QUIT is best-effort - the message is already accepted.
491 (void)command(send, recv, ctx, "QUIT\r\n");
492 return SmtpResult::SMTP_OK;
493}
494
495// ---------------------------------------------------------------------------
496// Real-transport binding (Arduino): pc_client, plus a pc_tls csess for SMTPS.
497// ---------------------------------------------------------------------------
498
499#if defined(ARDUINO)
500
501namespace
502{
503struct SmtpXport;
504
505/** @brief Owned state: which transport the TLS BIO callbacks act on.
506 *
507 * pc_tls_client_session_begin() carries no context pointer, so mbedtls calls the BIO with a ctx
508 * that is not ours. The active transport is parked here for the life of the session instead. */
509struct SmtpTlsCtx
510{
511 SmtpXport *xport;
512};
513
514struct SmtpXport
515{
516 int cid;
517 uint32_t deadline;
518 const char *host; ///< TLS SNI name, needed when the upgrade happens mid-dialogue
519 bool tls_active; ///< set once a STARTTLS upgrade has completed on this connection
520};
521
522static SmtpTlsCtx s_smtp_tls = {nullptr};
523
524// Plaintext seam over pc_client.
525int cl_send(void *ctx, const uint8_t *data, size_t len)
526{
527 SmtpXport *x = (SmtpXport *)ctx;
528 size_t sent = 0;
529 while (sent < len)
530 {
531 size_t chunk = len - sent;
532 if (chunk > 0xFFFF)
533 {
534 chunk = 0xFFFF;
535 }
536 if (!pc_client_send(x->cid, data + sent, chunk))
537 {
538 return -1;
539 }
540 sent += chunk;
541 }
542 return (int)len;
543}
544int cl_recv(void *ctx, uint8_t *buf, size_t cap)
545{
546 SmtpXport *x = (SmtpXport *)ctx;
547 while ((int32_t)(x->deadline - millis()) > 0)
548 {
549 size_t n = pc_client_read(x->cid, buf, cap);
550 if (n > 0)
551 {
552 return (int)n;
553 }
554 if (pc_client_is_closed(x->cid) && pc_client_available(x->cid) == 0)
555 {
556 return -1;
557 }
558 pcdelay(5);
559 }
560 return -1; // timeout
561}
562
563#if PC_ENABLE_SMTP_TLS
564// TLS ciphertext BIO: the csess handshake/records read/write the wire via pc_client.
565int tls_bio_send(void *ctx, const unsigned char *buf, size_t len)
566{
567 (void)ctx; // not ours - see SmtpTlsCtx
568 SmtpXport *x = s_smtp_tls.xport;
569 if (!x)
570 {
571 return MBEDTLS_ERR_SSL_WANT_WRITE;
572 }
573 return pc_client_send(x->cid, buf, len) ? (int)len : MBEDTLS_ERR_SSL_WANT_WRITE;
574}
575int tls_bio_recv(void *ctx, unsigned char *buf, size_t len)
576{
577 (void)ctx; // not ours - see SmtpTlsCtx
578 SmtpXport *x = s_smtp_tls.xport;
579 if (!x)
580 {
581 return MBEDTLS_ERR_SSL_WANT_READ;
582 }
583 size_t n = pc_client_read(x->cid, buf, len);
584 if (n == 0)
585 {
586 return pc_client_is_closed(x->cid) ? 0 : MBEDTLS_ERR_SSL_WANT_READ;
587 }
588 return (int)n;
589}
590// Application seam over the established TLS session.
591int tls_send(void *ctx, const uint8_t *data, size_t len)
592{
593 (void)ctx;
594 return pc_tls_client_session_write(data, len) == (int)len ? (int)len : -1;
595}
596int tls_recv(void *ctx, uint8_t *buf, size_t cap)
597{
598 SmtpXport *x = (SmtpXport *)ctx;
599 while ((int32_t)(x->deadline - millis()) > 0)
600 {
601 int n = pc_tls_client_session_read(buf, cap);
602 if (n > 0)
603 {
604 return n;
605 }
606 if (n < 0 && n != MBEDTLS_ERR_SSL_WANT_READ && n != MBEDTLS_ERR_SSL_WANT_WRITE)
607 {
608 return -1;
609 }
610 pcdelay(5);
611 }
612 return -1; // timeout
613}
614#endif // PC_ENABLE_SMTP_TLS
615
616// Switching seam. The dialogue engine gets exactly one send/recv pair for the whole exchange; a
617// STARTTLS upgrade flips these underneath it, so the engine never swaps transports mid-conversation
618// and cannot accidentally keep writing plaintext after the upgrade.
619int xp_send(void *ctx, const uint8_t *data, size_t len)
620{
621#if PC_ENABLE_SMTP_TLS
622 if (((SmtpXport *)ctx)->tls_active)
623 {
624 return tls_send(ctx, data, len);
625 }
626#endif
627 return cl_send(ctx, data, len);
628}
629int xp_recv(void *ctx, uint8_t *buf, size_t cap)
630{
631#if PC_ENABLE_SMTP_TLS
632 if (((SmtpXport *)ctx)->tls_active)
633 {
634 return tls_recv(ctx, buf, cap);
635 }
636#endif
637 return cl_recv(ctx, buf, cap);
638}
639
640// Upgrade the live connection in place, after the server's 220 to STARTTLS.
641bool xp_starttls(void *ctx)
642{
643 SmtpXport *x = (SmtpXport *)ctx;
644#if PC_ENABLE_SMTP_TLS
645 if (!pc_tls_client_session_begin(x->host, tls_bio_send, tls_bio_recv))
646 {
647 return false;
648 }
649 // Fresh budget: the deadline carried here was set at connect time and has already funded the
650 // greeting, EHLO and STARTTLS round trips. Reusing whatever is left of it can abandon the
651 // handshake before the ClientHello even goes out, which the server sees as a silent hang.
652 x->deadline = millis() + PC_SMTP_TIMEOUT_MS;
653 int h;
654 while ((h = pc_tls_client_session_handshake()) == 0 && (int32_t)(x->deadline - millis()) > 0)
655 {
656 pcdelay(5);
657 }
658 if (h != 1) // 1 = established; 0 = still pending at timeout; <0 = fatal
659 {
660 pc_tls_client_session_end();
661 return false;
662 }
663 x->tls_active = true; // every later xp_send/xp_recv now goes through the session
664 return true;
665#else
666 (void)x;
667 return false; // STARTTLS requested but TLS not built in
668#endif
669}
670} // namespace
671
672SmtpResult smtp_send(const SmtpConfig *cfg, const SmtpMessage *msg)
673{
674 if (!cfg || !cfg->host)
675 {
677 }
678
679 SmtpXport x;
680 x.cid = pc_client_open(cfg->host, cfg->port, PC_SMTP_TIMEOUT_MS);
681 if (x.cid < 0)
682 {
684 }
685 x.deadline = millis() + PC_SMTP_TIMEOUT_MS;
686 x.host = cfg->host;
687 x.tls_active = false;
688#if PC_ENABLE_SMTP_TLS
689 s_smtp_tls.xport = &x; // the BIO callbacks read this, not their ctx argument
690#endif
691
692 SmtpResult rc;
694 {
695#if PC_ENABLE_SMTP_TLS
696 if (!pc_tls_client_session_begin(cfg->host, tls_bio_send, tls_bio_recv))
697 {
698 pc_client_close(x.cid);
700 }
701 int h;
702 while ((h = pc_tls_client_session_handshake()) == 0 && (int32_t)(x.deadline - millis()) > 0)
703 {
704 pcdelay(5);
705 }
706 if (h != 1) // 1 = established; 0 = still pending at timeout; <0 = fatal
707 {
708 pc_tls_client_session_end();
709 pc_client_close(x.cid);
711 }
712 rc = smtp_run(cfg, msg, tls_send, tls_recv, nullptr, &x);
713 pc_tls_client_session_end();
714#else
715 pc_client_close(x.cid);
716 return SmtpResult::SMTP_ERR_TLS; // SMTPS requested but TLS not built in
717#endif
718 }
719 else
720 {
721 rc = smtp_run(cfg, msg, xp_send, xp_recv, xp_starttls, &x);
722 }
723
724 pc_client_close(x.cid);
725#if PC_ENABLE_SMTP_TLS
726 s_smtp_tls.xport = nullptr; // x is about to go out of scope
727#endif
728 return rc;
729}
730
731#else // host build: no lwIP. smtp_run() above is host-testable; smtp_send() is a stub.
732
734{
736}
737
738#endif // ARDUINO
739
740#endif // PC_ENABLE_SMTP
void pc_base64_encode(const uint8_t *src, size_t src_len, char *dst)
Encode src_len bytes of src as Base64.
Definition base64.cpp:29
Base64 encoder/decoder.
bool pc_client_send(int, const void *, size_t)
Queue len wire bytes for transmission (marshaled tcp_write + output).
Definition client.cpp:366
size_t pc_client_read(int, uint8_t *, size_t)
Drain up to cap buffered wire bytes into buf; returns the count.
Definition client.cpp:374
bool pc_client_is_closed(int)
True once the peer closed (FIN) or the connection errored.
Definition client.cpp:362
int pc_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:354
size_t pc_client_available(int)
Wire bytes currently buffered and ready to read.
Definition client.cpp:370
void pc_client_close(int)
Tear down the connection (marshaled) and return the slot to the pool.
Definition client.cpp:378
Layer 4 outbound TCP client transport - the client-side peer of the (server) transport in tcp....
Pluggable monotonic clock for all library timing.
void pcdelay(uint32_t ms)
Block for at least ms milliseconds - the library's single delay primitive.
Definition clock.h:89
User-facing configuration for ProtoCore.
#define PC_SMTP_MSG_MAX
Max size of the assembled DATA payload (headers + dot-stuffed body), bytes.
#define PC_SMTP_REPLY_MAX
Max size of one (possibly multi-line) server reply held while parsing, bytes.
#define PC_SMTP_LINE_MAX
Max length of one SMTP command / address line (bytes, incl. CRLF).
#define PC_SMTP_TIMEOUT_MS
SMTP connect / per-reply timeout in milliseconds.
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 (pc_client, plus TLS when cfg->tls)....
SmtpResult smtp_run(const SmtpConfig *cfg, const SmtpMessage *msg, SmtpSendFn send, SmtpRecvFn recv, SmtpStartTlsFn starttls, void *ctx)
Drive the full SMTP exchange over send / recv. Pure - no lwIP or TLS - so it is host-testable with a ...
@ SMTP_STARTTLS
connect in the clear, then upgrade in band (submission, port 587).
@ SMTP_TLS
implicit TLS from the first byte (SMTPS, port 465).
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:60
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_NO_STARTTLS
STARTTLS was required but the server did not advertise it.
@ SMTP_ERR_CONNECT
could not open the transport (DNS / connect)
int(* SmtpRecvFn)(void *ctx, uint8_t *buf, size_t cap)
Definition smtp.h:61
bool(* SmtpStartTlsFn)(void *ctx)
Upgrade the live connection to TLS in place (RFC 3207), after the server's 220.
Definition smtp.h:71
Bounded no-heap string builder that fails closed on overflow (one shared copy).
size_t pc_sb_finish(pc_sb *b)
NUL-terminate and return the built length, or 0 if the build overflowed.
Definition strbuf.h:648
void pc_sb_put(pc_sb *b, const char *s)
Append NUL-terminated s; leaves the buffer untouched and clears ok if it would not fit.
Definition strbuf.h:60
Server address + credentials for one send. Addresses are bare (no angle brackets).
Definition smtp.h:75
const char * user
AUTH LOGIN username (null or empty => skip AUTH)
Definition smtp.h:79
SmtpSecurity security
how to secure the connection
Definition smtp.h:78
const char * pass
AUTH LOGIN password.
Definition smtp.h:80
const char * from
envelope sender + From: header address
Definition smtp.h:81
uint16_t port
25 (plain) / 587 (STARTTLS) / 465 (implicit TLS)
Definition smtp.h:77
const char * host
server hostname (also the TLS SNI name)
Definition smtp.h:76
const char * helo
EHLO domain to announce (null => "esp32")
Definition smtp.h:82
One plain-text message.
Definition smtp.h:87
const char * body
plain-text UTF-8 body; LF or CRLF line ends, dot-stuffed for you
Definition smtp.h:90
const char * to
single recipient address (envelope + To: header)
Definition smtp.h:88
const char * subject
Subject: header (null => empty)
Definition smtp.h:89
Bump-append target; ok latches false once an append would overflow cap.
Definition strbuf.h:30
bool ok
Definition strbuf.h:34
Deterministic TLS engine: mbedTLS over a static memory pool (PC_ENABLE_TLS).