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