Skip to main content

rusmes_core/
transport.rs

1//! Mail transport abstraction for outgoing delivery
2//!
3//! The [`MailTransport`] trait decouples JMAP submission from a specific SMTP
4//! client implementation, enabling dependency injection and testing with mock
5//! transports.
6
7use async_trait::async_trait;
8use rusmes_proto::Mail;
9
10/// SMTP envelope carrying sender/recipients for outgoing mail.
11///
12/// Separate from the [`Mail`] message so that the transport layer can override
13/// RFC 5321 envelope addresses independently of RFC 5322 headers.
14#[derive(Debug, Clone)]
15pub struct SmtpEnvelope {
16    /// RFC 5321 `MAIL FROM` address (bare, no angle brackets)
17    pub mail_from: String,
18    /// RFC 5321 `RCPT TO` addresses (bare, no angle brackets)
19    pub rcpt_to: Vec<String>,
20}
21
22/// Abstraction over mail delivery — allows mocking in tests and swapping
23/// delivery backends without touching higher-level code.
24#[async_trait]
25pub trait MailTransport: Send + Sync {
26    /// Deliver a message immediately.
27    ///
28    /// Returns a server-assigned submission ID (typically a UUID).
29    async fn send(&self, envelope: SmtpEnvelope, mail: &Mail) -> anyhow::Result<String>;
30
31    /// Schedule delivery at a specific UTC instant.
32    ///
33    /// If `at` is in the past or within 5 seconds of now, implementations
34    /// SHOULD deliver immediately.  Returns a submission ID that can be passed
35    /// to [`cancel`](Self::cancel).
36    async fn send_at(
37        &self,
38        envelope: SmtpEnvelope,
39        mail: &Mail,
40        at: chrono::DateTime<chrono::Utc>,
41    ) -> anyhow::Result<String>;
42
43    /// Cancel a queued send.
44    ///
45    /// Returns `true` if the submission was still queued and has been removed,
46    /// `false` if it has already been delivered or is unknown.
47    async fn cancel(&self, submission_id: &str) -> anyhow::Result<bool>;
48}
49
50// ── NullMailTransport ─────────────────────────────────────────────────────────
51
52/// A no-op [`MailTransport`] that records nothing and always reports success.
53///
54/// Useful as a default in contexts where outgoing SMTP delivery has not been
55/// configured yet (e.g. `dispatch_method` in the JMAP handler layer before a
56/// real relay has been wired in).
57#[derive(Debug, Default)]
58pub struct NullMailTransport;
59
60#[async_trait]
61impl MailTransport for NullMailTransport {
62    async fn send(&self, _envelope: SmtpEnvelope, _mail: &Mail) -> anyhow::Result<String> {
63        Ok(uuid::Uuid::new_v4().to_string())
64    }
65
66    async fn send_at(
67        &self,
68        _envelope: SmtpEnvelope,
69        _mail: &Mail,
70        _at: chrono::DateTime<chrono::Utc>,
71    ) -> anyhow::Result<String> {
72        Ok(uuid::Uuid::new_v4().to_string())
73    }
74
75    async fn cancel(&self, _submission_id: &str) -> anyhow::Result<bool> {
76        Ok(false)
77    }
78}