nx_email_mime.nx source
↩ module page · 322 lines · 13163 B
1// nx_email_mime.nx -- EMAIL RUNG R2: RFC 5322 message + MIME codec.
2//
3// module: nishi-core.email.mime
4// depends: nishi-core.codec.base64
5// capability: CORE_EMAIL
6//
7// (Distinct from runtime/nx_mime.nx, which maps file extensions to
8// content-type strings for the web server. THIS is the email message
9// FORMAT codec.)
10//
11// The message FORMAT layer: what actually travels inside the SMTP DATA
12// phase (R1) and what a retrieval client (R4) hands back. Pure codec,
13// no syscalls -- fully gateable offline. Four parts:
14//
15// 1. RFC 5322 STRUCTURE -- split header block from body at the blank
16// line (CRLFCRLF); case-insensitive header-field lookup that
17// UNFOLDS folded continuation lines (§2.2.3); header append.
18// 2. QUOTED-PRINTABLE (RFC 2045 §6.7) -- binary-safe encode/decode
19// with "=XX" hex escapes and "=CRLF" soft line breaks at 76 cols.
20// 3. BASE64 for MIME -- composes the canonical nx_base64 b64_encode/
21// b64_decode (NOT reimplemented) + the 76-column CRLF wrap RFC
22// 2045 requires, and a whitespace-tolerant decode.
23// 4. MULTIPART (RFC 2046) -- assemble parts under a boundary +
24// extract the Nth part's content.
25//
26// license_tier: INDEPENDENT_REDERIVE
27// genealogy_id: international-research-sources/ietf/rfc_5322 + rfc_2045 + rfc_2046
28// lineage_id: nishi_email_mime_r2
29//
30// nx_safety_envelope:
31// intended_use: "RFC 5322 header/body + MIME transfer-encodings
32// + multipart. Email message format substrate."
33// sil_target: SIL2 (header injection / boundary confusion)
34// evidence: [RFC_5322_2045_2046_basis, header_unfold,
35// qp_roundtrip, base64_composes_canonical,
36// prefix-exact_header_match, multipart_extract]
37// hazard_register: [bug-tape-header-injection-CRLF,
38// bug-tape-mime-boundary-collision]
39// residual_risk: "Encoded-word (RFC 2047) non-ASCII header
40// values + nested multipart are later sub-rungs."
41// verdict: NOT_YET_EVALUATED
42
43import "nx_base64.nx"
44
45const NX_MIME_BUF_FULL: i64 = 2
46const NX_MIME_BAD_QP: i64 = 3
47const NX_MIME_NOT_FOUND: i64 = 4
48
49// ---- small helpers ----
50func mm_lower(c: i64) -> i64 { if c >= 65 && c <= 90 { return c + 32 } return c }
51func mm_ci_eq(a: *u8, b: *u8, len: i64) -> i64 {
52 var i: i64 = 0
53 while i < len { if mm_lower(a[i] & 0xff) != mm_lower(b[i] & 0xff) { return 0 } i = i + 1 }
54 return 1
55}
56func mm_match(a: *u8, b: *u8, len: i64) -> i64 {
57 var i: i64 = 0
58 while i < len { if (a[i] & 0xff) != (b[i] & 0xff) { return 0 } i = i + 1 }
59 return 1
60}
61func mm_cat(out: *u8, oi: i64, s: *u8) -> i64 {
62 var k: i64 = 0
63 while s[k] != (0 as u8) { out[oi] = s[k]; oi = oi + 1; k = k + 1 }
64 return oi
65}
66func mm_catn(out: *u8, oi: i64, s: *u8, len: i64) -> i64 {
67 var k: i64 = 0
68 while k < len { out[oi] = s[k]; oi = oi + 1; k = k + 1 }
69 return oi
70}
71func mm_hexu(v: i64) -> i64 { let w: i64 = v & 0xf; if w < 10 { return 48 + w } return 65 + (w - 10) }
72func mm_hexval(c: i64) -> i64 {
73 if c >= 48 && c <= 57 { return c - 48 }
74 if c >= 65 && c <= 70 { return c - 65 + 10 }
75 if c >= 97 && c <= 102 { return c - 97 + 10 }
76 return 0 - 1
77}
78
79// ---- 1. RFC 5322 structure -------------------------------------------
80
81// Find the header/body boundary (CRLFCRLF). Returns header length (bytes
82// before the blank line), *out_body_off = first body byte. -1 if none.
83func nx_mime_split_body(msg: *u8, n: i64, out_body_off: *i64) -> i64 {
84 var i: i64 = 0
85 while i + 4 <= n {
86 if msg[i] == 13 && msg[i + 1] == 10 && msg[i + 2] == 13 && msg[i + 3] == 10 {
87 *out_body_off = i + 4
88 return i
89 }
90 i = i + 1
91 }
92 *out_body_off = n
93 return 0 - 1
94}
95
96// Case-insensitive header-field lookup with continuation-line unfolding.
97// Matches the field name EXACTLY (length + content) -- "Subject" never
98// matches "X-Subject" or "Subject-Extra". Returns value length (folded
99// lines joined by a single space) or -NOT_FOUND.
100func nx_mime_header_find(hdr: *u8, hlen: i64, name: *u8, namelen: i64, out: *u8, out_cap: i64) -> i64 {
101 var i: i64 = 0
102 while i < hlen {
103 var colon: i64 = 0 - 1
104 var j: i64 = i
105 while j < hlen && (hdr[j] & 0xff) != 10 {
106 if (hdr[j] & 0xff) == 58 && colon < 0 { colon = j }
107 j = j + 1
108 }
109 let line_end: i64 = j
110 if colon > i {
111 let fn_len: i64 = colon - i
112 if fn_len == namelen && mm_ci_eq(hdr + i, name, namelen) == 1 {
113 var vs: i64 = colon + 1
114 if vs < hlen && (hdr[vs] & 0xff) == 32 { vs = vs + 1 }
115 var ve: i64 = line_end
116 if ve > vs && (hdr[ve - 1] & 0xff) == 13 { ve = ve - 1 }
117 var oi: i64 = 0
118 var p: i64 = vs
119 while p < ve && oi < out_cap - 1 { out[oi] = hdr[p]; oi = oi + 1; p = p + 1 }
120 var ni: i64 = line_end + 1
121 while ni < hlen && ((hdr[ni] & 0xff) == 32 || (hdr[ni] & 0xff) == 9) {
122 if oi < out_cap - 1 { out[oi] = 32 as u8; oi = oi + 1 }
123 var q: i64 = ni
124 while q < hlen && ((hdr[q] & 0xff) == 32 || (hdr[q] & 0xff) == 9) { q = q + 1 }
125 var qe: i64 = q
126 while qe < hlen && (hdr[qe] & 0xff) != 10 { qe = qe + 1 }
127 var ce: i64 = qe
128 if ce > q && (hdr[ce - 1] & 0xff) == 13 { ce = ce - 1 }
129 while q < ce && oi < out_cap - 1 { out[oi] = hdr[q]; oi = oi + 1; q = q + 1 }
130 ni = qe + 1
131 }
132 out[oi] = 0 as u8
133 return oi
134 }
135 }
136 i = line_end + 1
137 while i < hlen && ((hdr[i] & 0xff) == 32 || (hdr[i] & 0xff) == 9) {
138 var k: i64 = i
139 while k < hlen && (hdr[k] & 0xff) != 10 { k = k + 1 }
140 i = k + 1
141 }
142 }
143 return 0 - NX_MIME_NOT_FOUND
144}
145
146// Append "Name: value\r\n". Returns new offset.
147func nx_mime_header_append(out: *u8, oi: i64, name: *u8, value: *u8) -> i64 {
148 oi = mm_cat(out, oi, name)
149 oi = mm_cat(out, oi, ": " as *u8)
150 oi = mm_cat(out, oi, value)
151 out[oi] = 13 as u8; out[oi + 1] = 10 as u8
152 return oi + 2
153}
154
155// ---- 2. Quoted-printable (RFC 2045 §6.7, binary-safe) ----------------
156
157func nx_mime_qp_encode(src: *u8, n: i64, out: *u8, cap: i64) -> i64 {
158 var oi: i64 = 0
159 var col: i64 = 0
160 var i: i64 = 0
161 while i < n {
162 let c: i64 = src[i] & 0xff
163 var literal: i64 = 0
164 if c == 9 || c == 32 { literal = 1 }
165 else { if c >= 33 && c <= 126 && c != 61 { literal = 1 } }
166 if literal == 1 {
167 if col >= 75 {
168 if oi + 3 > cap { return 0 - NX_MIME_BUF_FULL }
169 out[oi] = 61 as u8; out[oi + 1] = 13 as u8; out[oi + 2] = 10 as u8
170 oi = oi + 3; col = 0
171 }
172 if oi + 1 > cap { return 0 - NX_MIME_BUF_FULL }
173 out[oi] = c as u8; oi = oi + 1; col = col + 1
174 } else {
175 if col >= 73 {
176 if oi + 3 > cap { return 0 - NX_MIME_BUF_FULL }
177 out[oi] = 61 as u8; out[oi + 1] = 13 as u8; out[oi + 2] = 10 as u8
178 oi = oi + 3; col = 0
179 }
180 if oi + 3 > cap { return 0 - NX_MIME_BUF_FULL }
181 out[oi] = 61 as u8
182 out[oi + 1] = mm_hexu((c >> 4) & 0xf) as u8
183 out[oi + 2] = mm_hexu(c & 0xf) as u8
184 oi = oi + 3; col = col + 3
185 }
186 i = i + 1
187 }
188 return oi
189}
190
191func nx_mime_qp_decode(src: *u8, n: i64, out: *u8, cap: i64) -> i64 {
192 var oi: i64 = 0
193 var i: i64 = 0
194 while i < n {
195 let c: i64 = src[i] & 0xff
196 if c == 61 {
197 if i + 2 < n && (src[i + 1] & 0xff) == 13 && (src[i + 2] & 0xff) == 10 {
198 i = i + 3
199 } else {
200 if i + 1 < n && (src[i + 1] & 0xff) == 10 {
201 i = i + 2
202 } else {
203 if i + 2 < n {
204 let h: i64 = mm_hexval(src[i + 1] & 0xff)
205 let l: i64 = mm_hexval(src[i + 2] & 0xff)
206 if h >= 0 && l >= 0 {
207 if oi + 1 > cap { return 0 - NX_MIME_BUF_FULL }
208 out[oi] = ((h << 4) | l) as u8
209 oi = oi + 1; i = i + 3
210 } else { return 0 - NX_MIME_BAD_QP }
211 } else { return 0 - NX_MIME_BAD_QP }
212 }
213 }
214 } else {
215 if oi + 1 > cap { return 0 - NX_MIME_BUF_FULL }
216 out[oi] = c as u8; oi = oi + 1; i = i + 1
217 }
218 }
219 return oi
220}
221
222// ---- 3. Base64 for MIME (composes canonical nx_base64) ---------------
223
224// 76-column CRLF-wrapped base64 (RFC 2045). 57 input bytes per line.
225func nx_mime_b64_encode_wrap(src: *u8, n: i64, out: *u8, cap: i64) -> i64 {
226 var oi: i64 = 0
227 var p: i64 = 0
228 while p < n {
229 var chunk: i64 = 57
230 if n - p < 57 { chunk = n - p }
231 if oi + 80 > cap { return 0 - NX_MIME_BUF_FULL }
232 let w: i64 = b64_encode(src + p, chunk, out + oi)
233 oi = oi + w
234 p = p + chunk
235 if p < n { out[oi] = 13 as u8; out[oi + 1] = 10 as u8; oi = oi + 2 }
236 }
237 return oi
238}
239
240// Whitespace-tolerant base64 decode: strips CR/LF/SP/TAB into `scratch`
241// then runs the canonical b64_decode. Returns bytes or -1 (invalid).
242func nx_mime_b64_decode_ws(src: *u8, n: i64, scratch: *u8, out: *u8) -> i64 {
243 var si: i64 = 0
244 var i: i64 = 0
245 while i < n {
246 let c: i64 = src[i] & 0xff
247 if c != 13 && c != 10 && c != 32 && c != 9 { scratch[si] = c as u8; si = si + 1 }
248 i = i + 1
249 }
250 return b64_decode(scratch, si, out)
251}
252
253// ---- 4. Multipart (RFC 2046) -----------------------------------------
254
255func nx_mime_multipart_assemble(
256 boundary: *u8, blen: i64,
257 ct1: *u8, ct1len: i64, part1: *u8, p1len: i64,
258 ct2: *u8, ct2len: i64, part2: *u8, p2len: i64,
259 out: *u8, cap: i64
260) -> i64 {
261 var oi: i64 = 0
262 out[oi] = 45 as u8; out[oi + 1] = 45 as u8; oi = oi + 2
263 oi = mm_catn(out, oi, boundary, blen)
264 out[oi] = 13 as u8; out[oi + 1] = 10 as u8; oi = oi + 2
265 oi = mm_cat(out, oi, "Content-Type: " as *u8); oi = mm_catn(out, oi, ct1, ct1len)
266 out[oi] = 13 as u8; out[oi + 1] = 10 as u8; out[oi + 2] = 13 as u8; out[oi + 3] = 10 as u8; oi = oi + 4
267 oi = mm_catn(out, oi, part1, p1len)
268 out[oi] = 13 as u8; out[oi + 1] = 10 as u8; oi = oi + 2
269 out[oi] = 45 as u8; out[oi + 1] = 45 as u8; oi = oi + 2
270 oi = mm_catn(out, oi, boundary, blen)
271 out[oi] = 13 as u8; out[oi + 1] = 10 as u8; oi = oi + 2
272 oi = mm_cat(out, oi, "Content-Type: " as *u8); oi = mm_catn(out, oi, ct2, ct2len)
273 out[oi] = 13 as u8; out[oi + 1] = 10 as u8; out[oi + 2] = 13 as u8; out[oi + 3] = 10 as u8; oi = oi + 4
274 oi = mm_catn(out, oi, part2, p2len)
275 out[oi] = 13 as u8; out[oi + 1] = 10 as u8; oi = oi + 2
276 out[oi] = 45 as u8; out[oi + 1] = 45 as u8; oi = oi + 2
277 oi = mm_catn(out, oi, boundary, blen)
278 out[oi] = 45 as u8; out[oi + 1] = 45 as u8; out[oi + 2] = 13 as u8; out[oi + 3] = 10 as u8; oi = oi + 4
279 return oi
280}
281
282// Extract the idx-th (0-based) part's content (between its CRLFCRLF and
283// the next boundary). Returns content length (null-terminated) or -verdict.
284func nx_mime_multipart_part(body: *u8, n: i64, boundary: *u8, blen: i64, idx: i64, out: *u8, out_cap: i64) -> i64 {
285 var occ: i64 = 0 - 1
286 var i: i64 = 0
287 var content_start: i64 = 0 - 1
288 while i + 2 + blen <= n && content_start < 0 {
289 if (body[i] & 0xff) == 45 && (body[i + 1] & 0xff) == 45 && mm_match(body + i + 2, boundary, blen) == 1 {
290 occ = occ + 1
291 var ds: i64 = i + 2 + blen
292 if occ == idx {
293 if ds + 1 < n && (body[ds] & 0xff) == 13 && (body[ds + 1] & 0xff) == 10 { ds = ds + 2 }
294 var hb: i64 = ds
295 var found: i64 = 0 - 1
296 while hb + 4 <= n && found < 0 {
297 if (body[hb] & 0xff) == 13 && (body[hb + 1] & 0xff) == 10 && (body[hb + 2] & 0xff) == 13 && (body[hb + 3] & 0xff) == 10 {
298 found = hb + 4
299 } else { hb = hb + 1 }
300 }
301 if found < 0 { return 0 - NX_MIME_NOT_FOUND }
302 content_start = found
303 } else {
304 i = ds
305 }
306 } else { i = i + 1 }
307 }
308 if content_start < 0 { return 0 - NX_MIME_NOT_FOUND }
309 var ce: i64 = 0 - 1
310 var j: i64 = content_start
311 while j + 4 + blen <= n && ce < 0 {
312 if (body[j] & 0xff) == 13 && (body[j + 1] & 0xff) == 10 && (body[j + 2] & 0xff) == 45 && (body[j + 3] & 0xff) == 45 && mm_match(body + j + 4, boundary, blen) == 1 {
313 ce = j
314 } else { j = j + 1 }
315 }
316 if ce < 0 { return 0 - NX_MIME_NOT_FOUND }
317 var oi: i64 = 0
318 var p: i64 = content_start
319 while p < ce && oi < out_cap - 1 { out[oi] = body[p]; oi = oi + 1; p = p + 1 }
320 out[oi] = 0 as u8
321 return oi
322}