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