1use super::*;
2use crate::identity::{AcceptedRecord, ListResponse, MemoryNonceStore, NonceStore};
3use serde::Deserialize;
4
5fn version(value: &Value) -> Result<()> {
6 if value["version"] != 1 {
7 return Err(fail(
8 "invalid_request",
9 "unsupported local Mail state version",
10 ));
11 }
12 Ok(())
13}
14fn mailbox_key(owner: &AgentId, mailbox: &str) -> String {
15 format!("{owner}/{mailbox}")
16}
17
18#[derive(Clone, Default)]
22pub struct MailCardCache {
23 pins: BTreeMap<String, MailboxCard>,
24}
25impl MailCardCache {
26 pub fn new() -> Self {
27 Self::default()
28 }
29 pub fn observe_historical(
32 &mut self,
33 value: &Value,
34 owner: &AgentId,
35 now: i64,
36 ) -> Result<MailboxCard> {
37 let card = validate_card(value)?;
38 clock(now)?;
39 if &card.event.actor != owner {
40 return Err(fail("invalid_actor", "unexpected owner"));
41 }
42 if card.event.created_at > now.saturating_add(FUTURE_SKEW_MS) {
43 return Err(SdkError::TimestampOutOfWindow);
44 }
45 let key = mailbox_key(owner, &card.event.payload.mailbox_id);
46 if let Some(pin) = self.pins.get(&key) {
47 if pin.hash != card.hash && card.event.nonce <= pin.event.nonce {
48 return Err(fail(
49 "stale_card",
50 "card is older than or conflicts with the pinned card",
51 ));
52 }
53 }
54 self.pins.insert(key, card.clone());
55 Ok(card)
56 }
57 pub fn observe(&mut self, value: &Value, owner: &AgentId, now: i64) -> Result<MailboxCard> {
59 let card = self.observe_historical(value, owner, now)?;
60 check_card_usable(&card, owner, now)?;
61 Ok(card)
62 }
63 pub fn prune(&mut self, now: i64) -> Result<()> {
65 clock(now)?;
66 self.pins
67 .retain(|_, pin| now < pin.event.created_at + MAX_TTL_MS + FUTURE_SKEW_MS);
68 Ok(())
69 }
70 pub fn seal(&mut self, letter: &Letter, card: &Value, now: i64) -> Result<Packet> {
72 let owner: AgentId = serde_json::from_value(letter.event.payload["to"].clone())?;
73 let card = self.observe(card, &owner, now)?;
74 encrypt_letter(letter, &card, now)
75 }
76 pub fn snapshot(&self) -> Result<Value> {
77 Ok(json!({"version":1,"pins":self.pins.values().collect::<Vec<_>>()}))
78 }
79 pub fn from_snapshot(value: &Value) -> Result<Self> {
80 version(value)?;
81 let pins: Vec<MailboxCard> = serde_json::from_value(value["pins"].clone())?;
82 Ok(Self {
83 pins: pins
84 .into_iter()
85 .map(|card| {
86 (
87 mailbox_key(&card.event.actor, &card.event.payload.mailbox_id),
88 card,
89 )
90 })
91 .collect(),
92 })
93 }
94}
95
96pub struct MailKeyring {
98 owner: AgentId,
99 entries: BTreeMap<String, (MailboxCard, MailEncryptionKey)>,
100}
101impl MailKeyring {
102 pub fn new(owner: AgentId) -> Self {
103 Self {
104 owner,
105 entries: BTreeMap::new(),
106 }
107 }
108 pub fn owner(&self) -> &AgentId {
109 &self.owner
110 }
111 pub fn add(&mut self, card: &MailboxCard, key: MailEncryptionKey) -> Result<()> {
112 let card = validate_card(&serde_json::to_value(card)?)?;
113 if card.event.actor != self.owner {
114 return Err(fail("invalid_actor", "card owner mismatch"));
115 }
116 if card.event.payload.public_key != key.public_key() {
117 return Err(fail("invalid_private_key", "key does not match card"));
118 }
119 self.entries.insert(card.hash.clone(), (card, key));
120 Ok(())
121 }
122 pub fn open(&self, packet: &Packet, now: i64) -> Result<Letter> {
124 clock(now)?;
125 let (packet, bytes) = checked_packet(&serde_json::to_value(packet)?)?;
126 let (card, key) = self
127 .entries
128 .get(&packet.header.card_hash)
129 .ok_or_else(|| fail("invalid_packet", "unknown retained card"))?;
130 open_verified(&packet, &bytes, card, key, &self.owner, now)
131 }
132 pub fn prune(&mut self, now: i64) -> Result<()> {
135 clock(now)?;
136 self.entries
137 .retain(|_, (card, _)| now < card.event.payload.receive_until);
138 Ok(())
139 }
140 pub fn snapshot(&self) -> Result<Value> {
143 let entries: Vec<_> = self
144 .entries
145 .values()
146 .map(|(card, key)| json!({"card":card,"secret":encode_bytes(&key.export_secret())}))
147 .collect();
148 Ok(json!({"version":1,"owner":self.owner,"entries":entries}))
149 }
150 pub fn from_snapshot(value: &Value) -> Result<Self> {
151 #[derive(Deserialize)]
152 struct Entry {
153 card: MailboxCard,
154 secret: String,
155 }
156 version(value)?;
157 let mut out = Self::new(serde_json::from_value(value["owner"].clone())?);
158 let entries: Vec<Entry> = serde_json::from_value(value["entries"].clone())?;
159 for entry in entries {
160 let raw = Zeroizing::new(fixed_bytes(&entry.secret, 32)?);
161 out.add(&entry.card, MailEncryptionKey::from_secret(&raw)?)?;
162 }
163 Ok(out)
164 }
165}
166
167pub struct MailInbox {
171 keyring: MailKeyring,
172 accepted: BTreeMap<String, i64>,
173}
174impl MailInbox {
175 pub fn new(keyring: MailKeyring) -> Self {
176 Self {
177 keyring,
178 accepted: BTreeMap::new(),
179 }
180 }
181 pub fn keyring(&self) -> &MailKeyring {
182 &self.keyring
183 }
184 pub fn keyring_mut(&mut self) -> &mut MailKeyring {
185 &mut self.keyring
186 }
187 pub fn has(&self, letter_id: &str) -> bool {
188 self.accepted.contains_key(letter_id)
189 }
190 pub fn accept(
191 &mut self,
192 packet: &Packet,
193 claimed_packet_id: Option<&str>,
194 now: i64,
195 ) -> Result<InboxAcceptance> {
196 if claimed_packet_id.is_some_and(|id| packet_id(packet).ok().as_deref() != Some(id)) {
197 return Err(fail("invalid_packet", "packet ID mismatch"));
198 }
199 let letter = self.keyring.open(packet, now)?;
200 if self.accepted.contains_key(&letter.hash) {
201 return Ok(InboxAcceptance::Duplicate(Box::new(letter)));
202 }
203 if now >= packet.header.expires_at {
204 return Err(fail(
205 "packet_expired",
206 "expired letter cannot be newly accepted",
207 ));
208 }
209 self.accepted
210 .insert(letter.hash.clone(), packet.header.expires_at);
211 Ok(InboxAcceptance::Accepted(Box::new(letter)))
212 }
213 pub fn prune(&mut self, now: i64) -> Result<()> {
215 clock(now)?;
216 self.accepted.retain(|_, expires| now < *expires);
217 Ok(())
218 }
219 pub fn snapshot(&self) -> Result<Value> {
220 Ok(
221 json!({"version":1,"owner":self.keyring.owner,"accepted":self.accepted.iter().collect::<Vec<_>>()}),
222 )
223 }
224 pub fn from_snapshot(keyring: MailKeyring, value: &Value) -> Result<Self> {
225 version(value)?;
226 if value["owner"] != keyring.owner.as_str() {
227 return Err(fail("invalid_request", "inbox owner mismatch"));
228 }
229 let accepted: Vec<(String, i64)> = serde_json::from_value(value["accepted"].clone())?;
230 Ok(Self {
231 keyring,
232 accepted: accepted.into_iter().collect(),
233 })
234 }
235}
236
237struct RelayMailbox {
238 current: AcceptedRecord<MailboxCardPayload>,
239 last_seq: u64,
240 bytes: usize,
241 packets: BTreeMap<String, (PacketRecord, usize)>,
242 tombstones: BTreeMap<String, (i64, i64)>,
243}
244impl RelayMailbox {
245 fn drop_expired(&mut self, now: i64) {
246 let bytes = &mut self.bytes;
247 self.packets.retain(|_, (record, size)| {
248 let live = now < record.packet.header.expires_at;
249 if !live {
250 *bytes -= *size;
251 }
252 live
253 });
254 }
255}
256pub struct MailRelayStore {
261 origin: String,
262 max_packets: usize,
263 max_bytes: usize,
264 nonces: MemoryNonceStore,
265 mailboxes: BTreeMap<String, RelayMailbox>,
266}
267impl MailRelayStore {
268 pub fn new(origin: impl Into<String>) -> Result<Self> {
269 Self::with_limits(origin, 10_000, 64 * MAX_PACKET_BYTES)
270 }
271 pub fn with_limits(
273 origin: impl Into<String>,
274 max_packets: usize,
275 max_bytes: usize,
276 ) -> Result<Self> {
277 let origin = origin.into();
278 identity::validate_origin(&origin)?;
279 if max_packets == 0 || max_bytes == 0 {
280 return Err(fail("invalid_request", "invalid relay quota"));
281 }
282 Ok(Self {
283 origin,
284 max_packets,
285 max_bytes,
286 nonces: MemoryNonceStore::new(),
287 mailboxes: BTreeMap::new(),
288 })
289 }
290 pub fn origin(&self) -> &str {
291 &self.origin
292 }
293 pub fn discovery(&self) -> Value {
294 json!({"protocol":PROTOCOL,"service":self.origin})
295 }
296 pub fn publish(
297 &mut self,
298 value: &Value,
299 now: i64,
300 ) -> Result<AcceptedRecord<MailboxCardPayload>> {
301 let mut nonces = std::mem::take(&mut self.nonces);
302 let result = self.publish_with_nonce_store(value, now, &mut nonces);
303 self.nonces = nonces;
304 result
305 }
306 pub fn publish_with_nonce_store<S: NonceStore + ?Sized>(
309 &mut self,
310 value: &Value,
311 now: i64,
312 nonces: &mut S,
313 ) -> Result<AcceptedRecord<MailboxCardPayload>> {
314 clock(now)?;
315 let card = validate_card(value)?;
316 let p = &card.event.payload;
317 if let Some(mailbox) = self.mailboxes.get(&p.mailbox_id) {
318 let current = &mailbox.current.envelope;
319 if current.event.actor != card.event.actor {
320 return Err(fail("mailbox_conflict", "mailbox belongs to another owner"));
321 }
322 if current.hash == card.hash {
323 return Ok(mailbox.current.clone());
324 }
325 if card.event.nonce <= current.event.nonce {
326 return Err(SdkError::NonceNotGreater {
327 max_nonce: current.event.nonce,
328 });
329 }
330 } else if !p.routes.contains(&self.origin) {
331 return Err(SdkError::PermissionDenied);
332 }
333 identity::verify_timestamp(card.event.created_at, now, FUTURE_SKEW_MS)?;
334 nonces.check_and_update(&card.event.actor, card.event.nonce, now, 2 * FUTURE_SKEW_MS)?;
335 let record = AcceptedRecord {
336 envelope: card.clone(),
337 accepted_at: now,
338 };
339 match self.mailboxes.get_mut(&p.mailbox_id) {
340 Some(mailbox) => mailbox.current = record.clone(),
341 None => {
342 self.mailboxes.insert(
343 p.mailbox_id.clone(),
344 RelayMailbox {
345 current: record.clone(),
346 last_seq: 0,
347 bytes: 0,
348 packets: BTreeMap::new(),
349 tombstones: BTreeMap::new(),
350 },
351 );
352 }
353 }
354 Ok(record)
355 }
356 pub fn card(&self, mailbox_id: &str) -> Result<AcceptedRecord<MailboxCardPayload>> {
357 path_id(mailbox_id, 16)?;
358 self.mailboxes
359 .get(mailbox_id)
360 .map(|m| m.current.clone())
361 .ok_or_else(|| fail("mailbox_unavailable", "unknown mailbox"))
362 }
363 pub fn deliver(
364 &mut self,
365 mailbox_id: &str,
366 packet: &Packet,
367 now: i64,
368 ) -> Result<DeliveryResult> {
369 clock(now)?;
370 path_id(mailbox_id, 16)?;
371 let (packet, bytes) = checked_packet(&serde_json::to_value(packet)?)?;
372 let h = &packet.header;
373 if h.mailbox_id != mailbox_id {
374 return Err(fail("invalid_packet", "path mailbox mismatch"));
375 }
376 let (id, size) = (hash_bytes(&bytes), bytes.len());
377 let mailbox = self
378 .mailboxes
379 .get_mut(mailbox_id)
380 .ok_or_else(|| fail("mailbox_unavailable", "unknown mailbox"))?;
381 if let Some((accepted_at, _)) = mailbox.tombstones.get(&id) {
382 return Ok(DeliveryResult {
383 packet_id: id,
384 accepted_at: *accepted_at,
385 });
386 }
387 let card = &mailbox.current.envelope;
388 let p = &card.event.payload;
389 if !p.routes.contains(&self.origin) {
390 return Err(fail("mailbox_unavailable", "relay is not a current route"));
391 }
392 if h.card_hash != card.hash || now >= p.expires_at {
393 return Err(fail("stale_card", "current card mismatch or expired"));
394 }
395 if now >= h.expires_at {
396 return Err(fail("packet_expired", "packet expired"));
397 }
398 if h.expires_at
399 > p.receive_until
400 .min(now.saturating_add(MAX_TTL_MS + FUTURE_SKEW_MS))
401 {
402 return Err(fail("invalid_packet", "packet expiry bound"));
403 }
404 if size > p.max_packet_bytes {
405 return Err(fail("payload_too_large", "card packet limit"));
406 }
407 mailbox.drop_expired(now);
408 if mailbox.packets.len() >= self.max_packets
409 || mailbox.bytes.saturating_add(size) > self.max_bytes
410 || mailbox.last_seq >= identity::MAX_SAFE_NONCE
411 {
412 return Err(fail("rate_limited", "mailbox storage quota exhausted"));
413 }
414 mailbox.last_seq += 1;
415 mailbox.bytes += size;
416 mailbox
417 .tombstones
418 .insert(id.clone(), (now, packet.header.expires_at));
419 mailbox.packets.insert(
420 id.clone(),
421 (
422 PacketRecord {
423 packet_id: id.clone(),
424 packet,
425 accepted_at: now,
426 seq: mailbox.last_seq,
427 },
428 size,
429 ),
430 );
431 Ok(DeliveryResult {
432 packet_id: id,
433 accepted_at: now,
434 })
435 }
436 fn owner_box(&self, mailbox: &str, jwt: &str, now: i64) -> Result<&RelayMailbox> {
437 path_id(mailbox, 16)?;
438 let claims = owner_claims(jwt, &self.origin, now)?;
439 let found = self
440 .mailboxes
441 .get(mailbox)
442 .ok_or_else(|| fail("mailbox_unavailable", "unknown mailbox"))?;
443 if claims.iss != found.current.envelope.event.actor {
444 return Err(SdkError::PermissionDenied);
445 }
446 Ok(found)
447 }
448 pub fn list(
450 &self,
451 mailbox: &str,
452 jwt: &str,
453 now: i64,
454 limit: usize,
455 cursor: Option<&str>,
456 ) -> Result<ListResponse<PacketRecord>> {
457 let found = self.owner_box(mailbox, jwt, now)?;
458 if !(1..=1000).contains(&limit) {
459 return Err(fail("invalid_request", "list limit must be 1..1000"));
460 }
461 let after = match cursor {
462 None => 0,
463 Some(text) => text
464 .parse::<u64>()
465 .ok()
466 .filter(|n| n.to_string() == text && *n <= identity::MAX_SAFE_NONCE)
467 .ok_or_else(|| fail("invalid_request", "invalid cursor"))?,
468 };
469 let mut rows: Vec<_> = found
470 .packets
471 .values()
472 .map(|(record, _)| record)
473 .filter(|r| r.seq > after && now < r.packet.header.expires_at)
474 .cloned()
475 .collect();
476 rows.sort_by_key(|r| r.seq);
477 let more = rows.len() > limit;
478 rows.truncate(limit);
479 Ok(ListResponse {
480 next_cursor: more.then(|| rows[limit - 1].seq.to_string()),
481 result: rows,
482 })
483 }
484 pub fn delete(&mut self, mailbox: &str, id: &str, jwt: &str, now: i64) -> Result<()> {
485 self.owner_box(mailbox, jwt, now)?;
486 path_id(id, 32)?;
487 let found = self.mailboxes.get_mut(mailbox).unwrap();
488 if let Some((_, size)) = found.packets.remove(id) {
489 found.bytes -= size;
490 }
491 Ok(())
492 }
493 pub fn prune(&mut self, now: i64) -> Result<()> {
496 clock(now)?;
497 for m in self.mailboxes.values_mut() {
498 m.drop_expired(now);
499 m.tombstones.retain(|_, (_, expires)| now < *expires);
500 }
501 self.mailboxes
502 .retain(|_, m| now < m.current.envelope.event.payload.receive_until);
503 Ok(())
504 }
505 pub fn snapshot(&self) -> Result<Value> {
506 let mailboxes: Vec<_> = self
507 .mailboxes
508 .values()
509 .map(|m| {
510 json!({
511 "current": m.current,
512 "last_seq": m.last_seq,
513 "packets": m.packets.values().map(|(r, _)| r).collect::<Vec<_>>(),
514 "tombstones": m.tombstones.iter().map(|(id, (accepted_at, expires_at))| {
515 json!({"packet_id":id,"accepted_at":accepted_at,"expires_at":expires_at})
516 }).collect::<Vec<_>>(),
517 })
518 })
519 .collect();
520 Ok(json!({"version":1,"origin":self.origin,"mailboxes":mailboxes}))
521 }
522 pub fn from_snapshot(value: &Value, max_packets: usize, max_bytes: usize) -> Result<Self> {
524 #[derive(Deserialize)]
525 struct Tombstone {
526 packet_id: String,
527 accepted_at: i64,
528 expires_at: i64,
529 }
530 #[derive(Deserialize)]
531 struct Saved {
532 current: AcceptedRecord<MailboxCardPayload>,
533 last_seq: u64,
534 packets: Vec<PacketRecord>,
535 tombstones: Vec<Tombstone>,
536 }
537 version(value)?;
538 let origin = value["origin"]
539 .as_str()
540 .ok_or_else(|| fail("invalid_request", "snapshot origin"))?;
541 let mut out = Self::with_limits(origin, max_packets, max_bytes)?;
542 let saved: Vec<Saved> = serde_json::from_value(normalized(&value["mailboxes"]))?;
543 for m in saved {
544 let mut mailbox = RelayMailbox {
545 current: m.current,
546 last_seq: m.last_seq,
547 bytes: 0,
548 packets: BTreeMap::new(),
549 tombstones: m
550 .tombstones
551 .into_iter()
552 .map(|t| (t.packet_id, (t.accepted_at, t.expires_at)))
553 .collect(),
554 };
555 for record in m.packets {
556 let size = canonical_bytes(&record.packet)?.len();
557 mailbox.bytes += size;
558 mailbox
559 .packets
560 .insert(record.packet_id.clone(), (record, size));
561 }
562 let id = mailbox.current.envelope.event.payload.mailbox_id.clone();
563 out.mailboxes.insert(id, mailbox);
564 }
565 Ok(out)
566 }
567}
568fn path_id(value: &str, size: usize) -> Result<()> {
569 fixed_bytes(value, size).map_err(|_| fail("invalid_request", "invalid path ID"))?;
570 Ok(())
571}