nx_email_retrieve.nx source
↩ module page · 173 lines · 7280 B
1// nx_email_retrieve.nx -- EMAIL RUNG R4: POP3 + IMAP retrieval codec.
2//
3// module: nishi-core.email.retrieve
4// depends: (none -- pure RFC 1939 / RFC 3501 line codec)
5// capability: CORE_EMAIL
6//
7// The client side of FETCHING mail (the receive counterpart to R1's
8// SMTP send). Pure parse/build over caller buffers -- no syscalls, so
9// it gates offline against scripted server transcripts; the live socket
10// wiring is a later nx_email_retrieve_io.nx (the nx_dns/nx_dns_io split).
11//
12// POP3 (RFC 1939): "+OK"/"-ERR" status, STAT count/octets, a generic
13// command builder, and multi-line handling -- de-dot-stuffing (the
14// inverse of R1's SMTP dot-stuffing: a body "." line is sent as
15// ".." and a lone "." terminates) plus terminator detection.
16// IMAP4rev1 (RFC 3501): tagged command builder, tagged-completion
17// status (OK/NO/BAD) that correctly SKIPS untagged "*" data lines
18// (the classic "* OK ..." vs "A001 OK ..." confusion), and literal
19// "{n}" octet-count parse.
20//
21// license_tier: INDEPENDENT_REDERIVE
22// genealogy_id: international-research-sources/ietf/rfc_1939 + rfc_3501
23// lineage_id: nishi_email_retrieve_r4
24//
25// nx_safety_envelope:
26// intended_use: "POP3/IMAP client response parse + command
27// build. Mail retrieval substrate."
28// sil_target: SIL2 (mis-parse drops/duplicates mail)
29// evidence: [RFC_1939_3501_basis, pop3_dedot_inverse,
30// imap_tagged_vs_untagged, literal_size]
31// hazard_register: [bug-tape-pop3-dot-unstuff, bug-tape-imap-untagged-confusion]
32// residual_risk: "IMAP literal continuation streaming + UIDPLUS/
33// CONDSTORE extensions are later sub-rungs."
34// verdict: NOT_YET_EVALUATED
35
36func er_match(a: *u8, b: *u8, len: i64) -> i64 {
37 var i: i64 = 0
38 while i < len { if (a[i] & 0xff) != (b[i] & 0xff) { return 0 } i = i + 1 }
39 return 1
40}
41func er_cat(out: *u8, oi: i64, s: *u8) -> i64 {
42 var k: i64 = 0
43 while s[k] != (0 as u8) { out[oi] = s[k]; oi = oi + 1; k = k + 1 }
44 return oi
45}
46func er_catn(out: *u8, oi: i64, s: *u8, len: i64) -> i64 {
47 var k: i64 = 0
48 while k < len { out[oi] = s[k]; oi = oi + 1; k = k + 1 }
49 return oi
50}
51
52// ---- POP3 (RFC 1939) -------------------------------------------------
53
54// +OK -> 1, -ERR -> 0, neither -> -1.
55func nx_pop3_reply_ok(line: *u8, n: i64) -> i64 {
56 if n >= 3 && (line[0] & 0xff) == 43 && (line[1] & 0xff) == 79 && (line[2] & 0xff) == 75 { return 1 }
57 if n >= 4 && (line[0] & 0xff) == 45 && (line[1] & 0xff) == 69 && (line[2] & 0xff) == 82 && (line[3] & 0xff) == 82 { return 0 }
58 return 0 - 1
59}
60
61// Parse "+OK <count> <octets>" -> *oc, *oo. 1 ok / 0 malformed.
62func nx_pop3_parse_stat(line: *u8, n: i64, oc: *i64, oo: *i64) -> i64 {
63 if nx_pop3_reply_ok(line, n) != 1 { return 0 }
64 var i: i64 = 3
65 while i < n && (line[i] & 0xff) == 32 { i = i + 1 }
66 var c: i64 = 0; var g1: i64 = 0
67 while i < n && (line[i] & 0xff) >= 48 && (line[i] & 0xff) <= 57 { c = c * 10 + ((line[i] & 0xff) - 48); i = i + 1; g1 = 1 }
68 if g1 == 0 { return 0 }
69 while i < n && (line[i] & 0xff) == 32 { i = i + 1 }
70 var o: i64 = 0; var g2: i64 = 0
71 while i < n && (line[i] & 0xff) >= 48 && (line[i] & 0xff) <= 57 { o = o * 10 + ((line[i] & 0xff) - 48); i = i + 1; g2 = 1 }
72 if g2 == 0 { return 0 }
73 *oc = c; *oo = o
74 return 1
75}
76
77// Build "VERB[ arg]\r\n". Returns length.
78func nx_pop3_build(out: *u8, cap: i64, verb: *u8, arg: *u8, alen: i64) -> i64 {
79 var oi: i64 = er_cat(out, 0, verb)
80 if alen > 0 { out[oi] = 32 as u8; oi = oi + 1; oi = er_catn(out, oi, arg, alen) }
81 out[oi] = 13 as u8; out[oi + 1] = 10 as u8
82 return oi + 2
83}
84
85// De-dot-stuff a POP3 multi-line response body (inverse of SMTP dot-
86// stuffing). A leading ".." becomes "."; a lone "." line terminates.
87// Returns the recovered body length (terminator excluded).
88func nx_pop3_dedot(src: *u8, n: i64, out: *u8, cap: i64) -> i64 {
89 var oi: i64 = 0
90 var at_ls: i64 = 1
91 var i: i64 = 0
92 var done: i64 = 0
93 while i < n && done == 0 {
94 let c: i64 = src[i] & 0xff
95 if at_ls == 1 && c == 46 {
96 if i + 1 >= n {
97 done = 1
98 } else {
99 let c1: i64 = src[i + 1] & 0xff
100 if c1 == 13 || c1 == 10 {
101 done = 1
102 } else {
103 i = i + 1; at_ls = 0 // drop the stuffed dot
104 }
105 }
106 } else {
107 if oi < cap { out[oi] = c as u8; oi = oi + 1 }
108 if c == 10 { at_ls = 1 } else { at_ls = 0 }
109 i = i + 1
110 }
111 }
112 return oi
113}
114
115// 1 iff buf holds a complete POP3 multi-line response (ends "\r\n.\r\n"
116// or is the empty ".\r\n").
117func nx_pop3_multiline_complete(buf: *u8, n: i64) -> i64 {
118 if n >= 5 && (buf[n - 5] & 0xff) == 13 && (buf[n - 4] & 0xff) == 10 && (buf[n - 3] & 0xff) == 46 && (buf[n - 2] & 0xff) == 13 && (buf[n - 1] & 0xff) == 10 { return 1 }
119 if n >= 3 && (buf[0] & 0xff) == 46 && (buf[1] & 0xff) == 13 && (buf[2] & 0xff) == 10 { return 1 }
120 return 0
121}
122
123// ---- IMAP4rev1 (RFC 3501) --------------------------------------------
124
125// Build "<tag> <cmd>\r\n".
126func nx_imap_build_tagged(out: *u8, cap: i64, tag: *u8, tl: i64, cmd: *u8, cl: i64) -> i64 {
127 var oi: i64 = er_catn(out, 0, tag, tl)
128 out[oi] = 32 as u8; oi = oi + 1
129 oi = er_catn(out, oi, cmd, cl)
130 out[oi] = 13 as u8; out[oi + 1] = 10 as u8
131 return oi + 2
132}
133
134// Scan a response for the TAGGED completion line "<tag> OK|NO|BAD".
135// Untagged "*" data lines are skipped. *out_status: 0 OK / 1 NO / 2 BAD
136// / -1 unknown. Returns 1 if a tagged completion is present, 0 if the
137// response is still only untagged data (incomplete).
138func nx_imap_reply_status(buf: *u8, n: i64, tag: *u8, tl: i64, out_status: *i64) -> i64 {
139 var i: i64 = 0
140 while i < n {
141 let ls: i64 = i
142 var j: i64 = i
143 while j < n && (buf[j] & 0xff) != 10 { j = j + 1 }
144 let le: i64 = j
145 if le - ls > tl && er_match(buf + ls, tag, tl) == 1 && (buf[ls + tl] & 0xff) == 32 {
146 let ss: i64 = ls + tl + 1
147 if ss + 2 <= le && (buf[ss] & 0xff) == 79 && (buf[ss + 1] & 0xff) == 75 { *out_status = 0; return 1 }
148 if ss + 2 <= le && (buf[ss] & 0xff) == 78 && (buf[ss + 1] & 0xff) == 79 { *out_status = 1; return 1 }
149 if ss + 3 <= le && (buf[ss] & 0xff) == 66 && (buf[ss + 1] & 0xff) == 65 && (buf[ss + 2] & 0xff) == 68 { *out_status = 2; return 1 }
150 *out_status = 0 - 1; return 1
151 }
152 i = le + 1
153 }
154 return 0
155}
156
157// Parse an IMAP literal "{n}" octet count from a line. 1 ok / 0 none.
158func nx_imap_literal_size(line: *u8, n: i64, out: *i64) -> i64 {
159 var i: i64 = 0
160 var found: i64 = 0
161 while i < n {
162 if (line[i] & 0xff) == 123 {
163 var k: i64 = i + 1
164 var v: i64 = 0
165 var got: i64 = 0
166 while k < n && (line[k] & 0xff) >= 48 && (line[k] & 0xff) <= 57 { v = v * 10 + ((line[k] & 0xff) - 48); k = k + 1; got = 1 }
167 if k < n && (line[k] & 0xff) == 125 && got == 1 { *out = v; found = 1 }
168 }
169 i = i + 1
170 }
171 if found == 1 { return 1 }
172 return 0
173}