Inbound Email API
/api/inbound-emails backs the Inbound Email screen — every email
SABLE has ingested, with the project it landed on or the candidates for one.
GET /api/inbound-emails?scope=all|unassigned|rejected
all and unassigned both hold back rejected rows; pass scope=rejected to see those instead.
Admin/lead only.
POST /api/inbound-emails
{ "id": "email-source-uuid", "project_id": "project-uuid", "remember_domain": true }
{ "id": "email-source-uuid", "action": "reject", "reason": "wrong client" }
{ "id": "email-source-uuid", "action": "restore" }
Why this route never returns the email body
Email bodies are client correspondence; this route deliberately returns metadata and the subject line only, never the body. Reading a thread's content is treated as a different permission from triaging where it belongs — the account page already shows attributed mail's content to the people who actually own the project, so the triage screen doesn't need to duplicate that exposure.
Assignment is not cosmetic
Setting project_id here does real work, not just a relabel. A source with no project_id skips
knowledge indexing and the living project-note entry at ingest time. fathom-sync's bounded
link-repair sweep (see fathom-sync) completes
exactly those side effects once project_id becomes non-null — so an assignment made through this
route is what makes the email searchable and puts it into the project's note, on the next
scheduled poll, with no separate re-extraction step.
remember_domain
Setting this alongside an assignment teaches the router: future email from the same sending domain gets a stronger signal toward the same project, without requiring the same manual assignment every time.
Where the code lives
sable-agents-demo/web/api/inbound-emails.js,_inbound_email.jssable-agents-demo/supabase/functions/email-webhook/index.ts,email-syncsable-agents-demo/web/inbound.html