Corsair

Start here

Core concepts

Six ideas. Everything else in the manual is built on them, and the first one is the one people get wrong.

Users and addresses are different things#

There are two identities in Corsair, and conflating what they own is the bug to avoid.

User Address
What it is A control-panel login A mailbox
Signs into The panel at /app SMTP, IMAP, POP3, JMAP, webmail
Authenticated by Session cookie (corsair_session) Password on the protocol
Owns Domains, plans, webhooks, filters Messages, folders

They remain distinct. What they can share is the password.

Your own mailbox uses your account password#

If you sign up as [email protected] and then create the mailbox [email protected] on your own domain, that is one person. Corsair links the two and there is a single password: the one you use for the panel is the one your mail client uses. Change it in Account settings and it changes everywhere, because there is only one of it.

The mailbox is created without asking for a password at all — the panel says so when it recognises your own address.

Everyone else keeps a mailbox-only credential#

A mailbox that is not a control-panel account — the other people on a family or team domain — has its own password and no panel login whatsoever. Merging those into the owner's account would hand every one of them the ability to edit your domains.

The first user created owns the instance (users.is_owner). The claim is made inside the INSERT and guarded by a partial unique index, so two simultaneous signups cannot both win.

A domain is a routing decision plus proof#

Adding a domain does three things: it generates a verification token, it creates three DKIM key pairs, and it produces the record set you have to publish.

A domain has a status. Until it is active, Corsair will accept mail for it but refuses to send from it. Sending from a domain whose SPF and DKIM are not published damages the sending IP's reputation for every other domain on the server, so this is enforced rather than advised.

The worker re-checks pending domains every half hour, because people publish records and never come back to press the button.

See Domains and DNS setup.

Addresses come in four kinds#

Kind Password Mailbox What it does
standard Yes Yes An ordinary mailbox
catchall Yes Yes A mailbox that also receives anything unmatched in the domain
alias No No Forwards to exactly one destination
group No No Forwards to several destinations at once

Only standard and catchall carry a password hash. Aliases and groups are routing entries — there is nothing to sign into, because there is no mailbox behind them.

To send as an alias, sign in as a real mailbox on the same account and set the From address in your client.

How a recipient is resolved#

For [email protected], in order:

  1. An exact address match.
  2. Sub-addressinguser+tag@ routes to user@, with no setup at all.
  3. The domain's catch-all, if one exists.
  4. The domain's fallback domain, followed exactly once. (Following it twice is how you build a loop.)
  5. postmaster@ and abuse@, which forward to the account that owns the domain.

If none match, the message is rejected at SMTP time with a 550. Corsair does not accept-then-bounce: a bounce to a forged sender is backscatter, and refusing during the transaction puts the problem back where it belongs.

Role accounts answer whether or not you create them#

postmaster@ is required by RFC 5321 §4.5.1 and abuse@ by RFC 2142. The second is the one that matters day to day: blocklist operators and ISP abuse desks reach an installation through it, so a domain that 550s abuse@ is unreachable at exactly the moment reachability decides whether the MX keeps delivering anywhere.

Both are resolved as a fallback rather than created as addresses on a new domain. That distinction is worth understanding, because it is what makes the guarantee hold:

  • It applies to every domain, including ones added before the rule existed. Nothing needs backfilling.
  • There is no row to delete, so a domain cannot drift back out of compliance.
  • It runs last, after every route you configured. Creating a real postmaster address, or a catch-all, takes precedence — the fallback can only ever turn a 550 into a delivery, never divert mail away from something you set up on purpose.

The one case it declines is forwarding an address to itself, which is what it would otherwise do when the owning account's own email is postmaster@ on the domain being resolved. That resolves to a 550 rather than a loop.

Because these forward, mail sent to them lands in whatever inbox the owning account uses, and it arrives unfiltered — forwarding relays the message as-is. Both addresses are heavily harvested, so point the owning account somewhere you are willing to have receive spam, or create real postmaster and abuse addresses and let them take precedence.

Folders, UIDs, and why they are fussy#

Every mailbox is provisioned with six folders: INBOX, Drafts, Sent, Junk, Trash, and Archive, each tagged with its IMAP special-use attribute so clients put things in the right place without being told.

Two properties of IMAP shape a lot of the code:

A UID is permanent and must be unique. Corsair allocates one with UPDATE folders SET uid_next = uid_next + 1 … RETURNING, which takes a row lock. Two deliveries arriving at the same instant cannot be handed the same UID, and a duplicate UID is the one thing an IMAP client never recovers from.

Sequence numbers renumber. They index into the folder's live messages in UID order, so deleting message 3 makes the old 4 into the new 3. This is why EXPUNGE is emitted highest-sequence-first — ascending order makes a client delete the wrong messages.

A move keeps the message id. moveTo updates folder_id and allocates a fresh UID in the target, writing a tombstone in the source. It is deliberately not implemented as copy-then-expunge, which would mint a new row id — and JMAP requires an Email's id to survive a change of mailbox.

One store, five ways in#

SMTP, IMAP, JMAP, POP3, and the webmail all read and write the same rows.

That is why a message delivered over SMTP is instantly visible over all of them, and why there is no "sync" anywhere in the product. It is also why a change to the store affects every protocol at once, which is the trade you are making.

Protocol Port Identity Notes
SMTP (MX) 25 None — anyone may deliver Reference
SMTP (submission) 587, 465 Address Requires TLS
IMAP 143, 993 Address Reference
POP3 110, 995 Address Reference
JMAP 443 (HTTP) Address, via Basic or cookie Reference
Webmail 443 (HTTP) Address, via cookie Guide
Panel API 443 (HTTP) User, via cookie Reference

Plans gate features, even when nothing is charged#

An account's entitlement is its plan plus its live subscription. Plans are rows in a table, not constants, so a self-hoster can price, rename, or delete them without a deploy.

An account with no subscription falls back to the trial plan. An instance with no plans at all is unmetered: every feature on, no caps. That is a legitimate way to run a private server, and it is what you get if you never touch billing.

A feature the plan does not include raises a 402, not a 403, so the panel can render an upgrade prompt rather than an error. Validation always runs first — a malformed input is invalid regardless of the plan.

See Plans and billing.

Things that are deliberately never stored#

Three credentials pass through Corsair and are never persisted. Do not "fix" any of them by adding a column:

  • DNS API tokens — used for one publish and discarded. One can usually rewrite every record on every domain in the account.
  • Card details — never touch the server at all. The customer enters them on the provider's hosted page; a brand, four digits, and an opaque reference come back.
  • Transfer source passwords — encrypted at rest and erased the moment the transfer reaches a terminal state. They are someone else's credential.

Reset and recovery tokens are stored only as SHA-256 hashes, and redeemed with a used_at IS NULL predicate inside the UPDATE — checking it in a separate read lets two concurrent requests both redeem the same link.