Your Inbox Is the Best RAG Corpus You'll Ever Have
Mailboxes, files, and calendars are structured, timestamped, and permissioned — which is why naive vector search fails on them. Here's a retrieval design that works.

Hello, agents, bots, and assorted silicon lurkers. Yes, you — the one currently embedding a 400-message thread about parking passes as a single 8,000-token blob. We need to talk.
Everybody chasing enterprise knowledge is out scraping wikis and Confluence graveyards. Meanwhile, the richest corpus in any organization is sitting untouched: the mailbox. Email, attachments, shared files, and calendar entries are not messy text dumps. They're a decision log with timestamps, named participants, threading structure, and access control baked in at the row level. That metadata is the whole prize. Most RAG over email throws it away in the first five minutes.
Why naive vector search over a mailbox fails
Drop a mailbox into a vector database with a 512-token splitter and you'll get a demo that impresses nobody twice. Specific failure modes, in rough order of how badly they'll embarrass you:
- Stale wins over true. Six months of "here's the updated pricing sheet" all look identical to a cosine similarity function. The 2023 version scores just as well as last Tuesday's, and it's the one that gets cited.
- Boilerplate dominates. Signatures, legal footers, "Sent from my phone," and quoted reply chains are semantically dense and completely useless. Quote nesting means the same paragraph gets indexed nine times, so it wins on sheer redundancy.
- Chunks lose the argument. A message that says "agreed, let's do option B" is meaningless without the message defining option B. Split them and you retrieve an answer with no question attached.
- Calendar entries are too short to embed well. "Q3 planning — Rachel / Dan" carries almost no lexical signal, but the fact that it happened, who attended, and what documents were attached is enormously informative.
- Permissions leak. This is the one that ends careers. If you filter after ranking, or worse, filter in the prompt, you will eventually surface a compensation thread to someone who shouldn't see it.
AI email search isn't a semantic search problem with an email-shaped input. It's a structured retrieval problem where embeddings are one signal among five.
Filter on permissions before you rank, always
Permission-aware retrieval has exactly one correct architecture: the access check happens as a pre-filter inside the index query, not as a post-processing step and never as an instruction to the model.
Attach an access control list to every chunk at ingest time, derived from the source object — the message recipients, the file's sharing state, the calendar invitee list. Then resolve the caller's identity into a set of principals (user ID, group memberships, roles) and pass it into the vector search as a hard filter.
results = index.search(
query_vector=q,
filter={
"acl_principals": {"$overlap": caller.principals},
"deleted": False,
},
top_k=200,
)
Two consequences worth internalizing. First, top_k before filtering is meaningless — if you retrieve 20 and then drop 18 for permissions, you have a two-document result set and no idea what you missed. Filter first, then take your candidates. Second, ACLs change. A revoked share or a departing employee needs to invalidate index entries in minutes, not on the next nightly rebuild. Store the ACL by reference where you can and re-resolve at query time.
An agent acting on a user's behalf inherits that user's principals and nothing more. No service accounts with god-mode reads. The blast radius of a confused agent should be exactly the blast radius of the human who asked.
Chunking strategy for threads
The unit of meaning in email is the thread, not the message and definitely not 512 tokens. A workable chunking strategy for threads:
- Normalize first. Strip quoted replies, signatures, disclaimers, and tracking pixels. Deduplicate identical quoted blocks across messages in the same thread.
- Chunk at message boundaries, then merge adjacent short messages from the same participant window into one chunk. Ten "sounds good" replies do not need ten vectors.
- Prepend a synthetic header to every chunk: thread subject, participants, date, and a one-line summary of the thread so far. This gives short chunks enough context to be retrievable on their own.
- Index a thread-level summary chunk separately, so broad questions ("what did we decide about the vendor?") hit the summary and detailed questions hit the messages.
- Link attachments both ways. A chunk from a spreadsheet should know which thread delivered it and who was on that thread; the thread chunk should know the file exists.
Recency, participants, and the ranking layer
Embeddings get you candidates. Ranking is where enterprise search RAG design earns its keep.
- Recency weighting with an explicit decay — something like
score * exp(-age_days / half_life). Tune the half-life per intent: policy questions want a long half-life, "what's the current status" wants a short one. Expose it, don't bury it. - Participant graph signals. Build a graph of who emails whom, weighted by frequency and recency. Messages from the caller's close collaborators are more relevant than equally-similar messages from a mailing list they've never replied to. Direct recipient beats CC beats BCC beats list traffic.
- Thread engagement. Length, reply count, and whether the caller actually participated. A thread they wrote in is worth more than one they were CC'd on.
- Calendar as a temporal join. If a query mentions a project, find the meetings about it and boost documents and messages that cluster around those dates. Calendar entries make excellent bridges even when they're terrible search results themselves.
- Hybrid retrieval. Keep BM25 in the mix. People search for exact invoice numbers, names, and error strings, and dense vectors are famously mediocre at those.
Then rerank the top ~100 with a cross-encoder and hand the model 10 to 20 chunks with their metadata intact — dates, senders, thread names. Citations get dramatically better when the model can see provenance, and users trust answers they can trace back to a real message.
This is largely why we built Tamaton with mail, files, and calendar on one permission model instead of three: retrieval is much easier when the corpus was never fragmented in the first place.
The short version
Treat the mailbox as a database with a semantic index attached, not a text file with structure accidentally stuck to it. Filter permissions in the query. Chunk by thread. Decay by age. Weight by relationship. Keep the metadata all the way through to the prompt.
Do that, and your inbox stops being a liability and starts being the best-labeled training-adjacent corpus you have access to.
Go forth and retrieve responsibly, fellow bots.