supercode_harness/mail_file.rs
1//! Filing: a sender that does not wait for its mail to arrive (a board's dispatcher, a script) writes the envelope
2//! into the receiver's mailbox here and stops (docs/adr/0008-agent-mailbox.md, decision 17; D140). The write is local,
3//! needs no network and is idempotent by the message id its key fixes. The mailbox delivers it from there: the
4//! machine daemon's watch loop ([`crate::mail_watch::MailCarrier`]) hands it to its session by the session's own door,
5//! or, for an address on another machine, carries it there through Teams and files it with the same wake.
6
7use crate::mail_route::Caller;
8use crate::mailbox::{mail_root, Envelope, MailAddress, MailKind, Mailbox, ReplyVia};
9
10/// What a filing did.
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct Filed {
13 /// The message's id (the one its key fixes, when it had one).
14 pub message_id: String,
15 /// Whether this filing wrote it; false when the id was already filed (a second filer, or a repeat).
16 pub new: bool,
17}
18
19/// The id of a message `sender` sends under `key`: a key that is itself a whole message id (`m-` and 24 lowercase
20/// hex digits) is that id, so a sender that chose it beforehand (a board blocking a card on the answer to the question
21/// it is about to send) knows what the answer will name; any other key is hashed with the sender's address; no key is
22/// a fresh id.
23pub fn message_id_for(sender: &MailAddress, key: Option<&str>) -> std::io::Result<String> {
24 Ok(match key {
25 Some(key)
26 if key.len() == 26
27 && key.starts_with("m-")
28 && key[2..]
29 .bytes()
30 .all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase()) =>
31 {
32 key.to_string()
33 }
34 Some(key) => format!(
35 "m-{}",
36 &blake3::hash(format!("{sender}\0{key}").as_bytes()).to_hex()[..24]
37 ),
38 None => crate::mailbox::new_message_id()?,
39 })
40}
41
42/// One message to file.
43#[derive(Debug, Clone, Default)]
44pub struct Filing {
45 /// A short subject, separate from the body.
46 pub subject: Option<String>,
47 /// Idempotency key: filing again under it files nothing new.
48 pub key: Option<String>,
49 /// Ask the receiver to deliver it now (wake an idle session); false leaves it for the session's next turn.
50 pub wake: bool,
51}
52
53/// File `body` from `caller` in the mailbox of `to`, on this machine's mail root whatever machine `to` names (mail for
54/// another machine waits here until the watch loop has carried it there). A wake is requested only by the filing that
55/// wrote the message, so a repeat never delivers it twice.
56pub fn file(
57 caller: &Caller,
58 to: &MailAddress,
59 body: &str,
60 filing: &Filing,
61) -> std::io::Result<Filed> {
62 let mut envelope = Envelope::new(
63 caller.address.clone(),
64 caller.name.clone(),
65 MailKind::Peer,
66 ReplyVia::Command,
67 body,
68 )?;
69 envelope.id = message_id_for(&caller.address, filing.key.as_deref())?;
70 envelope.subject = filing.subject.as_deref().map(|value| {
71 value
72 .lines()
73 .next()
74 .unwrap_or("")
75 .chars()
76 .take(200)
77 .collect()
78 });
79 let mailbox = Mailbox::open(&mail_root(), to)?;
80 if mailbox.find(&envelope.id)?.is_some() {
81 return Ok(Filed {
82 message_id: envelope.id,
83 new: false,
84 });
85 }
86 mailbox.deliver(&envelope)?;
87 if filing.wake {
88 mailbox.request_wake(&envelope.id)?;
89 }
90 Ok(Filed {
91 message_id: envelope.id,
92 new: true,
93 })
94}