Skip to main content

ma_core/
outbox.rs

1//! Transport-agnostic send handle to a remote ma service.
2//!
3//! An `Outbox` wraps the transport details and exposes a single `send()`
4//! method. Outboxes are lightweight and meant to be kept alive for the
5//! duration of a session — `ma-core` manages the underlying connections.
6//!
7//! `send()` takes a [`Message`], validates it, applies the message type's
8//! encryption policy, and transmits. Malformed or expired messages are
9//! rejected before anything hits the wire.
10//!
11//! Requires the `iroh` feature.
12//!
13//! ```ignore
14//! let mut outbox = ep.outbox(&resolver, "did:ma:k51qzi5uqu5d…", INBOX_PROTOCOL_ID).await?;
15//! outbox.send(&message).await?;
16//! // Keep the outbox alive — no need to close it.
17//! ```
18
19use crate::error::{Error, Result};
20use crate::{Did, Document, Message};
21use async_trait::async_trait;
22
23#[async_trait]
24pub(crate) trait OutboxWire: Send + std::fmt::Debug {
25    async fn send_payload(&mut self, payload: &[u8]) -> Result<()>;
26    fn close_box(self: Box<Self>);
27}
28
29/// A transport-agnostic write handle to a remote service.
30///
31/// The caller doesn't need to know the underlying transport.
32#[derive(Debug)]
33pub struct Outbox {
34    inner: Option<Box<dyn OutboxWire>>,
35    recipient_document: Document,
36    local: bool,
37    did: String,
38    protocol: String,
39}
40
41impl Outbox {
42    /// Create an outbox backed by a transport implementation.
43    pub(crate) fn from_transport<T>(
44        transport: T,
45        recipient_document: Document,
46        local: bool,
47        did: String,
48        protocol: String,
49    ) -> Self
50    where
51        T: OutboxWire + 'static,
52    {
53        Self {
54            inner: Some(Box::new(transport)),
55            recipient_document,
56            local,
57            did,
58            protocol,
59        }
60    }
61
62    /// Send a ma message to the remote service.
63    ///
64    /// Validates the message headers, encrypts non-broadcast remote messages,
65    /// and transmits the resulting CBOR payload.
66    ///
67    /// # Errors
68    /// Returns an error if validation, serialization, or transport send fails.
69    pub async fn send(&mut self, message: &Message) -> Result<()> {
70        message.headers().validate()?;
71        let unencrypted = message.message_type == crate::service::MESSAGE_TYPE_BROADCAST
72            || (self.local && same_base_did(&message.from, &message.to)?);
73        let payload = if unencrypted {
74            message.encode()?
75        } else {
76            message.enclose_for(&self.recipient_document)?.encode()?
77        };
78        match self.inner.as_mut() {
79            Some(transport) => transport.send_payload(&payload).await,
80            None => Err(Error::ConnectionClosed("outbox is closed".to_string())),
81        }
82    }
83
84    /// The DID this outbox delivers to.
85    #[must_use]
86    pub fn did(&self) -> &str {
87        &self.did
88    }
89
90    /// The protocol this outbox is connected to.
91    #[must_use]
92    pub fn protocol(&self) -> &str {
93        &self.protocol
94    }
95
96    /// Close the outbox gracefully.
97    pub fn close(mut self) {
98        if let Some(transport) = self.inner.take() {
99            transport.close_box();
100        }
101    }
102}
103
104fn same_base_did(from: &str, to: &str) -> Result<bool> {
105    let (from_base, _) = Did::parse(from)?;
106    let (to_base, _) = Did::parse(to)?;
107    Ok(from_base == to_base)
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113    use crate::{generate_identity_from_secret, Envelope, SigningKey};
114    use std::sync::{Arc, Mutex};
115
116    #[derive(Debug, Clone)]
117    struct CaptureWire(Arc<Mutex<Vec<u8>>>);
118
119    #[async_trait]
120    impl OutboxWire for CaptureWire {
121        async fn send_payload(&mut self, payload: &[u8]) -> Result<()> {
122            self.0
123                .lock()
124                .expect("capture lock")
125                .extend_from_slice(payload);
126            Ok(())
127        }
128
129        fn close_box(self: Box<Self>) {}
130    }
131
132    fn signing_key(identity: &crate::GeneratedIdentity) -> SigningKey {
133        let did = Did::new_url(&identity.subject_url.ipns, Some("sign")).expect("signing DID");
134        let bytes = hex::decode(&identity.signing_private_key_hex).expect("private key hex");
135        SigningKey::from_private_key_bytes(did, bytes.try_into().expect("private key length"))
136            .expect("signing key")
137    }
138
139    fn outbox(recipient_document: &Document, local: bool) -> (Outbox, Arc<Mutex<Vec<u8>>>) {
140        let captured = Arc::new(Mutex::new(Vec::new()));
141        let outbox = Outbox::from_transport(
142            CaptureWire(captured.clone()),
143            recipient_document.clone(),
144            local,
145            recipient_document.id.clone(),
146            crate::service::INBOX_PROTOCOL_ID.to_string(),
147        );
148        (outbox, captured)
149    }
150
151    fn inbox_url(identity: &crate::GeneratedIdentity) -> String {
152        format!("{}#inbox", identity.document.id)
153    }
154
155    #[tokio::test]
156    async fn remote_message_is_transmitted_as_envelope() {
157        let sender = generate_identity_from_secret([1; 32]).expect("sender identity");
158        let recipient = generate_identity_from_secret([2; 32]).expect("recipient identity");
159        let message = Message::new(
160            sender.document.id.clone(),
161            inbox_url(&recipient),
162            crate::service::MESSAGE_TYPE_MESSAGE,
163            "text/plain",
164            b"secret",
165            &signing_key(&sender),
166        )
167        .expect("message");
168        let (mut outbox, captured) = outbox(&recipient.document, false);
169
170        outbox.send(&message).await.expect("send message");
171
172        let payload = captured.lock().expect("capture lock");
173        Envelope::decode(&payload).expect("encrypted envelope");
174        assert!(
175            Message::decode(&payload).is_err(),
176            "raw message reached wire"
177        );
178    }
179
180    #[tokio::test]
181    async fn broadcast_is_transmitted_as_raw_message() {
182        let sender = generate_identity_from_secret([1; 32]).expect("sender identity");
183        let recipient = generate_identity_from_secret([2; 32]).expect("recipient identity");
184        let message = Message::new(
185            sender.document.id.clone(),
186            String::new(),
187            crate::service::MESSAGE_TYPE_BROADCAST,
188            "text/plain",
189            b"public",
190            &signing_key(&sender),
191        )
192        .expect("broadcast");
193        let (mut outbox, captured) = outbox(&recipient.document, false);
194
195        outbox.send(&message).await.expect("send broadcast");
196
197        let payload = captured.lock().expect("capture lock");
198        assert_eq!(Message::decode(&payload).expect("raw message"), message);
199    }
200
201    #[tokio::test]
202    async fn local_same_base_did_is_transmitted_as_raw_message() {
203        let identity = generate_identity_from_secret([1; 32]).expect("identity");
204        let from = format!("{}#sender", identity.document.id);
205        let to = format!("{}#recipient", identity.document.id);
206        let message = Message::new(
207            from,
208            to,
209            crate::service::MESSAGE_TYPE_MESSAGE,
210            "text/plain",
211            b"local",
212            &signing_key(&identity),
213        )
214        .expect("message");
215        let (mut outbox, captured) = outbox(&identity.document, true);
216
217        outbox.send(&message).await.expect("send local message");
218
219        let payload = captured.lock().expect("capture lock");
220        assert_eq!(Message::decode(&payload).expect("raw message"), message);
221    }
222
223    #[tokio::test]
224    async fn local_wire_does_not_exempt_different_base_dids() {
225        let sender = generate_identity_from_secret([1; 32]).expect("sender identity");
226        let recipient = generate_identity_from_secret([2; 32]).expect("recipient identity");
227        let message = Message::new(
228            sender.document.id.clone(),
229            inbox_url(&recipient),
230            crate::service::MESSAGE_TYPE_MESSAGE,
231            "text/plain",
232            b"secret",
233            &signing_key(&sender),
234        )
235        .expect("message");
236        let (mut outbox, captured) = outbox(&recipient.document, true);
237
238        outbox.send(&message).await.expect("send message");
239
240        let payload = captured.lock().expect("capture lock");
241        Envelope::decode(&payload).expect("encrypted envelope");
242    }
243}