code wiki / (root) / nx_smtp_client.nx

nx_smtp_client.nx source

↩ module page · 284 lines · 11516 B

1// nx_smtp_client.nx -- EMAIL RUNG R1: SMTP client wire protocol (RFC 5321). 2// 3// module: nishi-core.email.smtp_client 4// depends: (none -- pure RFC 5321 line codec + state machine) 5// capability: CORE_EMAIL 6// 7// The conversation grammar a client speaks to a mail server, ABOVE the 8// MX host R0 discovered and the nx_socket TCP rung. Three pure parts, 9// no syscalls (live socket I/O composes in a later nx_smtp_client_io.nx, 10// mirroring the nx_dns / nx_dns_io split) so the whole protocol is 11// deterministically gateable offline against a scripted transcript: 12// 13// 1. REPLY PARSER -- 3-digit reply codes with multiline continuation 14// (RFC 5321 §4.2.1: "250-" continues, "250 " is final). Detects a 15// complete vs still-streaming reply, enforces the code matches 16// across all continuation lines (a mismatch is a malformed/forged 17// reply), rejects non-digit codes and illegal separators. 18// 19// 2. COMMAND BUILDERS -- EHLO / MAIL FROM / RCPT TO / DATA / QUIT etc. 20// with correct CRLF framing and <addr> bracketing. 21// 22// 3. DOT-STUFFING (RFC 5321 §4.5.2) -- the correctness/SECURITY crux: 23// a body line beginning with "." gets an extra "." prepended so a 24// lone "." in the message can never be mistaken for the 25// "<CRLF>.<CRLF>" end-of-DATA terminator (premature-termination / 26// SMTP-injection defense). 27// 28// 4. CLIENT STATE MACHINE -- the deterministic send sequence 29// INIT(220) -> EHLO(2xx) -> MAIL(2xx) -> RCPT(2xx) -> DATA(354) -> 30// BODY(2xx) -> QUIT(221) -> DONE, with any unexpected code routing 31// to ABORT/FAIL (the conversation then closes with QUIT). 32// 33// license_tier: INDEPENDENT_REDERIVE 34// genealogy_id: international-research-sources/ietf/rfc_5321 35// lineage_id: nishi_email_smtp_client_r1 36// 37// nx_safety_envelope: 38// intended_use: "SMTP client reply parse + command build + 39// dot-stuffing + send-sequence state machine. 40// RFC 5321 substrate." 41// sil_target: SIL2 (mis-parse => mail to wrong recipient 42// / premature DATA termination) 43// evidence: [RFC_5321_canonical_basis, multiline_reply_code, 44// cross-line_code_match_gate, dot_stuffing, 45// sealed_verdict_enum, deterministic_state_machine] 46// hazard_register: [bug-tape-SMTP-dot-injection, 47// bug-tape-multiline-reply-confusion] 48// residual_risk: "AUTH + TLS upgrade are R6; this rung is the 49// cleartext command/reply grammar only." 50// verdict: NOT_YET_EVALUATED 51 52// ---- Sealed verdict (negated on the parser's error returns). ---- 53const NX_SMTP_BAD_CODE: i64 = 2 // reply code not 3 digits 54const NX_SMTP_CODE_MISMATCH: i64 = 3 // continuation line code != first line 55const NX_SMTP_BAD_LINE: i64 = 4 // illegal separator after code 56const NX_SMTP_BUF_FULL: i64 = 5 // builder ran out of caller buffer 57const NX_SMTP_N: i64 = 6 58 59// ---- Client states. ---- 60const SMTP_S_INIT: i64 = 0 // awaiting 220 greeting 61const SMTP_S_EHLO: i64 = 1 // EHLO sent, awaiting 250 62const SMTP_S_MAIL: i64 = 2 // MAIL FROM sent 63const SMTP_S_RCPT: i64 = 3 // RCPT TO sent 64const SMTP_S_DATA: i64 = 4 // DATA sent, awaiting 354 65const SMTP_S_BODY: i64 = 5 // message body sent, awaiting 250 66const SMTP_S_QUIT: i64 = 6 // QUIT sent, awaiting 221 67const SMTP_S_DONE: i64 = 7 // conversation complete 68const SMTP_S_FAIL: i64 = 8 // aborted 69 70// ---- Driver actions. ---- 71const SMTP_A_NONE: i64 = 0 72const SMTP_A_SEND_EHLO: i64 = 1 73const SMTP_A_SEND_MAIL: i64 = 2 74const SMTP_A_SEND_RCPT: i64 = 3 75const SMTP_A_SEND_DATA: i64 = 4 76const SMTP_A_SEND_BODY: i64 = 5 77const SMTP_A_SEND_QUIT: i64 = 6 78const SMTP_A_DONE: i64 = 7 79const SMTP_A_ABORT: i64 = 8 80 81const SMTP_MAX_REPLY_LINES: i64 = 64 82 83func nx_smtp_action_name(a: i64) -> *u8 { 84 if a == SMTP_A_NONE { return "NONE" } 85 if a == SMTP_A_SEND_EHLO { return "SEND_EHLO" } 86 if a == SMTP_A_SEND_MAIL { return "SEND_MAIL" } 87 if a == SMTP_A_SEND_RCPT { return "SEND_RCPT" } 88 if a == SMTP_A_SEND_DATA { return "SEND_DATA" } 89 if a == SMTP_A_SEND_BODY { return "SEND_BODY" } 90 if a == SMTP_A_SEND_QUIT { return "SEND_QUIT" } 91 if a == SMTP_A_DONE { return "DONE" } 92 if a == SMTP_A_ABORT { return "ABORT" } 93 return "?" 94} 95 96func smtp_is_digit(c: i64) -> i64 { 97 if c >= 48 && c <= 57 { return 1 } 98 return 0 99} 100 101func smtp_is_2xx(code: i64) -> i64 { 102 if code >= 200 && code < 300 { return 1 } 103 return 0 104} 105 106// ---- Parse an SMTP reply (possibly multiline). ---- 107// 108// Returns the 3-digit reply code (>= 0) on a well-formed reply, or a 109// NEGATIVE verdict on malformed input. *out_complete = 1 when the final 110// line ("<code><SP>...") was seen within [0,n); 0 when the buffer ends 111// mid-reply (caller must read more). *out_consumed = bytes the complete 112// reply occupied (0 if incomplete). Every continuation line's code MUST 113// equal the first line's -- a mismatch is rejected (forged/garbled). 114func nx_smtp_parse_reply(buf: *u8, n: i64, out_complete: *i64, out_consumed: *i64) -> i64 { 115 *out_complete = 0 116 *out_consumed = 0 117 var i: i64 = 0 118 var first_code: i64 = 0 - 1 119 var lines: i64 = 0 120 while lines < SMTP_MAX_REPLY_LINES { 121 if i + 3 > n { return first_code_or_badline(first_code) } 122 let c0: i64 = buf[i] & 0xff 123 let c1: i64 = buf[i + 1] & 0xff 124 let c2: i64 = buf[i + 2] & 0xff 125 if smtp_is_digit(c0) == 0 { return 0 - NX_SMTP_BAD_CODE } 126 if smtp_is_digit(c1) == 0 { return 0 - NX_SMTP_BAD_CODE } 127 if smtp_is_digit(c2) == 0 { return 0 - NX_SMTP_BAD_CODE } 128 let code: i64 = (c0 - 48) * 100 + (c1 - 48) * 10 + (c2 - 48) 129 if first_code < 0 { first_code = code } 130 if code != first_code { return 0 - NX_SMTP_CODE_MISMATCH } 131 // separator after the code 132 if i + 3 >= n { return first_code } // code present but unterminated -> incomplete 133 let sep: i64 = buf[i + 3] & 0xff 134 var is_final: i64 = 0 135 if sep == 45 { 136 is_final = 0 // '-' continuation 137 } else { 138 if sep == 32 || sep == 13 || sep == 10 { 139 is_final = 1 // SP / CR / LF -> final line 140 } else { 141 return 0 - NX_SMTP_BAD_LINE 142 } 143 } 144 // find end of this line (the LF) 145 var le: i64 = 0 - 1 146 var j: i64 = i + 3 147 while j < n { 148 if (buf[j] & 0xff) == 10 { le = j; j = n } else { j = j + 1 } 149 } 150 if le < 0 { return first_code } // line unterminated -> incomplete 151 let next_i: i64 = le + 1 152 if is_final == 1 { 153 *out_complete = 1 154 *out_consumed = next_i 155 return first_code 156 } 157 i = next_i 158 lines = lines + 1 159 } 160 return 0 - NX_SMTP_BAD_LINE // too many continuation lines 161} 162 163// Helper for the incomplete-at-code path: a code we already read but a 164// truncated buffer is NOT malformed -- return the code (out_complete 165// stays 0). Only a buffer with no parseable code yet is BAD_LINE. 166func first_code_or_badline(first_code: i64) -> i64 { 167 if first_code >= 0 { return first_code } 168 return 0 - NX_SMTP_BAD_LINE 169} 170 171// ---- Command builders (return bytes written or -BUF_FULL). ---- 172 173func sm_cat(out: *u8, oi: i64, s: *u8) -> i64 { 174 var k: i64 = 0 175 while s[k] != (0 as u8) { out[oi] = s[k]; oi = oi + 1; k = k + 1 } 176 return oi 177} 178func sm_catn(out: *u8, oi: i64, s: *u8, len: i64) -> i64 { 179 var k: i64 = 0 180 while k < len { out[oi] = s[k]; oi = oi + 1; k = k + 1 } 181 return oi 182} 183 184func nx_smtp_build_ehlo(out: *u8, cap: i64, domain: *u8, dlen: i64) -> i64 { 185 if cap < dlen + 7 { return 0 - NX_SMTP_BUF_FULL } 186 var oi: i64 = 0 187 oi = sm_cat(out, oi, "EHLO " as *u8) 188 oi = sm_catn(out, oi, domain, dlen) 189 oi = sm_cat(out, oi, "\r\n" as *u8) 190 return oi 191} 192 193func nx_smtp_build_mail_from(out: *u8, cap: i64, addr: *u8, alen: i64) -> i64 { 194 if cap < alen + 14 { return 0 - NX_SMTP_BUF_FULL } 195 var oi: i64 = 0 196 oi = sm_cat(out, oi, "MAIL FROM:<" as *u8) 197 oi = sm_catn(out, oi, addr, alen) 198 oi = sm_cat(out, oi, ">\r\n" as *u8) 199 return oi 200} 201 202func nx_smtp_build_rcpt_to(out: *u8, cap: i64, addr: *u8, alen: i64) -> i64 { 203 if cap < alen + 12 { return 0 - NX_SMTP_BUF_FULL } 204 var oi: i64 = 0 205 oi = sm_cat(out, oi, "RCPT TO:<" as *u8) 206 oi = sm_catn(out, oi, addr, alen) 207 oi = sm_cat(out, oi, ">\r\n" as *u8) 208 return oi 209} 210 211func nx_smtp_build_simple(out: *u8, cap: i64, verb: *u8) -> i64 { 212 var oi: i64 = 0 213 oi = sm_cat(out, oi, verb) 214 if oi + 2 > cap { return 0 - NX_SMTP_BUF_FULL } 215 oi = sm_cat(out, oi, "\r\n" as *u8) 216 return oi 217} 218 219// ---- Dot-stuff a message body for the DATA phase. ---- 220// 221// Prepends an extra "." to every line that begins with "." and appends 222// the "<CRLF>.<CRLF>" terminator. Input lines are CRLF-delimited (RFC 223// 5322). Returns bytes written or -BUF_FULL. 224func nx_smtp_dot_stuff(msg: *u8, mlen: i64, out: *u8, cap: i64) -> i64 { 225 var oi: i64 = 0 226 var at_line_start: i64 = 1 227 var i: i64 = 0 228 while i < mlen { 229 let c: i64 = msg[i] & 0xff 230 if at_line_start == 1 && c == 46 { 231 if oi + 1 > cap { return 0 - NX_SMTP_BUF_FULL } 232 out[oi] = 46 as u8 // stuff the leading dot 233 oi = oi + 1 234 } 235 if oi + 1 > cap { return 0 - NX_SMTP_BUF_FULL } 236 out[oi] = c as u8 237 oi = oi + 1 238 if c == 10 { at_line_start = 1 } else { at_line_start = 0 } 239 i = i + 1 240 } 241 if oi + 3 > cap { return 0 - NX_SMTP_BUF_FULL } 242 out[oi] = 46 as u8; oi = oi + 1 // . 243 out[oi] = 13 as u8; oi = oi + 1 // CR 244 out[oi] = 10 as u8; oi = oi + 1 // LF 245 return oi 246} 247 248// ---- The client send-sequence state machine. ---- 249// 250// Given the current state (the command we last sent / are awaiting a 251// reply for) and the reply code received, set *out_action to the next 252// thing to send and return the next state. Any unexpected code -> ABORT 253// / SMTP_S_FAIL (caller closes with QUIT). 254func nx_smtp_advance(state: i64, code: i64, out_action: *i64) -> i64 { 255 if state == SMTP_S_INIT { 256 if code == 220 { *out_action = SMTP_A_SEND_EHLO; return SMTP_S_EHLO } 257 *out_action = SMTP_A_ABORT; return SMTP_S_FAIL 258 } 259 if state == SMTP_S_EHLO { 260 if smtp_is_2xx(code) == 1 { *out_action = SMTP_A_SEND_MAIL; return SMTP_S_MAIL } 261 *out_action = SMTP_A_ABORT; return SMTP_S_FAIL 262 } 263 if state == SMTP_S_MAIL { 264 if smtp_is_2xx(code) == 1 { *out_action = SMTP_A_SEND_RCPT; return SMTP_S_RCPT } 265 *out_action = SMTP_A_ABORT; return SMTP_S_FAIL 266 } 267 if state == SMTP_S_RCPT { 268 if smtp_is_2xx(code) == 1 { *out_action = SMTP_A_SEND_DATA; return SMTP_S_DATA } 269 *out_action = SMTP_A_ABORT; return SMTP_S_FAIL 270 } 271 if state == SMTP_S_DATA { 272 if code == 354 { *out_action = SMTP_A_SEND_BODY; return SMTP_S_BODY } 273 *out_action = SMTP_A_ABORT; return SMTP_S_FAIL 274 } 275 if state == SMTP_S_BODY { 276 if smtp_is_2xx(code) == 1 { *out_action = SMTP_A_SEND_QUIT; return SMTP_S_QUIT } 277 *out_action = SMTP_A_ABORT; return SMTP_S_FAIL 278 } 279 if state == SMTP_S_QUIT { 280 *out_action = SMTP_A_DONE; return SMTP_S_DONE 281 } 282 *out_action = SMTP_A_NONE 283 return state 284}