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 &mdash; " 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>&#9993; " 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>&larr; 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>&#9993; 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 &rarr;</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, "\">&#9998; 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, "\">&larr; 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>&#9993; " 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, "\">&larr; Inbox</a> &nbsp; <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}