Gmail connection
A Gmail connection is a Skarbiec-backed authentication adapter for the same IMAP/SMTP mailbox model. Message transport remains Gmail IMAP and SMTP; Skrzynka does not use a provider message API.
Overview#
A Gmail connection is a Skarbiec-backed authentication adapter for the same IMAP/SMTP mailbox model. Message transport remains Gmail IMAP and SMTP; Skrzynka does not use a provider message API.
Which path this account can use#
skrzynka --organization <ID> account connection --provider gmail --email <address> --bind <loopback-address:port> and GET /v1/gmail/connection?email=<address> measure the three supported paths independently. The CLI requires the callback address intended for OAuth; the API uses its configured callback. The report's authorization and delegation commands preserve the requesting organization, and OAuth guidance names the observed redirect so the same address is used for authorization. Every verdict is measured when the report is produced: the credential the mailbox already names is put through a real imap.gmail.com:993 login, the stored OAuth client and this process's loopback redirect are handed to Google once, and a Workspace address gets a real delegated token mint.
Qualify a connected account with node tests/gmail/connection.mjs --organization <ID> --email <address> --bind <loopback-address:port> --usable-path app_password from the Skrzynka checkout. Select delegation instead when that is the real authenticated path. The test contacts the installed CLI, vault and Google, requires the selected path to authenticate, checks organization and callback guidance, refuses a non-loopback callback, and compares persisted inbox counts before and after. Reports under .build/real-tests/gmail-connection/ retain the source revision and diff, executable and test digests, commands, exit statuses and output. A missing command or unavailable dependency is a failed run, not a pass. This read-only check does not prove mail import or the graphical screen; those require their own real flows.
Each of app_password, oauth and delegation is reported usable when something authenticated just now, refused when the provider, the vault or Skrzynka refused just now, and unproven when nothing here can decide — no credential stored, no account named, or the provider did not answer the question. A readable Skarbiec item earns unproven, never usable. Google shows a sign-in page before validating the redirect for some clients, so an OAuth probe that is not refused is unproven too: only a completed authorization proves a redirect is registered.
A consumer @gmail.com or @googlemail.com address is refused for delegation as GOOGLE_DELEGATION_NOT_APPLICABLE without contacting Google, because domain-wide delegation exists only inside a Workspace domain and no administrator can grant it for such an address. The report writes nothing and carries no secret: observed holds client IDs, the redirect URI, the admin-console URL and the Skarbiec item that was authenticated. Full output and exit statuses are in the CLI reference.
Single-account app-password lifecycle#
- The account owner generates a Google app-specific password. This needs neither a Workspace administrator nor an OAuth client.
- For the CLI path,
skrzynka account app-password --provider gmail --email <login-address>reads that secret only from stdin and accepts optional--mailbox <id-or-address>. For the API path, the operator first stores it in Skarbiec Desktop and sends the resultingskarbiec_item_idplus optionalmailboxselector toPOST /v1/gmail/app-password. - The core resolves the optional mailbox selector, then logs in to
imap.gmail.com:993over TLS before changing local state; the CLI also performs this proof before it writes the deterministicskrzynka-gmail-app-password-<email-digest>bundle to Skarbiec. - Without a mailbox selector, behavior is unchanged: the account address identifies the mailbox, and a new row uses Gmail IMAP plus
smtp.gmail.com:587STARTTLS. - With a mailbox selector, the verified item becomes only that row's receiving credential. Gmail IMAP and
enabledare set; the existing address, display name, SMTP endpoint, TLS mode, and sending credential are preserved. This is how an alias can keep its publicFromaddress while IMAP logs in as a differently named account. - Connecting the same deterministic CLI item or the same API-selected item again without a selector returns the existing mailbox.
The app-specific password never enters argv, a loopback API request or response, SQLite, or a log line. Skrzynka Desktop does not read it: Skarbiec remains the credential store and the core resolves the selected item only inside the authentication task.
Desktop OAuth lifecycle#
GET /v1/gmail/profileslists deduplicated Google-looking Skarbiec login identities without secrets. A failed read of a listed login propagates its original credential error instead of silently omitting that identity or returning a successful empty list; Desktop shows the same failure on the Google account surface.POST /v1/gmail/oauth/startresolves the selected identity and the fixedskrzynka-google-oauth-desktopitem.- Skrzynka creates a PKCE flow and returns its UUID and trusted Google authorization URL; the flow stays pending until the callback settles it.
- Google redirects the one-use code to the core's loopback callback.
- The core exchanges the code, looks up the authorized account email, and rejects an account mismatch.
- It writes a dedicated
skrzynka-gmail-<email-digest>bundle into Skarbiec and creates or reuses the mailbox. - The desktop polls the flow as
pending,processing,completed, orfailed.
The OAuth client must be an installed Desktop-app JSON. Authorization and token endpoints must exactly match Google's canonical HTTPS endpoints. Codes, client secrets, access tokens, and refresh tokens never cross the JSON API or enter SQLite.
skrzynka --organization <ID> account authorize --provider gmail --skarbiec-item <item-id> --bind <loopback-address:port> uses the requested loopback address for its callback. If Google refuses that redirect, Skrzynka reports GMAIL_OAUTH_REDIRECT_NOT_REGISTERED with the client ID and the redirect actually presented. Correct the client registration or select a Desktop-app client that accepts the callback; a different installation's client or callback is not evidence about this one.
A single-account app-specific password is the path that works without either an OAuth client or a Workspace administrator. Domain-wide delegation also needs no OAuth client or browser, but it does require a Workspace administrator to grant the service-account client ID.
Workspace delegation lifecycle#
The fixed skrzynka-google-service-account item contains a canonical Google service-account JSON. skrzynka --organization <ID> account connection --provider gmail --bind <loopback-address:port> --email <address> reports the account's email, numeric client ID, scope https://mail.google.com/, and admin-console grant URL, together with whether a grant actually exists for the named address. A readable item is not a grant, so a readable item alone is reported unproven.
skrzynka --organization <ID> account delegate --provider gmail --email <user> signs a one-hour RFC 7523 JWT assertion with that key, asks Google for a delegated token, saves a non-secret-reference Gmail bundle, and creates or reuses the mailbox in that organization. There is no consent screen or refresh token. A token is minted again from the service-account key when needed.
The grant itself is not Skrzynka's to perform. It exists only in the Workspace admin console, so a missing grant is reported as GOOGLE_DELEGATION_NOT_GRANTED carrying the client ID, the scope and the console URL an administrator needs — never attempted, automated, or driven through another tool from here.
Invariants#
- Callback base URL is credential-free loopback HTTP.
- Authorization host is
accounts.google.com; token host is Google's canonical OAuth endpoint. - OAuth flow state belongs to one organization and is single-consumption.
- Access tokens are memory-only. The cache deadline uses Google's integer expires_in measured from request start; no clamp or guessed safety margin is applied. Missing lifetime means current-call use without caching; malformed or unrepresentable lifetime is refused.
- Cache reuse also requires the current minting credentials to match: refresh token and OAuth client/endpoint, or delegated subject and service-account key/scope. Current vault declarations are read before cache lookup; changed or unreadable declarations cannot reuse an old token solely because its mailbox key is unchanged. The cache retains a length-framed digest of those inputs rather than another plaintext credential copy.
- Consumer
@gmail.comidentities cannot use Workspace domain-wide delegation. - The app-password API request and response never contain the app-specific password; only the selected Skarbiec item ID and optional non-secret mailbox selector cross the loopback API.
Refusal families#
OAuth uses GMAIL_OAUTH_CLIENT_INVALID, GMAIL_OAUTH_STATE_MISSING, GMAIL_OAUTH_STATE_INVALID, GMAIL_OAUTH_CODE_INVALID, GMAIL_OAUTH_FLOW_CONSUMED, GMAIL_OAUTH_REJECTED, GMAIL_OAUTH_RESPONSE_INVALID, GMAIL_REFRESH_TOKEN_MISSING, GMAIL_IDENTITY_UNAVAILABLE, and GMAIL_OAUTH_ACCOUNT_MISMATCH.
Runtime refresh uses GMAIL_TOKEN_REFRESH_UNAVAILABLE, GMAIL_TOKEN_RESPONSE_INVALID, and GMAIL_AUTHORIZATION_EXPIRED. Only Google's invalid_grant response is classified as expired authorization and recommends reconnecting. Other refresh refusals retain their HTTP status, OAuth error and error description under GMAIL_TOKEN_REFRESH_UNAVAILABLE; server errors and rate limiting are retryable, not evidence that the saved authorization expired. Responses are not logged wholesale, so access and refresh tokens are not copied into diagnostics. Delegation uses GOOGLE_TOKEN_UNAVAILABLE, GOOGLE_DELEGATION_NOT_GRANTED, and GOOGLE_DELEGATION_REJECTED. Exact operator actions are in the runbook.
Delegation refusals include the observed HTTP status, OAuth error and error description alongside the client ID, scope and grant guidance. An unknown refusal preserves those observations instead of asserting that the Workspace grant is absent. Key decoding and assertion signing retain their actual library causes without including the private key or signed assertion.
An undecodable token response retains the observed server-error or rate-limit retry classification, alongside the HTTP status and decoding cause. Invalid OAuth-client or service-account JSON reports the parser cause and location without copying the credential document.
The app-password paths use GMAIL_APP_PASSWORD_INPUT_INVALID, GMAIL_APP_PASSWORD_ITEM_INVALID, GMAIL_PROFILE_INVALID, IMAP_UNAVAILABLE, and GMAIL_IMAP_PASSWORD_REJECTED. A supplied mailbox selector can also return NOT_FOUND or MAILBOX_SELECTOR_AMBIGUOUS. The selector and credential are checked before the CLI writes its Skarbiec item or changes a mailbox. The connection report adds two codes of its own, neither of them a command failure: GOOGLE_DELEGATION_NOT_APPLICABLE for a consumer account that no administrator can delegate, and GMAIL_APP_PASSWORD_NOT_STORED when no mailbox names the account yet, so nothing could be authenticated. Exact sentences and operator actions are in the CLI reference and runbook.