nx_smtp_server.nx source
↩ module page · 277 lines · 12606 B
1// nx_smtp_server.nx -- EMAIL RUNG R2: SMTP server receive grammar (RFC 5321 server side).
2//
3// module: nishi-core.email.smtp_server
4// depends: (none -- pure RFC 5321 command parser + reply builders + un-dot-stuff + sequence machine)
5// capability: CORE_EMAIL
6//
7// The MIRROR of nx_smtp_client (R1): the conversation grammar a MAIL SERVER
8// speaks to a connecting client, ABOVE the nx_socket TCP rung. Pure (no
9// syscalls -- live socket I/O composes in a later nx_smtp_server_io.nx,
10// mirroring the nx_smtp_client / nx_dns split) so the whole receive protocol
11// is deterministically gateable offline against a scripted CLIENT transcript:
12//
13// 1. COMMAND PARSER -- classify a client command line (EHLO/HELO/MAIL/RCPT/
14// DATA/QUIT/RSET/NOOP/VRFY) case-insensitively, requiring a delimiter
15// after the 4-char verb (so "MAILBOX" never matches "MAIL"), and extract
16// the <addr> span for MAIL/RCPT and the domain span for EHLO/HELO.
17//
18// 2. REPLY BUILDERS -- 3-digit reply + text, CRLF framed (220 greeting,
19// 250 ok, 354 start-data, 221 bye, 500 syntax, 501 bad-args, 503 bad-
20// sequence, 550 no-such-user). Exact bytes for protocol conformance.
21//
22// 3. UN-DOT-STUFF + DATA TERMINATOR (RFC 5321 4.5.2) -- the receive crux /
23// SECURITY: reverse the client's leading-dot stuffing AND detect the lone
24// "<CRLF>.<CRLF>" end-of-DATA, such that a STUFFED ".." line can never be
25// mistaken for the terminator (premature-termination / truncation /
26// SMTP-smuggling defense -- the exact inverse of nx_smtp_dot_stuff).
27//
28// 4. SEQUENCE STATE MACHINE -- enforce RFC 5321 command ordering: MAIL only
29// from the greeted/idle state; RCPT only after MAIL; DATA only after >=1
30// RCPT. Any out-of-sequence verb -> 503 with the state LEFT UNCHANGED
31// (no envelope is ever accepted without sender+recipient). A completed
32// DATA body resets to the greeted state (ready for the next transaction).
33//
34// license_tier: INDEPENDENT_REDERIVE
35// genealogy_id: international-research-sources/ietf/rfc_5321
36// lineage_id: nishi_email_smtp_server_r2
37//
38// nx_safety_envelope:
39// intended_use: "SMTP server command parse + reply build + un-dot-
40// stuff/terminator + receive-sequence state machine.
41// RFC 5321 server substrate."
42// sil_target: SIL2 (mis-sequence => accept mail with no sender/
43// recipient; un-dot-stuff error => corrupted or
44// truncated delivered message)
45// evidence: [RFC_5321_canonical_basis, verb_delimiter_guard,
46// undot_roundtrip_vs_dot_stuff, terminator_not_stuffed,
47// sealed_verdict_enum, sequence_503_neg_controls]
48// hazard_register: [bug-tape-SMTP-dot-injection,
49// bug-tape-out-of-sequence-envelope]
50// residual_risk: "AUTH + STARTTLS are R6; this rung is the cleartext
51// receive command/reply grammar only."
52// verdict: NOT_YET_EVALUATED
53
54// ---- Sealed verdict (negated on the parser's error returns). ----
55const NX_SMTPS_BAD_LINE: i64 = 2 // command line empty / no verb
56const NX_SMTPS_BAD_ADDR: i64 = 3 // MAIL/RCPT missing <...> address
57const NX_SMTPS_BUF_FULL: i64 = 4 // builder ran out of caller buffer
58const NX_SMTPS_N: i64 = 5
59
60// ---- Verb enum (classified from the command line). ----
61const SV_EHLO: i64 = 0
62const SV_HELO: i64 = 1
63const SV_MAIL: i64 = 2
64const SV_RCPT: i64 = 3
65const SV_DATA: i64 = 4
66const SV_QUIT: i64 = 5
67const SV_RSET: i64 = 6
68const SV_NOOP: i64 = 7
69const SV_VRFY: i64 = 8
70const SV_UNKNOWN: i64 = 9
71
72// ---- Server states (what command we are READY to accept next). ----
73const SS_INIT: i64 = 0 // 220 greeting sent; awaiting EHLO/HELO
74const SS_GREETED: i64 = 1 // EHLO ok; transaction idle; awaiting MAIL FROM
75const SS_MAIL: i64 = 2 // MAIL FROM accepted; awaiting RCPT TO
76const SS_RCPT: i64 = 3 // >=1 RCPT accepted; awaiting more RCPT or DATA
77const SS_DATA: i64 = 4 // DATA accepted (354 sent); reading the body
78const SS_QUIT: i64 = 5 // QUIT seen; 221 sent; conversation closed
79
80func nx_smtp_srv_verb_name(v: i64) -> *u8 {
81 if v == SV_EHLO { return "EHLO" }
82 if v == SV_HELO { return "HELO" }
83 if v == SV_MAIL { return "MAIL" }
84 if v == SV_RCPT { return "RCPT" }
85 if v == SV_DATA { return "DATA" }
86 if v == SV_QUIT { return "QUIT" }
87 if v == SV_RSET { return "RSET" }
88 if v == SV_NOOP { return "NOOP" }
89 if v == SV_VRFY { return "VRFY" }
90 return "UNKNOWN"
91}
92
93// ---- byte helpers ----
94func ss_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
95func ss_cat(out: *u8, oi: i64, s: *u8) -> i64 { var k: i64 = 0; while s[k] != (0 as u8) { out[oi] = s[k]; oi = oi + 1; k = k + 1 } return oi }
96func ss_upper(c: i64) -> i64 { if c >= 97 { if c <= 122 { return c - 32 } } return c }
97func ss_is_delim(c: i64) -> i64 { if c == 32 { return 1 } if c == 13 { return 1 } if c == 10 { return 1 } return 0 }
98
99// case-insensitive: does buf[off..off+litlen) equal lit (lit given UPPERCASE)?
100func ss_kw_ci(buf: *u8, off: i64, len: i64, lit: *u8, litlen: i64) -> i64 {
101 if off + litlen > len { return 0 }
102 var i: i64 = 0
103 while i < litlen {
104 if ss_upper(buf[off + i] & 0xff) != (lit[i] as i64) { return 0 }
105 i = i + 1
106 }
107 return 1
108}
109
110// a 4-char verb match that ALSO requires a delimiter (or end) after it, so
111// "MAILBOX" does not classify as "MAIL".
112func ss_verb_is(buf: *u8, len: i64, lit: *u8) -> i64 {
113 if ss_kw_ci(buf, 0, len, lit, 4) == 0 { return 0 }
114 if len <= 4 { return 1 }
115 if ss_is_delim(buf[4] & 0xff) == 1 { return 1 }
116 return 0
117}
118
119// domain/argument span after the verb: skip spaces from `start`, then take
120// until CR/LF/space/end. Sets *out_off/*out_len; returns 1 if a span exists.
121func ss_span_after_sp(buf: *u8, start: i64, len: i64, out_off: *i64, out_len: *i64) -> i64 {
122 var i: i64 = start
123 while i < len {
124 let c: i64 = buf[i] & 0xff
125 if c == 32 { i = i + 1 } else { break }
126 }
127 let s: i64 = i
128 while i < len {
129 let c: i64 = buf[i] & 0xff
130 if c == 13 || c == 10 || c == 32 { break }
131 i = i + 1
132 }
133 if i > s { *out_off = s; *out_len = i - s; return 1 }
134 *out_off = 0 - 1; *out_len = 0
135 return 0
136}
137
138// address span between the FIRST '<' and the NEXT '>' at/after `start`.
139func ss_addr_angle(buf: *u8, start: i64, len: i64, out_off: *i64, out_len: *i64) -> i64 {
140 var i: i64 = start
141 var lt: i64 = 0 - 1
142 while i < len {
143 if (buf[i] & 0xff) == 60 { lt = i; break }
144 i = i + 1
145 }
146 if lt < 0 { *out_off = 0 - 1; *out_len = 0; return 0 }
147 var j: i64 = lt + 1
148 var gt: i64 = 0 - 1
149 while j < len {
150 if (buf[j] & 0xff) == 62 { gt = j; break }
151 j = j + 1
152 }
153 if gt < 0 { *out_off = 0 - 1; *out_len = 0; return 0 }
154 *out_off = lt + 1
155 *out_len = gt - (lt + 1)
156 return 1
157}
158
159// ---- Classify one command line. ----
160//
161// Returns the verb enum. For MAIL/RCPT, *out_aoff/*out_alen give the <addr>
162// span; for EHLO/HELO they give the domain span; otherwise the span is empty
163// (*out_aoff = -1). buf[0..len) is one line (a trailing CRLF may be present).
164func nx_smtp_srv_parse(buf: *u8, len: i64, out_aoff: *i64, out_alen: *i64) -> i64 {
165 *out_aoff = 0 - 1
166 *out_alen = 0
167 if len < 4 { return SV_UNKNOWN }
168 if ss_verb_is(buf, len, "EHLO" as *u8) == 1 { ss_span_after_sp(buf, 4, len, out_aoff, out_alen); return SV_EHLO }
169 if ss_verb_is(buf, len, "HELO" as *u8) == 1 { ss_span_after_sp(buf, 4, len, out_aoff, out_alen); return SV_HELO }
170 if ss_verb_is(buf, len, "MAIL" as *u8) == 1 { ss_addr_angle(buf, 4, len, out_aoff, out_alen); return SV_MAIL }
171 if ss_verb_is(buf, len, "RCPT" as *u8) == 1 { ss_addr_angle(buf, 4, len, out_aoff, out_alen); return SV_RCPT }
172 if ss_verb_is(buf, len, "DATA" as *u8) == 1 { return SV_DATA }
173 if ss_verb_is(buf, len, "QUIT" as *u8) == 1 { return SV_QUIT }
174 if ss_verb_is(buf, len, "RSET" as *u8) == 1 { return SV_RSET }
175 if ss_verb_is(buf, len, "NOOP" as *u8) == 1 { return SV_NOOP }
176 if ss_verb_is(buf, len, "VRFY" as *u8) == 1 { return SV_VRFY }
177 return SV_UNKNOWN
178}
179
180// ---- Build a single-line reply: "<code> <text>\r\n" (code is 3 digits). ----
181func nx_smtp_srv_reply(out: *u8, cap: i64, code: i64, text: *u8) -> i64 {
182 if cap < 6 { return 0 - NX_SMTPS_BUF_FULL }
183 let d0: i64 = (code / 100) % 10
184 let d1: i64 = (code / 10) % 10
185 let d2: i64 = code % 10
186 var oi: i64 = 0
187 out[oi] = (48 + d0) as u8; oi = oi + 1
188 out[oi] = (48 + d1) as u8; oi = oi + 1
189 out[oi] = (48 + d2) as u8; oi = oi + 1
190 out[oi] = 32 as u8; oi = oi + 1
191 oi = ss_cat(out, oi, text)
192 if oi + 2 > cap { return 0 - NX_SMTPS_BUF_FULL }
193 out[oi] = 13 as u8; oi = oi + 1
194 out[oi] = 10 as u8; oi = oi + 1
195 return oi
196}
197
198// ---- Un-dot-stuff the DATA stream + detect end-of-DATA. ----
199//
200// `recv` is the raw received DATA body: dot-stuffed CRLF lines terminated by
201// "<CRLF>.<CRLF>". Writes the ORIGINAL message (leading "." un-stuffed) to
202// `out` and returns its length. *out_complete = 1 iff the lone "." terminator
203// line was seen (a STUFFED ".." line is NOT the terminator -- the inverse of
204// nx_smtp_dot_stuff, and the receive-side truncation/injection defense). When
205// the terminator is absent the partial body is returned with complete=0
206// (caller reads more).
207func nx_smtp_srv_undot(recv: *u8, n: i64, out: *u8, cap: i64, out_complete: *i64) -> i64 {
208 *out_complete = 0
209 var oi: i64 = 0
210 var i: i64 = 0
211 var at_line_start: i64 = 1
212 while i < n {
213 if at_line_start == 1 && (recv[i] & 0xff) == 46 {
214 let n1: i64 = i + 1
215 var is_term: i64 = 0
216 if n1 >= n {
217 is_term = 1 // '.' at very end -> terminator
218 } else {
219 let c1: i64 = recv[n1] & 0xff
220 if c1 == 13 || c1 == 10 { is_term = 1 }
221 }
222 if is_term == 1 {
223 *out_complete = 1
224 return oi
225 }
226 i = i + 1 // drop ONE stuffed leading dot
227 at_line_start = 0
228 continue
229 }
230 let c: i64 = recv[i] & 0xff
231 if oi + 1 > cap { return 0 - NX_SMTPS_BUF_FULL }
232 out[oi] = c as u8
233 oi = oi + 1
234 if c == 10 { at_line_start = 1 } else { at_line_start = 0 }
235 i = i + 1
236 }
237 return oi // no terminator -> incomplete
238}
239
240// ---- The receive-sequence state machine. ----
241//
242// Given the current state and a parsed verb, set *out_code to the reply code
243// the server must send and return the next state. Out-of-sequence verbs yield
244// 503 and LEAVE THE STATE UNCHANGED. EHLO/HELO/RSET reset the transaction to
245// the greeted (idle) state. QUIT -> 221/closed. After a completed DATA body
246// the caller invokes nx_smtp_srv_data_complete to send 250 and reset.
247func nx_smtp_srv_advance(state: i64, verb: i64, out_code: *i64) -> i64 {
248 if verb == SV_QUIT { *out_code = 221; return SS_QUIT }
249 if verb == SV_NOOP { *out_code = 250; return state }
250 if verb == SV_EHLO { *out_code = 250; return SS_GREETED }
251 if verb == SV_HELO { *out_code = 250; return SS_GREETED }
252 if verb == SV_RSET {
253 if state == SS_INIT { *out_code = 503; return SS_INIT }
254 *out_code = 250; return SS_GREETED
255 }
256 if verb == SV_VRFY {
257 if state == SS_INIT { *out_code = 503; return SS_INIT }
258 *out_code = 252; return state // 252: cannot VRFY, will accept
259 }
260 if verb == SV_MAIL {
261 if state == SS_GREETED { *out_code = 250; return SS_MAIL }
262 *out_code = 503; return state // sender out of sequence
263 }
264 if verb == SV_RCPT {
265 if state == SS_MAIL { *out_code = 250; return SS_RCPT }
266 if state == SS_RCPT { *out_code = 250; return SS_RCPT }
267 *out_code = 503; return state // recipient with no sender
268 }
269 if verb == SV_DATA {
270 if state == SS_RCPT { *out_code = 354; return SS_DATA }
271 *out_code = 503; return state // data with no recipient
272 }
273 *out_code = 500; return state // unknown command
274}
275
276// A completed DATA body: accept (250) and reset to the greeted/idle state.
277func nx_smtp_srv_data_complete(out_code: *i64) -> i64 { *out_code = 250; return SS_GREETED }