Mailbox

Crate hopf-mailbox: mailbox storage SPI (Gumdrop org.bluezoo.gumdrop.mailbox port) — Maildir++ and mbox backends with SEARCH, flags, and optional .gidx indexing. This is not an IMAP wire server; hopf-imap sits on top of these traits.

SMTP local delivery is wired stock via LocalDeliveryService, which APPENDs each accepted message to the recipient's INBOX through this crate's MailboxFactory on the Runtime storage pool. POP3 and IMAP (hopf-pop3, hopf-imap) read from and write to mailboxes the same way, so all three protocols can share one backend in a single composed process — see Composition.

Standards and layouts

Reference Role
RFC 9051 IMAP SEARCH §6.4.4; system flags
RFC 822 / Internet Message Format Message bytes
Maildir++ Hierarchical folders, :2, flags, COPY/MOVE
mbox Single-file mailbox + .flags sidecar
.gidx Gumdrop-compatible index: v1 headers, v2 body

Architecture

App / hopf-imap / SMTP delivery
            │
            ▼
MailboxFactory → MailboxStore → Mailbox
            │
            ├── mbox:  {root}/{user}  + .flags + .gidx
            └── Maildir++: {root}/{user}/ cur|new|tmp + .uidlist …
            │
            └── ALWAYS run search/index I/O on StorageExecutor

Never perform heavy mailbox I/O on a reactor thread.

Backends compared

Capability mbox Maildir++
Layout file + .flags + .gidx cur/new/tmp, .uidlist, .keywords, .gidx, .subscriptions
CREATE / DELETE / RENAME folders Unsupported (INBOX only) Yes
COPY / MOVE Unsupported Yes
Subscriptions no-op-ish Yes
Keywords via .flags Yes
CONDSTORE (HIGHESTMODSEQ / MODSEQ / CHANGEDSINCE) via .flags via .uidlist
Locking flock + dotlock (.lock) N/A (per-message files, no whole-mailbox lock needed)

Both backends track a real, persisted, monotonic CONDSTORE mod-sequence per message (bumped on append and on any flag/keyword change, never decreasing even across expunge) — QRESYNC's VANISHED (EARLIER) history is the one CONDSTORE-adjacent piece still out of scope (see Limitations).

Factories:

Public types

Type Purpose
Mailbox Open mailbox operations
MailboxStore Per-user store
MailboxFactory Create stores
MessageDescriptor Message metadata
MailboxInfo / MailboxAttribute LIST-style metadata
Flag Seen / Answered / Flagged / Deleted / Draft / Recent
SearchCriteria / MessageContext SEARCH tree + eval
MessageSet / MessageRange 1:5,7,*
IndexConfig Body indexing knobs
MailboxError / MailboxResult Io / Unsupported / Invalid / Corrupt / NotFound / ReadOnly
MailboxNameCodec =XX filesystem-safe names
index::* .gidx (IndexFile, MessageIndex, …)

Consts: INDEX_MAGIC, INDEX_VERSION_HEADERS = 1, INDEX_VERSION_BODY = 2.

Configuration

IndexConfig

Property Type Default Notes
body_indexing bool false Index body for TEXT/BODY SEARCH
max_body_bytes usize 64 * 1024 Cap indexed body bytes

Builders: IndexConfig::headers_only(), IndexConfig::with_body_indexing().

No Cargo features on this crate.

Mailbox API

Typical flow:

use hopf_mailbox::{IndexConfig, MaildirFactory, SearchCriteria};

let factory = MaildirFactory::new("/var/mail")
    .with_index_config(IndexConfig::with_body_indexing());
let mut store = factory.create_store();
store.open("alice")?;
let mut mb = store.open_mailbox("INBOX", false)?; // read_only flag
mb.append_message(rfc822_bytes, &flags, None)?;
let hits = mb.search(&SearchCriteria::text("needle"))?;
mb.close(true)?; // expunge when true

Operations (trait surface): close(expunge), counts/size, messages, read_message, UID / uidvalidity / uidnext, flags / keywords, set/replace flags, append stream, search, copy / move (Maildir).

Streaming reads: read_message is a default method built on a lower-level start_read / read_chunk / end_read triad — callers that don't want a whole message pulled into memory (POP3 RETR/TOP, IMAP FETCH) can drive the triad directly and process fixed-size chunks as they arrive. append_content is likewise chunk-fed rather than taking one whole buffer, so SMTP delivery and IMAP APPEND can stream message bytes straight through to storage. Maildir implements both ends as true streaming (bounded memory regardless of message size); mbox's start_read materializes the one requested message (not the whole mailbox) because its >From-line unescaping is stateful and unsafe to chunk naively — see Limitations.

Criteria include: ALL, system flags, NEW/OLD, KEYWORD, LARGER/SMALLER, BEFORE/ON/SINCE, SENT*, HEADER, BODY, TEXT, UID / sequence sets, MODSEQ, AND / OR / NOT.

BODY / TEXT: use the .gidx body index when body_indexing is enabled; otherwise parse message content on demand.

HEADER: six fields (From/Sender/To/Cc/Bcc/Subject/Message-ID) resolve straight from the index; any other header name falls back to a raw per-message header scan (search::HeaderExtractor) rather than reporting empty.

MODSEQ: matches against the real, persisted per-message mod-sequence described under Backends compared above.

Indexing

.gidx is Gumdrop-compatible:

Body indexing is off by default (disk cost). Enable with IndexConfig::with_body_indexing().

IndexBuilder::build_streaming feeds any Read to rmimeparser's incremental MessageParser in fixed-size chunks (carrying a small buffer for lines split across chunk boundaries) rather than reading the whole message into a Vec<u8> first; build (whole-slice) is a thin wrapper over it. Maildir's mailbox-open indexing loop uses build_streaming directly against an open File.

Storage pool helpers

use hopf_mailbox::pool::{run_on_storage, search_on_storage};

// Prefer these from protocol servers so SEARCH never blocks a reactor:
search_on_storage(&storage, handle, mailbox_arc, criteria, callback);
run_on_storage(&storage, handle, op, callback);

Examples / tests

There is no examples/mailbox binary. Coverage lives in:

cargo test -p hopf-mailbox
# notably: tests/mailbox_integration.rs
#  - mbox append / flags / search
#  - mbox rejects COPY
#  - Maildir body index

Wire from SMTP:

// Prefer LocalDeliveryService for stock local MX; custom handlers can also
// APPEND in MessageDataHandler::message_complete via StorageExecutor:
storage.submit_on(handle, move || {
    let mut store = factory.create_store();
    store.open("alice")?;
    let mut mb = store.open_mailbox("INBOX", false)?;
    mb.append_message(&bytes, &flags, None)?;
    mb.close(false)?;
    Ok(())
}, |result| { /* reply 250/550 via ConnHandle */ });

Limitations