code wiki / (root) / nx_email_webmail.nx

nx_email_webmail.nx source

↩ module page · 209 lines · 9375 B

1// nx_email_webmail.nx -- EMAIL LIVE LAYER: web UI to manage a mailbox. 2// 3// module: nishi-core.email.webmail 4// depends: nishi-core.email.mailbox, nishi-core.email.mime 5// capability: CORE_EMAIL 6// 7// The MANAGE half of a working mailbox: an HTTP/1.1 handler that renders 8// the R3 store as a webmail inbox -- list messages (Subject/From parsed 9// via R2 nx_mime_header_find), view one, and DELETE one (soft-delete via 10// R3 tombstone). Pure HTML over the store; no third-party web framework. 11// nx_webmail_serve_conn drops into an accept loop (the gate forks a real 12// HTTP client at it; a daemon would loop accept()). 13// 14// Routes: GET / -> inbox list 15// GET /msg?n=K -> message K full text 16// GET /del?n=K -> soft-delete K, redraw inbox 17// 18// license_tier: INDEPENDENT_REDERIVE 19// genealogy_id: international-research-sources/ietf/rfc_9110 20// lineage_id: nishi_email_webmail 21// 22// nx_safety_envelope: 23// intended_use: "Render/manage a mailbox over HTTP/1.1. Compose 24// R3 store + R2 header parse." 25// sil_target: SIL1 (display; delete is soft/reversible) 26// evidence: [list_shows_subjects, delete_tombstones, 27// html_escaped, redraw_reflects_delete] 28// hazard_register: [bug-tape-html-injection, bug-tape-stored-xss] 29// residual_risk: "AuthN on the UI + CSRF token on /del + pagination 30// are follow-ons (bind 127.0.0.1 only for now)." 31// verdict: NOT_YET_EVALUATED 32 33import "nx_syscalls.nx" 34import "nx_email_mailbox.nx" 35import "nx_email_mime.nx" 36const WM_MAGIC_2048: i64 = 2048 37const WM_MAGIC_8192: i64 = 8192 38const WM_MAGIC_8191: i64 = 8191 39const WM_MAGIC_1024: i64 = 1024 40const WM_MAGIC_262144: i64 = 262144 41const WM_MAGIC_270000: i64 = 270000 42 43func wm_cat(out: *u8, oi: i64, s: *u8) -> i64 { 44 var k: i64 = 0 45 while s[k] != (0 as u8) { out[oi] = s[k]; oi = oi + 1; k = k + 1 } 46 return oi 47} 48func wm_int(out: *u8, oi: i64, v: i64) -> i64 { 49 if v == 0 { out[oi] = 48 as u8; return oi + 1 } 50 let t: *u8 = sys_mmap(28); var m: i64 = v; var k: i64 = 0 51 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 52 while k > 0 { k = k - 1; out[oi] = t[k]; oi = oi + 1 } 53 return oi 54} 55// append src[0..n) HTML-escaped. 56func wm_esc(out: *u8, oi: i64, src: *u8, n: i64) -> i64 { 57 var i: i64 = 0 58 while i < n { 59 let c: i64 = src[i] & 0xff 60 if c == 60 { oi = wm_cat(out, oi, "&lt;" as *u8) } 61 else { if c == 62 { oi = wm_cat(out, oi, "&gt;" as *u8) } 62 else { if c == 38 { oi = wm_cat(out, oi, "&amp;" as *u8) } 63 else { out[oi] = c as u8; oi = oi + 1 } } } 64 i = i + 1 65 } 66 return oi 67} 68 69const WM_STYLE: *u8 = "<style>body{font-family:system-ui,sans-serif;margin:2rem;background:#0b1020;color:#e6e9f0}a{color:#7ab8ff;text-decoration:none}h1{font-size:1.25rem}ul{list-style:none;padding:0}li{padding:.6rem .8rem;border:1px solid #243049;border-radius:8px;margin:.45rem 0;background:#121a2e}.f{color:#9fb3d9;font-size:.85rem;margin-top:.2rem}.s{font-weight:600}pre{white-space:pre-wrap;background:#121a2e;border:1px solid #243049;border-radius:8px;padding:1rem}</style>" 70 71// Render the inbox list HTML into out. Returns length. 72func nx_webmail_render_inbox(prefix: *u8, mailbox: *u8, out: *u8, cap: i64) -> i64 { 73 var oi: i64 = 0 74 oi = wm_cat(out, oi, "<!doctype html><html><head><meta charset=utf-8><title>Nishi Mail</title>" as *u8) 75 oi = wm_cat(out, oi, WM_STYLE) 76 oi = wm_cat(out, oi, "</head><body><h1>Nishi Mail &mdash; " as *u8) 77 oi = wm_cat(out, oi, mailbox) 78 oi = wm_cat(out, oi, "</h1>" as *u8) 79 let n: i64 = nx_mbox_count(prefix, mailbox) 80 let pp: *i64 = sys_mmap(8) as *i64 81 let ll: *i64 = sys_mmap(8) as *i64 82 let bo: *i64 = sys_mmap(8) as *i64 83 let val: *u8 = sys_mmap(WM_MAGIC_2048) 84 var shown: i64 = 0 85 oi = wm_cat(out, oi, "<ul>" as *u8) 86 var i: i64 = 0 87 while i < n { 88 if nx_mbox_get_n(prefix, mailbox, i, pp, ll) == 1 { 89 let raw: *u8 = pp[0] as *u8 90 let rlen: i64 = ll[0] 91 var hlen: i64 = nx_mime_split_body(raw, rlen, bo) 92 if hlen < 0 { hlen = rlen } 93 oi = wm_cat(out, oi, "<li><div class=s>" as *u8) 94 let sl: i64 = nx_mime_header_find(raw, hlen, "Subject" as *u8, 7, val, WM_MAGIC_2048) 95 if sl >= 0 { oi = wm_esc(out, oi, val, sl) } else { oi = wm_cat(out, oi, "(no subject)" as *u8) } 96 oi = wm_cat(out, oi, "</div><div class=f>From: " as *u8) 97 let fl: i64 = nx_mime_header_find(raw, hlen, "From" as *u8, 4, val, WM_MAGIC_2048) 98 if fl >= 0 { oi = wm_esc(out, oi, val, fl) } 99 oi = wm_cat(out, oi, " &middot; <a href=\"/msg?n=" as *u8); oi = wm_int(out, oi, i) 100 oi = wm_cat(out, oi, "\">view</a> &middot; <a href=\"/del?n=" as *u8); oi = wm_int(out, oi, i) 101 oi = wm_cat(out, oi, "\">delete</a></div></li>" as *u8) 102 shown = shown + 1 103 } 104 i = i + 1 105 } 106 oi = wm_cat(out, oi, "</ul>" as *u8) 107 if shown == 0 { oi = wm_cat(out, oi, "<p>(inbox empty)</p>" as *u8) } 108 oi = wm_cat(out, oi, "</body></html>" as *u8) 109 return oi 110} 111 112// Render a single message view. Returns length. 113func nx_webmail_render_message(prefix: *u8, mailbox: *u8, idx: i64, out: *u8, cap: i64) -> i64 { 114 var oi: i64 = 0 115 oi = wm_cat(out, oi, "<!doctype html><html><head><meta charset=utf-8><title>Nishi Mail</title>" as *u8) 116 oi = wm_cat(out, oi, WM_STYLE) 117 oi = wm_cat(out, oi, "</head><body><a href=\"/\">&larr; inbox</a><h1>Message #" as *u8) 118 oi = wm_int(out, oi, idx) 119 oi = wm_cat(out, oi, "</h1><pre>" as *u8) 120 let pp: *i64 = sys_mmap(8) as *i64 121 let ll: *i64 = sys_mmap(8) as *i64 122 if nx_mbox_get_n(prefix, mailbox, idx, pp, ll) == 1 { 123 oi = wm_esc(out, oi, pp[0] as *u8, ll[0]) 124 } else { 125 oi = wm_cat(out, oi, "(message not found)" as *u8) 126 } 127 oi = wm_cat(out, oi, "</pre></body></html>" as *u8) 128 return oi 129} 130 131// Wrap an HTML body in an HTTP/1.1 200 response. Returns total length. 132func nx_webmail_http_response(out: *u8, cap: i64, body: *u8, blen: i64) -> i64 { 133 var oi: i64 = 0 134 oi = wm_cat(out, oi, "HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\nContent-Length: " as *u8) 135 oi = wm_int(out, oi, blen) 136 oi = wm_cat(out, oi, "\r\nConnection: close\r\n\r\n" as *u8) 137 var i: i64 = 0 138 while i < blen { out[oi] = body[i]; oi = oi + 1; i = i + 1 } 139 return oi 140} 141 142// extract the request path token ("GET <path> HTTP/1.1") into out. 143func wm_req_path(req: *u8, n: i64, out: *u8, cap: i64) -> i64 { 144 var i: i64 = 0 145 while i < n && (req[i] & 0xff) != 32 { i = i + 1 } // skip method 146 i = i + 1 147 var oi: i64 = 0 148 while i < n && (req[i] & 0xff) != 32 && (req[i] & 0xff) != 13 && (req[i] & 0xff) != 10 && oi < cap - 1 { 149 out[oi] = req[i]; oi = oi + 1; i = i + 1 150 } 151 out[oi] = 0 as u8 152 return oi 153} 154// parse "n=<digits>" query param; -1 if absent. 155func wm_n_param(path: *u8, plen: i64) -> i64 { 156 var i: i64 = 0 157 while i + 1 < plen { 158 if (path[i] & 0xff) == 110 && (path[i + 1] & 0xff) == 61 { 159 var k: i64 = i + 2; var v: i64 = 0; var got: i64 = 0 160 while k < plen && (path[k] & 0xff) >= 48 && (path[k] & 0xff) <= 57 { v = v * 10 + ((path[k] & 0xff) - 48); k = k + 1; got = 1 } 161 if got == 1 { return v } 162 } 163 i = i + 1 164 } 165 return 0 - 1 166} 167func wm_starts(path: *u8, plen: i64, pfx: *u8, pl: i64) -> i64 { 168 if plen < pl { return 0 } 169 var i: i64 = 0 170 while i < pl { if (path[i] & 0xff) != (pfx[i] & 0xff) { return 0 } i = i + 1 } 171 return 1 172} 173 174// Serve ONE HTTP connection: read the request line, route, respond. 175// segctr is the monotonic segment id source for delete commits. 176func nx_webmail_serve_conn(fd: i64, prefix: *u8, mailbox: *u8, segctr: *i64) -> i64 { 177 let req: *u8 = sys_mmap(WM_MAGIC_8192) 178 var rn: i64 = 0 179 // read up to the end of the request line (LF) 180 while rn < WM_MAGIC_8191 { 181 let r: i64 = sys_read(fd, (req as i64 + rn) as *u8, 1) 182 if r <= 0 { rn = rn } else { let c: i64 = req[rn] & 0xff; rn = rn + 1; if c == 10 { rn = WM_MAGIC_8191 } } 183 if r <= 0 { rn = WM_MAGIC_8191 } 184 } 185 let path: *u8 = sys_mmap(WM_MAGIC_1024) 186 let plen: i64 = wm_req_path(req, WM_MAGIC_8191, path, WM_MAGIC_1024) 187 let body: *u8 = sys_mmap(WM_MAGIC_262144) 188 var blen: i64 = 0 189 if wm_starts(path, plen, "/del" as *u8, 4) == 1 { 190 let nn: i64 = wm_n_param(path, plen) 191 if nn >= 0 { nx_mbox_delete_n(prefix, mailbox, nn, *segctr); *segctr = *segctr + 1 } 192 blen = nx_webmail_render_inbox(prefix, mailbox, body, WM_MAGIC_262144) 193 } else { 194 if wm_starts(path, plen, "/msg" as *u8, 4) == 1 { 195 let nn2: i64 = wm_n_param(path, plen) 196 blen = nx_webmail_render_message(prefix, mailbox, nn2, body, WM_MAGIC_262144) 197 } else { 198 blen = nx_webmail_render_inbox(prefix, mailbox, body, WM_MAGIC_262144) 199 } 200 } 201 let resp: *u8 = sys_mmap(WM_MAGIC_270000) 202 let total: i64 = nx_webmail_http_response(resp, WM_MAGIC_270000, body, blen) 203 var w: i64 = 0 204 while w < total { 205 let k: i64 = sys_write(fd, (resp as i64 + w) as *u8, total - w) 206 if k <= 0 { w = total } else { w = w + k } 207 } 208 return blen 209}