Mail Store
Byte-level mail-store substrate — the foundation every above-the- wire mail primitive composes (b.mail.agent at v0.9.20, b.mail.server.mx at v0.9.23, b.mail.server.submission at v0.9.24, IMAP/JMAP/POP3 at v0.9.27-29, ManageSieve at v0.9.30, DAV at v0.9.32).
No auth, no audit, no posture-enforcement at THIS layer — those live in the agent above. The store is the lowest-level atomic-append + sealed-column shape over a pluggable backend.
**Pluggable backend**: sqlite via b.db (default), operator's b.externalDb (Postgres), or any object exposing prepare(sql) → { run, get, all }. Schema is bootstrapped at create() when init !== false.
**Sealed by default**: subject / from_addr / to_addrs / body_text / body_html are registered as sealed via b.cryptoField.sealRow. A DB dump leaks zero recoverable PII content. Plaintext (forensic-queryable without unsealing): objectid, modseq, internal_date, received_at, flags, size_bytes, legal_hold, from_hash, message_id_hash.
**CONDSTORE-ready**: per-folder monotonic modseq counter (RFC 7162). Every state-changing op (append / setFlags / delete) bumps modseq atomically.
**JMAP-ready**: per-message objectid (RFC 8474) — stable cross-protocol identity. IMAP's UID + UIDVALIDITY + JMAP's Email/get's id all map to objectid.
**Threading at append**: JWZ algorithm + RFC 5256/9051 root via Message-Id + In-Reply-To + References. Threading state is maintained in the messages table itself (thread_root_id column) so JMAP Thread/get is a single index lookup.
**Quota substrate**: per-user + per-folder usedBytes / usedCount counters maintained atomically with append/delete. The v0.9.33 IMAP-QUOTA / JMAP-Quotas surface reads these directly.
**Legal hold**: legal_hold column composes existing b.legalHold primitive. Held messages refuse delete regardless of caller; only b.legalHold.release can flip the flag.
Parses messages on append via b.safeMime.parse (bounded substrate, defends CVE-2024-39929 + CVE-2026-26312). Validates Message-Id via b.guardMessageId.validate.
b.mailStore.fts.tokenize(text) #
Split text into a deduplicated, lowercased, NFC-normalised token array. Drops stopwords + tokens outside the 2..64-codepoint band. Splits on every non-letter / non-digit codepoint, including the @ + . boundaries of email addresses so local-part + domain labels become independent tokens.
b.mailStore.fts.tokenize("Hello world from alice@example.com");
// → ["hello", "world", "alice", "example", "com"]
b.mailStore.fts.hashToken(table, field, token) #
Keyed hash of one token under the (table, field) namespace. Routes through b.cryptoField.computeNamespacedHash in hmac-shake256 mode — the same keyed-MAC machinery that protects sealed-column derived hashes — so rotating the vault key invalidates every FTS hash in step with every sealed-column hash. Returns a 16-char hex prefix.
var h = b.mailStore.fts.hashToken("mail_messages", "body", "kubernetes");
/^[0-9a-f]{16}$/.test(h); // → true
b.mailStore.fts.hashTokens(table, field, tokens) #
Hash an array of tokens → space-separated hash string suitable for direct insertion into an FTS5 column. Empty + duplicate token- hashes drop on the way out.
b.mailStore.fts.hashTokens("t", "subject", ["hello", "world"]);
// → "<16hex> <16hex>"
b.mailStore.fts.hashText(table, field, text) #
Tokenize + hash + join in one step. Convenience wrapper — equivalent to hashTokens(table, field, tokenize(text)).
b.mailStore.fts.hashText("mail_messages", "body", "kubernetes deploy");
// → "<16hex> <16hex>"
b.mailStore.fts.rowFromMessage(table, msg) #
Build the FTS5 row payload { objectid, subject_toks, addr_toks, body_toks } from a { objectid, subject, from, to, body } plaintext message. from + to share addr_toks.
b.mailStore.fts.rowFromMessage("t", { objectid:"o1", subject:"Hi", from:"a@x", to:"b@x", body:"hello" });
// → { objectid:"o1", subject_toks:"", addr_toks:" ", body_toks:"" }
b.mailStore.fts.columnAndFieldFor(filterKey) #
Map a search filter key (subject / body / from / to) to the FTS5 column it indexes into PLUS the namespace pseudo-field the indexer uses when hashing tokens. Used by the search path so the query-side hash transform matches the index-side one byte- for-byte.
b.mailStore.fts.columnAndFieldFor("from");
// → { column: "addr_toks", field: "addr" }
b.mailStore.fts.buildMatchExpression(table, field, term) #
Tokenize + hash an operator's query term and produce the FTS5 MATCH expression that selects rows containing every surviving token. Returns null when no tokens survive the tokenize + stopword filter (caller skips the FTS join in that case).
var expr = b.mailStore.fts.buildMatchExpression("t", "body", "kubernetes deploy");
// → "<16hex> AND <16hex>"
b.mailStore.fts.createSql(qFtsTable) #
Returns the CREATE VIRTUAL TABLE IF NOT EXISTS SQL for the sealed-token FTS5 table. The caller passes the quoted table identifier (e.g. "blamejs_mail_messages_fts").
db.prepare(b.mailStore.fts.createSql('"mail_fts"')).run();
b.mailStore.create(opts) #
{
backend: object, // required — sqlite-shaped { prepare(sql) → { run, get, all }, transaction(fn) }
tablePrefix: string, // default "blamejs_mail" — validated via safeSql.validateIdentifier
init: boolean, // default true — bootstrap schema + register sealed fields + insert default folders
compliance: string, // hipaa | pci-dss | gdpr | soc2 — pins sealing posture (default off → sealed-by-default uses framework defaults)
maxMessageBytes: number, // default 50 MiB
maxBodyBytes: number, // default 25 MiB
safeMimeOpts: object, // pass-through to b.safeMime.parse
}
Build a mail-store handle. Returns an object with appendMessage / fetchByObjectId / search / queryByModseq / setFlags / createFolder / listFolders / threadFor / quota / moveMessages / setLegalHold / hardExpunge.
var b = require("blamejs");
await b.vault.init({ dataDir });
await b.db.init({ dataDir, schema: [] });
var store = b.mailStore.create({ backend: b.db });
var meta = store.appendMessage("INBOX", messageBuffer);
meta.objectid; // → "obj_01HXYZ..."
meta.modseq; // → 42 (monotonic)
Last updated 2026-08-08T16:39:49.652Z by seeder.