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/// Per-domain DKIM key map built from `MAILRS_DKIM_KEYS`.
50pub mod dkim_env;
51/// DKIM signing helpers (RFC 6376): config + sign-and-prepend.
52pub mod dkim_sign;
53/// Delivery Status Notification (RFC 3464) formatting.
54pub mod dsn;
55/// MTA-STS (RFC 8461) policy lookup + caching.
56pub mod mta_sts;
57/// Queue row type, status enum, and retry-attempt bookkeeping.
58pub mod queue;
59/// Exponential-backoff retry math + "is it permanent?" classifier.
60pub mod retry;
61/// Pluggable [`QueueStore`] trait + an in-memory reference impl.
62pub mod store;
63
64/// Postgres-backed [`QueueStore`] implementation (feature-gated).
65#[cfg(feature = "pg")]
66pub mod pg_store;
67
68/// Connection pool of the active SQL backend (PostgreSQL by default,
69/// spg-embedded behind the `spg` feature).
70#[cfg(all(feature = "pg", not(feature = "spg")))]
71pub type BackendPool = sqlx::PgPool;
72/// Connection pool of the active SQL backend (PostgreSQL by default,
73/// spg-embedded behind the `spg` feature).
74#[cfg(feature = "spg")]
75pub type BackendPool = spg_sqlx::SpgPool;
76/// Async delivery worker that drains the queue + dispatches via SMTP (feature-gated).
77#[cfg(feature = "pg")]
78pub mod worker;
79
80pub use dkim_sign::{DkimDomainKey, DkimSignConfig};
81pub use queue::{QueueStatus, QueuedMessage};
82pub use retry::{retry_delay_secs, retry_delay_secs_jittered, should_bounce};
83pub use store::{
84    InMemoryNotifier, InMemoryQueueStore, NoopNotifier, Notifier, QueueStore, StoreError,
85};
86
87#[cfg(feature = "pg")]
88pub use pg_store::{KevyNotifier, PgQueueStore};
89#[cfg(feature = "pg")]
90pub use worker::{DeliveryWorker, WorkerConfig, group_by_domain};
91
92/// Outbound delivery event for external observers (admin UI, audit log,
93/// metrics pipeline, TLSRPT reporter).
94#[derive(Debug, Clone)]
95pub enum DeliveryEvent {
96    /// A delivery attempt is starting for `queue_id` targeting `domain`.
97    Attempt {
98        /// Queue row id being attempted.
99        queue_id: i64,
100        /// Destination domain for this attempt.
101        domain: String,
102    },
103    /// The STARTTLS phase completed (success or failure). Emitted
104    /// once per MX connection right after the TLS handshake, before
105    /// any RCPT TO / DATA. Carries structured outcome suitable for
106    /// TLSRPT (RFC 8460) reporting.
107    ///
108    /// Not emitted when the connection is plain (no STARTTLS
109    /// attempted), in which case the caller should record the
110    /// session as untrusted-TLS via its own logging.
111    TlsAttempt {
112        /// Destination domain (the recipient's, not the MX's).
113        domain: String,
114        /// MX hostname we connected to.
115        mx_host: String,
116        /// Structured outcome of the TLS attempt.
117        outcome: TlsAttemptOutcome,
118    },
119    /// The message was accepted by the remote MX.
120    Success {
121        /// Queue row id that just succeeded.
122        queue_id: i64,
123        /// Destination domain that accepted the message.
124        domain: String,
125    },
126    /// A delivery attempt failed; the message is rescheduled for retry.
127    Failed {
128        /// Queue row id that failed this attempt.
129        queue_id: i64,
130        /// Destination domain that was attempted.
131        domain: String,
132        /// Human-readable error from the SMTP client (typically the
133        /// remote's response text).
134        error: String,
135    },
136    /// The message bounced permanently and will not be retried; a DSN was
137    /// queued back to the original `sender`.
138    Bounced {
139        /// Queue row id that bounced.
140        queue_id: i64,
141        /// Original envelope sender — the DSN gets queued back to them.
142        sender: String,
143    },
144}
145
146/// Outcome of one STARTTLS attempt, carried by
147/// [`DeliveryEvent::TlsAttempt`]. The four variants discriminate
148/// between TLS success, server-side refusal (still safely usable
149/// in plain), and handshake failure (with the structured
150/// [`mailrs_smtp_client::TlsOutcome`] underneath).
151#[derive(Debug, Clone)]
152pub enum TlsAttemptOutcome {
153    /// STARTTLS completed; encrypted channel established.
154    /// `policy` tells the report which policy gated this attempt
155    /// (`"dane"`, `"sts"`, or `"opportunistic"`).
156    Success {
157        /// Which policy class was active for this attempt.
158        policy: &'static str,
159    },
160    /// Server did not advertise STARTTLS in the EHLO response.
161    /// Maps to RFC 8460 `starttls-not-supported`.
162    NotAdvertised,
163    /// Server rejected the `STARTTLS` command (returned 4xx/5xx).
164    Rejected {
165        /// SMTP response code.
166        code: u16,
167        /// SMTP response text.
168        message: String,
169    },
170    /// TLS handshake started but failed mid-way. The wrapped
171    /// [`mailrs_smtp_client::TlsOutcome`] is RFC 8460 §4.3-aligned.
172    HandshakeFailed(mailrs_smtp_client::TlsOutcome),
173}
174
175/// Callback channel for [`DeliveryEvent`] notifications. Wrapped in `Arc` so
176/// the worker can clone it across spawned delivery tasks.
177pub type DeliveryEventSender = Arc<dyn Fn(DeliveryEvent) + Send + Sync>;