Documentation
¶
Overview ¶
Package imap is the durable mailbox store behind the chassis `imap` personality and the txco://imap/* ops: accounts, a per-account tree of mailboxes, and per-mailbox UID-ordered message rows whose retained object (a canonical Record, or verbatim RFC 5322 bytes) lives in the filecas by sha256. Everything IMAP needs to serve a message without parsing it — ENVELOPE, BODYSTRUCTURE, size, flags — is cached on the row at append.
Storage is dialect-aware (registry.Dialect, the same seam auth and the scheduled store use); the bundled backend is a SQLite file of its own. It is deliberately NOT a set of runtime tables: the dbcache watcher reloads the whole runtime mirror on any runtime-DB write, and a mailbox index is written on every \Seen.
Immutability rules (the design doc's §25.6): bytes under a UID never change; same object_key + same sha is a no-op; a different sha is a NEW UID with the old one expunged; uidnext is stored, never derived; uidvalidity changes only on an explicit reset.
Index ¶
- Constants
- Variables
- func HasFlag(flags []string, f string) bool
- func NormalizeFlags(in []string) []string
- func NormalizeMailboxName(name string) string
- func NormalizeUsername(u string) string
- func Register(name string, c Constructor)
- func Render(rec *Record, sha string, opt RenderOptions) ([]byte, error)
- func WithOrigin(ctx context.Context, origin any) context.Context
- type Account
- type AppendResult
- type Change
- type ChangeKind
- type Config
- type Constructor
- type Expunged
- type Mailbox
- type Message
- type MessageHead
- type PartRef
- type Record
- type RenderOptions
- type Store
- func (s *Store) AppendMessage(ctx context.Context, mailboxID string, m Message) (AppendResult, error)
- func (s *Store) Close() error
- func (s *Store) CopyMessage(ctx context.Context, srcID string, uid uint32, destID string) (AppendResult, error)
- func (s *Store) CountByMailbox(ctx context.Context, mailboxID string) (uint32, uint32, error)
- func (s *Store) CreateMailbox(ctx context.Context, tenant, username, name, role string, attrs []string, ...) (Mailbox, error)
- func (s *Store) DB() *sql.DB
- func (s *Store) DeleteMailbox(ctx context.Context, tenant, username, name string) (Mailbox, error)
- func (s *Store) EnsureMailbox(ctx context.Context, tenant, username, name string) (Mailbox, bool, error)
- func (s *Store) EnsureSchema(ctx context.Context) error
- func (s *Store) Expunge(ctx context.Context, mailboxID string, uids []uint32) ([]Expunged, error)
- func (s *Store) GetAccount(ctx context.Context, username string) (Account, bool, error)
- func (s *Store) GetAccountByLocalPart(ctx context.Context, local string) (Account, int, error)
- func (s *Store) GetMailbox(ctx context.Context, tenant, username, name string) (Mailbox, bool, error)
- func (s *Store) GetMailboxByID(ctx context.Context, id string) (Mailbox, bool, error)
- func (s *Store) GetMailboxByRole(ctx context.Context, tenant, username, role string) (Mailbox, bool, error)
- func (s *Store) GetMessage(ctx context.Context, mailboxID string, uid uint32) (Message, bool, error)
- func (s *Store) GetMessageByKey(ctx context.Context, mailboxID, objectKey string) (Message, bool, error)
- func (s *Store) ListMailboxes(ctx context.Context, tenant, username string) ([]Mailbox, error)
- func (s *Store) ListMessageHeads(ctx context.Context, mailboxID string) ([]MessageHead, error)
- func (s *Store) ListMessages(ctx context.Context, mailboxID string, after uint32, limit int, flags []string) ([]Message, uint32, error)
- func (s *Store) MoveMessages(ctx context.Context, srcID string, uids []uint32, destID string) (map[uint32]uint32, []Expunged, error)
- func (s *Store) RemoveMessage(ctx context.Context, mailboxID string, uid uint32) (bool, error)
- func (s *Store) RenameMailbox(ctx context.Context, tenant, username, oldName, newName string) (Mailbox, error)
- func (s *Store) ResetMailbox(ctx context.Context, id string) (Mailbox, error)
- func (s *Store) SetFlags(ctx context.Context, mailboxID string, uid uint32, flags []string) ([]string, error)
- func (s *Store) SetOnChange(fn func(Change))
- func (s *Store) SetSubscribed(ctx context.Context, mailboxID string, subscribed bool) error
- func (s *Store) UpdateMailbox(ctx context.Context, id string, role *string, attrs []string, ...) (Mailbox, error)
- func (s *Store) UpsertAccount(ctx context.Context, tenant, username, pwHash, status string, ...) (created bool, err error)
Constants ¶
const ( KindRecord = "record" KindVerbatim = "verbatim" )
Kinds of retained object.
const ( StatusActive = "active" StatusDisabled = "disabled" )
Account statuses.
const FormatVersion = 1
FormatVersion is the formatter version stamped on every record row at append. A message keeps the version it was appended under for life: the rendered bytes must be byte-identical on every FETCH (IMAP promises the client that a UID's content never changes), so a change to the renderer below is a NEW version for new messages, never a re-render of old ones.
const WordPasswordDefaultWords = apppass.WordPasswordDefaultWords
WordPasswordDefaultWords is apppass.WordPasswordDefaultWords.
Variables ¶
var ( // ErrMailboxExists is returned by CreateMailbox / RenameMailbox when // the target name is already a live mailbox. ErrMailboxExists = errors.New("imap: mailbox already exists") // ErrMailboxNotFound is returned when the named mailbox is absent. ErrMailboxNotFound = errors.New("imap: no such mailbox") // ErrINBOX is returned for DELETE / RENAME of INBOX (RFC 3501 §5.1: // INBOX always exists). ErrINBOX = errors.New("imap: INBOX cannot be deleted or renamed") )
var ( // ErrBadHash is returned when a stored hash is not a PHC argon2id string // apppass produced. ErrBadHash = apppass.ErrBadHash HashPassword = apppass.HashPassword VerifyPassword = apppass.VerifyPassword VerifyDummy = apppass.VerifyDummy GeneratePassword = apppass.GeneratePassword GenerateWordPassword = apppass.GenerateWordPassword )
The password mechanism lives in chassis/apppass, shared with the calendar head so one credential can open both. These names are kept so the ops, the head and their tests read as they always did.
var ErrUsernameTaken = errors.New("imap: username belongs to another tenant")
ErrUsernameTaken is returned by UpsertAccount when the username already belongs to another tenant (usernames are globally unique: they are the LOGIN identity).
Functions ¶
func NormalizeFlags ¶
NormalizeFlags trims, drops empties and \Recent (server-owned, never stored), dedupes case-insensitively, canonicalises system-flag spelling and sorts — so a flags column compares byte-wise.
func NormalizeMailboxName ¶
NormalizeMailboxName canonicalises a mailbox path: trims, collapses the delimiter, and maps any case of "inbox" to INBOX (RFC 3501 §5.1).
func NormalizeUsername ¶
NormalizeUsername lowercases and trims a LOGIN / op username. Usernames are e-mail addresses; the domain half is case-insensitive by RFC and the local part is treated so here (one account per mailbox name).
func Register ¶
func Register(name string, c Constructor)
Register adds a backend constructor. Called from a backend package's init().
func Render ¶
func Render(rec *Record, sha string, opt RenderOptions) ([]byte, error)
Render produces the RFC 5322 bytes for a record, deterministically: MIME boundaries derive from the record's canonical sha, header order is fixed, bodies are quoted-printable, lines end in CRLF. sha is the canonical sha256 hex Canonical returned (also the CAS key).
Types ¶
type Account ¶
type Account struct {
Tenant string
Username string
PwHash string
Status string
Policy json.RawMessage
CreatedAt time.Time
UpdatedAt time.Time
}
Account is an imap_accounts row.
type AppendResult ¶
type AppendResult struct {
UID uint32
UIDValidity uint32
// Noop: the mailbox already held this object_key with this sha.
Noop bool
// Replaced: the object_key existed with a different sha; ReplacedUID
// was expunged and UID is the new row.
Replaced bool
ReplacedUID uint32
}
AppendResult reports what AppendMessage did.
type Change ¶
type Change struct {
MailboxID string
Kind ChangeKind
UID uint32
Seq uint32
Total uint32
Flags []string
// Origin is whatever WithOrigin put on the writing context (the head
// passes its session tracker so a session is not told about its own
// STORE twice). nil for op-side writes.
Origin any
}
Change is a post-commit notification the head turns into IDLE/Poll updates. Seq is the message's sequence number in UID order at the moment of the change (for an expunge: its former position); Total is the live count after the change.
type ChangeKind ¶
type ChangeKind string
ChangeKind labels a post-commit change notification.
const ( ChangeAppend ChangeKind = "append" ChangeExpunge ChangeKind = "expunge" ChangeFlags ChangeKind = "flags" )
type Config ¶
type Config struct {
// DBPath is the bundled SQLite backend's file path (--imap-db-path).
DBPath string
}
Config carries backend-selecting options resolved from chassis config. The bundled "sqlite" backend uses DBPath. Adding a field here doesn't affect existing backends (same posture as scheduled.Config / vector.Config).
type Constructor ¶
Constructor builds a Store from resolved config. It is expected to open its backing DB and call Store.EnsureSchema before returning.
type Expunged ¶
Expunged is one row removed by Expunge / MoveMessages: its UID and its sequence number at the moment of removal, reported in descending sequence order so a client applying them in order stays consistent.
type Mailbox ¶
type Mailbox struct {
ID string
Tenant string
Username string
Name string
Role string
Attrs []string
Policy json.RawMessage
UIDValidity uint32
UIDNext uint32
ModSeq int64
Subscribed bool
CreatedAt time.Time
}
Mailbox is an imap_mailboxes row. Name is the full '/'-delimited path; Role is the stack's opaque stable key (survives RENAME); Attrs are special-use attributes.
type Message ¶
type Message struct {
MailboxID string
UID uint32
ObjectKey string
Kind string
SHA256 string
FormatVersion int
Size int64
InternalDate time.Time
Flags []string
Envelope json.RawMessage
BodyStructure json.RawMessage
Subject string
FromAddr string
TextExcerpt string
Parts json.RawMessage
ModSeq int64
State string
CreatedAt time.Time
}
Message is an imap_messages row.
type MessageHead ¶
type MessageHead struct {
UID uint32
Flags []string
Size int64
InternalDate time.Time
// ModSeq is the mailbox modseq at the row's last change (append or
// flags); the head diffs snapshots by it.
ModSeq int64
}
MessageHead is the per-message slice a session needs at SELECT/STATUS time: identity, flags, size, date — never the cached structures.
type PartRef ¶
type PartRef struct {
N int `json:"n"`
Name string `json:"name,omitempty"`
Type string `json:"type,omitempty"`
Size int64 `json:"size,omitempty"`
SHA256 string `json:"sha256,omitempty"`
}
PartRef is an attachment reference inside a Record.
type Record ¶
type Record struct {
Version int `json:"v"`
Headers map[string]string `json:"headers,omitempty"`
Text string `json:"text,omitempty"`
HTML string `json:"html,omitempty"`
Parts []PartRef `json:"parts,omitempty"`
}
Record is the canonical retained representation of a message the platform keeps — the thing a stack chose to remember, not the wire form it arrived in. RFC 5322 is derived from it by Render on FETCH.
Headers is a subset the stack chose (Message-ID, Date, From, To, Cc, Subject, In-Reply-To, References, plus whatever else it kept); Text is the normalized plain body; HTML the optional rich body; Parts are attachment references — name/type/size always, a sha256 only where retention was permitted and the bytes are in the CAS.
func ParseRecord ¶
ParseRecord decodes canonical record bytes read back from the CAS.
func (*Record) Canonical ¶
Canonical returns the record's canonical JSON — the bytes stored in the CAS and hashed for the row's sha256. encoding/json sorts map keys and emits struct fields in declaration order, so equal records give equal bytes.
func (*Record) Excerpt ¶
Excerpt returns the first n bytes of the record's plain text (or a tag-stripped slice of the HTML when there is no text), for the text_excerpt column that column-backed SEARCH BODY/TEXT reads.
func (*Record) Normalize ¶
func (r *Record) Normalize()
Normalize canonicalises header keys (textproto form), drops structural headers and empty values, strips CR/LF from every value (header injection guard) and stamps the format version. Called before Canonical so two stacks that spell "message-id" differently produce one sha.
type RenderOptions ¶
type RenderOptions struct {
// ObjectKey seeds the synthesized Message-ID when the record has none.
ObjectKey string
// Domain is the right-hand side of a synthesized Message-ID (the
// account's domain). Empty falls back to "txco.invalid".
Domain string
// InternalDate is the Date: fallback when the record carries none.
InternalDate time.Time
}
RenderOptions are the per-message inputs to the formatter that are not part of the record itself. All of them are stored on the row, so a re-render is reproducible.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store is the façade over the three tables. It carries the dialect (for `?`→`$n` rebinding) and a clock seam for tests, mirroring scheduled.Store.
func NewStore ¶
NewStore builds a Store over the opened DB and its dialect. A nil dialect defaults to SQLite (the in-tree default).
func Open ¶
Open constructs the named backend. Unknown name is a startup error listing what is available (so a misconfigured --imap-store fails loudly).
func (*Store) AppendMessage ¶
func (s *Store) AppendMessage(ctx context.Context, mailboxID string, m Message) (AppendResult, error)
AppendMessage stores a message row in the mailbox and returns its UID. The retained object must already be in the CAS (write order is CAS → sha rows → index row, so a crash between leaves nothing dangling that a retry can't heal). Semantics by object_key (§25.6): same key + same sha → Noop; same key + different sha → the old row is expunged and a NEW UID allocated (Replaced); empty key → always a new row.
Appends to one mailbox are serialized: the transaction first locks the mailbox row (LockClause — FOR UPDATE on Postgres; on SQLite the _txlock=immediate connection already holds the write lock), so the object_key check and the uidnext allocation can never interleave across nodes. A unique-index violation (a concurrent writer that won the same key between an out-of-tx check and this call, e.g. CopyMessage) is retried once.
func (*Store) CopyMessage ¶
func (s *Store) CopyMessage(ctx context.Context, srcID string, uid uint32, destID string) (AppendResult, error)
CopyMessage inserts a new row in dest over the same retained object as (src, uid). The object_key travels with the copy unless dest already holds that key, in which case the copy is keyless (a client COPY is not a stack projection). Returns the dest UID.
func (*Store) CountByMailbox ¶
CountByMailbox returns (messages, unseen) for a mailbox.
func (*Store) CreateMailbox ¶
func (s *Store) CreateMailbox(ctx context.Context, tenant, username, name, role string, attrs []string, policy json.RawMessage) (Mailbox, error)
CreateMailbox creates a live mailbox at name with the given role, special-use attrs and policy. Parents need not exist (they LIST as \Noselect until created). ErrMailboxExists when the name is taken.
func (*Store) DeleteMailbox ¶
DeleteMailbox soft-deletes a live mailbox (deleted_at). Its rows stay (immutable, unreferenced — the blob posture); children keep their names and the parent LISTs as \Noselect. INBOX is refused.
func (*Store) EnsureMailbox ¶
func (s *Store) EnsureMailbox(ctx context.Context, tenant, username, name string) (Mailbox, bool, error)
EnsureMailbox returns the live mailbox with this name, creating it when absent. created reports a fresh row. UIDVALIDITY is the creation second (monotonic across delete-and-recreate at one-second resolution, the conventional choice).
func (*Store) EnsureSchema ¶
EnsureSchema creates the tables + indexes if absent. Portable DDL: TEXT ids (hxid) instead of engine-specific autoincrement, TEXT RFC3339 timestamps, JSON as TEXT, native partial indexes, and BIGINT for the uint32/int64 counters (uidvalidity is a Unix timestamp, uid copies uidnext, size and modseq are int64) — SQLite reads BIGINT as INTEGER affinity, Postgres as int8, so one DDL serves both engines.
func (*Store) Expunge ¶
Expunge removes the rows flagged \Deleted (restricted to uids when non-empty) and returns them in descending sequence order.
func (*Store) GetAccount ¶
GetAccount looks an account up by username (the LOGIN identity).
func (*Store) GetAccountByLocalPart ¶
GetAccountByLocalPart resolves a bare local part ("paris") the way a mail client that drops the domain expects: the account whose username is <local>@<anything>, provided exactly one exists. n is the number of candidates, so a caller can tell "no such user" from "ambiguous" (both are a plain authentication failure on the wire).
func (*Store) GetMailbox ¶
func (s *Store) GetMailbox(ctx context.Context, tenant, username, name string) (Mailbox, bool, error)
GetMailbox returns the live mailbox at name.
func (*Store) GetMailboxByID ¶
GetMailboxByID returns a live mailbox by row id.
func (*Store) GetMailboxByRole ¶
func (s *Store) GetMailboxByRole(ctx context.Context, tenant, username, role string) (Mailbox, bool, error)
GetMailboxByRole returns the tenant/account's live mailbox carrying role (the stack's stable key). The first by name wins if several share it.
func (*Store) GetMessage ¶
func (s *Store) GetMessage(ctx context.Context, mailboxID string, uid uint32) (Message, bool, error)
GetMessage returns one full row.
func (*Store) GetMessageByKey ¶
func (s *Store) GetMessageByKey(ctx context.Context, mailboxID, objectKey string) (Message, bool, error)
GetMessageByKey returns the row holding object_key.
func (*Store) ListMailboxes ¶
ListMailboxes returns the account's live mailboxes sorted by name.
func (*Store) ListMessageHeads ¶
ListMessageHeads returns the mailbox's live messages in UID order — the slice a session holds while selected (sequence number = index + 1).
func (*Store) ListMessages ¶
func (s *Store) ListMessages(ctx context.Context, mailboxID string, after uint32, limit int, flags []string) ([]Message, uint32, error)
ListMessages is the windowed read for txco://imap/messages: rows with uid > after, in UID order, at most limit, optionally only those carrying ANY of flags. next is the last uid returned when the window was full.
func (*Store) MoveMessages ¶
func (s *Store) MoveMessages(ctx context.Context, srcID string, uids []uint32, destID string) (map[uint32]uint32, []Expunged, error)
MoveMessages copies each uid into dest and removes it from src, in one transaction per message (a crash between leaves a copy — never a loss). Returns src→dest UID pairs plus the src expunges in descending sequence.
func (*Store) RemoveMessage ¶
RemoveMessage expunges one row (an op-side removal). Returns false when no such UID. The mailbox row is bumped (and thereby locked) first so the reported sequence number cannot shift under a concurrent writer.
func (*Store) RenameMailbox ¶
func (s *Store) RenameMailbox(ctx context.Context, tenant, username, oldName, newName string) (Mailbox, error)
RenameMailbox renames a live mailbox and its whole subtree. UIDs, UIDVALIDITY, roles and rows are untouched — only names change. INBOX cannot be renamed; the new name must be free.
func (*Store) ResetMailbox ¶
ResetMailbox empties a mailbox and bumps UIDVALIDITY — the one sanctioned way UIDVALIDITY changes (§25.6). Sessions holding it selected must reselect; they are not notified (a reset is an operator act, not a client flow).
func (*Store) SetFlags ¶
func (s *Store) SetFlags(ctx context.Context, mailboxID string, uid uint32, flags []string) ([]string, error)
SetFlags replaces a message's flag set and returns the stored (normalised) set. Flags are the only mutable state under a UID.
func (*Store) SetOnChange ¶
SetOnChange installs the post-commit change listener (the head's notification hub). Called synchronously after each committing write, in order; the listener must not block.
func (*Store) SetSubscribed ¶
SetSubscribed persists the SUBSCRIBE/UNSUBSCRIBE state.
func (*Store) UpdateMailbox ¶
func (s *Store) UpdateMailbox(ctx context.Context, id string, role *string, attrs []string, policy json.RawMessage) (Mailbox, error)
UpdateMailbox sets a mailbox's role / attrs / policy (nil = keep).
func (*Store) UpsertAccount ¶
func (s *Store) UpsertAccount(ctx context.Context, tenant, username, pwHash, status string, policy json.RawMessage) (created bool, err error)
UpsertAccount creates the account (and its INBOX) or updates it. An empty pwHash / status / policy leaves the stored value unchanged on update; pwHash is required on create. created reports whether the row was new.