code wiki / (root) / nx_smtp_server.nx

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 }