Core behavior contract

A mailbox belongs to one verified organization namespace and has a required receiving Skarbiec item plus an optional sending item. It keeps its public sending address and SMTP profile independently from the account used for IMAP authentication.

Public resources#

Mailbox

A mailbox is identified by a generated UUID and belongs to one verified organization namespace. It contains a required receiving skarbiec_item_id, optional smtp_skarbiec_item_id, display name, sending address, IMAP/SMTP host and port, SMTP security mode, enabled state, last persisted IMAP UID, synchronization timestamps, and the latest normalized error. Authentication fields are never part of this resource.

Valid state changes are enabled → disabled → enabled and present → removed. Synchronization commits each message or skipped UID with its cursor; a later failure does not roll back earlier progress. Removing a mailbox cascades through Skrzynka's retained messages, reply attempts, and outbound messages, not provider-side mail.

Message

An inbound message is immutable normalized content keyed by (mailbox_id, IMAP UID). Headers include provider Message-ID, threading headers, sender, recipients, subject, provider date, receive time, text body, and a bounded snippet. Repeated synchronization of the same UID is a no-op.

Reply attempt

A reply attempt belongs to one inbound message and one caller-supplied idempotency key. States are:

text
pending -> sending -> sent
                   -> failed
                   -> uncertain (recovered after interrupted sending)

The same idempotency key returns its existing attempt and never sends twice. A retry after failed or uncertain requires a new key and is therefore an explicit new provider mutation. SMTP acceptance is the sent boundary; delivery to the recipient's final inbox is outside Skrzynka's authority.

Outbound message

An outbound message belongs to one mailbox and one caller-supplied idempotency key, and carries its own normalized recipients, optional cc, subject, and full plain-text body because no inbound message supplies them. It walks the identical delivery states, recovers to uncertain with OUTBOUND_UNCERTAIN after an interrupted sending, and writes no threading headers. Full contract: outbound message.

Invariants#

  • Every API mailbox, message, reply, and outbound read or mutation is constrained by the centrally verified organization from the request.
  • A valid user session without membership in the selected organization cannot observe whether that organization's local resources exist.
  • One mailbox always sends through its own SMTP profile and optional smtp_skarbiec_item_id, falling back to its receiving skarbiec_item_id when the sending reference is null.
  • An address is a usable mailbox selector only while it names exactly one mailbox; an ambiguous address is refused rather than resolved.
  • No API or database field accepts a mailbox password.
  • Secret resolution occurs immediately before IMAP/SMTP authentication and the value is dropped with the connection task.
  • API startup is loopback-only.
  • Message import takes every message past the cursor, each whole and committed with the cursor past it.
  • One mailbox failure does not stop synchronization of other mailboxes.
  • Provider-side messages, flags, folders, and deletion are never mutated by synchronization.
  • Unknown configuration and unsupported SMTP security values are rejected before persistence.

Concurrency and durability#

SQLite runs in WAL mode with foreign keys enabled. Mailbox, reply, and outbound idempotency constraints are database-enforced. Synchronizations for one mailbox are serialized in-process; concurrent service processes are unsupported. A transaction owns each local resource change. Network calls never hold a database transaction open.

Error classes#

Public errors contain a stable code, a safe message, whether retry may help, and an HTTP status. Credential values, provider payloads, message bodies, and stack traces are excluded. Invalid input is 400, missing resources 404, stale/idempotency conflicts 409, dependency failures 502 or 503, and unexpected local failures 500.

Retention and privacy#

Skrzynka retains normalized messages, reply bodies, and outbound message bodies until their mailbox is removed or the database is removed. There is no automatic retention deletion in schema 4. Raw MIME, attachments, remote HTML, Wisent sessions, and organization membership records are not stored. Operators must treat the SQLite file and its backups as sensitive message data even though it contains no mailbox passwords.

Resource behavior#

Each mailbox sync uses one blocking IMAP session and imports every message past the cursor, one commit each. Neither the IMAP nor the SMTP client sets a network timeout; each waits for the server's answer or the connection's own error. Polling defaults to 60 seconds and never overlaps a previous poll loop. API message lists return every row unless limit names a page. Messages are imported whole, snippets are truncated to 240 characters, and the SMTP server accepts or refuses reply and outbound bodies by its own limits.