nx_email_mta.nx source
↩ module page · 134 lines · 5556 B
1// nx_email_mta.nx -- EMAIL RUNG R7: receiving MTA (SMTP server logic).
2//
3// module: nishi-core.email.mta
4// depends: (none -- pure RFC 5321 server-side command parse + policy + FSM)
5// capability: CORE_EMAIL
6//
7// The server end of SMTP: classify inbound commands, enforce the RCPT
8// relay policy (accept mail for OUR domain, refuse to relay for others --
9// the open-relay defense), and drive the receive state machine emitting
10// the correct reply codes. Pure logic; the integration capstone (accept
11// loop over nx_socket, dot-unstuff the DATA via R4's nx_pop3_dedot, and
12// deliver into R3's nx_email_mailbox) is exercised end-to-end in the gate.
13//
14// license_tier: INDEPENDENT_REDERIVE
15// genealogy_id: international-research-sources/ietf/rfc_5321
16// lineage_id: nishi_email_mta_r7
17//
18// nx_safety_envelope:
19// intended_use: "Inbound SMTP command parse + RCPT relay policy +
20// server reply-code FSM. Receiving MTA substrate."
21// sil_target: SIL2 (open relay / misdelivery)
22// evidence: [cmd_classify, addr_extract, relay_refused,
23// out_of_sequence_503, reply_code_fsm]
24// hazard_register: [bug-tape-open-relay, bug-tape-smtp-state-confusion]
25// residual_risk: "Greylisting/rate-limit/SPF-at-RCPT (compose R5)
26// + live accept loop are upstream/_io concerns."
27// verdict: NOT_YET_EVALUATED
28
29// ---- inbound command enum ----
30const MTA_C_UNKNOWN: i64 = 0
31const MTA_C_EHLO: i64 = 1
32const MTA_C_HELO: i64 = 2
33const MTA_C_MAIL: i64 = 3
34const MTA_C_RCPT: i64 = 4
35const MTA_C_DATA: i64 = 5
36const MTA_C_QUIT: i64 = 6
37const MTA_C_RSET: i64 = 7
38const MTA_C_NOOP: i64 = 8
39
40// ---- server states ----
41const MTA_S_GREET: i64 = 0 // 220 sent, await EHLO
42const MTA_S_READY: i64 = 1 // after EHLO, await MAIL
43const MTA_S_MAIL: i64 = 2 // after MAIL, await RCPT
44const MTA_S_RCPT: i64 = 3 // >=1 RCPT accepted, await RCPT/DATA
45const MTA_S_DATA: i64 = 4 // in DATA collection
46const MTA_S_QUIT: i64 = 5
47
48func mta_upper(c: i64) -> i64 { if c >= 97 && c <= 122 { return c - 32 } return c }
49
50func mta_verb_eq(line: *u8, n: i64, v: *u8) -> i64 {
51 if n < 4 { return 0 }
52 var i: i64 = 0
53 while i < 4 { if mta_upper(line[i] & 0xff) != (v[i] & 0xff) { return 0 } i = i + 1 }
54 return 1
55}
56
57// Classify an inbound command line.
58func nx_mta_parse_cmd(line: *u8, n: i64) -> i64 {
59 if mta_verb_eq(line, n, "EHLO" as *u8) == 1 { return MTA_C_EHLO }
60 if mta_verb_eq(line, n, "HELO" as *u8) == 1 { return MTA_C_HELO }
61 if mta_verb_eq(line, n, "MAIL" as *u8) == 1 { return MTA_C_MAIL }
62 if mta_verb_eq(line, n, "RCPT" as *u8) == 1 { return MTA_C_RCPT }
63 if mta_verb_eq(line, n, "DATA" as *u8) == 1 { return MTA_C_DATA }
64 if mta_verb_eq(line, n, "QUIT" as *u8) == 1 { return MTA_C_QUIT }
65 if mta_verb_eq(line, n, "RSET" as *u8) == 1 { return MTA_C_RSET }
66 if mta_verb_eq(line, n, "NOOP" as *u8) == 1 { return MTA_C_NOOP }
67 return MTA_C_UNKNOWN
68}
69
70// Extract the address between '<' and '>' into out (null-terminated).
71// Returns address length or -1 if absent/malformed.
72func nx_mta_extract_addr(line: *u8, n: i64, out: *u8, cap: i64) -> i64 {
73 var lt: i64 = 0 - 1
74 var i: i64 = 0
75 while i < n { if (line[i] & 0xff) == 60 { lt = i; i = n } else { i = i + 1 } }
76 if lt < 0 { return 0 - 1 }
77 var oi: i64 = 0
78 var j: i64 = lt + 1
79 while j < n && (line[j] & 0xff) != 62 && oi < cap - 1 { out[oi] = line[j]; oi = oi + 1; j = j + 1 }
80 if j >= n { return 0 - 1 }
81 if (line[j] & 0xff) != 62 { return 0 - 1 }
82 out[oi] = 0 as u8
83 return oi
84}
85
86// RCPT relay policy: accept iff the recipient's domain (after the last
87// '@') matches OUR domain (case-insensitive). 1 accept / 0 refuse-relay.
88func nx_mta_rcpt_accept(addr: *u8, alen: i64, domain: *u8, dlen: i64) -> i64 {
89 var at: i64 = 0 - 1
90 var i: i64 = 0
91 while i < alen { if (addr[i] & 0xff) == 64 { at = i } i = i + 1 }
92 if at < 0 { return 0 }
93 let ds: i64 = at + 1
94 let dl: i64 = alen - ds
95 if dl != dlen { return 0 }
96 var k: i64 = 0
97 while k < dl {
98 if mta_upper(addr[ds + k] & 0xff) != mta_upper(domain[k] & 0xff) { return 0 }
99 k = k + 1
100 }
101 return 1
102}
103
104// Server FSM: given current state + parsed command (+ whether the RCPT
105// passed policy), set *out_next and return the SMTP reply code.
106func nx_mta_reply_for(state: i64, cmd: i64, rcpt_ok: i64, out_next: *i64) -> i64 {
107 if cmd == MTA_C_QUIT { *out_next = MTA_S_QUIT; return 221 }
108 if cmd == MTA_C_RSET { *out_next = MTA_S_READY; return 250 }
109 if cmd == MTA_C_NOOP { *out_next = state; return 250 }
110 if cmd == MTA_C_EHLO || cmd == MTA_C_HELO { *out_next = MTA_S_READY; return 250 }
111 if cmd == MTA_C_MAIL {
112 if state == MTA_S_READY { *out_next = MTA_S_MAIL; return 250 }
113 *out_next = state; return 503
114 }
115 if cmd == MTA_C_RCPT {
116 if state == MTA_S_MAIL || state == MTA_S_RCPT {
117 if rcpt_ok == 1 { *out_next = MTA_S_RCPT; return 250 }
118 *out_next = state; return 550 // relay denied
119 }
120 *out_next = state; return 503
121 }
122 if cmd == MTA_C_DATA {
123 if state == MTA_S_RCPT { *out_next = MTA_S_DATA; return 354 }
124 *out_next = state; return 503
125 }
126 *out_next = state; return 500
127}
128
129// Reply code after a DATA body has been received + delivered.
130func nx_mta_data_delivered(stored_ok: i64, out_next: *i64) -> i64 {
131 *out_next = MTA_S_READY
132 if stored_ok == 1 { return 250 }
133 return 451
134}