code wiki / _hdl_build / nx_email_portal_daemon.nx
nx_email_portal_daemon.nx source
↩ module page · 973 lines · 56025 B
1// nx_email_portal_daemon.nx -- the mail.<domain> EMAIL-PORTAL daemon on the CANONICAL ecosystem auth
2// (the SAME Modern Auth the doc portal / status use: OPAQUE-3DH + Argon2id + NO-COOKIE Ed25519 session).
3// Replaces the demo-grade nx_email_webmail_daemon (hardcoded single mailbox, NO auth) with a real
4// multi-user, per-domain portal:
5// GET / -> login + register + mail SPA shell (public)
6// POST /mail/register -> invite-gated OPAQUE self-registration (handle -> <handle>@<domain>)
7// POST /mail/login -> handle+passphrase -> 200 {"token":...} | 401
8// * /mail/* (token) -> X-Nishi-Session validates -> the mailbox is derived ONLY from the
9// session handle (per-user isolation BY CONSTRUCTION -- no request
10// parameter can address another user's mailbox):
11// GET /mail/inbox -> HTML fragment: newest-first message list (Subject/From, escaped)
12// GET /mail/msg?n=K -> HTML fragment: the raw message, HTML-escaped
13// POST /mail/del?n=K -> soft-delete (seg_store tombstone; history preserved, rule 13)
14// POST /mail/send -> to/subject/body form: rcpt@<domain> -> REAL local delivery into the
15// rcpt mailbox (same store the data-driven MTA delivers into, so real
16// inbound mail appears here the moment MX/relay is armed);
17// foreign rcpt -> appended to relay-queue@<domain> + an HONEST
18// "QUEUED-EXTERNAL (relay not armed)" receipt -- never a fake send.
19// GET /mail/whoami -> {"handle":...,"mailbox":...}
20// Loopback HTTP daemon behind the SNI router's mail.<d> row (sites.elf reverse-proxies; TLS terminates
21// at sites.elf with the per-domain wildcard). Every response carries Content-Length (the buffered-proxy
22// contract). Pure router epd_handle = bytes-in/bytes-out (the gate drives it in-process, no socket).
23// license_tier: ORIGINAL
24import "nx_status_daemon.nx"
25import "hub/nx_modern_auth_flow.nx"
26import "nx_syscalls.nx"
27import "nx_invite_token.nx" // invite-token store: inv_check / inv_consume (mint via nx_invite_issue_cli)
28import "nx_opaque_login.nx" // olg_register (OPAQUE one-shot over nx_modern_auth_register)
29import "nx_email_webmail.nx" // wm_esc + nx_email_mailbox (store) + nx_email_mime (header parse)
30const EPD_MAGIC_1024: i64 = 1024
31const EPD_MAGIC_8192: i64 = 8192
32
33const EPD_REQCAP: i64 = 65536
34const EPD_OUTCAP: i64 = 262144
35const EPD_BODYCAP: i64 = 131072
36const EPD_MSGCAP: i64 = 24576 // assembled outbound message cap
37const EPD_SHOWCAP: i64 = 16384 // per-message render cap (truncated view beyond)
38const EPD_INBOX_PAGE: i64 = 50 // newest-first page size
39
40// ---- handle hygiene: [a-z0-9][a-z0-9._-]{0,31} (defensive at the boundary; the mailbox is a
41// seg_store KEY so this is address hygiene + '#'/'@' key-scheme separation, not path safety) ----
42func epd_handle_ok(h: *u8, n: i64) -> i64 {
43 if n < 1 { return 0 }
44 if n > 32 { return 0 }
45 var i: i64 = 0
46 while i < n {
47 let c: i64 = h[i] as i64
48 var ok: i64 = 0
49 if c >= 97 { if c <= 122 { ok = 1 } }
50 if c >= 48 { if c <= 57 { ok = 1 } }
51 if i > 0 {
52 if c == 46 { ok = 1 }
53 if c == 95 { ok = 1 }
54 if c == 45 { ok = 1 }
55 }
56 if ok == 0 { return 0 }
57 i = i + 1
58 }
59 return 1
60}
61
62// ---- session-uid -> handle MAILMAP. The no-cookie session carries the 32-byte user-id HASH
63// (not the plaintext handle), so registration appends "MAP <64hex-uid> <handle>\n" to
64// "<storefile>.mailmap" (append-only, rule 13) and the session gate resolves hash -> handle.
65func epd_hex32(src: *u8, out: *u8) -> i64 {
66 let hx: *u8 = "0123456789abcdef" as *u8
67 var i: i64 = 0
68 while i < 32 {
69 out[i * 2] = hx[((src[i] as i64) >> 4) & 15]
70 out[i * 2 + 1] = hx[(src[i] as i64) & 15]
71 i = i + 1
72 }
73 out[64] = 0 as u8
74 return 64
75}
76
77func epd_mailmap_path(ctx: *NxAuthContext, out: *u8) -> i64 {
78 let sp: *u8 = ctx.account_store as *u8
79 var i: i64 = 0
80 while sp[i] != (0 as u8) { out[i] = sp[i]; i = i + 1 }
81 let suf: *u8 = ".mailmap" as *u8
82 var j: i64 = 0
83 while suf[j] != (0 as u8) { out[i] = suf[j]; i = i + 1; j = j + 1 }
84 out[i] = 0 as u8
85 return i
86}
87
88func epd_map_put(path: *u8, hexuid: *u8, h: *u8, hn: i64) -> i64 {
89 let row: *u8 = sys_mmap(256)
90 var o: i64 = sd_cat(row, 0, "MAP " as *u8)
91 var i: i64 = 0
92 while i < 64 { row[o] = hexuid[i]; o = o + 1; i = i + 1 }
93 row[o] = 32 as u8; o = o + 1
94 i = 0
95 while i < hn { row[o] = h[i]; o = o + 1; i = i + 1 }
96 row[o] = 10 as u8; o = o + 1
97 let fd: i64 = sys_openat_append(path, 0x1a4)
98 if fd < 0 { return 0 - 1 }
99 sys_write(fd, row, o)
100 sys_close(fd)
101 return 0
102}
103
104// resolve hexuid -> handle (latest row wins); returns handle length or 0.
105func epd_map_get(path: *u8, hexuid: *u8, out: *u8, cap: i64) -> i64 {
106 let szp: *i64 = sys_mmap(16) as *i64
107 szp[0] = 0
108 let b: *u8 = sys_read_file(path, szp)
109 if (b as i64) == 0 { return 0 }
110 let n: i64 = szp[0]
111 var best: i64 = 0
112 var cur: i64 = 0
113 while cur < n {
114 var le: i64 = cur
115 while le < n && (b[le] as i64) != 10 { le = le + 1 }
116 // row: "MAP " + 64 hex + " " + handle
117 if le - cur > 69 {
118 if (b[cur] as i64) == 77 {
119 var m: i64 = 1
120 var k: i64 = 0
121 while k < 64 { if b[cur + 4 + k] != hexuid[k] { m = 0; k = 64 } else { k = k + 1 } }
122 if m == 1 {
123 var w: i64 = 0
124 var s: i64 = cur + 69
125 while s < le { if w < cap - 1 { out[w] = b[s]; w = w + 1 } s = s + 1 }
126 out[w] = 0 as u8
127 best = w
128 }
129 }
130 }
131 cur = le + 1
132 }
133 return best
134}
135
136// case-insensitive ASCII equality of h[0..hn) vs NUL-terminated lit
137func epd_ci_eq(h: *u8, hn: i64, lit: *u8) -> i64 {
138 var ln: i64 = 0
139 while lit[ln] != (0 as u8) { ln = ln + 1 }
140 if hn != ln { return 0 }
141 var i: i64 = 0
142 while i < ln {
143 var a: i64 = h[i] as i64
144 var b: i64 = lit[i] as i64
145 if a >= 65 { if a <= 90 { a = a + 32 } }
146 if b >= 65 { if b <= 90 { b = b + 32 } }
147 if a != b { return 0 }
148 i = i + 1
149 }
150 return 1
151}
152
153// build "<handle>@<domain>" NUL-terminated; returns length
154func epd_mailbox(h: *u8, hn: i64, domain: *u8, out: *u8) -> i64 {
155 var o: i64 = 0
156 var i: i64 = 0
157 while i < hn { out[o] = h[i]; o = o + 1; i = i + 1 }
158 out[o] = 64 as u8; o = o + 1
159 i = 0
160 while domain[i] != (0 as u8) { out[o] = domain[i]; o = o + 1; i = i + 1 }
161 out[o] = 0 as u8
162 return o
163}
164
165// full HTTP response with Content-Length (the buffered-proxy contract)
166func epd_resp(out: *u8, status: *u8, ctype: *u8, body: *u8, blen: i64) -> i64 {
167 var o: i64 = 0
168 o = sd_cat(out, o, "HTTP/1.1 " as *u8)
169 o = sd_cat(out, o, status)
170 o = sd_cat(out, o, "\r\nContent-Type: " as *u8)
171 o = sd_cat(out, o, ctype)
172 o = sd_cat(out, o, "\r\nConnection: close\r\nContent-Length: " as *u8)
173 o = sd_catn(out, o, blen)
174 o = sd_cat(out, o, "\r\n\r\n" as *u8)
175 var i: i64 = 0
176 while i < blen { out[o] = body[i]; o = o + 1; i = i + 1 }
177 return o
178}
179
180// ?n=<digits> from the path (path includes the query). -1 if absent.
181func epd_qn(path: *u8, pn: i64) -> i64 {
182 var q: i64 = 0 - 1
183 var i: i64 = 0
184 while i < pn { if (path[i] as i64) == 63 { q = i; i = pn } i = i + 1 }
185 if q < 0 { return 0 - 1 }
186 var j: i64 = q + 1
187 while j + 1 < pn {
188 if (path[j] as i64) == 110 {
189 if (path[j + 1] as i64) == 61 {
190 var ok: i64 = 0
191 if j == q + 1 { ok = 1 }
192 if j > q + 1 { if (path[j - 1] as i64) == 38 { ok = 1 } }
193 if ok == 1 {
194 var v: i64 = 0
195 var k: i64 = j + 2
196 var any: i64 = 0
197 while k < pn {
198 let c: i64 = path[k] as i64
199 if c >= 48 { if c <= 57 { v = v * 10 + (c - 48); any = 1; k = k + 1 } else { k = pn } } else { k = pn }
200 }
201 if any == 1 { return v }
202 return 0 - 1
203 }
204 }
205 }
206 j = j + 1
207 }
208 return 0 - 1
209}
210
211// the no-cookie mail SPA: login/register -> token in sessionStorage -> inbox/read/compose via fetch
212// with the X-Nishi-Session header (NEVER a cookie). Body-first so the header carries Content-Length.
213func epd_shell(domain: *u8, out: *u8) -> i64 {
214 let body: *u8 = sys_mmap(EPD_BODYCAP)
215 var b: i64 = 0
216 b = sd_cat(body, b, "<!DOCTYPE html><meta charset=utf-8><meta name=viewport content=\"width=device-width,initial-scale=1\"><title>Mail — " as *u8)
217 b = sd_cat(body, b, domain)
218 b = sd_cat(body, b, "</title>" as *u8)
219 b = sd_cat(body, b, "<style>body{font-family:-apple-system,Segoe UI,sans-serif;max-width:840px;margin:5vh auto;padding:0 20px;color:#1c1c1e}input,textarea{width:100%;padding:9px;margin:.35rem 0;box-sizing:border-box;border:1px solid #ccc;border-radius:7px}textarea{min-height:120px}button{padding:9px 16px;border:0;border-radius:7px;background:#2a4d8f;color:#fff;font-size:.95rem;margin:.2rem .3rem .2rem 0;cursor:pointer}.e{color:#b00;min-height:1.2em}.ok{color:#0a6}ul{list-style:none;padding:0}li.m{padding:.55rem .7rem;border:1px solid #dcdce2;border-radius:8px;margin:.4rem 0;cursor:pointer;background:#fafafc}li.m:hover{background:#eef2fb}.f{color:#667;font-size:.85rem;margin-left:.5rem}pre{white-space:pre-wrap;background:#f6f6f9;border:1px solid #dcdce2;border-radius:8px;padding:1rem;overflow-x:auto}h2{margin:.4rem 0}.tab{margin-bottom:1rem}</style>" as *u8)
220 b = sd_cat(body, b, "<div id=login><h2>✉ " as *u8)
221 b = sd_cat(body, b, domain)
222 b = sd_cat(body, b, " mail</h2><form id=lf method=post action=/mail/login><input type=hidden name=ui value=1><input id=h name=handle placeholder=\"handle (your mailbox name)\" autocomplete=username autofocus><input id=p name=passphrase type=password placeholder=\"passphrase\" autocomplete=current-password><button type=submit id=b>Sign in</button></form><button id=sr>I have an invite</button><p id=e class=e></p></div>" as *u8)
223 b = sd_cat(body, b, "<div id=reg hidden><h2>Create your mailbox</h2><input id=rh placeholder=\"choose a handle: a-z 0-9 . _ - (becomes handle@" as *u8)
224 b = sd_cat(body, b, domain)
225 b = sd_cat(body, b, ")\"><input id=rp type=password placeholder=\"choose a passphrase\"><input id=ri placeholder=\"invite code\"><button id=rb>Register</button><button id=rc>Back</button><p id=re class=e></p><pre id=rm hidden></pre></div>" as *u8)
226 b = sd_cat(body, b, "<div id=mail hidden><div class=tab><button id=ib>Inbox</button><button id=cb>Compose</button><span id=me class=f></span></div><div id=inbox></div><div id=comp hidden><h2>Compose</h2><input id=ct placeholder=\"to (handle, or handle@" as *u8)
227 b = sd_cat(body, b, domain)
228 b = sd_cat(body, b, ")\"><input id=cs placeholder=subject><textarea id=cx></textarea><button id=cd>Send</button><p id=cr class=e></p></div><div id=view hidden><button id=vb>← Back</button><button id=vd>Delete</button><div id=vc></div></div></div>" as *u8)
229 b = sd_cat(body, b, "<script>var L=document.getElementById('login'),G=document.getElementById('reg'),M=document.getElementById('mail'),IB=document.getElementById('inbox'),CP=document.getElementById('comp'),VW=document.getElementById('view'),VC=document.getElementById('vc'),E=document.getElementById('e'),curN=-1;" as *u8)
230 b = sd_cat(body, b, "function H(){return{'X-Nishi-Session':sessionStorage.nx_mail||''}}" as *u8)
231 b = sd_cat(body, b, "function inbox(){CP.hidden=true;VW.hidden=true;IB.hidden=false;fetch('/mail/inbox',{headers:H()}).then(function(r){if(!r.ok)throw 0;return r.text()}).then(function(t){IB.innerHTML=t;var xs=IB.querySelectorAll('li.m');for(var i=0;i<xs.length;i++){xs[i].onclick=(function(n){return function(){openMsg(n)}})(xs[i].getAttribute('data-n'))}}).catch(function(){M.hidden=true;L.hidden=false})}" as *u8)
232 b = sd_cat(body, b, "function openMsg(n){curN=n;fetch('/mail/msg?n='+n,{headers:H()}).then(function(r){return r.text()}).then(function(t){IB.hidden=true;CP.hidden=true;VW.hidden=false;VC.innerHTML=t})}" as *u8)
233 b = sd_cat(body, b, "document.getElementById('vb').onclick=inbox;document.getElementById('vd').onclick=function(){fetch('/mail/del?n='+curN,{method:'POST',headers:H()}).then(inbox)};" as *u8)
234 b = sd_cat(body, b, "document.getElementById('ib').onclick=inbox;document.getElementById('cb').onclick=function(){IB.hidden=true;VW.hidden=true;CP.hidden=false};" as *u8)
235 b = sd_cat(body, b, "document.getElementById('cd').onclick=function(){var R=document.getElementById('cr');R.textContent='';var q='to='+encodeURIComponent(ct.value)+'&subject='+encodeURIComponent(cs.value)+'&body='+encodeURIComponent(cx.value);fetch('/mail/send',{method:'POST',headers:Object.assign({'Content-Type':'application/x-www-form-urlencoded'},H()),body:q}).then(function(r){return r.text()}).then(function(t){R.className='ok';R.textContent=t}).catch(function(){R.className='e';R.textContent='Send failed.'})};" as *u8)
236 b = sd_cat(body, b, "document.getElementById('lf').onsubmit=function(){E.textContent='';var q='handle='+encodeURIComponent(h.value)+'&passphrase='+encodeURIComponent(p.value);fetch('/mail/login',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:q}).then(function(r){if(r.ok)return r.json();throw 0}).then(function(j){sessionStorage.nx_mail=j.token;L.hidden=true;M.hidden=false;fetch('/mail/whoami',{headers:H()}).then(function(r){return r.json()}).then(function(w){document.getElementById('me').textContent=w.mailbox});inbox()}).catch(function(){E.textContent='Wrong handle or passphrase.'});return false};" as *u8)
237 b = sd_cat(body, b, "document.getElementById('sr').onclick=function(){L.hidden=true;G.hidden=false};document.getElementById('rc').onclick=function(){G.hidden=true;L.hidden=false};" as *u8)
238 b = sd_cat(body, b, "document.getElementById('rb').onclick=function(){var R=document.getElementById('re'),RM=document.getElementById('rm');R.textContent='';var q='handle='+encodeURIComponent(rh.value)+'&pw='+encodeURIComponent(rp.value)+'&invite='+encodeURIComponent(ri.value);fetch('/mail/register',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:q}).then(function(r){if(r.ok)return r.json();throw 0}).then(function(j){RM.hidden=false;RM.textContent='Mailbox created. SAVE this recovery mnemonic now:\\n\\n'+j.mnemonic+'\\n\\nThen go back and sign in.'}).catch(function(){R.textContent='Registration failed (bad invite, taken handle, or invalid handle characters).'})};</script>" as *u8)
239 return epd_resp(out, "200 OK" as *u8, "text/html; charset=utf-8" as *u8, body, b)
240}
241
242// newest-first inbox HTML FRAGMENT (the shell fetches this with the session header)
243func epd_inbox_frag(prefix: *u8, mailbox: *u8, body: *u8) -> i64 {
244 let cnt: i64 = nx_mbox_count(prefix, mailbox)
245 var b: i64 = 0
246 b = sd_cat(body, b, "<h2>Inbox</h2>" as *u8)
247 if cnt == 0 {
248 b = sd_cat(body, b, "<p class=f>No messages yet.</p>" as *u8)
249 return b
250 }
251 let pp: *i64 = sys_mmap(8) as *i64
252 let ll: *i64 = sys_mmap(8) as *i64
253 let subj: *u8 = sys_mmap(EPD_MAGIC_1024)
254 let from: *u8 = sys_mmap(EPD_MAGIC_1024)
255 b = sd_cat(body, b, "<ul>" as *u8)
256 var shown: i64 = 0
257 var i: i64 = cnt - 1
258 while i >= 0 {
259 if shown < EPD_INBOX_PAGE {
260 let rc: i64 = nx_mbox_get_n(prefix, mailbox, i, pp, ll)
261 if rc == 1 {
262 let raw: *u8 = pp[0] as *u8
263 let rn: i64 = ll[0]
264 let sn: i64 = nx_mime_header_find(raw, rn, "Subject" as *u8, 7, subj, 1023)
265 let fn: i64 = nx_mime_header_find(raw, rn, "From" as *u8, 4, from, 1023)
266 b = sd_cat(body, b, "<li class=m data-n=" as *u8)
267 b = sd_catn(body, b, i)
268 b = sd_cat(body, b, "><b>" as *u8)
269 if sn > 0 { b = wm_esc(body, b, subj, sn) } else { b = sd_cat(body, b, "(no subject)" as *u8) }
270 b = sd_cat(body, b, "</b><span class=f>" as *u8)
271 if fn > 0 { b = wm_esc(body, b, from, fn) }
272 b = sd_cat(body, b, "</span></li>" as *u8)
273 shown = shown + 1
274 }
275 }
276 i = i - 1
277 }
278 b = sd_cat(body, b, "</ul><p class=f>" as *u8)
279 b = sd_catn(body, b, cnt)
280 b = sd_cat(body, b, " message slot(s); deleted ones are tombstoned, never erased.</p>" as *u8)
281 return b
282}
283
284// assemble a minimal RFC-5322-shaped message (local delivery; the relay rung re-frames for the wire)
285func epd_assemble(fromb: *u8, tob: *u8, tn: i64, subjb: *u8, sn: i64, bodyb: *u8, bn: i64, msg: *u8) -> i64 {
286 var m: i64 = 0
287 m = sd_cat(msg, m, "From: " as *u8)
288 m = sd_cat(msg, m, fromb)
289 m = sd_cat(msg, m, "\r\nTo: " as *u8)
290 var i: i64 = 0
291 while i < tn { msg[m] = tob[i]; m = m + 1; i = i + 1 }
292 m = sd_cat(msg, m, "\r\nSubject: " as *u8)
293 i = 0
294 while i < sn { msg[m] = subjb[i]; m = m + 1; i = i + 1 }
295 m = sd_cat(msg, m, "\r\nDate-Unix: " as *u8)
296 m = sd_catn(msg, m, sys_now_realtime_sec())
297 m = sd_cat(msg, m, "\r\n\r\n" as *u8)
298 i = 0
299 while i < bn { msg[m] = bodyb[i]; m = m + 1; i = i + 1 }
300 return m
301}
302
303// ===== NISHI-FIRST no-JS webmail (2026-07-05 doctrine). Full-page routes carrying the no-cookie session
304// in ?s= (shared nx_sa_validate_qs), so the Nishi browser reads + composes mail with ZERO JS -- fetch/SPA
305// stays for the third-party last mile. NO Set-Cookie (C1 preserved). =====
306
307// auth via ?s= + resolve the mailbox (identity ONLY from the validated session -> mailmap -> handle@dom).
308// returns 1 (authed, shand/shn/mbox filled) or 0. 8 args (arg-cap safe).
309func epd_qs_mailbox(ctx: *NxAuthContext, domain: *u8, path: *u8, pn: i64, now: i64, shand: *u8, shn_p: *i64, mbox: *u8) -> i64 {
310 shn_p[0] = 0
311 let suid: *u8 = sys_mmap(64)
312 let sun: *i64 = sys_mmap(8) as *i64
313 sun[0] = 0
314 if nx_sa_validate_qs(ctx, path, pn, now, suid, 64, sun) != NX_MAUTH_OK { return 0 }
315 if sun[0] != 32 { return 0 }
316 let uhex: *u8 = sys_mmap(72)
317 epd_hex32(suid, uhex)
318 let mmp: *u8 = sys_mmap(512)
319 epd_mailmap_path(ctx, mmp)
320 let hn: i64 = epd_map_get(mmp, uhex, shand, 64)
321 if hn <= 0 { return 0 }
322 if epd_handle_ok(shand, hn) != 1 { return 0 }
323 shn_p[0] = hn
324 epd_mailbox(shand, hn, domain, mbox)
325 return 1
326}
327
328// splice the raw (already %-encoded) session token into a body buffer at offset b; returns new offset.
329func epd_splice(body: *u8, b: i64, sraw: *u8, srn: i64) -> i64 {
330 var o: i64 = b
331 var i: i64 = 0
332 while i < srn { body[o] = sraw[i]; o = o + 1; i = i + 1 }
333 return o
334}
335
336// the no-JS landing after a form login: link to the inbox carrying the session in ?s=.
337func epd_landing_mail(out: *u8, b64: *u8, b64n: i64) -> i64 {
338 let body: *u8 = sys_mmap(EPD_BODYCAP)
339 var b: i64 = 0
340 b = sd_cat(body, b, "<!DOCTYPE html><meta charset=utf-8><title>Signed in</title><body style=\"font-family:sans-serif;max-width:760px;margin:6vh auto;padding:0 20px\"><h2>✉ Signed in</h2><p>No-JS webmail (nishi-first).</p><p><a href=\"/mail/box?s=" as *u8)
341 b = nx_sa_tok_urlenc(b64, b64n, body, b)
342 b = sd_cat(body, b, "\">Open your mailbox →</a></p>" as *u8)
343 return epd_resp(out, "200 OK" as *u8, "text/html; charset=utf-8" as *u8, body, b)
344}
345
346// full no-JS inbox page: each message is an <a> to /mail/read?s=..&n=K (no data-n, no JS).
347func epd_inbox_page(prefix: *u8, mbox: *u8, sraw: *u8, srn: i64, out: *u8) -> i64 {
348 let body: *u8 = sys_mmap(EPD_BODYCAP)
349 var b: i64 = 0
350 b = sd_cat(body, b, "<!DOCTYPE html><meta charset=utf-8><meta name=viewport content=\"width=device-width,initial-scale=1\"><title>Inbox</title><body style=\"font-family:sans-serif;max-width:760px;margin:4vh auto;padding:0 20px\"><h2>Inbox</h2><p><a href=\"/mail/write?s=" as *u8)
351 b = epd_splice(body, b, sraw, srn)
352 b = sd_cat(body, b, "\">✎ Compose</a></p>" as *u8)
353 let cnt: i64 = nx_mbox_count(prefix, mbox)
354 if cnt == 0 { b = sd_cat(body, b, "<p style=color:#667>No messages yet.</p>" as *u8) }
355 else {
356 let pp: *i64 = sys_mmap(8) as *i64
357 let ll: *i64 = sys_mmap(8) as *i64
358 let subj: *u8 = sys_mmap(EPD_MAGIC_1024)
359 let from: *u8 = sys_mmap(EPD_MAGIC_1024)
360 b = sd_cat(body, b, "<ul style=\"list-style:none;padding:0\">" as *u8)
361 var shown: i64 = 0
362 var i: i64 = cnt - 1
363 while i >= 0 {
364 if shown < EPD_INBOX_PAGE {
365 let rc: i64 = nx_mbox_get_n(prefix, mbox, i, pp, ll)
366 if rc == 1 {
367 let raw: *u8 = pp[0] as *u8
368 let rn: i64 = ll[0]
369 let sn: i64 = nx_mime_header_find(raw, rn, "Subject" as *u8, 7, subj, 1023)
370 let fn: i64 = nx_mime_header_find(raw, rn, "From" as *u8, 4, from, 1023)
371 b = sd_cat(body, b, "<li style=\"padding:.5rem .7rem;border:1px solid #dcdce2;border-radius:8px;margin:.4rem 0\"><a href=\"/mail/read?s=" as *u8)
372 b = epd_splice(body, b, sraw, srn)
373 b = sd_cat(body, b, "&n=" as *u8)
374 b = sd_catn(body, b, i)
375 b = sd_cat(body, b, "\"><b>" as *u8)
376 if sn > 0 { b = wm_esc(body, b, subj, sn) } else { b = sd_cat(body, b, "(no subject)" as *u8) }
377 b = sd_cat(body, b, "</b></a> <span style=\"color:#667;font-size:.85rem\">" as *u8)
378 if fn > 0 { b = wm_esc(body, b, from, fn) }
379 b = sd_cat(body, b, "</span></li>" as *u8)
380 shown = shown + 1
381 }
382 }
383 i = i - 1
384 }
385 b = sd_cat(body, b, "</ul>" as *u8)
386 }
387 return epd_resp(out, "200 OK" as *u8, "text/html; charset=utf-8" as *u8, body, b)
388}
389
390// no-JS read page: the message + a delete FORM + back link (all carry ?s=).
391func epd_read_page(prefix: *u8, mbox: *u8, idx: i64, sraw: *u8, srn: i64, out: *u8) -> i64 {
392 let body: *u8 = sys_mmap(EPD_BODYCAP)
393 var b: i64 = 0
394 b = sd_cat(body, b, "<!DOCTYPE html><meta charset=utf-8><title>Message</title><body style=\"font-family:sans-serif;max-width:760px;margin:4vh auto;padding:0 20px\"><p><a href=\"/mail/box?s=" as *u8)
395 b = epd_splice(body, b, sraw, srn)
396 b = sd_cat(body, b, "\">← Inbox</a></p>" as *u8)
397 let pp: *i64 = sys_mmap(8) as *i64
398 let ll: *i64 = sys_mmap(8) as *i64
399 let rc: i64 = nx_mbox_get_n(prefix, mbox, idx, pp, ll)
400 if rc == 1 {
401 b = sd_cat(body, b, "<pre style=\"white-space:pre-wrap;background:#f6f6f9;border:1px solid #dcdce2;border-radius:8px;padding:1rem;overflow-x:auto\">" as *u8)
402 var show: i64 = ll[0]
403 if show > EPD_SHOWCAP { show = EPD_SHOWCAP }
404 b = wm_esc(body, b, pp[0] as *u8, show)
405 if ll[0] > EPD_SHOWCAP { b = sd_cat(body, b, "\n[... truncated view ...]" as *u8) }
406 b = sd_cat(body, b, "</pre><form method=post action=\"/mail/rm?s=" as *u8)
407 b = epd_splice(body, b, sraw, srn)
408 b = sd_cat(body, b, "&n=" as *u8)
409 b = sd_catn(body, b, idx)
410 b = sd_cat(body, b, "\"><button style=\"padding:8px 16px;border:0;border-radius:7px;background:#b00;color:#fff\">Delete</button></form>" as *u8)
411 } else { b = sd_cat(body, b, "<p style=color:#667>This message was deleted (tombstoned).</p>" as *u8) }
412 return epd_resp(out, "200 OK" as *u8, "text/html; charset=utf-8" as *u8, body, b)
413}
414
415// no-JS compose page: a real form POSTing to /mail/xsend?s=.
416func epd_write_page(domain: *u8, sraw: *u8, srn: i64, out: *u8) -> i64 {
417 let body: *u8 = sys_mmap(EPD_BODYCAP)
418 var b: i64 = 0
419 b = sd_cat(body, b, "<!DOCTYPE html><meta charset=utf-8><meta name=viewport content=\"width=device-width,initial-scale=1\"><title>Compose</title><body style=\"font-family:sans-serif;max-width:760px;margin:4vh auto;padding:0 20px\"><h2>Compose</h2><form method=post action=\"/mail/xsend?s=" as *u8)
420 b = epd_splice(body, b, sraw, srn)
421 b = sd_cat(body, b, "\"><p>To: <input name=to placeholder=\"handle, or handle@" as *u8)
422 b = sd_cat(body, b, domain)
423 b = sd_cat(body, b, "\" style=\"width:100%;padding:8px\"></p><p>Subject: <input name=subject style=\"width:100%;padding:8px\"></p><p><textarea name=body rows=12 style=\"width:100%\"></textarea></p><button type=submit style=\"padding:9px 16px;border:0;border-radius:7px;background:#2a4d8f;color:#fff\">Send</button> <a href=\"/mail/box?s=" as *u8)
424 b = epd_splice(body, b, sraw, srn)
425 b = sd_cat(body, b, "\">Cancel</a></form>" as *u8)
426 return epd_resp(out, "200 OK" as *u8, "text/html; charset=utf-8" as *u8, body, b)
427}
428
429// deliver a composed message from `shand` -> `tob`: local recipient (same domain) into their mailbox,
430// foreign -> the honest relay-queue (durable, relay a named unarmed rung). lens[0]=shn lens[1]=tn
431// lens[2]=sn lens[3]=bn. Writes a human result into rmsg; returns its length. Shared by the JS /mail/send
432// and the no-JS /mail/xsend (one delivery engine, two front doors). 8 args (arg-cap safe).
433func epd_deliver(domain: *u8, prefix: *u8, shand: *u8, tob: *u8, sub: *u8, bod: *u8, lens: *i64, rmsg: *u8) -> i64 {
434 let shn: i64 = lens[0]
435 let tn: i64 = lens[1]
436 let sn: i64 = lens[2]
437 let bn: i64 = lens[3]
438 let fromb: *u8 = sys_mmap(256)
439 epd_mailbox(shand, shn, domain, fromb)
440 let msg: *u8 = sys_mmap(EPD_MSGCAP)
441 let mlen: i64 = epd_assemble(fromb, tob, tn, sub, sn, bod, bn, msg)
442 var at: i64 = 0 - 1
443 var ti: i64 = 0
444 while ti < tn { if (tob[ti] as i64) == 64 { if at < 0 { at = ti } } ti = ti + 1 }
445 var local_n: i64 = tn
446 var dom_off: i64 = 0 - 1
447 var dom_n: i64 = 0
448 if at >= 0 { local_n = at; dom_off = at + 1; dom_n = tn - at - 1 }
449 var is_local: i64 = 1
450 if at >= 0 { let dp: *u8 = tob + dom_off; is_local = epd_ci_eq(dp, dom_n, domain) }
451 let ts: i64 = sys_now_us()
452 var r: i64 = 0
453 if is_local == 1 {
454 if epd_handle_ok(tob, local_n) == 1 {
455 let rmb: *u8 = sys_mmap(256)
456 epd_mailbox(tob, local_n, domain, rmb)
457 let seq: i64 = nx_mbox_append(prefix, rmb, msg, mlen, ts)
458 if seq >= 0 { r = sd_cat(rmsg, 0, "Delivered to " as *u8); r = sd_cat(rmsg, r, rmb) }
459 else { r = sd_cat(rmsg, 0, "Delivery failed." as *u8) }
460 } else { r = sd_cat(rmsg, 0, "Bad recipient handle (a-z 0-9 . _ -)." as *u8) }
461 } else {
462 let qmb: *u8 = sys_mmap(256)
463 var qo: i64 = sd_cat(qmb, 0, "relay-queue@" as *u8)
464 qo = sd_cat(qmb, qo, domain)
465 qmb[qo] = 0 as u8
466 let qseq: i64 = nx_mbox_append(prefix, qmb, msg, mlen, ts)
467 if qseq >= 0 { r = sd_cat(rmsg, 0, "Queued for external relay (relay not armed yet; stored durably, nothing pretended)." as *u8) }
468 else { r = sd_cat(rmsg, 0, "Queue failed." as *u8) }
469 }
470 return r
471}
472
473// no-JS send RESULT page: the delivery outcome + links back to compose / inbox (session in ?s=).
474func epd_sent_page(rmsg: *u8, rlen: i64, sraw: *u8, srn: i64, out: *u8) -> i64 {
475 let body: *u8 = sys_mmap(EPD_BODYCAP)
476 var b: i64 = 0
477 b = sd_cat(body, b, "<!DOCTYPE html><meta charset=utf-8><title>Sent</title><body style=\"font-family:sans-serif;max-width:760px;margin:5vh auto;padding:0 20px\"><h2>✉ " as *u8)
478 b = wm_esc(body, b, rmsg, rlen)
479 b = sd_cat(body, b, "</h2><p><a href=\"/mail/box?s=" as *u8)
480 b = epd_splice(body, b, sraw, srn)
481 b = sd_cat(body, b, "\">← Inbox</a> <a href=\"/mail/write?s=" as *u8)
482 b = epd_splice(body, b, sraw, srn)
483 b = sd_cat(body, b, "\">Compose another</a></p>" as *u8)
484 return epd_resp(out, "200 OK" as *u8, "text/html; charset=utf-8" as *u8, body, b)
485}
486
487// ---- THE ROUTER: request bytes -> response bytes (no socket). The gate drives this directly. ----
488func epd_handle(ctx: *NxAuthContext, domain: *u8, prefix: *u8, req: *u8, req_n: i64, out: *u8) -> i64 {
489 let poff: *i64 = sys_mmap(8) as *i64
490 let plen: *i64 = sys_mmap(8) as *i64
491 poff[0] = 0
492 plen[0] = 0
493 sd_find_path(req, req_n, poff, plen)
494 let path: *u8 = ((req as i64) + poff[0]) as *u8
495 let pn: i64 = plen[0]
496 let is_post: i64 = (req[0] == 80 as u8) as i64
497 var o: i64 = 0
498
499 // ===== NISHI-FIRST no-JS full-page routes (session in ?s=; early-return). Matched BEFORE the JS
500 // /mail/ header gate so their /mail/ prefix doesn't fall into it. =====
501 if sd_starts(path, pn, "/mail/box" as *u8) == 1 {
502 let now_b: i64 = sys_now_realtime_sec()
503 let shand: *u8 = sys_mmap(128)
504 let shn: *i64 = sys_mmap(8) as *i64
505 let mbox: *u8 = sys_mmap(256)
506 if epd_qs_mailbox(ctx, domain, path, pn, now_b, shand, shn, mbox) == 1 {
507 let voff: *i64 = sys_mmap(8) as *i64
508 let srn: i64 = nx_sa_qs_raw(path, pn, voff)
509 let sraw: *u8 = ((path as i64) + voff[0]) as *u8
510 return epd_inbox_page(prefix, mbox, sraw, srn, out)
511 }
512 return sd_emit_401_json(out)
513 }
514 if sd_starts(path, pn, "/mail/read" as *u8) == 1 {
515 let now_r: i64 = sys_now_realtime_sec()
516 let shand: *u8 = sys_mmap(128)
517 let shn: *i64 = sys_mmap(8) as *i64
518 let mbox: *u8 = sys_mmap(256)
519 if epd_qs_mailbox(ctx, domain, path, pn, now_r, shand, shn, mbox) == 1 {
520 let voff: *i64 = sys_mmap(8) as *i64
521 let srn: i64 = nx_sa_qs_raw(path, pn, voff)
522 let sraw: *u8 = ((path as i64) + voff[0]) as *u8
523 let idx: i64 = epd_qn(path, pn)
524 return epd_read_page(prefix, mbox, idx, sraw, srn, out)
525 }
526 return sd_emit_401_json(out)
527 }
528 if sd_starts(path, pn, "/mail/write" as *u8) == 1 {
529 let now_w: i64 = sys_now_realtime_sec()
530 let shand: *u8 = sys_mmap(128)
531 let shn: *i64 = sys_mmap(8) as *i64
532 let mbox: *u8 = sys_mmap(256)
533 if epd_qs_mailbox(ctx, domain, path, pn, now_w, shand, shn, mbox) == 1 {
534 let voff: *i64 = sys_mmap(8) as *i64
535 let srn: i64 = nx_sa_qs_raw(path, pn, voff)
536 let sraw: *u8 = ((path as i64) + voff[0]) as *u8
537 return epd_write_page(domain, sraw, srn, out)
538 }
539 return sd_emit_401_json(out)
540 }
541 if sd_starts(path, pn, "/mail/xsend" as *u8) == 1 {
542 if is_post == 1 {
543 let now_x: i64 = sys_now_realtime_sec()
544 let shand: *u8 = sys_mmap(128)
545 let shn: *i64 = sys_mmap(8) as *i64
546 let mbox: *u8 = sys_mmap(256)
547 if epd_qs_mailbox(ctx, domain, path, pn, now_x, shand, shn, mbox) == 1 {
548 let voff: *i64 = sys_mmap(8) as *i64
549 let srn: i64 = nx_sa_qs_raw(path, pn, voff)
550 let sraw: *u8 = ((path as i64) + voff[0]) as *u8
551 let body_off: i64 = sd_body_off(req, req_n)
552 let fb: *u8 = ((req as i64) + body_off) as *u8
553 let fbn: i64 = req_n - body_off
554 let toff: *i64 = sys_mmap(8) as *i64
555 let tnf: *i64 = sys_mmap(8) as *i64
556 let soff: *i64 = sys_mmap(8) as *i64
557 let snf: *i64 = sys_mmap(8) as *i64
558 let boff: *i64 = sys_mmap(8) as *i64
559 let bnf: *i64 = sys_mmap(8) as *i64
560 var got: i64 = 0
561 if sd_form_field(fb, fbn, "to" as *u8, 2, toff, tnf) == 1 {
562 if sd_form_field(fb, fbn, "subject" as *u8, 7, soff, snf) == 1 {
563 if sd_form_field(fb, fbn, "body" as *u8, 4, boff, bnf) == 1 { got = 1 }
564 }
565 }
566 if got == 1 {
567 let tob: *u8 = sys_mmap(320)
568 let sub: *u8 = sys_mmap(EPD_MAGIC_1024)
569 let bod: *u8 = sys_mmap(EPD_SHOWCAP)
570 let tsrc: *u8 = ((fb as i64) + toff[0]) as *u8
571 let ssrc: *u8 = ((fb as i64) + soff[0]) as *u8
572 let bsrc: *u8 = ((fb as i64) + boff[0]) as *u8
573 let t_dec: i64 = sd_urldecode(tsrc, tnf[0], tob, 300)
574 let s_dec: i64 = sd_urldecode(ssrc, snf[0], sub, 1000)
575 let bcapm: i64 = EPD_SHOWCAP - 1
576 let b_dec: i64 = sd_urldecode(bsrc, bnf[0], bod, bcapm)
577 if t_dec > 0 {
578 let lens: *i64 = sys_mmap(64) as *i64
579 lens[0] = shn[0]
580 lens[1] = t_dec
581 lens[2] = s_dec
582 lens[3] = b_dec
583 let rmsg: *u8 = sys_mmap(512)
584 let rlen: i64 = epd_deliver(domain, prefix, shand, tob, sub, bod, lens, rmsg)
585 return epd_sent_page(rmsg, rlen, sraw, srn, out)
586 }
587 }
588 return epd_resp(out, "400 Bad Request" as *u8, "text/plain" as *u8, "missing to/subject/body" as *u8, 23)
589 }
590 return sd_emit_401_json(out)
591 }
592 return sd_emit_401_json(out)
593 }
594 if sd_starts(path, pn, "/mail/rm" as *u8) == 1 {
595 if is_post == 1 {
596 let now_d: i64 = sys_now_realtime_sec()
597 let shand: *u8 = sys_mmap(128)
598 let shn: *i64 = sys_mmap(8) as *i64
599 let mbox: *u8 = sys_mmap(256)
600 if epd_qs_mailbox(ctx, domain, path, pn, now_d, shand, shn, mbox) == 1 {
601 let voff: *i64 = sys_mmap(8) as *i64
602 let srn: i64 = nx_sa_qs_raw(path, pn, voff)
603 let sraw: *u8 = ((path as i64) + voff[0]) as *u8
604 let idx: i64 = epd_qn(path, pn)
605 let ts: i64 = sys_now_us()
606 var rmv: i64 = 0
607 if idx >= 0 { if nx_mbox_delete_n(prefix, mbox, idx, ts) == 0 { rmv = 1 } }
608 let rmsg: *u8 = sys_mmap(64)
609 var rl: i64 = 0
610 if rmv == 1 { rl = sd_cat(rmsg, 0, "Deleted." as *u8) } else { rl = sd_cat(rmsg, 0, "Delete failed." as *u8) }
611 return epd_sent_page(rmsg, rl, sraw, srn, out)
612 }
613 return sd_emit_401_json(out)
614 }
615 return sd_emit_401_json(out)
616 }
617
618 if sd_starts(path, pn, "/mail/register" as *u8) == 1 {
619 // INVITE-GATED OPAQUE self-registration (mirrors the doc portal) + the handle-hygiene guard:
620 // the handle BECOMES the mailbox address, so charset is enforced at the boundary.
621 if is_post == 1 {
622 let body_off: i64 = sd_body_off(req, req_n)
623 let body: *u8 = ((req as i64) + body_off) as *u8
624 let body_n: i64 = req_n - body_off
625 let hoff: *i64 = sys_mmap(8) as *i64
626 let hn: *i64 = sys_mmap(8) as *i64
627 let woff: *i64 = sys_mmap(8) as *i64
628 let wn: *i64 = sys_mmap(8) as *i64
629 let ioff: *i64 = sys_mmap(8) as *i64
630 let inn: *i64 = sys_mmap(8) as *i64
631 var got: i64 = 0
632 if sd_form_field(body, body_n, "handle" as *u8, 6, hoff, hn) == 1 {
633 if sd_form_field(body, body_n, "pw" as *u8, 2, woff, wn) == 1 {
634 if sd_form_field(body, body_n, "invite" as *u8, 6, ioff, inn) == 1 { got = 1 }
635 }
636 }
637 var done: i64 = 0
638 if got == 1 {
639 let hbuf: *u8 = sys_mmap(256)
640 let pbuf: *u8 = sys_mmap(512)
641 let ibuf: *u8 = sys_mmap(128)
642 let h_dec: i64 = sd_urldecode(((body as i64) + hoff[0]) as *u8, hn[0], hbuf, 255)
643 let p_dec: i64 = sd_urldecode(((body as i64) + woff[0]) as *u8, wn[0], pbuf, 511)
644 let i_dec: i64 = sd_urldecode(((body as i64) + ioff[0]) as *u8, inn[0], ibuf, 127)
645 if h_dec > 0 { if p_dec > 0 { if i_dec > 0 {
646 if epd_handle_ok(hbuf, h_dec) == 0 {
647 let hb: *u8 = sys_mmap(256)
648 let hbn: i64 = sd_cat(hb, 0, "{\"error\":\"handle must be a-z 0-9 . _ - (1..32, start a-z0-9)\"}" as *u8)
649 o = epd_resp(out, "400 Bad Request" as *u8, "application/json" as *u8, hb, hbn)
650 done = 1
651 } else {
652 let invpath: *u8 = sys_mmap(512)
653 var q: i64 = 0
654 let sp: *u8 = ctx.account_store as *u8
655 while sp[q] != (0 as u8) { invpath[q] = sp[q]; q = q + 1 }
656 let suf: *u8 = ".invites" as *u8
657 var w: i64 = 0
658 while suf[w] != (0 as u8) { invpath[q] = suf[w]; q = q + 1; w = w + 1 }
659 invpath[q] = 0 as u8
660 let now_s: i64 = sys_now_realtime_sec()
661 let lvl: i64 = inv_check(invpath, ibuf, i_dec, ctx.realm_id, ctx.realm_id_n, now_s)
662 if lvl <= 0 {
663 o = sd_cat(out, 0, "HTTP/1.1 403 Forbidden\r\nContent-Type: application/json\r\nConnection: close\r\nContent-Length: 37\r\n\r\n{\"error\":\"invalid or expired invite\"}" as *u8)
664 done = 1
665 } else {
666 let mn: *u8 = sys_mmap(512)
667 let mnn: *i64 = sys_mmap(16) as *i64
668 if olg_register(ctx, hbuf, h_dec, pbuf, p_dec, mn, 512, mnn) == NX_MAUTH_OK {
669 inv_consume(invpath, ibuf, i_dec, now_s)
670 // mailmap: session tokens carry the uid HASH -> record hash->handle now
671 let uidh: *u8 = sys_mmap(32)
672 if nx_ncs_derive_user_id_hash(ctx.realm_id, ctx.realm_id_n, hbuf, h_dec, uidh) == NX_NCS_OK {
673 let uhex: *u8 = sys_mmap(72)
674 epd_hex32(uidh, uhex)
675 let mmp: *u8 = sys_mmap(512)
676 epd_mailmap_path(ctx, mmp)
677 epd_map_put(mmp, uhex, hbuf, h_dec)
678 }
679 o = sd_cat(out, 0, "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nConnection: close\r\nContent-Length: " as *u8)
680 o = sd_catn(out, o, 15 + mnn[0])
681 o = sd_cat(out, o, "\r\n\r\n{\"mnemonic\":\"" as *u8)
682 var z: i64 = 0
683 while z < mnn[0] { out[o] = mn[z]; o = o + 1; z = z + 1 }
684 o = sd_cat(out, o, "\"}" as *u8)
685 done = 1
686 } else {
687 o = sd_cat(out, 0, "HTTP/1.1 400 Bad Request\r\nContent-Type: application/json\r\nConnection: close\r\nContent-Length: 27\r\n\r\n{\"error\":\"register failed\"}" as *u8)
688 done = 1
689 }
690 }
691 }
692 } } }
693 }
694 if done == 0 { o = sd_cat(out, 0, "HTTP/1.1 400 Bad Request\r\nContent-Type: application/json\r\nConnection: close\r\nContent-Length: 27\r\n\r\n{\"error\":\"register failed\"}" as *u8) }
695 } else { o = epd_shell(domain, out) }
696 } else { if sd_starts(path, pn, "/mail/login" as *u8) == 1 {
697 if is_post == 1 {
698 let body_off: i64 = sd_body_off(req, req_n)
699 let body: *u8 = ((req as i64) + body_off) as *u8
700 let body_n: i64 = req_n - body_off
701 let hoff: *i64 = sys_mmap(8) as *i64
702 let hn: *i64 = sys_mmap(8) as *i64
703 let woff: *i64 = sys_mmap(8) as *i64
704 let wn: *i64 = sys_mmap(8) as *i64
705 var got: i64 = 0
706 if sd_form_field(body, body_n, "handle" as *u8, 6, hoff, hn) == 1 {
707 if sd_form_field(body, body_n, "passphrase" as *u8, 10, woff, wn) == 1 { got = 1 }
708 }
709 // ui=1 rides ONLY the native <form> submit (nishi-first no-JS) -> answer HTML, not JSON.
710 var uiflag: i64 = 0
711 let uoff: *i64 = sys_mmap(8) as *i64
712 let un: *i64 = sys_mmap(8) as *i64
713 if sd_form_field(body, body_n, "ui" as *u8, 2, uoff, un) == 1 { uiflag = 1 }
714 var ok: i64 = 0
715 if got == 1 {
716 let hbuf: *u8 = sys_mmap(256)
717 let pbuf: *u8 = sys_mmap(512)
718 let h_dec: i64 = sd_urldecode(((body as i64) + hoff[0]) as *u8, hn[0], hbuf, 255)
719 let p_dec: i64 = sd_urldecode(((body as i64) + woff[0]) as *u8, wn[0], pbuf, 511)
720 if h_dec > 0 { if p_dec > 0 {
721 let tok: *u8 = sys_mmap(NX_MAUTH_SESSION_TOKEN_BYTES)
722 let tok_n: *i64 = sys_mmap(8) as *i64
723 tok_n[0] = 0
724 if nx_modern_auth_login(ctx, hbuf, h_dec, pbuf, p_dec, tok, NX_MAUTH_SESSION_TOKEN_BYTES, tok_n) == NX_MAUTH_OK {
725 let b64: *u8 = sys_mmap(256)
726 let b64_n: i64 = b64_encode(tok, NX_MAUTH_SESSION_TOKEN_BYTES, b64)
727 if uiflag == 1 { o = epd_landing_mail(out, b64, b64_n) }
728 else {
729 o = sd_cat(out, o, "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nConnection: close\r\nContent-Length: " as *u8)
730 o = sd_catn(out, o, 12 + b64_n)
731 o = sd_cat(out, o, "\r\n\r\n{\"token\":\"" as *u8)
732 var z: i64 = 0
733 while z < b64_n { out[o] = b64[z]; o = o + 1; z = z + 1 }
734 o = sd_cat(out, o, "\"}" as *u8)
735 }
736 ok = 1
737 }
738 } }
739 }
740 if ok == 0 {
741 if uiflag == 1 { o = sd_cat(out, 0, "HTTP/1.1 401 Unauthorized\r\nContent-Type: text/html; charset=utf-8\r\nConnection: close\r\nContent-Length: 101\r\n\r\n<!DOCTYPE html><meta charset=utf-8><p>Wrong handle or passphrase.</p><p><a href=\"/\">Try again</a></p>" as *u8) }
742 else { o = sd_emit_401_json(out) }
743 }
744 } else { o = epd_shell(domain, out) }
745 } else { if sd_starts(path, pn, "/mail/" as *u8) == 1 {
746 // session-gated: identity comes ONLY from the validated token. The token carries the 32-byte
747 // user-id HASH; the register-time mailmap resolves it to the handle -> the mailbox is
748 // <session-handle>@<domain>. No request parameter can select another mailbox.
749 let now_s: i64 = sys_now_realtime_sec()
750 let suid: *u8 = sys_mmap(64)
751 let sun: *i64 = sys_mmap(8) as *i64
752 sun[0] = 0
753 var authed: i64 = 0
754 let shand: *u8 = sys_mmap(128)
755 var shn0: i64 = 0
756 if nx_sa_validate_handle(ctx, req, req_n, now_s, suid, 64, sun) == NX_MAUTH_OK {
757 if sun[0] == 32 {
758 let uhex2: *u8 = sys_mmap(72)
759 epd_hex32(suid, uhex2)
760 let mmp2: *u8 = sys_mmap(512)
761 epd_mailmap_path(ctx, mmp2)
762 shn0 = epd_map_get(mmp2, uhex2, shand, 64)
763 if shn0 > 0 { if epd_handle_ok(shand, shn0) == 1 { authed = 1 } }
764 }
765 }
766 if authed == 1 {
767 let mbox: *u8 = sys_mmap(256)
768 epd_mailbox(shand, shn0, domain, mbox)
769 let frag: *u8 = sys_mmap(EPD_BODYCAP)
770 if sd_starts(path, pn, "/mail/inbox" as *u8) == 1 {
771 let fb: i64 = epd_inbox_frag(prefix, mbox, frag)
772 o = epd_resp(out, "200 OK" as *u8, "text/html; charset=utf-8" as *u8, frag, fb)
773 } else { if sd_starts(path, pn, "/mail/msg" as *u8) == 1 {
774 let idx: i64 = epd_qn(path, pn)
775 var served: i64 = 0
776 if idx >= 0 {
777 let pp: *i64 = sys_mmap(8) as *i64
778 let ll: *i64 = sys_mmap(8) as *i64
779 let rc: i64 = nx_mbox_get_n(prefix, mbox, idx, pp, ll)
780 if rc == 1 {
781 var fb: i64 = sd_cat(frag, 0, "<pre>" as *u8)
782 var show: i64 = ll[0]
783 if show > EPD_SHOWCAP { show = EPD_SHOWCAP }
784 fb = wm_esc(frag, fb, pp[0] as *u8, show)
785 if ll[0] > EPD_SHOWCAP { fb = sd_cat(frag, fb, "\n[... truncated view ...]" as *u8) }
786 fb = sd_cat(frag, fb, "</pre>" as *u8)
787 o = epd_resp(out, "200 OK" as *u8, "text/html; charset=utf-8" as *u8, frag, fb)
788 served = 1
789 }
790 if rc == 0 {
791 let fb2: i64 = sd_cat(frag, 0, "<p class=f>This message was deleted (tombstoned).</p>" as *u8)
792 o = epd_resp(out, "200 OK" as *u8, "text/html; charset=utf-8" as *u8, frag, fb2)
793 served = 1
794 }
795 }
796 if served == 0 {
797 let fb3: i64 = sd_cat(frag, 0, "<p class=f>No such message.</p>" as *u8)
798 o = epd_resp(out, "404 Not Found" as *u8, "text/html; charset=utf-8" as *u8, frag, fb3)
799 }
800 } else { if sd_starts(path, pn, "/mail/del" as *u8) == 1 {
801 var deld: i64 = 0
802 if is_post == 1 {
803 let idx2: i64 = epd_qn(path, pn)
804 if idx2 >= 0 {
805 if nx_mbox_delete_n(prefix, mbox, idx2, sys_now_us()) == 0 {
806 let fb4: i64 = sd_cat(frag, 0, "OK" as *u8)
807 o = epd_resp(out, "200 OK" as *u8, "text/plain" as *u8, frag, fb4)
808 deld = 1
809 }
810 }
811 }
812 if deld == 0 {
813 let fb5: i64 = sd_cat(frag, 0, "delete failed" as *u8)
814 o = epd_resp(out, "400 Bad Request" as *u8, "text/plain" as *u8, frag, fb5)
815 }
816 } else { if sd_starts(path, pn, "/mail/send" as *u8) == 1 {
817 var sent: i64 = 0
818 if is_post == 1 {
819 let body_off: i64 = sd_body_off(req, req_n)
820 let body: *u8 = ((req as i64) + body_off) as *u8
821 let body_n: i64 = req_n - body_off
822 let toff: *i64 = sys_mmap(8) as *i64
823 let tn: *i64 = sys_mmap(8) as *i64
824 let soff: *i64 = sys_mmap(8) as *i64
825 let sn: *i64 = sys_mmap(8) as *i64
826 let boff: *i64 = sys_mmap(8) as *i64
827 let bn: *i64 = sys_mmap(8) as *i64
828 var got: i64 = 0
829 if sd_form_field(body, body_n, "to" as *u8, 2, toff, tn) == 1 {
830 if sd_form_field(body, body_n, "subject" as *u8, 7, soff, sn) == 1 {
831 if sd_form_field(body, body_n, "body" as *u8, 4, boff, bn) == 1 { got = 1 }
832 }
833 }
834 if got == 1 {
835 let tob: *u8 = sys_mmap(320)
836 let sub: *u8 = sys_mmap(EPD_MAGIC_1024)
837 let bod: *u8 = sys_mmap(EPD_SHOWCAP)
838 let t_dec: i64 = sd_urldecode(((body as i64) + toff[0]) as *u8, tn[0], tob, 300)
839 let s_dec: i64 = sd_urldecode(((body as i64) + soff[0]) as *u8, sn[0], sub, 1000)
840 let b_dec: i64 = sd_urldecode(((body as i64) + boff[0]) as *u8, bn[0], bod, EPD_SHOWCAP - 1)
841 if t_dec > 0 { if s_dec >= 0 { if b_dec >= 0 {
842 // split to = local[@dom]
843 var at: i64 = 0 - 1
844 var ti: i64 = 0
845 while ti < t_dec { if (tob[ti] as i64) == 64 { if at < 0 { at = ti } } ti = ti + 1 }
846 var local_n: i64 = t_dec
847 var dom_off: i64 = 0 - 1
848 var dom_n: i64 = 0
849 if at >= 0 { local_n = at; dom_off = at + 1; dom_n = t_dec - at - 1 }
850 var is_local: i64 = 1
851 if at >= 0 { is_local = epd_ci_eq(((tob as i64) + dom_off) as *u8, dom_n, domain) }
852 // sender identity for From:
853 let fromb: *u8 = sys_mmap(256)
854 epd_mailbox(shand, shn0, domain, fromb)
855 let msg: *u8 = sys_mmap(EPD_MSGCAP)
856 let mlen: i64 = epd_assemble(fromb, tob, t_dec, sub, s_dec, bod, b_dec, msg)
857 if is_local == 1 {
858 if epd_handle_ok(tob, local_n) == 1 {
859 let rmb: *u8 = sys_mmap(256)
860 epd_mailbox(tob, local_n, domain, rmb)
861 let seq: i64 = nx_mbox_append(prefix, rmb, msg, mlen, sys_now_us())
862 if seq >= 0 {
863 var fb6: i64 = sd_cat(frag, 0, "DELIVERED-LOCAL to=" as *u8)
864 fb6 = sd_cat(frag, fb6, rmb)
865 fb6 = sd_cat(frag, fb6, " seq=" as *u8)
866 fb6 = sd_catn(frag, fb6, seq)
867 o = epd_resp(out, "200 OK" as *u8, "text/plain" as *u8, frag, fb6)
868 sent = 1
869 }
870 } else {
871 let fb7: i64 = sd_cat(frag, 0, "bad recipient handle (a-z 0-9 . _ -)" as *u8)
872 o = epd_resp(out, "400 Bad Request" as *u8, "text/plain" as *u8, frag, fb7)
873 sent = 1
874 }
875 } else {
876 // HONEST external queue: stored durably, relay is a named, unarmed rung.
877 let qmb: *u8 = sys_mmap(256)
878 var qo: i64 = sd_cat(qmb, 0, "relay-queue@" as *u8)
879 qo = sd_cat(qmb, qo, domain)
880 qmb[qo] = 0 as u8
881 let qseq: i64 = nx_mbox_append(prefix, qmb, msg, mlen, sys_now_us())
882 if qseq >= 0 {
883 var fb8: i64 = sd_cat(frag, 0, "QUEUED-EXTERNAL seq=" as *u8)
884 fb8 = sd_catn(frag, fb8, qseq)
885 fb8 = sd_cat(frag, fb8, " (external relay not armed yet; queued durably, nothing pretended)" as *u8)
886 o = epd_resp(out, "200 OK" as *u8, "text/plain" as *u8, frag, fb8)
887 sent = 1
888 }
889 }
890 } } }
891 }
892 }
893 if sent == 0 {
894 let fb9: i64 = sd_cat(frag, 0, "send failed" as *u8)
895 o = epd_resp(out, "400 Bad Request" as *u8, "text/plain" as *u8, frag, fb9)
896 }
897 } else { if sd_starts(path, pn, "/mail/whoami" as *u8) == 1 {
898 var fbw: i64 = sd_cat(frag, 0, "{\"handle\":\"" as *u8)
899 fbw = wm_esc(frag, fbw, shand, shn0)
900 fbw = sd_cat(frag, fbw, "\",\"mailbox\":\"" as *u8)
901 fbw = sd_cat(frag, fbw, mbox)
902 fbw = sd_cat(frag, fbw, "\"}" as *u8)
903 o = epd_resp(out, "200 OK" as *u8, "application/json" as *u8, frag, fbw)
904 } else {
905 let fbn: i64 = sd_cat(frag, 0, "<p class=f>No such mail route.</p>" as *u8)
906 o = epd_resp(out, "404 Not Found" as *u8, "text/html; charset=utf-8" as *u8, frag, fbn)
907 } } } } }
908 } else { o = sd_emit_401_json(out) }
909 } else { o = epd_shell(domain, out) } } }
910 return o
911}
912
913func main(argc: i64, argv: *i64) -> i64 {
914 if argc < 8 { sys_write(2, "usage: nx_email_portal_daemon <port> <keysfile> <storefile> <realm> <domain> <mailprefix> <budget>\n" as *u8, 100); return 1 }
915 let port: i64 = sd_atoi(argv[1] as *u8)
916 let keysfile: *u8 = argv[2] as *u8
917 let storefile: *u8 = argv[3] as *u8
918 let realm: *u8 = argv[4] as *u8
919 let domain: *u8 = argv[5] as *u8
920 let prefix: *u8 = argv[6] as *u8
921 let budget: i64 = sd_atoi(argv[7] as *u8)
922 let realm_n: i64 = sd_len(realm)
923
924 // ensure the mail store dir exists (prefix = "<dir>/"): mkdir the dir part, EEXIST is fine.
925 let dirbuf: *u8 = sys_mmap(512)
926 var dn: i64 = 0
927 while prefix[dn] != (0 as u8) { dirbuf[dn] = prefix[dn]; dn = dn + 1 }
928 if dn > 0 { if (dirbuf[dn - 1] as i64) == 47 { dirbuf[dn - 1] = 0 as u8 } }
929 sys_mkdir(dirbuf, 511)
930
931 // fail-fast: arm the realm context at startup (Rule 20), exactly like the doc portal
932 let oprf_seed: *u8 = sys_mmap(32)
933 let akp: *u8 = sys_mmap(32)
934 let akb: *u8 = sys_mmap(33)
935 let edp: *u8 = sys_mmap(32)
936 let edb: *u8 = sys_mmap(32)
937 if nx_uas_server_keys_load_or_init(keysfile, oprf_seed, akp, akb, edp, edb) != NX_UAS_OK { sys_write(2, "FATAL: server-key bundle\n" as *u8, 24); return 2 }
938 let ctx: *NxAuthContext = sys_mmap(256) as *NxAuthContext
939 if nx_auth_context_init(ctx, realm, realm_n, realm, realm_n, storefile as i64, oprf_seed, edp, edb, 900, EPD_MAGIC_8192, 1, 1, 5, 1) != NX_MAUTH_OK { sys_write(2, "FATAL: context init\n" as *u8, 20); return 3 }
940
941 let addr: *u8 = sys_mmap(16)
942 if nx_http_server_addr_loopback(addr, port) != 16 { sys_write(2, "FATAL: addr_loopback != 16\n" as *u8, 27); return 4 }
943 let lv: *i64 = sys_mmap(8) as *i64
944 let lfd: i64 = nx_http_server_listen(addr, 64, lv)
945 if lfd < 0 { sys_write(2, "FATAL: listen failed (port busy or denied)\n" as *u8, 43); return 4 }
946 sys_write(1, "nx_email_portal_daemon listening loopback (mail.<domain> behind the SNI router)\n" as *u8, 81)
947
948 let req: *u8 = sys_mmap(EPD_REQCAP)
949 let out: *u8 = sys_mmap(EPD_OUTCAP)
950 var served: i64 = 0
951 while served < budget {
952 let av: *i64 = sys_mmap(8) as *i64
953 let cfd: i64 = nx_http_server_accept_one(lfd, av)
954 if cfd < 0 { served = served + 1 }
955 if cfd >= 0 {
956 let om: *i64 = sys_mmap(8) as *i64
957 let opo: *i64 = sys_mmap(8) as *i64
958 let opl: *i64 = sys_mmap(8) as *i64
959 let ocl: *i64 = sys_mmap(8) as *i64
960 let obo: *i64 = sys_mmap(8) as *i64
961 let orn: *i64 = sys_mmap(8) as *i64
962 let rrc: i64 = nx_http_server_read_request(cfd, req, EPD_REQCAP, om, opo, opl, ocl, obo, orn)
963 if rrc == NXS_OK {
964 let oN: i64 = epd_handle(ctx, domain, prefix, req, orn[0], out)
965 nx_http_server_send_response_nokeep_close(cfd, out, oN)
966 }
967 sys_close(cfd)
968 served = served + 1
969 }
970 }
971 sys_close(lfd)
972 return 0
973}