The attack this exists to stop
Everything here happens before classification sees a word of it. Using Support has the pipeline in order, the reference the verdicts, and the MX constraint is the other half of getting mail at all.
From: is an unauthenticated claim. Anyone can send mail saying it came from your customer, and for every domain publishing DMARC p=none — most small domains and plenty of large ones — a receiving server delivers it rather than rejecting it.
That claim is the join key for the customer link. It resolves to a user row and pulls that account’s name, entitlements and itemized purchase history into a support console, where a human reads it and acts.
Rendering an attacker’s thread decorated with a real customer’s billing history is the standard opening move of support-driven account takeover — and this capability’s own account_access and privacy_request categories route exactly those messages to an operator already primed to act on them.
What counts as authenticated
DMARC is the only verdict that asserts what is actually needed — that the From: domain is aligned with something that passed.
spf=pass alone says the envelope sender’s domain passed, which is a different domain, and is exactly the gap DMARC exists to close. It counts only when the envelope sender aligns with the header From. dkim=pass alone has the same alignment gap.
Absence is not a pass. A message with no authentication results at all is unauthenticated — which is the right answer for mail that reached you through something that never checked.
Alignment is a public-suffix lookup, not label arithmetic
Relaxed alignment is defined in terms of the Organizational Domain, and that term is defined by the public suffix list.
Three states, and a console must render them differently
accountLinkSource | What it means |
|---|---|
session | An authenticated request proved it. The account is the caller |
email_address | Matched against the address in a From: header nobody proved |
null | No link — the address belongs to nobody with an account |
The same operator action — a refund, a password reset — follows from very different evidence, and a single boolean cannot say which one is on screen.
So the thread records how it came to name an account, not just whether to believe it.
The in-app channel does not have this problem
POST /support/feedback wears requireAuth() and nothing weaker.
An in-app submission has no From: to spoof. The session is the identity, and the gate proved it before the handler ran.
Which is why two hundred lines of authenticity code earn the right to say a thread belongs to a customer on the mail path — and why the app path needs none of it.
The recipient side gets the same rule, in the other direction
Support claims what is addressed to it on the SMTP envelope recipient, never on a To: header — because anyone can put your support address in a header on a message routed somewhere else.
The envelope is authority; headers are not. Provenance is that rule applied to the sender; recipient matching is the same rule applied to the recipient.
Channel rides on the thread and on every message
email or app, and the inbox filters on it.
It is per message as well as per thread because the two genuinely differ: one app thread can hold an answer that was mailed and an answer that was not. On an outbound row, channel is how the answer was delivered.
A mailed reply to an app thread is email, and the submission is minted a message id on the way in — so when the customer answers that reply, their mail client threads it back instead of opening a new one.
emailJobId follows from that and carries no information of its own: it is present exactly when a row is outbound and email. Ask channel whether an answer went out — never the absence of a job id, which would mean both this arrived and this is waiting in the app.
Threading is on headers, never on the subject
In-Reply-To and References.
Subject matching is what puts two unrelated people who both wrote “Refund” in one thread, and what splits a real conversation the moment a client localizes Re: to Aw:.