ProtoCore v1.0.16
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
smtp.h
Go to the documentation of this file.
1// ProtoCore v1.0.16 - Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file smtp.h
6 * @brief Layer 7 (Application) - the client half of one SMTP session (RFC 5321).
7 *
8 * RFC 5321 sec 3.1: an SMTP session is initiated when a client opens a connection to a server and
9 * the server responds with an opening message. This module drives that session end to end: the 220
10 * Greeting (sec 4.2), EHLO (sec 4.1.1.1), an optional in-band TLS upgrade (RFC 3207 sec 4), an
11 * optional AUTH exchange (RFC 4954 sec 4), the mail transaction MAIL / RCPT / DATA (sec 3.3,
12 * sec 4.1.1.2 - 4.1.1.4), and QUIT (sec 4.1.1.10).
13 *
14 * What DATA carries is an RFC 5322 message: a From: field (sec 3.6.2), a To: field (sec 3.6.3), a
15 * Subject: field (sec 3.6.5), an empty line, and the body (sec 2.1), labeled MIME-Version: 1.0
16 * (RFC 2045 sec 4) and text/plain (RFC 2045 sec 5.1). The body is normalized to CRLF line endings
17 * and dot-stuffed (RFC 5321 sec 4.5.2) ahead of the "<CRLF>.<CRLF>" end of mail data indication
18 * (sec 4.1.1.4).
19 *
20 * ::SmtpNs::run walks the dialogue over the byte seam in @ref SmtpNs::transport, so a scripted
21 * transport runs the whole exchange on the host. ::SmtpNs::send binds that seam to the outbound
22 * client transport (::TcpClient) and blocks until the message is accepted.
23 *
24 * Zero heap; every buffer is a compile-time size (PROTOCORE_SMTP_*). Gated by PROTOCORE_ENABLE_SMTP.
25 *
26 * @author Douglas Quigg (dstroy0)
27 * @date 2026
28 */
29
30#ifndef PROTOCORE_SMTP_H
31#define PROTOCORE_SMTP_H
32
33#include "protocore_config.h" // the entry point: protocore_types.h for the widths
34
35#if PROTOCORE_ENABLE_SMTP
36
38
39/** @brief How one session ended. 0 is a delivered message; every failure is a distinct code. */
40typedef enum PROTO_ENUM_PACKED
41{
42 SMTP_OK = 0,
43 SMTP_ERR_ARG = -1, ///< host, reverse-path or forward-path was null or empty
44 SMTP_ERR_CONNECT = -2, ///< the transport never came up (name lookup or connect)
45 SMTP_ERR_TLS = -3, ///< the TLS handshake did not complete
46 SMTP_ERR_IO = -4, ///< a send or a recv failed, or a reply never arrived
47 SMTP_ERR_PROTOCOL = -5, ///< the reply code was not the one the step requires (RFC 5321 sec 4.2)
48 SMTP_ERR_AUTH = -6, ///< the AUTH exchange was rejected (RFC 4954 sec 6: 535)
49 SMTP_ERR_OVERFLOW = -7, ///< a command line or the message content outgrew its fixed buffer
50 SMTP_ERR_NO_STARTTLS = -8, ///< the EHLO reply carried no STARTTLS keyword (RFC 3207 sec 3)
51} SmtpResult;
52
53/** @brief How the channel is secured. */
54typedef enum PROTO_ENUM_PACKED
55{
56 SMTP_PLAIN = 0, ///< no TLS; credentials and content travel in the clear (port 25)
57 SMTP_TLS = 1, ///< implicit TLS from the first byte: the submissions service (RFC 8314 sec 3.3)
58 SMTP_STARTTLS = 2, ///< clear connect, then the in-band upgrade (RFC 3207 sec 4) on submission (RFC 6409 sec 3.1)
59} SmtpSecurity;
60
61/**
62 * @brief The byte seam the dialogue rides. Every octet leaves and arrives through these two, so the
63 * same engine runs over a socket or over a scripted mock.
64 *
65 * @return send: bytes written, which must equal @p len, or < 0 on error.
66 * @return recv: bytes read (> 0), or <= 0 on close, error or timeout.
67 */
68typedef int (*SmtpSendFn)(void *ctx, const uint8_t *data, size_t len);
69typedef int (*SmtpRecvFn)(void *ctx, uint8_t *buf, size_t cap);
70
71/**
72 * @brief Upgrade the live channel to TLS in place, after the server's 220 to STARTTLS
73 * (RFC 3207 sec 4).
74 *
75 * Called once, mid-session. Every later send and recv on the same @p ctx carries TLS records, so
76 * the switch belongs to the transport and the engine keeps the one pair of function pointers.
77 * @return true when the handshake completed.
78 */
79typedef proto_bool (*SmtpStartTlsFn)(void *ctx);
80
81/** @brief RFC 5321 sec 3.1: the server a session is opened with, and how it is secured. */
82typedef struct
83{
84 const char *host; ///< the server it dials; also the TLS SNI name
85 uint16_t port; ///< 25, submission 587 (RFC 6409 sec 3.1), submissions 465 (RFC 8314 sec 3.3)
86 SmtpSecurity security; ///< how the channel is secured
87 const char *client_name; ///< the Domain the EHLO argument carries (RFC 5321 sec 4.1.1.1)
88} SmtpSessionArgs;
89
90/**
91 * @brief RFC 4954 sec 4: the identity the AUTH exchange presents.
92 *
93 * The mechanism is AUTH LOGIN: the username then the password, each base64 (RFC 4648 sec 4) on its
94 * own line, each answering a 334 challenge. LOGIN is not defined by any RFC; the IANA SASL
95 * Mechanisms registry carries it with usage OBSOLETE, referencing draft-murchison-sasl-login-00.
96 * RFC 4954 sec 4 defines the AUTH verb and the 334 / 235 replies the exchange uses.
97 */
98typedef struct
99{
100 const char *user; ///< the authentication identity; null or empty skips AUTH entirely
101 const char *pass; ///< its password
102} SmtpAuthArgs;
103
104/** @brief RFC 5321 sec 3.3: the two paths one mail transaction names. Bare mailboxes, no brackets. */
105typedef struct
106{
107 const char *reverse_path; ///< the sender mailbox MAIL carries (RFC 5321 sec 4.1.1.2)
108 const char *forward_path; ///< the recipient mailbox RCPT carries (RFC 5321 sec 4.1.1.3)
109} SmtpEnvelopeArgs;
110
111/** @brief RFC 5322: the message DATA carries. Nothing the envelope reads. */
112typedef struct
113{
114 const char *subject; ///< the Subject: field body (RFC 5322 sec 3.6.5); null writes an empty one
115 const char *body; ///< the body (RFC 5322 sec 2.1); LF or CRLF ends, dot-stuffed on the way out
116} SmtpContentArgs;
117
118/** @brief The seam the octets move through, and the transport state handed back to it. */
119typedef struct
120{
121 SmtpSendFn send; ///< writes octets to the server
122 SmtpRecvFn recv; ///< reads octets from the server
123 SmtpStartTlsFn starttls; ///< upgrades the channel in place (RFC 3207 sec 4); null when it cannot
124 void *ctx; ///< the transport's own handle, passed back to all three
125} SmtpTransportArgs;
126
127/**
128 * @brief The SMTP client.
129 *
130 * A caller sets the members a call takes, invokes it through ::Smtp, and reads the outcome off the
131 * same handle. There is no slot member: the module drives one session at a time, so no call has a
132 * row to name.
133 *
134 * @var SmtpNs::session the server a session is opened with (RFC 5321 sec 3.1)
135 * @var SmtpNs::auth the identity the AUTH exchange presents (RFC 4954 sec 4)
136 * @var SmtpNs::envelope the reverse-path and forward-path of the transaction (RFC 5321 sec 3.3)
137 * @var SmtpNs::content the RFC 5322 message DATA carries
138 * @var SmtpNs::transport the seam the octets move through
139 * @var SmtpNs::ok a call's true/false outcome: the message was accepted
140 * @var SmtpNs::result the same outcome as a distinct ::SmtpResult code
141 * @var SmtpNs::code the reply code of the last reply read (RFC 5321 sec 4.2)
142 * @var SmtpNs::run walk the whole session over the seam in @c transport
143 * @var SmtpNs::send open the outbound client transport, walk the session, close
144 */
145typedef struct
146{
147 SmtpSessionArgs session; ///< the server a session is opened with
148 SmtpAuthArgs auth; ///< the identity AUTH presents
149 SmtpEnvelopeArgs envelope; ///< the two paths of one mail transaction
150 SmtpContentArgs content; ///< the message DATA carries
151 SmtpTransportArgs transport; ///< the seam the octets move through
152 proto_bool ok;
153 SmtpResult result;
154 int16_t code;
155} SmtpVars;
156
157/** @brief The operands and the outcome. */
158extern SmtpVars SmtpV;
159
160/** @brief The entries. */
161typedef struct
162{
163 void (*const run)(uint8_t *work);
164 void (*const send)(uint8_t *work);
165} SmtpNs;
166
167// What the table binds, defined once in the .c and taking one parameter each: everything
168// else an entry needs is an operand in SmtpV or a region of the borrow at a fixed offset.
169void protocore_smtp_run(uint8_t *work);
170void protocore_smtp_send(uint8_t *work);
171
172// `static const`, initialised HERE rather than `extern` against a definition in the .c: a
173// const object whose initializer every translation unit can see is a COMPILE-TIME FACT, so
174// `Smtp.run(work)` resolves to a named function and becomes a DIRECT call. An extern table
175// leaves the call indirect and the symbol live at every level, -O2 -flto included.
176static const SmtpNs Smtp __attribute__((unused)) = {
177 .run = protocore_smtp_run,
178 .send = protocore_smtp_send,
179};
180
181/**
182 * @brief The PROTOCORE_SMTP_BORROW bytes this module's state lives in.
183 *
184 * Stated beside the namespace rather than on it: an entry takes a borrow, and this is where
185 * that borrow comes from. Taken once from the end of the pool, which no mark and no release
186 * walks, so the state lasts the life of the program.
187 *
188 * @return the span.
189 */
190uint8_t *protocore_smtp_span(void);
191
193
194#endif // PROTOCORE_ENABLE_SMTP
195
196#endif // PROTOCORE_SMTP_H
PROTO_ENUM_PACKED
Application protocol spoken on a listener port or connection slot.
#define PROTOCORE_BEGIN_DECLS
Give a header's declarations C linkage, so their symbol names carry no parameter types.
Definition types.h:96
_Bool proto_bool
The truth value.
Definition types.h:64
#define PROTOCORE_END_DECLS
Definition types.h:97