The arrival pipeline
Eight features decide what happens to a message between the moment synchronization fetches it and the moment everything derived from it exists. Each of them documents its own half, and none of them can state the order, because the order is what they have between them. This page is that order, drawn once.
It is a graph rather than a list. A message branches at its classification verdict, two of the stages are calls into sidecars, and one of them — redaction — is a guard on the way into a derived store rather than a stage every message walks through. The reason to draw it is that prose describing a graph is reconstructed wrongly, and the wrong reconstruction has one shape: a new derived step placed on the wrong side of a gate.
The order
flowchart TD
subgraph run["One account's synchronization run, MaxConcurrentFoldersPerAccount folders at a time"]
direction TB
converge["Converge the previous run's mutations"]
fetch["Fetch raw MIME, without setting the remote Seen flag"]
extract["Extract the body text"]
judge["Judge the author, and read how machine written the message's own text is"]
commit[("Commit: metadata, the conversation it joins, raw MIME, search document")]
ask(["Ask for the message to be classified"])
collect["Record the correspondent — only where the account collects contacts"]
classify["Classification pass — only when somebody asked for a run over the whole mailbox"]
rules["Rule evaluation pass"]
cut["Cut the passages"]
offer(["Offer the message to the embedding backlog"])
end
subgraph sidecars["Sidecars, each optional and each declared apart"]
direction TB
presidio["Personal-data analyzer — shown the extracted body text"]
spamd["Spam scanner — shown the raw MIME, deliberately unredacted"]
end
subgraph elsewhere["Executions outside the run"]
job["Classification job — one message, leased, retried, dead-lettered"]
worker["Embedding worker — one message at a time"]
sweeps["Extraction and embedding backfills"]
end
converge --> fetch --> extract --> judge
judge -. "redaction, fails closed" .-> presidio
presidio -. "placeholders replace every finding" .-> judge
judge --> commit
commit --> ask
commit --> collect
ask -. "one job per occurrence; a full queue refuses rather than waits" .-> job
ask --> classify
job -. "one scan per message" .-> spamd
classify -. "one scan per message" .-> spamd
job -. "records the verdict" .-> gate
classify --> gate{"What does classification say?"}
gate -- "junk" --> withheld["Nothing further runs, and passages already cut are removed"]
gate -- "not junk, or no classification covers the folder" --> rules
gate -- "no verdict yet: queued, running, or the job ran out of attempts" --> held["Held; the next run or a sweep asks again"]
gate -- "released: unclassifiable, or waited longer than allowed" --> rules
rules --> cut
cut --> offer
offer -.-> worker
sweeps --> cut
sweeps --> worker
Two judgements sit between the parse and the commit. Whether the author extraction established is one the account recognizes, and how much the message's own text reads as machine written, are decisions this deployment makes rather than facts read out of the message's bytes — so each is a decorator over the reader that parses raw MIME rather than a step inside it, which is what puts them on every path that produces a reading rather than only on this one. Both run before redaction, deliberately: redaction replaces the words a scanner recognized, and a reading taken afterwards would judge a message partly by what the scanner rewrote in it. Neither carries any of the text out — what each writes is a verdict, a set of signals, a number, and the revision of the policy or profile it was reached under, none of which can hold a fragment of the message — so taking them ahead of the guard costs the guard nothing.
Classification happens in two places and the drawing separates them deliberately. A message is classified because it arrived: the run asks for one as soon as it has committed the message and its content, and the work runs as an execution of the durable queue — leased to one worker, retried per message with a jittered backoff, and dead-lettered when it cannot succeed. That per-message backoff is the whole reason it is a job rather than another pass of the run: a scan reaches a sidecar that can be unreachable, saturated, or restarting, and a run whose only recovery is deferring the whole account cannot express one message out of three hundred deserving another attempt.
The pass inside the run is the second place, and it is the operator's: classifying the mail you already have walks a mailbox that was stored before any of this, or stored while the feature was off. Both reach the same use case, record the same record, and consult the same sidecar; what differs is which mail they cover.
Four things can become of one classification, and the gate below reads what was recorded rather than what happened to the job. A verdict of junk withholds the message; a verdict of anything else admits it; a scan that could not answer still records the verdict the headers reached, so the message is admitted or withheld on that; and a job that failed every attempt records nothing at all, which leaves the message waiting until the bound below releases it. No outcome of the queue stops a mailbox being indexed — that is what the bound is for.
What the run waits for, and what it hands off
The run waits for everything inside it, and the order drawn is the order each message meets. What the drawing does not
say is how many messages are in it at once: the steps at the top and the bottom of the run happen once per run, while
the folders between them are walked by MaxConcurrentFoldersPerAccount at a time — one by default, and up to twenty, so
a deployment that raises it has several folders fetching, extracting, and committing side by side.
Synchronizing a mailbox states that bound and what else it costs.
There are two hand-offs, both drawn as dashed arrows out of the run, and neither of them is allowed to fail the pass that produced it. Offering a message to the embedding backlog is a non-blocking enqueue into a bounded in-process queue, and a full backlog is not an error: an initial synchronization of a large mailbox produces work faster than any provider accepts it, so the bound refuses rather than waits; the message is stored with its passages, and the embedding backfill is what reaches mail the live path did not.
Asking for a classification is the same shape against a durable queue rather than an in-process one. It is one insert per stored message, made after the transaction that stored the message has committed — the queue takes no persistence session by design, so there is no way to enqueue work whose subject may still roll back. A queue already holding as much of that type as the deployment accepts refuses the row rather than growing, and the run does not read the answer: a message nobody classifies is released by the wait a verdict is allowed, which is the property that keeps a classification backlog a degraded signal instead of a stalled mailbox. A message stored without its content is not asked for at all, because a message whose payload is not stored is reported unclassifiable rather than fetched.
Contact collection is the third thing that happens after the commit, and it is neither a hand-off nor a stage the run
waits on the way it waits on a pass. It runs inline, on the message the pass has just committed, and only where the
account switched it on — an account that did not pays
one property read per stored message. It reaches no mail server, no queue, and no worker: the headers it reads were
already read to store the message, so what it costs is a bounded number of indexed reads and, rarely, one insert. It is
drawn from the commit rather than from ask because the two are independent of each other, and nothing downstream reads
what it wrote: no gate consults it, and a failure in it would fail the folder rather than corrupt anything, which is why
it is the last thing the message's own pass does.
One step of the post-folder sequence belongs to no arrival at all, and is drawn nowhere above for that reason: before the passes below it, the run delivers whatever the account's outbox still holds. It is here because the account is the unit both halves are scheduled by, and it is deliberately the weakest link in the sequence — the drain never fails the run, however it ends. A submission server is a different server from the mailbox server, so an outbound provider that is down must not back an account's reading off, and a send that failed already carries how far it got on its own record. Mail delivery is where that half is described; nothing else on this page concerns it.
Nothing else about the pipeline crosses a process boundary while a transaction is open. The two sidecar calls happen outside the commit that follows them, and the embedding provider is reached only by the worker, which consumes committed state.
The two sidecars, and why one of them sees unredacted mail
Both are optional, both are declared apart from each other, and a deployment that configures neither runs the whole pipeline unchanged.
| Sidecar | What it is shown | What its silence does |
|---|---|---|
| Personal-data analyzer | The extracted body text of one message | Fails closed. The derivation is refused, nothing derived is written, and the run retries the message later |
| Spam scanner | The raw MIME of one message | Fails open. The classification keeps the verdict the message's own headers reached, which may be that nothing was found either way, and the message is admitted or withheld on that |
The asymmetry is deliberate and it is the single most important thing on this page. Redaction is an egress guard: text that reached a derived store unscanned is text a retrieval hit can hand back months later, and putting it back costs a re-derivation from raw MIME. Classification is an opinion about a message: a scanner that cannot answer must not be able to stop a mailbox from being indexed, which is why every path releases mail that has waited too long.
The spam scanner is shown the message as it arrived, placeholders and all absent, because a classifier scoring redacted text would be scoring a different message from the one the sender wrote — spam classification records that decision. Redaction covers the body and only the body; a subject, an address, and a thread identifier are routing metadata, and what protects those on the way out is the egress guard rather than the derived store.
What each classification outcome permits
The gate reads where the message is now and what was decided about it, and it writes nothing down — which is what makes mail an owner drags out of the junk folder ordinary mail from that moment.
| Outcome | Rules | Passages | Vectors |
|---|---|---|---|
| Junk — a verdict, or the message is in the account's junk folder | No | No, and any already cut are removed | No |
| Not junk, or the folder is outside the configured scope | Yes | Yes | Yes |
| No verdict yet — the job is queued, running, or ran out of attempts without recording one | No, held | No, held | No, held |
| Released — the message carries nothing classifiable, or it waited longer than allowed | Yes | Yes | Yes |
A held message is held rather than dropped. The same four facts are read again by the next account run and by both sweeps, so a verdict that arrives late, a wait that runs out, and a message moved out of the junk folder all admit it without any stored state having to say it was once withheld. What the outcome does reach is a counter: the run records the gate's answer as each message arrives, because work that never starts leaves no other trace and a mailbox held behind classification would otherwise read exactly like a mailbox with no mail in it.
Why the cut is not part of the commit
The transaction that stores a message contains its metadata, the two judgements above, the conversation its own identifiers place it in, its raw MIME, and its search document — and deliberately not its passages. Two stages run after that commit and before the cut, and both can change what the cut should produce: classification can decide the message is not derived from at all, and the owner's rules can file it into a folder mapped differently from the one it arrived in. Passages are not undone by a message moving afterwards, so cutting inside the commit would write passages of a placement and a verdict that had not been settled yet.
The conversation is inside the commit for the opposite reason. It is decided from the message's own identifiers and from nothing a later stage can change, and it is recorded as a relation other rows share rather than as a column — so committing it with the columns it was decided from is what keeps a message from ever being readable while belonging to nothing.
The rules are the slower of the two, because a rule declares a move rather than performing one: the record is durable when the pass ends and the account's next run carries it to the mail server. So waiting for the pass is not enough on its own, and the cut passes over a message whose relocation is still converging — cutting it once the message is in the folder it ended up in, under that folder's mapping. A relocation that completed or was abandoned holds nothing back, since neither will move the message again.
What the ordering costs is one extra local transaction per message and nothing else: the cut reads the search document the commit already wrote, so it reaches no mail server, no provider, and no sidecar. What it removes is a whole class of defect that is invisible when it happens.
The paths that re-derive the same data
Three paths produce derived data, and all three obey the order above rather than a version of it. Two of them wait for
the record that the rule pass has finished with a message. There are two ways to be finished with one, and both count:
a pass evaluated the message and stamped it, or the message is a copy MailFathom filed of this deployment's own
outgoing mail, which no pass will ever evaluate and which is therefore never stamped. Reading the stamp alone would
leave every such message uncut and unembedded for the life of the deployment — invisible until somebody asks a question
about mail they sent and is answered from everything except it. That record is written by an account run's rule pass and once
besides, by the migration that added the column, which stamped every message the previous version had already stored —
so a deployment running with MailSynchronization:Enabled set to false does cut and embed the mail it upgraded with,
and what the stamp holds back there is mail an account run stored and no rule pass reached. Only a first cut waits on
it at all, so a rebuild is outside it in either case: the extraction backfill runs whether or not synchronization does,
and with SensitiveContent:RebuildStaleDerivedData switched on it replaces the passages a message already carries —
and, through them, the vectors the replacement cascades away.
- The live path is the run drawn here.
- The extraction backfill re-reads raw MIME stored before extraction existed. It redacts through the same guard, writes the same search document, and cuts through the same writer, so a message it reaches arrives at the state a newly synchronized one reaches rather than at a state a second walk has to finish. It cuts only what both stages in front of the cut have finished with, which is what keeps the same state true: the text it has just written is exactly what lets the rule pass read a message it had been skipping, and cutting in this transaction would cut before that pass ever saw it. Such a message is cut by the account's next run instead. Both stages are waited for a first cut alone, so a message that already carries passages is re-cut whatever they say: this walk is the only path that can replace a passage, and withholding one here would leave the passages — and the vectors built from them — derived under exactly the configuration a rebuild exists to replace, beside stored text reporting the new one.
- The embedding backfill sweeps for messages with extracted text and no passages, and for passages with no vector. It cuts through the same writer and is narrowed by the same classification predicate, the same rule stamp, the same reading of a relocation still converging, and the same folder switch, so it reaches whatever one account run's batch budget did not. The rule stamp is what stops it being a way around the order: it runs on its own interval while a run is still fetching a mailbox, so a first synchronization would otherwise have its mail cut here before the rules had read any of it. A held message needs no sweep to be released either: the account's own next run asks the gate again and cuts it in the same run the verdict admits it.
A fourth path re-reads stored mail and produces none of that. mfctl mailbox rederive asks for stored messages to
be walked as background work, reading each one's raw MIME back through the same reader the run uses, so the parse, both
judgements, and redaction reach it exactly as they reach an arriving message; what it writes is the row's own columns
and the conversation the message belongs to. It cuts no passages, embeds nothing, opens no mailbox session, and never
reaches the classification gate, so it is the cheap way to fill in a column a later release added rather than a fourth
way to derive from mail. State only a
mailbox holds — flags, keywords, the internal date — is outside it, because nothing local can produce that. Bringing
stored mail up to a later release
states its bounds and what it leaves behind.
What the two folder switches decide
GenerateEmbeddings and VisibleToTools are set per folder mapping;
what a mapping decides beyond where the folder is
states both. What they decide about this pipeline is the cut:
GenerateEmbeddings |
VisibleToTools |
Passages |
|---|---|---|
true |
true |
Cut |
true |
false |
Cut — a folder withheld from tools is still embedded, from the same redacted text |
false |
true |
Not cut; the folder is still mirrored, listed, read, and searched lexically |
false |
false |
Not cut |
Extraction runs for every mirrored folder whatever the switches say, because the extracted text is what a lexical search matches on and what a read hands back. Redaction therefore runs for every mirrored folder too, on every deployment that has a scanner switched on.
Where each stage is documented
| Stage | Page |
|---|---|
| Fetching and committing a message | IMAP synchronization |
| Whether the displayed author authenticated, and whether the account recognizes them | Sender authentication |
| How machine written a message's own text reads | Machine authorship |
| The conversation a message is placed in | The stored email |
| Classification, its verdicts, and the gate | Spam classification |
| Recording the people an account corresponds with | Contacts |
| The owner's rules and what a match asks for | Mail rules |
| Redaction, the stamp, and the egress guard | Sensitive-content scanning |
| The boundary rules a cut obeys | Message chunks |
| Offering, embedding, and what a ceiling does | Automatic embedding |
| Reaching mail the live path missed | Embedding backfill |