code wiki / (root) / nx_email_mime.nx

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}