# mailrs-mailbox
[](https://crates.io/crates/mailrs-mailbox)
[](https://docs.rs/mailrs-mailbox)
[](#license)
[](https://crates.io/crates/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`](https://docs.rs/mailrs-mailbox/latest/mailrs_mailbox/store/trait.MailboxStore.html), a 24-method async trait covering the IMAP and JMAP intersection. Swap the backend without changing handler code.
- **Two reference implementations included** —
[`pg::PgMailboxStore`](https://docs.rs/mailrs-mailbox/latest/mailrs_mailbox/pg/struct.PgMailboxStore.html) (PostgreSQL, the production-tested one) and [`fixtures::InMemoryMailboxStore`](https://docs.rs/mailrs-mailbox/latest/mailrs_mailbox/fixtures/struct.InMemoryMailboxStore.html) (in-process, for tests and the trait-conformance smell test).
- **CONDSTORE built-in** — per-message `modseq`, `store_flags_if_unchanged` compare-and-swap, `messages_changed_since` for IMAP CHANGEDSINCE and JMAP `Email/changes`.
- **Threading helpers** — pure-function `extract_message_id` / `extract_in_reply_to` / `normalize_message_id` / `resolve_thread_id` in [`threading`](https://docs.rs/mailrs-mailbox/latest/mailrs_mailbox/threading/index.html), no I/O.
- **Flag bitmask interop** — `FLAG_*` constants matching [`mailrs-maildir`](https://crates.io/crates/mailrs-maildir) plus `maildir_flags_to_bitmask` / `bitmask_to_maildir_flags` if you're pairing with filesystem delivery.
## Two-tier API: portable trait vs PG-EXT inherent
The crate intentionally exposes two surfaces:
- **`MailboxStore` trait** — 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).
- **`PgMailboxStore` inherent 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 as `PG-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:
| 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`](https://docs.rs/mailrs-mailbox/latest/mailrs_mailbox/threading/index.html) (Message-ID parsing, thread resolution) and bitmask conversions in [`types`](https://docs.rs/mailrs-mailbox/latest/mailrs_mailbox/types/index.html).
## Quick start
```rust,no_run
use mailrs_mailbox::{MailboxStore, PgMailboxStore};
# async fn run() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
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);
# Ok(())
# }
```
For testing without a database, use [`InMemoryMailboxStore`](https://docs.rs/mailrs-mailbox/latest/mailrs_mailbox/fixtures/struct.InMemoryMailboxStore.html):
```rust,no_run
use mailrs_mailbox::fixtures::{InMemoryMailboxStore, EXAMPLE_USER};
use mailrs_mailbox::MailboxStore;
# async fn run() {
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:
| `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](https://crates.io/crates/testcontainers)) — schema application, modseq atomicity, sqlx integration |
| `tests/perf_gate.rs` | 4 | Threading-helper regression budgets (see [BUDGETS.md](./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):
```bash
cargo test -p mailrs-mailbox --test trait_contract
```
Run the PG suite (needs Docker):
```bash
cargo test -p mailrs-mailbox --test smoke -- --test-threads=1
```
## Performance
Two bench files cover this crate:
- [`benches/threading.rs`](benches/threading.rs) — pure-helper microbenchmarks (header extraction, message-id normalization, thread resolution).
- [`benches/store_ops.rs`](benches/store_ops.rs) — `InMemoryMailboxStore` ops, exercising the trait dispatch + RwLock + Vec backing.
Measured with criterion 0.8 on Apple Silicon (M-series), `cargo bench`, release profile.
| `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`](./BUDGETS.md) for the regression budgets gated by [`tests/perf_gate.rs`](tests/perf_gate.rs).
## Versioning
`1.x` follows semver. The stable public surface:
- `MailboxStore` trait method signatures
- `StoreError` type alias
- All types in `mailrs_mailbox::types` (marked `#[non_exhaustive]` where
growth is anticipated, so new fields are minor-bump compatible)
- `pg::PgMailboxStore::new` + `pool` accessor
- Pure helpers in `threading::*` and the `FLAG_*` 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](LICENSE-APACHE) or
[MIT license](LICENSE-MIT) at your option.
[mailrs]: https://github.com/goliajp/mailrs
[`Mailbox`]: https://docs.rs/mailrs-mailbox/latest/mailrs_mailbox/types/struct.Mailbox.html
[`Message`]: https://docs.rs/mailrs-mailbox/latest/mailrs_mailbox/types/struct.Message.html
[`MailboxStatus`]: https://docs.rs/mailrs-mailbox/latest/mailrs_mailbox/types/struct.MailboxStatus.html
[`Inserted`]: https://docs.rs/mailrs-mailbox/latest/mailrs_mailbox/types/struct.Inserted.html
[`InMemoryMailboxStore`]: https://docs.rs/mailrs-mailbox/latest/mailrs_mailbox/fixtures/struct.InMemoryMailboxStore.html