# Mailboxes

A mailbox is one connected email identity plus the settings that govern it: a role, a signature, its own autonomy rule, its own health verdicts, and the transport and credentials its provider adapter is built from. A workspace can run several. Adding one adds a source of mail and a possible sending identity; it does not change how any existing mailbox behaves.

- **Status:** Available
- **Audience:** both
- **Channels:** email
- **In the app:** #/mailboxes
- **Last verified:** 2026-09-10
- **Canonical:** https://connectbyjbrh.com/docs/email/mailboxes/

## What the row owns

| Field | What it decides | Where you change it |
|---|---|---|
| `transport` | Which provider adapter `providers.build()` constructs | Fixed when you connect; change it by reconnecting |
| `config` | The credentials and settings that adapter needs | The connection form, or refreshed automatically by OAuth |
| `role` | How the mailbox is used — see [Mailbox roles](/docs/email/mailbox-roles/) | The mailbox row |
| `signature` | What is appended to a reply sent from here | The mailbox row |
| `autonomy` | What Connect may do without asking, for this address only | The mailbox row, or [What Connect may do](/docs/autonomy/) |
| Health verdicts | What the row reports about itself — see [Mailbox health](/docs/email/mailbox-health/) | Not editable; derived from what actually happened |

`config` is a real setter rather than a computed property, and that is not an implementation detail you can ignore. `oauth.store_tokens` rebuilds the whole dictionary and assigns it back after a token refresh; against a read-only property every refreshed access token would be silently dropped, and the mailbox would work until the current token expired and then stop for reasons no screen could explain.

## Several in one workspace

There is no single-inbox assumption anywhere in the channel. A workspace commonly runs a general address, a sales address and a support address at once, and each carries its own settings. Two of them can sit at different autonomy modes on the same day: sales replies going out on their own while everything arriving at the support address waits for a person.

What is shared is the workspace: one set of contacts, one thread list, one memory, one knowledge bank, one audit trail. A person who writes to two of your addresses is one [relationship](/docs/relationships/), not two, because identity resolution happens after canonicalisation and does not care which mailbox the message came through.

1. Open `#/mailboxes` and add a mailbox. Both audiences answer the same question here — which provider — because add-a-mailbox is one flow.
   - Result: You are sent into that provider's connection path: Google's consent screen, the Microsoft flow, or the IMAP and SMTP form.
2. Give the new row a role, and a signature if it needs its own.
   - Result: The role is what later decides which mailbox a reply is sent from, so it is worth setting deliberately rather than leaving at the default.
3. Set its autonomy, or leave it to inherit.
   - Result: An unset mailbox falls through to the channel's rule, then the workspace's. Nothing is enabled by adding a mailbox that was not already enabled for the channel.
4. Let the first sync run.
   - Result: History is read in, threads appear in Conversations, and the row starts reporting health. [Syncing an inbox](/docs/email/inbox-sync/) covers what the first pass does differently from every later one.

## Two tables, one policy

Customer mailboxes live in `tenant_mailboxes` and the Owner's in `mailboxes`. They are not identical rows: `tenant_mailboxes` has real foreign keys to `workspaces` and `platform_users`, which `mailboxes` does not. What is identical is the policy — `mailbox_console` runs over `mailboxes.py` for both audiences, so a rule about roles, health or autonomy is written once and cannot drift between them.

Every by-id action on a mailbox goes through `mailbox_console.row_for`, which resolves the row inside the caller's workspace. Four routes once did not — the connection form, sync, and the OAuth start and disconnect each took a mailbox id straight from a browser and looked it up with a bare `db.get`, which the workspace kernel's query filter can miss entirely when the row is already in the identity map. They answer only for their own workspace's rows now. One inbound webhook route still resolves its mailbox before any workspace is known, because it has nothing else to resolve it by; that is the documented exception rather than an oversight.

## The mailbox that was invisible rather than missing

A mailbox row stamped with an empty `workspace_id` matches no scope and no row-level security policy. It is not deleted and it is not broken: every by-id action on it still works, while every list of mailboxes omits it. Two connected Gmail identities can disappear from the Mailboxes screen this way, with no duplicate warning and no error anywhere.

> **Careful** The dangerous half is the uniqueness check. `find_by_key` could not see such a row either, so reconnecting the same address would have written a **second** row for one real inbox. The list and the uniqueness check now both ask the one query that can see them.

That query is deliberately read-only. Stamping the row from a request is refused by the policy's `WITH CHECK`, which would turn a missing row into a 500 — the repair belongs to `migrate.backfill_canonical_workspace`, which runs as the schema owner on every boot. If a mailbox you know you connected is not on the screen, this is the first thing to suspect, and [A row with no workspace stamp is invisible, not missing](/research/invisible-not-missing/) is the longer write-up.

## Questions

### How many mailboxes can one workspace have?

The model does not fix a number — a mailbox is a row, and a workspace may hold several. What bounds you in practice is the daily allowance on a customer plan, which counts processed mail across the workspace rather than per address.

### If I connect the same address twice, do I get two of everything?

You should not: the uniqueness check refuses a second row for an address already connected in that workspace. That check was the thing an empty workspace stamp defeated, which is why the visibility fix and the duplicate fix are the same fix.

### Does deleting a mailbox delete its conversations?

No. Threads, messages and contacts are canonical workspace records and outlive the connection that produced them. [Disconnecting a mailbox](/docs/email/mailbox-disconnect/) sets out exactly what stops and what is kept.

## Related

- [Email in Connect](https://connectbyjbrh.com/docs/email/)
- [Mailbox roles](https://connectbyjbrh.com/docs/email/mailbox-roles/)
- [Mailbox health](https://connectbyjbrh.com/docs/email/mailbox-health/)
- [Disconnecting a mailbox](https://connectbyjbrh.com/docs/email/mailbox-disconnect/)
- [A row with no workspace stamp is invisible, not missing](https://connectbyjbrh.com/research/invisible-not-missing/)

## What this page is based on

- Connect source pack — channels, §1 email: mailboxes and the three defects
- Connect source pack — architecture: workspace scoping and RLS (`docs-source/sources/GENERAL.md`)
- Connect capability registry (`docs-source/facts.py`) — `mailbox_roles`, `mailbox_health`
