Expand description
§mailrs-mailbox
Mailbox-metadata storage for Rust mail servers — the IMAP/JMAP-shaped abstraction every project building an inbox needs, plus a PostgreSQL reference implementation. Extracted from mailrs so any IMAP, JMAP, or chat-style mail UI can lean on the same battle-tested store.
This is, at the time of writing, the only standalone server-side mailbox-metadata library on crates.io: a portable trait covering mailbox CRUD, message storage, IMAP CONDSTORE flag ops, threading, and JMAP-shape change tracking — plus an in-memory fixture that doubles as a test harness and as proof the trait is genuinely abstract.
§Highlights
- Trait-first — code against
MailboxStore, a 24-method async trait covering the IMAP and JMAP intersection. Swap the backend without changing handler code. - Two reference implementations included —
pg::PgMailboxStore(PostgreSQL, the production-tested one) andfixtures::InMemoryMailboxStore(in-process, for tests and the trait-conformance smell test). - CONDSTORE built-in — per-message
modseq,store_flags_if_unchangedcompare-and-swap,messages_changed_sincefor IMAP CHANGEDSINCE and JMAPEmail/changes. - Threading helpers — pure-function
extract_message_id/extract_in_reply_to/normalize_message_id/resolve_thread_idinthreading, no I/O. - Flag bitmask interop —
FLAG_*constants matchingmailrs-maildirplusmaildir_flags_to_bitmask/bitmask_to_maildir_flagsif you’re pairing with filesystem delivery.
§Two-tier API: portable trait vs PG-EXT inherent
The crate intentionally exposes two surfaces:
-
MailboxStoretrait — 24 methods rooted in IMAP / JMAP primitives. This is the portable contract; downstream consumers should program against&dyn MailboxStore. Trait methods return store-agnostic types (Mailbox,Message,MailboxStatus,Inserted, etc). -
PgMailboxStoreinherent methods — the PostgreSQL implementation carries additional methods for content projections, contact-tracking, semantic search via pgvector, thread-level UI state (pin / archive / snooze), and similar product-shape concerns from the parent mailrs project. These methods are public so mailrs can consume them, but documented asPG-EXT— they are NOT part of the trait contract and should not be relied on by store-agnostic code.
The split is the cleanest expression of “open source isn’t just stripping
the pub keyword off internal code”. The trait covers what mail-server
projects actually share. The PG-EXT methods carry mailrs’s specific
product surface without contaminating the abstraction.
§Methods covered (1.0)
The MailboxStore trait covers 24 operations grouped by concern:
| Group | Methods | Purpose |
|---|---|---|
| Mailbox CRUD | 7 (create_mailbox, delete_mailbox, rename_mailbox, list_mailboxes, get_mailbox, get_mailbox_by_id, mailbox_status) | IMAP CREATE/DELETE/LIST/RENAME/STATUS; JMAP Mailbox/{get,set,query} |
| Message CRUD | 8 (insert_message, get_message_by_uid, get_message, find_by_message_id, copy_message, move_message, expunge, messages_changed_since) | IMAP APPEND/FETCH/COPY/MOVE/EXPUNGE/CHANGEDSINCE; JMAP Email/{get,set,changes} |
| Flags + CONDSTORE | 4 (set_flags, add_flags, remove_flags, store_flags_if_unchanged) | IMAP STORE / STORE.SILENT / UNCHANGEDSINCE compare-and-swap (RFC 7162) |
| Threading | 3 (thread_id_for_message, thread_message_ids, thread_references) | JMAP Thread/get; ancestry walk for inReplyToId display |
| Query | 1 (query_messages) | JMAP Email/query-shape filter: mailbox + text + has_keyword + not_keyword + pagination |
| Quota | 1 (user_storage_bytes) | per-user byte sum |
Plus pure helpers in threading (Message-ID parsing, thread resolution) and bitmask conversions in types.
§Quick start
use mailrs_mailbox::{MailboxStore, PgMailboxStore};
let pool = sqlx::PgPool::connect("postgres://localhost/mailrs").await?;
let store = PgMailboxStore::new(pool);
let mb = MailboxStore::create_mailbox(&store, "alice@example.com", "INBOX").await?;
let status = MailboxStore::mailbox_status(&store, mb.id).await?;
println!("INBOX: {} total, {} unread", status.total, status.unread);For testing without a database, use InMemoryMailboxStore:
use mailrs_mailbox::fixtures::{InMemoryMailboxStore, EXAMPLE_USER};
use mailrs_mailbox::MailboxStore;
let store = InMemoryMailboxStore::new();
let inbox = store.create_mailbox(EXAMPLE_USER, "INBOX").await.unwrap();
assert_eq!(inbox.name, "INBOX");§Schema (PG impl)
The PG reference impl expects the mailrs PostgreSQL schema. The
authoritative DDL lives at scripts/init-schema.sql in the mailrs repo;
the minimum tables PgMailboxStore reads from are mailboxes and
messages. PG-EXT methods additionally touch email_analysis,
contacts, sender_feedback, snoozed_conversations.
sqlx is used in runtime-query mode (sqlx::query / query_as),
not compile-time-checked macros. No DATABASE_URL needed at build time.
If you want a different schema, implement MailboxStore against your
own schema and the trait-driven handlers using this crate just work.
§Tested
1.0.0 ships 111 tests across 4 layers:
| Layer | Count | Surface |
|---|---|---|
src/*/tests (inline) | 67 | Pure helpers (threading, flag bitmask conversion, type roundtrips) |
tests/trait_contract.rs | 35 | Every trait method against InMemoryMailboxStore — the portable contract suite |
tests/smoke.rs | 5 | PG-specific behaviour against a real Postgres 18 + pgvector container (via testcontainers) — schema application, modseq atomicity, sqlx integration |
tests/perf_gate.rs | 4 | Threading-helper regression budgets (see BUDGETS.md) |
Total density: ~31 tests/kloc, in the same band as the published
mailrs-jmap (28) and mailrs-dav (34).
Run the portable suite (no Docker needed):
cargo test -p mailrs-mailbox --test trait_contractRun the PG suite (needs Docker):
cargo test -p mailrs-mailbox --test smoke -- --test-threads=1§Performance
Two bench files cover this crate:
benches/threading.rs— pure-helper microbenchmarks (header extraction, message-id normalization, thread resolution).benches/store_ops.rs—InMemoryMailboxStoreops, exercising the trait dispatch + RwLock + Vec backing.
Measured with criterion 0.8 on Apple Silicon (M-series), cargo bench, release profile.
| Operation | Median | Notes |
|---|---|---|
extract_message_id(short header) | ~150 ns | typical 6-header message |
extract_message_id(15-line marketing header) | ~470 ns | scans the full header block |
extract_in_reply_to(short header) | ~180 ns | early-exit when missing |
extract_in_reply_to(15-line marketing header) | ~485 ns | scans for the In-Reply-To: line |
normalize_message_id(" <abc-123@…> ") | ~8 ns | trim + lowercase |
resolve_thread_id(<new root>) | ~16 ns | no parent lookup |
resolve_thread_id(<known parent>) | ~14 ns | with parent-lookup closure |
insert_message (first, empty mailbox) | ~1.2 µs | metadata write through RwLock + 12 string allocations |
query_messages (mailbox scope, paginate first 50 of 1k) | ~115 µs | clone + sort_unstable across 1000 messages; the PG impl uses SQL ORDER BY + LIMIT |
query_messages (text substring match on 1k) | ~120 µs | three case-insensitive substring scans per message |
add_flags (hot path) | ~55 ns | one Vec lookup + flag OR + modseq bump |
store_flags_if_unchanged (CONDSTORE) | ~57 ns | compare-and-swap, same cost as add_flags |
mailbox_status(1k messages) | ~520 ns | total / unread / recent counts |
Run with cargo bench -p mailrs-mailbox. The query_messages and insert_into_1k_mailbox numbers are dev-fixture cost — the PG impl pushes the work into the database and is not benched here (its cost is dominated by network + planner latency).
See BUDGETS.md for the regression budgets gated by tests/perf_gate.rs.
§Versioning
1.x follows semver. The stable public surface:
MailboxStoretrait method signaturesStoreErrortype alias- All types in
mailrs_mailbox::types(marked#[non_exhaustive]where growth is anticipated, so new fields are minor-bump compatible) pg::PgMailboxStore::new+poolaccessor- Pure helpers in
threading::*and theFLAG_*constants
The set of inherent PG-EXT methods on PgMailboxStore may grow or
re-shape within 1.x to track the parent mailrs project’s needs.
Trait-first code is unaffected.
§License
Licensed under either Apache License, Version 2.0 or MIT license at your option.
Re-exports§
pub use store::MailboxStore;pub use store::StoreError;pub use types::bitmask_to_maildir_flags;pub use types::maildir_flags_to_bitmask;pub use types::ConversationSummary;pub use types::EmailAnalysisRow;pub use types::FlagAction;pub use types::FlagOp;pub use types::InsertMessage;pub use types::Inserted;pub use types::Mailbox;pub use types::MailboxStatus;pub use types::Message;pub use types::MessageMeta;pub use types::QueryFilter;pub use types::FLAG_ANSWERED;pub use types::FLAG_DELETED;pub use types::FLAG_DRAFT;pub use types::FLAG_FLAGGED;pub use types::FLAG_RECENT;pub use types::FLAG_SEEN;pub use pg::EmailAnalysisInput;pub use pg::PgMailboxStore;
Modules§
- fixtures
- In-memory
MailboxStoreimplementation suitable for tests, examples, and downstream-consumer test harnesses. - pg
- PostgreSQL reference implementation of
MailboxStore. - store
- Portable
MailboxStoretrait — the IMAP/JMAP-shaped abstraction backend implementations must satisfy. TheMailboxStoretrait — the abstraction every mailbox metadata backend implements. - threading
- Pure-function helpers for message-ID parsing and thread resolution. No I/O; safe to call from hot paths.
- types
- Store-agnostic data types (
Mailbox,Message,Inserted…) and theFLAG_*bitmask constants shared across every backend.
Type Aliases§
- Legacy
Mailbox Store Deprecated - Back-compat alias for the legacy struct name. Prefer
PgMailboxStorein new code. Will be removed in 2.0.