What is Skrzynka
Skrzynka is a self-hosted email operations service: one local process that receives mail from any number of IMAP mailboxes into a single local inbox, sends replies back out through the exact mailbox that received each message, and originates new mail from any connected mailbox. The whole
Overview#
Skrzynka is a self-hosted email operations service: one local process that receives mail from any number of IMAP mailboxes into a single local inbox, sends replies back out through the exact mailbox that received each message, and originates new mail from any connected mailbox. The whole product is three moving parts: a credential boundary that keeps every secret in Skarbiec, a bounded synchronization loop that imports provider mail into SQLite, and a delivery state machine that treats every send, reply or originated, as an idempotency-keyed provider mutation.
What works now#
- Skarbiec owns the mailbox list: any Skarbiec
loginorbundleitem taggedskrzynka:mailboxis a mailbox.mailbox declare, the loopback declare API, and Skrzynka Desktop tag the item and import its first page through one product-owned resolver and atomic storage operation; tagging the item in Skarbiec directly works the same way. - Gmail exposes three connection paths: one account with an app-specific password and no administrator or OAuth client; one Google identity through loopback OAuth; or Workspace users through administrator-granted domain-wide delegation. OAuth requires a Desktop-app client that accepts the chosen loopback callback; see Gmail connection.
- Import or synchronize bounded IMAP batches into the organization-scoped local inbox, reply through the receiving mailbox, and originate mail from any connected mailbox.
- Expose the same organization-scoped import, mailbox, message, reply, outbound, and synchronization state through the CLI, authenticated loopback API, and native desktop.
Credentials stay in Skarbiec#
Skrzynka never stores a mailbox password in SQLite. A mailbox row contains a required receiving Skarbiec item ID, an optional sending Skarbiec item ID, and non-secret connection settings (address, hosts, ports, security mode). Synchronization resolves the receiving item; replies and originated mail resolve the sending item when present and otherwise fall back to the receiving item. The core runs the local skarbiec CLI as a child process (skarbiec get <item-id>, bounded to 15 seconds; its whole output is read), uses the secret inside the connection task, and drops it with the task. No API request or response, database column, or log line carries an app-specific password, OAuth code, or provider token.
Skarbiec declares the mailboxes; the first import is atomic#
Every mailbox list, sync, and background poll first reads which Skarbiec items carry skrzynka:mailbox. It creates a mailbox for a newly declared item, adopts profile changes of a declared one, and stops polling a mailbox whose item lost the tag — that mailbox keeps its mail and reports MAILBOX_NOT_DECLARED. sync reports what this changed under reconciliation.
skrzynka mailbox declare --skarbiec-item <item-id>, POST /v1/mailboxes/declare, and Desktop's Declare call the same core operation. It resolves an existing canonical login or complete bundle, validates the non-secret profile, tags the item, then reads and normalizes every INBOX message in ascending UID order, committing each with the UID cursor past it; the first commit adopts the mailbox. The result exposes imported, unchanged, conflicting, and rejected counts and rejection reasons, but no credential or provider message body.
An equal known UID is unchanged. Declaring again adopts source display-name, SMTP and poll-interval changes and reports mailbox_state: updated. A changed receiving address or IMAP endpoint, or differing normalized data for a retained UID, is refused and preserves the old mailbox, messages, and cursor. Authentication and source failures create no mailbox; a fetch failure part way keeps every message committed before it, and skrzynka sync continues from the cursor. Unsupported provider rows are counted by reason rather than silently reduced. Declaring nothing leaves an empty usable product.
Synchronization is bounded and resumable#
Each enabled mailbox carries a cursor: the highest IMAP UID already persisted. A sync opens one IMAP session over required TLS, searches UID <cursor+1>:* in INBOX, and imports every message it finds, whole, oldest UID first: each is normalized to plain text and inserted keyed by (mailbox_id, UID) in its own commit with the cursor past it. A failure part way keeps what was committed and records a normalized last_error_code/last_error_message on the mailbox. skrzynka serve synchronizes and then waits for provider notifications through IMAP IDLE; the CLI skrzynka sync runs one pass on demand. No mailbox poll interval exists. Nothing Skrzynka does mutates provider state: no flags, no folders, no deletion. Details: synchronization, message.
Sends are explicit provider mutations#
A reply belongs to one stored inbound message and goes out through that message's own mailbox — its sending Skarbiec item when configured (otherwise its receiving item), its SMTP host, and its From address — with In-Reply-To/References preserved so threads stay intact. Every reply request carries an idempotency key: the same key returns the existing attempt and never sends twice. The attempt walks pending → sending → sent | failed | uncertain; sent means the configured SMTP server accepted the message, and uncertain means the process lost the terminal SMTP evidence — Skrzynka refuses to resend automatically because the provider may already have accepted it. Retrying after failed or uncertain requires a new key, which is a deliberate new mutation. Details: reply attempt.
Originated mail is the same machinery with its own noun. skrzynka message send --mailbox <id-or-address> --to <address> --subject <text> --body-file <path> claims an outbound message by its idempotency key before anything reaches the provider, carries its own recipients, cc and subject, writes no threading headers, and walks the same five delivery states. skrzynka message outbound reads back what went out.
What Skrzynka is not#
Skrzynka does not manage provider-side folders, delete or flag messages, download attachments, render remote HTML, generate reply text, or send automatic replies. It is not a shared server: the JSON API refuses any non-loopback bind address, and every /v1 request is verified against central Wisent identity with organization-scoped database access (organization scope). It is not a credential store — Skarbiec owns encryption, rotation, and revocation — and it is not the mail authority: the provider owns the mailbox, the UIDs, and delivery.
The first three commands#
skrzynka statusDatabase path, schema version, mailbox/message counts, poll interval, and whether skarbiec can be started. No credential is resolved.
skrzynka mailbox declare --skarbiec-item <item-id>Tag the item skrzynka:mailbox in Skarbiec, resolve the credential only inside Skrzynka, and import every INBOX message, each committed with the UID cursor past it.
skrzynka message listRead the normalized local inbox without contacting the provider. The end-to-end path is quick-start; the full command surface is cli.
The rest of the corpus#
- Nouns — mailbox, message, reply attempt, outbound message, synchronization, skarbiec item, organization scope, Gmail connection.
- Interfaces — CLI reference, loopback JSON API.
- Executed end to end — local mailbox lifecycle, the loopback API from zero, runnable examples.
- When it fails — every error code and exact sentence, with meaning and fix: runbook.
- Boundaries — what Skrzynka owns, refuses to own, and which network edges exist: architecture; every knob and default: configuration.
- Contracts — product, core behavior, integrations, onboarding, release.