Skip to main content

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}