Skip to main content

mailrs_outbound_queue/
lib.rs

1#![deny(missing_docs)]
2#![deny(rustdoc::broken_intra_doc_links)]
3
4//! Outbound mail queue primitives: DKIM signing, DSN generation, MTA-STS
5//! lookup, retry/backoff, plus a pluggable store trait and a Postgres
6//! reference implementation.
7//!
8//! `mailrs-outbound-queue` extracts the queue + delivery layer from
9//! [mailrs] so it can be reused — or driven by a custom store — in any Rust
10//! MTA. The "pure" pieces ([`dkim_sign`], [`dsn`], [`mta_sts`], [`retry`])
11//! depend only on hashes / DNS / pure arithmetic; the [`QueueStore`] +
12//! [`Notifier`] traits in [`store`] decouple delivery state from any
13//! particular backend.
14//!
15//! # Feature flags
16//!
17//! | Feature | Default | Includes |
18//! |---------|---------|----------|
19//! | `pg`    | on      | [`PgQueueStore`] + [`KevyNotifier`] + the bundled [`DeliveryWorker`] that consumes them. Pulls in `sqlx` and `kevy-embedded`. |
20//!
21//! Disable the `pg` feature if you want only the traits + pure primitives:
22//!
23//! ```toml
24//! mailrs-outbound-queue = { version = "1", default-features = false }
25//! ```
26//!
27//! # Two paths
28//!
29//! The crate exposes two parallel APIs against the same underlying queue
30//! semantics:
31//!
32//! - **Trait API** ([`QueueStore`], [`Notifier`]) — the portable surface.
33//!   Use this if you want a non-PG backend, or if you want your own
34//!   delivery loop. An always-available [`InMemoryQueueStore`] is included
35//!   for tests + pilots.
36//! - **PG free functions** in the [`queue`] module — convenience for the
37//!   common case where you already have a `sqlx::PgPool` and just want
38//!   `queue::enqueue(pool, ...)`. These back the bundled
39//!   [`DeliveryWorker`] and are what mailrs itself uses.
40//!
41//! Both APIs are stable for v1.x and back-compatible. The trait API plus a
42//! generic worker is the target for v2.
43//!
44//! [mailrs]: https://github.com/goliajp/mailrs
45//! [`mailrs-smtp-client`]: https://crates.io/crates/mailrs-smtp-client
46
47use std::sync::Arc;
48
49/// DKIM signing helpers (RFC 6376): config + sign-and-prepend.
50pub mod dkim_sign;
51/// Delivery Status Notification (RFC 3464) formatting.
52pub mod dsn;
53/// MTA-STS (RFC 8461) policy lookup + caching.
54pub mod mta_sts;
55/// Queue row type, status enum, and retry-attempt bookkeeping.
56pub mod queue;
57/// Exponential-backoff retry math + "is it permanent?" classifier.
58pub mod retry;
59/// Pluggable [`QueueStore`] trait + an in-memory reference impl.
60pub mod store;
61
62/// Postgres-backed [`QueueStore`] implementation (feature-gated).
63#[cfg(feature = "pg")]
64pub mod pg_store;
65/// Async delivery worker that drains the queue + dispatches via SMTP (feature-gated).
66#[cfg(feature = "pg")]
67pub mod worker;
68
69pub use dkim_sign::{DkimDomainKey, DkimSignConfig};
70pub use queue::{QueueStatus, QueuedMessage};
71pub use retry::{retry_delay_secs, retry_delay_secs_jittered, should_bounce};
72pub use store::{
73    InMemoryNotifier, InMemoryQueueStore, NoopNotifier, Notifier, QueueStore, StoreError,
74};
75
76#[cfg(feature = "pg")]
77pub use pg_store::{KevyNotifier, PgQueueStore};
78#[cfg(feature = "pg")]
79pub use worker::{DeliveryWorker, WorkerConfig, group_by_domain};
80
81/// Outbound delivery event for external observers (admin UI, audit log,
82/// metrics pipeline, TLSRPT reporter).
83#[derive(Debug, Clone)]
84pub enum DeliveryEvent {
85    /// A delivery attempt is starting for `queue_id` targeting `domain`.
86    Attempt {
87        /// Queue row id being attempted.
88        queue_id: i64,
89        /// Destination domain for this attempt.
90        domain: String,
91    },
92    /// The STARTTLS phase completed (success or failure). Emitted
93    /// once per MX connection right after the TLS handshake, before
94    /// any RCPT TO / DATA. Carries structured outcome suitable for
95    /// TLSRPT (RFC 8460) reporting.
96    ///
97    /// Not emitted when the connection is plain (no STARTTLS
98    /// attempted), in which case the caller should record the
99    /// session as untrusted-TLS via its own logging.
100    TlsAttempt {
101        /// Destination domain (the recipient's, not the MX's).
102        domain: String,
103        /// MX hostname we connected to.
104        mx_host: String,
105        /// Structured outcome of the TLS attempt.
106        outcome: TlsAttemptOutcome,
107    },
108    /// The message was accepted by the remote MX.
109    Success {
110        /// Queue row id that just succeeded.
111        queue_id: i64,
112        /// Destination domain that accepted the message.
113        domain: String,
114    },
115    /// A delivery attempt failed; the message is rescheduled for retry.
116    Failed {
117        /// Queue row id that failed this attempt.
118        queue_id: i64,
119        /// Destination domain that was attempted.
120        domain: String,
121        /// Human-readable error from the SMTP client (typically the
122        /// remote's response text).
123        error: String,
124    },
125    /// The message bounced permanently and will not be retried; a DSN was
126    /// queued back to the original `sender`.
127    Bounced {
128        /// Queue row id that bounced.
129        queue_id: i64,
130        /// Original envelope sender — the DSN gets queued back to them.
131        sender: String,
132    },
133}
134
135/// Outcome of one STARTTLS attempt, carried by
136/// [`DeliveryEvent::TlsAttempt`]. The four variants discriminate
137/// between TLS success, server-side refusal (still safely usable
138/// in plain), and handshake failure (with the structured
139/// [`mailrs_smtp_client::TlsOutcome`] underneath).
140#[derive(Debug, Clone)]
141pub enum TlsAttemptOutcome {
142    /// STARTTLS completed; encrypted channel established.
143    /// `policy` tells the report which policy gated this attempt
144    /// (`"dane"`, `"sts"`, or `"opportunistic"`).
145    Success {
146        /// Which policy class was active for this attempt.
147        policy: &'static str,
148    },
149    /// Server did not advertise STARTTLS in the EHLO response.
150    /// Maps to RFC 8460 `starttls-not-supported`.
151    NotAdvertised,
152    /// Server rejected the `STARTTLS` command (returned 4xx/5xx).
153    Rejected {
154        /// SMTP response code.
155        code: u16,
156        /// SMTP response text.
157        message: String,
158    },
159    /// TLS handshake started but failed mid-way. The wrapped
160    /// [`mailrs_smtp_client::TlsOutcome`] is RFC 8460 §4.3-aligned.
161    HandshakeFailed(mailrs_smtp_client::TlsOutcome),
162}
163
164/// Callback channel for [`DeliveryEvent`] notifications. Wrapped in `Arc` so
165/// the worker can clone it across spawned delivery tasks.
166pub type DeliveryEventSender = Arc<dyn Fn(DeliveryEvent) + Send + Sync>;