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}