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>;