Mail state — Implementation (L2)
1. Implementation boundary
1.1 Contract
Mail state implementation code builds canonical records, derived projections, query windows/pages, conversation freshness tokens, and message change assertions from the authority-server local store.
The implementation may optimize storage and indexing, but optimized tables and caches must still obey the L1 ownership rules.
2. Authority-server storage shape
2.1 Canonical record families
The store keeps canonical record families for:
- accounts/source linkage needed by mail rows
- mailboxes
- messages
- message-mailbox membership
- message keywords and user tags
- body parts, sanitized bodies, attachment metadata, body tokens, attachment tokens, and local cache availability
- provider sync cursors and local metadata-version state
Writes that update related families run in one transaction when they must be observed together by API reads.
2.2 Projection indexes
The store may keep indexes or materialized helper tables for:
- sender, subject, preview, date, mailbox, keyword, and attachment query predicates
- mailbox and smart-mailbox counts
- conversation grouping and sort keys
- query-page seek keys or event catch-up watermarks
These helpers are invalid if they disagree with canonical records. Rebuilding them from canonical records must restore the same client-visible state except for opaque token values allowed to change after rebuild.
2.3 Conversation projection tables
A store may persist conversation IDs and conversation-message membership as a derived index. This is allowed because stable conversation IDs are useful across pages, events, and focused views.
Those rows do not make conversations canonical. Message records and membership inputs remain the source from which conversation envelopes, counts, latest-message pointers, and freshness tokens are computed or repaired.
3. Projection constructors
3.1 Message constructors
A message summary constructor reads canonical message metadata, mailbox IDs, keywords, bodyToken, attachmentToken, cache availability, and the current ConversationRef.
A message detail constructor starts from the same source as the summary constructor, then adds sanitized body alternatives and attachment metadata.
The summary and detail constructors share field mapping for overlapping fields.
3.2 Conversation constructors
A conversation envelope constructor reads the complete authority-server message set for one conversation ID. It computes the ordered message references and all aggregate fields from those messages.
A conversation view constructor builds or embeds the envelope, then adds the message projections needed by the reader.
Conversation constructors must not read or write an independently mutable conversation object. If a materialized conversation index exists, it is a cache for this constructor, not a separate source of truth.
3.3 Query constructors
A query constructor takes a normalized QueryScope and returns a QueryPage, count, or membership support result from authority-server query evaluation.
The runtime may compute query dependency metadata when it can describe which coarse state fields affect membership or ordering. Missing or unknown dependency data forces conservative active-view recomputation.
Message pages and conversation pages use the same predicate semantics for the same query text. Conversation pages group matching messages into derived conversation projections after authority-server predicate evaluation.
4. Query evaluator
4.1 Parse and normalize
The authority server parses user query text into a predicate tree before store execution. Prefixes, aliases, negation, quoted values, relative dates, saved-query references, concrete source/mailbox references, conversation references, and empty-value errors are authority-server semantics.
The renderer-side parser is an early validation and completion aid. It must not be the authoritative evaluator.
4.2 Scope composition
The query evaluator composes the base scope with the parsed query predicate before pagination:
base scope predicate + optional parsed query predicate + sort and cursor predicate -> indexed store read -> QueryPageThe authority server pushes scope and query predicates into the store read. It must not fetch a broad application-level result set and filter it in API or client code when the predicate can be represented in the store query.
A saved-query selector such as in:Inbox, a concrete source selector such as in:Account/, a concrete mailbox selector such as in:Account/Archive, and a global search query with the same remaining text are different query scopes and different cache keys.
4.3 Predicate execution
The store compiles predicates into indexed local reads. Text predicates such as from:Alex match sender name and sender email according to authority-server query semantics.
Source, mailbox, smart-mailbox, global search, conversation search, and automation preview paths compose scope predicates with the parsed query predicate rather than filtering result pages after the fact.
4.4 Pagination and ordering
Pagination cursors encode the authority-server sort key and resource identity needed to continue a stable ordered result. Cursor validation rejects cursors that do not belong to the requested account or scope.
A query result page or active query window is a projection of runtime state at the time it is read. Runtime state assertions drive active-view recomputation.
Exact page/window repair is a runtime/authority-server concern. The store may use indexes, query plans, or materialized authority-server projections to make recomputation cheap. The renderer does not maintain a second authoritative query engine.
4.5 Runtime mail-list windows
A runtime mail-list window is built from the same query evaluator as HTTP query pages. The runtime adds view state around that query result; it does not change membership semantics.
The state shape is:
MailListViewState { scope: QueryScope, projectionKind, sort, windowRequest, rows, continuation, readWatermark, coverage, knownTotalCount, lifecycle, pendingMutations, anchor}projectionKind is message or conversation for the first bundled implementation. knownTotalCount is optional; absence means the runtime has not counted the full result set. lifecycle is a renderer affordance supplied by the runtime, not a replica coverage proof.
coverage is runtime-authored. In bundled authority mode it asserts complete finite-window coverage for the scope, projection kind, window request, and read watermark. Future replica coverage details are intentionally deferred, but they must live in this field rather than in renderer query logic.
Mail-list row internals (MailListRowState: rowKey stability, opaque sortKey/orderKey, pendingMarkers) and window continuation / anchor resolution edge cases (WindowContinuation, around-anchor requests, anchor status kept/moved/removed/unknown) are implementation detail — see L3 §1–§2.
5. Conversation token construction
5.1 Token inputs
The conversation token changes when any envelope-visible input changes:
- message membership
- message ordering in the conversation view
- latest message and preview fields
- unread, flagged, attachment, and count aggregates
- participant/sender summary fields
5.2 Token representation
The token is opaque. Valid implementations include a hash over canonical inputs, a monotonic projection revision, or a store/event watermark tied to the affected conversation.
The API contract requires only comparison semantics: equal tokens mean the envelope is still fresh for the fields covered by the token; different tokens mean the envelope must be treated as stale.
5.3 Message read integration
Every message summary and detail read includes the current ConversationRef, bodyToken, and attachmentToken. This means ordinary message list pages, message detail reads, command responses that include message state, live message assertions, catch-up, and snapshots all expose the same conversation and content freshness signals.
6. Message change construction
6.1 Before-image capture
A sync write or command write that changes a message captures the previous MessageSummaryState before the mutation when the message existed.
The capture must include fields used by runtime view recomputation: account/source, mailbox IDs, keywords, sort fields, bodyToken, attachmentToken, and ConversationRef.
6.2 After-image construction
After the canonical write and affected projection refresh complete, event construction builds the current MessageSummaryState through the normal message summary constructor when the message still exists.
The resulting assertion uses this matrix:
| Change | before | after |
|---|---|---|
| create | null | current summary |
| update | previous summary | current summary |
| destroy | previous summary | null |
The event is appended only after the state it describes is durable or otherwise observable through API reads.
6.3 Collapse behavior
Collapsed catch-up keeps at most one assertion for a message. When multiple retained events collapse, the collapsed assertion carries the first retained before value and the latest after value.
This preserves enough context for recovery and stale marking without pretending to replay every intermediate operation.
7. Event effects
7.1 Message changes
After a sync write or command write changes a message, event construction emits a message change assertion with before/after summary state.
A non-destroy message change assertion also carries the post-change row projection and, when the change affects mailbox counts, countDeltas for the affected mailboxes, so a reactive consumer maintains rows and counts from the one assertion stream. Sync-side writes attach these the same way command writes do: an IMAP expunge / membership-location removal carries the post-removal projection plus the removed mailbox’s countDeltas; a full delete carries countDeltas for the message’s previous mailboxes; mailbox-deletion cleanup carries the projection. No message change reaches consumers as a bare change notice that forces a separate count or row refetch.
The authority server does not emit a separate conversation projection assertion merely because the message changed. The runtime uses the before/after ConversationRef values to stale and refresh visible conversation envelopes or views.
7.2 Query projection changes
The authority server does not emit complete query results for every possible query scope. A message change assertion gives the runtime enough summary context to recompute active scopes when the changed message intersects the base scope.
The authority server emits state assertions for non-message state that can affect query scopes. Smart-mailbox definition changes emit smart-mailbox before/after assertions. Mailbox metadata changes emit mailbox before/after assertions. Account changes emit account before/after assertions when they affect client-visible mail state. Search corpus or index state that can change independently of message bodyToken must have a named state assertion before clients depend on it.
The authority server must not ask renderers to decide global query membership by scanning locally loaded messages, and it must not emit renderer-specific query invalidation commands.
7.3 Snapshot and catch-up
Snapshot and catch-up constructors use the same message, conversation, and query constructors as ordinary HTTP reads. Snapshot frames are transport packaging, not a second state model.
Validation and test patterns for the mail-state constructors, query evaluator, conversation tokens, and message assertions are in L3 §3.
8. Assertions
| ID | Sev. | Assertion |
|---|---|---|
| canonical-record-transaction | MUST | Store writes keep related canonical record families transactionally consistent for API reads. |
| projection-cache-derived | MUST | Materialized projection/index state is rebuildable from canonical records and is not independently authoritative. |
| shared-message-constructor | MUST | Message summary/detail overlapping fields, content tokens, and ConversationRef are produced by shared mapping logic. |
| conversation-constructor-derived | MUST | Conversation envelopes/views are constructed from the complete authority-server message set for the conversation. |
| authority-server-query-compiler | MUST | Query text is parsed and executed by authority-server query code before pagination. |
| query-scope-cache-key | MUST | Base scope is part of the canonical query scope and cache key. |
| store-composes-query-scope | MUST | Authority-server query reads compose base-scope predicates and query predicates inside the store query when representable. |
| query-page-authority-server-settled | MUST | Exact query page/window repair, ordering, replacement rows, and cursor continuity are runtime/authority-server concerns. |
| runtime-mail-list-window-shape | MUST | Runtime mail-list windows include scope, projection kind, sort/window request, rows, continuation, read watermark, coverage, lifecycle, pending markers, and anchor state. |
| token-comparison-only | MUST | Conversation token semantics are equality comparison only; consumers do not inspect token contents. |
| message-assertion-before-after | MUST | Message change events carry before/after message summary state with nulls for create/destroy. |
| message-content-token-events | MUST | Body and attachment changes are observable through changed message summary tokens. |
| rows-counts-one-stream | MUST | Message change assertions from command and sync writes alike carry the post-change projection and affected-mailbox countDeltas, so consumers maintain rows and counts from one stream without side-channel refetch. |
| collapsed-before-after | MUST | Collapsed message catch-up preserves the first retained before summary and latest after summary. |
| no-duplicate-conversation-event | MUST | Message changes do not emit separate conversation projection assertions; the runtime refreshes stale visible conversations. |
| no-query-invalidation-events | MUST | The authority server emits state assertions, not renderer-specific query invalidation commands. |
| query-membership-server-owned | MUST | Query projection membership is owned by runtime/authority-server evaluation, not renderer-side filtering of loaded pages. |