nx_email_mailbox.nx source
↩ module page · 152 lines · 6559 B
1// nx_email_mailbox.nx -- EMAIL RUNG R3: additive-only mailbox store.
2//
3// module: nishi-core.email.mailbox
4// depends: nishi-core.store.seg_store
5// capability: CORE_EMAIL
6//
7// Where delivered mail LIVES. Composes the sovereign segment store
8// (nx_seg_store) rather than inventing persistence -- a message is one
9// keyed record (kind=1 put, key=Message-ID, value=raw RFC 5322 bytes);
10// deletion is a kind=2 TOMBSTONE appended in a new segment, never an
11// erase. This makes the mailbox obey Cardinal 13 (additive-only,
12// soft-delete via is_current, history is sacred) for free: a deleted
13// message vanishes from the current view (ss_get returns "tombstoned")
14// but every version is still walkable via ss_scan -- exactly what an
15// audit/undelete/IMAP-history needs.
16//
17// segid is supplied by the caller (a monotonic counter in production);
18// each deliver/delete commits one new immutable segment, so concurrent
19// readers never see a torn write (ss_commit renames into place).
20//
21// license_tier: INDEPENDENT_REDERIVE
22// genealogy_id: nishi_sovereign_segment_store + maildir_semantics
23// lineage_id: nishi_email_mailbox_r3
24//
25// nx_safety_envelope:
26// intended_use: "Deliver/fetch/soft-delete mail over the
27// sovereign segment store. Additive mailbox."
28// sil_target: SIL2 (lost/leaked mail)
29// evidence: [composes_nx_seg_store, byte-exact_fetch,
30// tombstone_softdelete, history_retained,
31// per-key_isolation]
32// hazard_register: [bug-tape-mail-loss, bug-tape-delete-erases-history]
33// residual_risk: "Monotonic segid allocation + folder hierarchy
34// (INBOX/Sent/...) are upstream concerns; this is
35// the single-mailbox record substrate."
36// verdict: NOT_YET_EVALUATED
37
38import "nx_seg_store.nx"
39import "nx_syscalls.nx"
40
41const NX_MBOX_CAP: i64 = 260
42const NX_MBOX_KIND_PUT: i64 = 1
43const NX_MBOX_KIND_TOMB: i64 = 2
44
45// Deliver a message: append a put record + commit a new segment.
46// Returns 0 on success, negative on failure.
47func nx_mbox_deliver(prefix: *u8, msgid: *u8, raw: *u8, rlen: i64, segid: i64) -> i64 {
48 let w: *i64 = ss_begin()
49 if ss_add(w, NX_MBOX_KIND_PUT, msgid, raw, rlen) != 0 { return 0 - 1 }
50 return ss_commit(prefix, w, segid)
51}
52
53// Soft-delete: append a tombstone record + commit. History is retained.
54func nx_mbox_delete(prefix: *u8, msgid: *u8, segid: i64) -> i64 {
55 let w: *i64 = ss_begin()
56 let empty: *u8 = sys_mmap(8)
57 if ss_add(w, NX_MBOX_KIND_TOMB, msgid, empty, 0) != 0 { return 0 - 1 }
58 return ss_commit(prefix, w, segid)
59}
60
61// Fetch the current version. Returns 1 (found; *ptrout/*lenout point at
62// the value bytes), 0 (tombstoned), or -1 (never existed).
63func nx_mbox_fetch(prefix: *u8, msgid: *u8, ptrout: *i64, lenout: *i64) -> i64 {
64 return ss_get_cap(prefix, msgid, ptrout, lenout, NX_MBOX_CAP)
65}
66
67// 1 iff the message currently exists (not tombstoned / absent).
68func nx_mbox_exists(prefix: *u8, msgid: *u8) -> i64 {
69 let pp: *i64 = sys_mmap(8) as *i64
70 let ll: *i64 = sys_mmap(8) as *i64
71 if ss_get_cap(prefix, msgid, pp, ll, NX_MBOX_CAP) == 1 { return 1 }
72 return 0
73}
74
75// Number of versions retained for a Message-ID (put + tombstones) --
76// the additive-history surface. > 1 after a delete proves nothing was
77// erased.
78func nx_mbox_history_count(prefix: *u8, msgid: *u8) -> i64 {
79 let kinds: *i64 = sys_mmap(8 * NX_MBOX_CAP) as *i64
80 let ptrs: *i64 = sys_mmap(8 * NX_MBOX_CAP) as *i64
81 let lens: *i64 = sys_mmap(8 * NX_MBOX_CAP) as *i64
82 return ss_scan(prefix, msgid, kinds, ptrs, lens)
83}
84
85// ---- multi-message mailbox (append-style, like Maildir new/) ----
86// A mailbox holds N messages keyed "<mailbox>#<seq>"; an index record
87// (key=<mailbox>) holds the ASCII message count. ONE segment commit per
88// append stores both the message and the bumped count atomically.
89
90func mbox_itoa(v: i64, out: *u8) -> i64 {
91 if v == 0 { out[0] = 48 as u8; return 1 }
92 let t: *u8 = sys_mmap(28); var m: i64 = v; var k: i64 = 0
93 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 }
94 var i: i64 = 0
95 while i < k { out[i] = t[k - 1 - i]; i = i + 1 }
96 return k
97}
98func mbox_atoi(s: *u8, n: i64) -> i64 {
99 var v: i64 = 0; var i: i64 = 0
100 while i < n && (s[i] & 0xff) >= 48 && (s[i] & 0xff) <= 57 { v = v * 10 + ((s[i] & 0xff) - 48); i = i + 1 }
101 return v
102}
103// build "<mailbox>#<seq>" (null-terminated) into out; returns key length.
104func mbox_key(mailbox: *u8, seq: i64, out: *u8) -> i64 {
105 var oi: i64 = 0
106 while mailbox[oi] != (0 as u8) { out[oi] = mailbox[oi]; oi = oi + 1 }
107 out[oi] = 35 as u8; oi = oi + 1 // '#'
108 let dl: i64 = mbox_itoa(seq, (out as i64 + oi) as *u8)
109 oi = oi + dl
110 out[oi] = 0 as u8
111 return oi
112}
113
114// Number of messages in a mailbox (the index record), 0 if none.
115func nx_mbox_count(prefix: *u8, mailbox: *u8) -> i64 {
116 let pp: *i64 = sys_mmap(8) as *i64
117 let ll: *i64 = sys_mmap(8) as *i64
118 if ss_get_cap(prefix, mailbox, pp, ll, NX_MBOX_CAP) != 1 { return 0 }
119 return mbox_atoi(pp[0] as *u8, ll[0])
120}
121
122// Append a message to a mailbox; returns its 0-based sequence number.
123func nx_mbox_append(prefix: *u8, mailbox: *u8, raw: *u8, rlen: i64, segid: i64) -> i64 {
124 let n: i64 = nx_mbox_count(prefix, mailbox)
125 let key: *u8 = sys_mmap(512)
126 mbox_key(mailbox, n, key)
127 let cnt: *u8 = sys_mmap(28)
128 let cl: i64 = mbox_itoa(n + 1, cnt)
129 let w: *i64 = ss_begin()
130 if ss_add(w, NX_MBOX_KIND_PUT, key, raw, rlen) != 0 { return 0 - 1 }
131 if ss_add(w, NX_MBOX_KIND_PUT, mailbox, cnt, cl) != 0 { return 0 - 1 }
132 if ss_commit(prefix, w, segid) != 0 { return 0 - 1 }
133 return n
134}
135
136// Fetch the idx-th message in a mailbox. 1 found / 0 tombstoned / -1 absent.
137func nx_mbox_get_n(prefix: *u8, mailbox: *u8, idx: i64, ptrout: *i64, lenout: *i64) -> i64 {
138 let key: *u8 = sys_mmap(512)
139 mbox_key(mailbox, idx, key)
140 return ss_get_cap(prefix, key, ptrout, lenout, NX_MBOX_CAP)
141}
142
143// Soft-delete the idx-th message in a mailbox (tombstone; index unchanged
144// so sequence numbers stay stable -- deleted slots read as tombstoned).
145func nx_mbox_delete_n(prefix: *u8, mailbox: *u8, idx: i64, segid: i64) -> i64 {
146 let key: *u8 = sys_mmap(512)
147 mbox_key(mailbox, idx, key)
148 let w: *i64 = ss_begin()
149 let empty: *u8 = sys_mmap(8)
150 if ss_add(w, NX_MBOX_KIND_TOMB, key, empty, 0) != 0 { return 0 - 1 }
151 return ss_commit(prefix, w, segid)
152}