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, "<" as *u8) }
61 else { if c == 62 { oi = wm_cat(out, oi, ">" as *u8) }
62 else { if c == 38 { oi = wm_cat(out, oi, "&" 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 — " 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, " · <a href=\"/msg?n=" as *u8); oi = wm_int(out, oi, i)
100 oi = wm_cat(out, oi, "\">view</a> · <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=\"/\">← 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}