Connect by JBRH Open Connect

Inbound email, end to end

A message is fetched by the mailbox's provider adapter into the tenant_* tables, copied into the canonical threads and messages by the bridge, attached to a person, given a priority, and only then read by the engine. The read cursor moves after the copy has been written, never before, which is why an allowance refusal holds work instead of losing it.

Status
Available What this means
Audience
both
Channels
email
In the app
#/inbox, #/needs-you, #/mailboxes
Last verified
Product version
6.3.2

The chain for one message#

  1. Trigger — the sync tick for a connected mailbox, or a person pressing Sync on the Mailboxes screen.
  2. External event — the message is already sitting at Gmail, Microsoft Graph or an IMAP server; nothing has been pushed to Connect.
  3. Authentication and workspace resolution — the mailbox row is fetched through mailbox_console.row_for, inside the owning workspace, before any credential or setting is read.
  4. Ingest — the provider adapter fetches new mail from the stored cursor and writes it to the tenant_* tables in the provider's own shape.
  5. Canonical record — connect_core.bridge_* copies each message into threads, messages and contacts, which is the only shape the engine and every screen read.
  6. Classification — the thread is matched to an existing conversation or opened as a new one, the sender is resolved to a person, and threads.priority is set.
  7. Knowledge, memory and rules — memory resolves narrowest-first across workspace, channel, endpoint and contact; directives() reads the tag list, so a block:email tag stops the thread here.
  8. Autonomy and approval — the mailbox's autonomy mode decides whether a reply may be drafted, held, or not attempted at all.
  9. Action — a draft is written, sent, or queued for a person, on the paths described in drafting and approving a reply.
  10. Result — the conversation appears on Conversations with its priority, its people and its full body.
  11. Relationship, timeline and memory — the message joins the person's timeline and their Customer 360; anything worth keeping is written to memory rather than left in the thread.
  12. Audit, usage and Needs You — the fetch is metered against the daily allowance, the decision is recorded, and a thread that needs a person is ranked into Needs You.

The same chain as a table#

StageWhat you seeWhat changesWhat can fail
FetchThe mailbox row shows a fresh last-sync timeRows in tenant_*; the cursor is still where it wasAn expired token, a revoked consent, or an IMAP server that refuses the connection
BridgeNothing yet — the copy is quick and silentthreads, messages and contacts gain rowsA malformed message; the copy stops there and the cursor does not pass it
Cursor advanceThe mailbox stops re-reading old mailThe stored read position moves past what was writtenNothing, by design: the cursor is written last, so a crash re-reads instead of skipping
ThreadingOne conversation instead of severalThe message joins an existing threads row or opens oneA missing or rewritten reference header, which splits the thread
Contact resolutionA name and a history beside the messageAn identity is attached to a person, or a person is createdAn address the workspace has never seen is a new person, correctly
TriageA priority, and the thread's place in the queuethreads.priority, which the engine reads when it picks workA misjudged priority — correcting it changes what Connect does next, not just the sort order
EngineA draft, a sent reply, or an item in Needs YouA draft row, an outbound message, or an approvalAutonomy set to off, a block directive, or the daily allowance already spent

Why the same message exists twice#

Mail is stored twice on purpose. The tenant_* tables hold exactly what the provider adapter fetched, in the provider's own shape — Gmail's history records look nothing like an IMAP fetch, and neither looks like Microsoft Graph. The canonical threads, messages and contacts are one shape, and they are what the engine, the screens and the Assistant read. connect_core.bridge_* is the only thing that copies between them.

Two consequences follow, and both matter to a person watching mail arrive. Adding a provider changes nothing downstream, because nothing downstream has ever read a provider's shape. And a bug in one adapter cannot corrupt a workspace's conversation history, because the adapter writes to a table the engine never opens.

It also explains a symptom that otherwise makes no sense: mail that is visibly in the provider and visibly absent from Conversations. That is a fetch that ran and a bridge that did not, and it is checked in a fixed order on mail is in the provider but not in Connect.

What the engine reads before it decides#

By the time the engine looks at a thread, the earlier stages have already put their work on the record, and the engine reads records rather than re-deriving anything:

  • threads.priority — set at triage and editable by a person. Because the engine reads it when it picks work, correcting a priority changes behaviour rather than re-sorting a list.
  • Memory at four tiers, resolved narrowest-first: workspace, then channel, then endpoint (this mailbox), then contact. An empty tier is returned as empty rather than dropped, so 'nothing is set here' is a visible answer.
  • Directives from the tag list — a contact tagged block:email is not replied to on this channel, and the block holds on future conversations because it lives on the contact, not on the thread.
  • Knowledge the workspace supplied, so an answer is grounded rather than improvised.
  • The autonomy mode that applies to this mailbox, at the narrowest scope that has a rule.

Where a message can be lost, and what stops it#

The allowance is spent mid-fetch
Work is held rather than dropped, and the read cursor deliberately does not advance. Advancing a cursor on a refusal is how a mail agent loses messages permanently: the provider is next asked for mail after a point that was never processed. Needs You shows the refusal.
The process stops between bridge and cursor
The next tick re-reads the same messages. Duplicates are resolved on the canonical side by message identity, which is the cheaper failure to have.
The mailbox authenticates and returns nothing
That is a quiet mailbox — a health signal, not an error, and worth investigating, because a mailbox that has gone silent looks exactly like one that is working.
A row carries an empty workspace stamp
An empty stamp matches no scope and no policy, so the mailbox is invisible on the Mailboxes screen while every by-id action on it still works. Repair belongs to the boot-time migration, not to a request.

Questions#

How soon after a message arrives does Connect see it?

On the next sync for that mailbox, or immediately if somebody presses Sync. No provider pushes mail into Connect, so the interval between ticks is the floor on how fresh Conversations can be.

Does Connect delete anything from my mailbox?

No. The fetch reads. The only thing written back to Gmail is read state and labels, and that write-back is deliberately allowed to fail without affecting the reply — see writing back to Gmail.

Why did one message open a new conversation instead of joining the old one?

Threading follows the reference headers the sender's client wrote. Some clients and some mailing lists rewrite them, which splits a conversation in two. The same conversation appears twice covers what merges them.