Skip to main content

rusmes_storage/
lib.rs

1//! Storage abstraction layer for RusMES
2//!
3//! This crate provides a unified storage interface for the RusMES mail server, covering
4//! mailbox management, message delivery, metadata tracking, and quota enforcement.
5//!
6//! # Architecture
7//!
8//! The storage layer is composed of three orthogonal trait abstractions:
9//!
10//! - [`MailboxStore`] — Create, rename, delete, subscribe, and list mailboxes per user.
11//! - [`MessageStore`] — Append, retrieve, copy, flag, search, and delete messages.
12//! - [`MetadataStore`] — Manage per-user quotas and per-mailbox counters (exists/recent/unseen).
13//!
14//! These three stores are obtained from a single [`StorageBackend`] factory, which bundles
15//! them together and can be shared across protocol handlers (IMAP, JMAP, POP3, SMTP).
16//!
17//! # Backends
18//!
19//! | Backend | Module | Notes |
20//! |---------|--------|-------|
21//! | Filesystem / Maildir | [`backends::filesystem`] | Atomic delivery via `tmp/` → `new/` rename; flag encoding in filenames |
22//! | AmateRS distributed KV | [`backends::amaters`] | Mock implementation with circuit-breaker and exponential-backoff retry logic |
23//! | PostgreSQL | [`backends::postgres`] | Full connection pool via `sqlx`, full-text search, migrations |
24//!
25//! # Example — Filesystem backend
26//!
27//! ```rust,no_run
28//! use rusmes_storage::{StorageBackend, MailboxStore, MailboxPath};
29//! use rusmes_storage::backends::filesystem::FilesystemBackend;
30//!
31//! # async fn example() -> anyhow::Result<()> {
32//! let backend = FilesystemBackend::new("/var/mail/rusmes");
33//! let mb_store = backend.mailbox_store();
34//!
35//! let user: rusmes_proto::Username = "alice@example.com".parse()?;
36//! let path = MailboxPath::new(user, vec!["INBOX".to_string()]);
37//! let id = mb_store.create_mailbox(&path).await?;
38//!
39//! let mailbox = mb_store.get_mailbox(&id).await?;
40//! println!("Created: {:?}", mailbox);
41//! # Ok(())
42//! # }
43//! ```
44//!
45//! # ModSeq (Modification Sequence Numbers)
46//!
47//! [`ModSeqGenerator`] produces monotonically increasing sequence numbers suitable for
48//! IMAP CONDSTORE / QRESYNC extensions.  Both mailbox-level and message-level ModSeq
49//! values are tracked via [`MailboxModSeq`] and [`MessageModSeq`] wrappers.
50//!
51//! # Metrics
52//!
53//! [`StorageMetrics`] records per-operation histograms for timing storage calls from
54//! higher-level protocol handlers, enabling Prometheus-compatible export.
55
56pub mod backends;
57pub mod backup;
58pub mod metrics;
59pub mod modseq;
60mod traits;
61mod types;
62
63pub use backup::{backup, restore};
64pub use metrics::{Histogram, MetricsSummary, StorageMetrics, StorageTimer};
65pub use modseq::{MailboxModSeq, MessageModSeq, ModSeq, ModSeqGenerator};
66pub use traits::{MailboxStore, MessageStore, MetadataStore, StorageBackend};
67pub use types::{
68    Mailbox, MailboxCounters, MailboxId, MailboxPath, MessageFlags, MessageMetadata, Quota,
69    SearchCriteria, SpecialUseAttributes,
70};
71
72use serde::{Deserialize, Serialize};
73use std::sync::Arc;
74use std::time::Duration;
75
76/// Events emitted by storage backends after write operations.
77///
78/// Consumed by rusmes-search (Cluster 9) for incremental indexing and by
79/// any other subscriber that needs to react to mailbox changes.
80#[derive(Debug, Clone, Serialize, Deserialize)]
81pub enum StorageEvent {
82    /// A message was stored in a mailbox.
83    MessageStored {
84        account: String,
85        mailbox: String,
86        uid: u32,
87    },
88    /// A message was expunged (permanently deleted) from a mailbox.
89    MessageExpunged {
90        account: String,
91        mailbox: String,
92        uid: u32,
93    },
94}
95
96/// Configuration for which storage backend to build.
97#[derive(Debug, Clone)]
98pub enum BackendKind {
99    /// Filesystem (maildir) backend.
100    Filesystem { path: String },
101    /// SQLite backend (file path, e.g. `"sqlite:///var/mail/rusmes.db?mode=rwc"`).
102    Sqlite { connection_string: String },
103    /// PostgreSQL backend.
104    Postgres { connection_string: String },
105    /// AmateRS distributed backend (mock only).
106    Amaters {
107        endpoints: Vec<String>,
108        replication_factor: usize,
109    },
110}
111
112/// Construct a storage backend from configuration.
113///
114/// For SQL backends, migrations are run before returning.
115/// For the PostgreSQL backend, a background VACUUM scheduler is also started
116/// with the default 24-hour interval.
117pub async fn build_storage(kind: &BackendKind) -> anyhow::Result<Arc<dyn StorageBackend>> {
118    match kind {
119        BackendKind::Filesystem { path } => {
120            use backends::filesystem::FilesystemBackend;
121            let backend = FilesystemBackend::new(path);
122            Ok(Arc::new(backend))
123        }
124        BackendKind::Sqlite { connection_string } => {
125            use backends::sqlite::SqliteBackend;
126            // SqliteBackend::new runs migrations automatically.
127            let backend = SqliteBackend::new(connection_string).await?;
128            Ok(Arc::new(backend))
129        }
130        BackendKind::Postgres { connection_string } => {
131            use backends::postgres::PostgresBackend;
132            // with_config starts the background VACUUM task automatically.
133            let backend = PostgresBackend::new(connection_string).await?;
134            // Also run the hand-rolled idempotent migration DDL for backwards compat.
135            backend.init_schema().await?;
136            Ok(Arc::new(backend))
137        }
138        BackendKind::Amaters {
139            endpoints,
140            replication_factor,
141        } => {
142            use backends::amaters::{AmatersBackend, AmatersConfig};
143            let config = AmatersConfig {
144                cluster_endpoints: endpoints.clone(),
145                replication_factor: *replication_factor,
146                ..Default::default()
147            };
148            let backend = AmatersBackend::new(config).await?;
149            Ok(Arc::new(backend))
150        }
151    }
152}
153
154/// Perform compaction: remove expunged messages older than `older_than`.
155///
156/// Dispatches to `backend.compact_expunged(older_than)`.
157pub async fn compact_expunged(
158    backend: &dyn StorageBackend,
159    older_than: Duration,
160) -> anyhow::Result<usize> {
161    backend.compact_expunged(older_than).await
162}