001Notes

Inside ThunderbirdAI: From Raw Email to a Grounded AI Answer

On this page21

An implementation-level walkthrough of how ThunderbirdAI queues mail, builds canonical local records, indexes evidence, enforces scope, and uses an optional endpoint without treating the model as the mailbox.

Article details
Status
Building Publicly
Subcategory
Thunderbird AI
Last reviewed
6 Sept 2026
Prerequisites
No Thunderbird internals or retrieval knowledge required
21 sections

The most misleading picture of mailbox AI is also the shortest:

inbox → language model → answer

That picture hides nearly every difficult part. An inbox is not one clean document. A language model does not automatically share Thunderbird’s storage. A semantically similar message is not necessarily the message that contains the requested transaction. A citation is not useful unless it maps back to something the user can open. And “All Mail” must not quietly become “copy the entire mailbox into a prompt.”

ThunderbirdAI was the project in which I had to make those distinctions concrete. It is an experimental source customization of Thunderbird that adds local mail analysis, retrieval, an endpoint-backed Assistant, privacy policy, security enrichment, and debugging surfaces. It is an independent project, not an official Mozilla or Thunderbird release, and it should be tested with a disposable or backed-up mail profile.

The source is public in the ThunderbirdAI repository. This article follows the implementation exported at 1b6cfc5, which records the locally compiled state from 6 September 2026. Examples below are fictional; no personal mailbox data is used.

The central lesson is this:

The model is neither the mailbox nor the source of truth. Thunderbird owns the mail, policy, scope, retrieval, evidence, persistence, and citations. A model is an optional, bounded worker inside that larger pipeline.

1. Start with one question and one source message

Suppose this message exists in a banking folder:

Subject: Debit Card transaction of INR 805 at Book Nook
From: Aster Bank Alerts <alerts@asterbank.test>
Date: 24 Aug 2026

INR 805.00 was debited from account ending 1842 at Book Nook.
Reference: TXN-HC-100005

Later, the user asks:

Find transaction TXN-HC-100005. Give me its amount and source message.

The desired answer looks small: INR 805.00 plus a link to the email. Producing it safely requires a chain of different jobs:

  1. Read and interpret the message correctly.
  2. Preserve where the readable body came from.
  3. Extract useful fields without turning derived metadata into source truth.
  4. Decide what text may remain local or cross an endpoint boundary.
  5. Persist a canonical analysis record.
  6. Build searchable child passages with parent-message identity.
  7. freeze the user’s selected scope.
  8. recognize the transaction ID as an exact constraint.
  9. retrieve a bounded evidence card.
  10. render it locally or let a configured model synthesize an answer.
  11. map the citation back to the source message.

Each step has a different correctness condition. MIME parsing can fail while retrieval is perfect. Retrieval can succeed while synthesis invents a number. The final prose can be correct while its citation points to the wrong message. Calling the whole chain “the AI” makes those failures harder to locate.

2. The system at a glance

The implementation has two main paths: an ingestion path that turns mail into local, inspectable records, and a question path that selects evidence from those records.

flowchart LR
  M[Thunderbird mail store] --> Q[Bounded analysis queue]
  Q --> P[Canonical body + parser sidecar]
  P --> L[Local analysis and policy]
  L --> C[(Canonical AI records)]
  C --> R[(Rebuildable RAG index)]

  U[Question + frozen scope] --> X[Query planner]
  X --> R
  X --> C
  R --> E[Bounded evidence cards]
  C --> E
  E --> D{Direct RAG?}
  D -->|on| A[Local evidence rendering]
  D -->|off| O[Configured Assistant endpoint]
  O --> V[Citation URI mapping]
  V --> A

This shape prevents several category errors:

  • The endpoint does not crawl Thunderbird folders.
  • The RAG index is not the canonical mailbox or the canonical analysis store.
  • A classifier label can help route a message but cannot prove a fact in its body.
  • Retrieval happens inside a frozen mailbox scope before evidence reaches synthesis.
  • Direct RAG can answer exact lookups without a language-model request.

The rest of the article walks through both paths.

3. Intake begins with Thunderbird, not the model

An analysis request can originate from new mail, a folder or message change, a background backfill, a forced reanalysis, or a targeted refresh of one artifact such as summaries or embeddings.

Those requests enter a bounded queue. They are deduplicated by stable message identity, so the same message does not need parallel copies of the same job. An explicit user request can promote work that was already waiting as background work. Background work may be paused without making a force-reanalyse command meaningless.

Workers consume bounded batches:

queue
  → worker batch
  → per-message analysis
  → changed-row checkpoint
  → observer and UI update
  → next batch

This scheduling layer is not glamorous, but it determines whether a mail client stays usable while thousands of existing messages are analysed. Unlimited parallelism would fight for CPU, memory, storage, and any local model endpoint. A one-message-at-a-time design would make a large backfill unnecessarily slow. ThunderbirdAI exposes an effective worker limit and uses bounded checkpoints instead of treating concurrency as an accident of promises or threads.

4. One message passes through ordered stages

For a full analysis, the common path is:

flowchart TD
  A[Read rendered message] --> B[Read parser and trust sidecar]
  B --> C[Local category, tags, baseline text]
  C --> D[Template match or mining]
  D --> E[Optional locally trained category]
  E --> F[Deterministic field extraction]
  F --> G[Security assessment]
  G --> H[PII decision and policy-safe text]
  H --> I[Embedding or deterministic fallback]
  I --> J[Summary with deterministic fallback]
  J --> K[Persist canonical record]
  K --> L[Index contextual child passages]

Order matters. PII policy must exist before optional endpoint work. Extraction must keep its route back to source text. The index should be generated from a committed canonical record, not from half-finished intermediate state.

The parser sidecar preserves how text became text

Thunderbird reads its locally rendered message and retains metadata about the body-selection decision: MIME and header flags, normalized headers, attachment manifests, the selected canonical body, and parser or trust evidence where available.

This is distinct from a cleaned model string. An HTML alternative, a plain-text alternative, quoted history, an attachment, and a nested forwarded message do not have identical roles. If the system loses those distinctions at intake, a better embedding model cannot restore them later.

The companion article Email Before AI develops MIME, canonical text, multilingual input, Unicode offsets, and PII handling from first principles. Here the important implementation point is that the canonical source remains locally traceable even when a shorter or redacted representation is used downstream.

Local classification is routing, not evidence

ThunderbirdAI first applies deterministic category and tag rules. A user-trained sparse classifier can contribute a category when labelled examples exist and stronger template or rule policy does not already own the case.

For the fictional Book Nook message, words such as transaction, debited, and bank are useful finance signals. The resulting category can improve search and UI organization. It cannot prove that the amount is INR 805. The source sentence or a source-linked extracted field must do that.

That difference can disappear in a product UI. “Finance, high confidence” describes a routing decision. It is not a factual confidence score for every number in the email.

The classification article builds that distinction from rules through local models.

Extraction creates candidates with provenance

The deterministic extractor recognizes bounded field shapes such as amounts, dates, transaction IDs, invoice numbers, tracking numbers, references, links, and statuses.

For the example message it can produce:

FieldValueAuthority
AmountINR 805.00Exact source span
Transaction IDTXN-HC-100005Exact source span
Date24 Aug 2026Header or body source
StatusdebitedExact source span
CategoryfinanceDerived routing metadata

An absent extracted field does not prove that the field is absent from the mail. It means this extractor did not recognize it under its current rules. The bounded source passage can still be retrieved and inspected.

Repeated machine-generated mail may also join a template family. Reviewed templates can provide stable slot boundaries, categories, or summaries. Template output remains derived state with a route to its parent messages. The article From Repeated Email to Structured Knowledge explains the mining and provenance side in detail.

5. Privacy is an execution decision, not a slogan

“Local AI” can mean several different things. Rules and SQLite running inside Thunderbird are local. Ollama listening on 127.0.0.1 is also local to the machine, but it is still a separate process receiving a request. A public OpenAI-compatible endpoint crosses a different trust boundary again.

ThunderbirdAI therefore computes a policy decision before endpoint work. The record may contain separate representations:

localRawText    exact locally available analysis text
redactedText    text after policy-driven replacements
externalSafeText
                the bounded representation permitted for the chosen endpoint

Deterministic PII rules recognize forms such as email addresses, phone numbers, UPI IDs, PAN-like tokens, Aadhaar-like numbers, API keys, JWTs, and payment-card candidates. Shape matching alone is not enough for every type; card candidates, for example, receive a Luhn check. Policy can allow, redact, or block endpoint text according to sensitivity and destination.

The key boundary is not “did we use an LLM?” It is:

Which exact representation left Thunderbird, for which operation, under which permission?

Metadata-only traces should not preserve raw bodies, bearer tokens, API keys, or unredacted evidence. A loopback endpoint reduces network exposure, but it does not erase the boundary between the mail client and the model server.

6. What remains local and what may use an endpoint

Normal analysis does not require Ollama. The following operations have local paths:

  • reading and canonicalizing messages;
  • category rules, tags, and the sparse personal classifier;
  • template and regular-expression extraction;
  • deterministic security and PII policy;
  • SQLite persistence and recovery;
  • FTS5 lexical search and deterministic passage vectors;
  • exact identifier, amount, and date filtering;
  • mailbox-scope enforcement, evidence budgets, and citation mapping;
  • deterministic summaries and Direct RAG rendering.

Endpoint work is conditional:

OperationEndpoint requirement
Normal deterministic message analysisNone
Per-message generated summaryOptional and policy-gated
Learned endpoint embeddingOptional and policy-gated
Direct RAG answerNone
Natural-language Assistant synthesisConfigured usable source
Exhaustive scope digestExplicit workflow plus private or loopback source

If an optional background summary or embedding call fails, the pipeline can retain its local deterministic fallback. An Assistant synthesis failure must not be presented as though a model answered successfully. The UI and trace need to distinguish endpoint output, direct evidence, fallback, and unavailable states.

This separation also keeps model choice from infecting every layer. Replacing an answer model does not change which Thunderbird folder the user selected. Replacing an embedder should not change the meaning of a transaction ID. Model upgrades remain bounded to the roles they actually perform.

7. Two SQLite stores have different authority

ThunderbirdAI keeps two profile-local databases:

StoreResponsibilityRebuildable?
ai/mail-intelligence.sqliteCanonical AI records, jobs, digest maps, provider state, tracesNot from the RAG index alone
ai/rag-index.sqliteFTS5 passages, vectors, contextual chunks, index-generation stateYes, from canonical records

This distinction turns cache invalidation into an explicit contract. When a canonical record changes, the affected RAG rows become dirty. A small change can replace a few records and chunks. A schema mismatch, reset, or missed generation can request a lazy rebuild. An interrupted rebuild does not claim to be current until its covered generation commits.

Canonical storage uses changed-row transactions and SQLite write-ahead logging. Bulk analysis suppresses a save after every message and checkpoints after bounded counts, elapsed time, errors, or session completion. This replaced the much less attractive shape of repeatedly serializing one large JSON document while a mailbox was changing.

The architectural lesson is broader than SQLite:

Name the canonical state, name the derived state, and make rebuildability directional.

If both databases appear equally authoritative, recovery becomes guesswork.

8. Passage indexing keeps the child connected to the parent

One vector per email is too coarse for long messages. A decision near the end of a twelve thousand character retrospective can be diluted by earlier discussion and boilerplate. ThunderbirdAI creates bounded contextual child passages instead.

Each child contains searchable context such as subject, sender, date, category, and selected entities, plus the actual passage. It also retains parent identity, body offsets, source field, schema version, input hash, embedding metadata, and redaction policy.

message L-88
  ├── chunk 0: parent context + early body passage
  ├── chunk 1: parent context + overlapping middle passage
  └── chunk 2: parent context + final decision passage

The child is useful for ranking; the parent remains useful for opening and citing. Search text and evidence text can differ in presentation without pretending they have different sources.

The index combines multiple local signals: FTS5 lexical matching, vector similarity, sender or domain matches, structured entities, templates, and bounded thread expansion. Reciprocal rank fusion can combine candidate lists, followed by an optional bounded reranker. None of those stages may widen the user’s mailbox scope.

The detailed mechanics—BM25, vectors, fusion, reranking, context packing, GraphRAG, and retrieval evaluation—are developed in RAG From Raw Email to Grounded Answers.

9. Freeze scope before ranking

The Assistant supports scopes such as selected messages, the current folder, an account, or All Mail. Scope is not a helpful hint to the model. It is a data-access boundary enforced before candidate ranking.

If the user selects five office messages and asks for banking transaction TXN-HC-100005, the correct pipeline is:

five selected message identities
  → zero in-scope transaction matches
  → no banking evidence card
  → not found or insufficient evidence

It must not silently search All Mail because that would produce a more satisfying answer. Relevance cannot grant access. Reranking cannot grant access. A tool request emitted by a model cannot grant access. The user may choose a wider scope and submit a new turn; the current turn remains bound to the scope with which it began.

This is one of the most reusable lessons from the project. Authorization belongs outside the probabilistic layer. The best-ranked forbidden record is still forbidden.

10. Exact constraints beat semantic plausibility

Now put the Assistant in the banking folder and ask for TXN-HC-100005. Another Book Nook message exists for INR 9,421. It is a strong semantic neighbour: same merchant, same bank, same kind of event. It is also the wrong answer.

The query planner recognizes transaction IDs, references, amounts, and dates as possible hard constraints. Exact and structured lookup runs before a semantic candidate can become authoritative.

flowchart LR
  Q[Question with TXN-HC-100005] --> C[Parse exact constraint]
  C --> S[Search inside frozen scope]
  S --> V[Verify identifier in record or passage]
  V --> L[Lock matching evidence]
  L --> B[Build one bounded evidence card]

For the example, the evidence card may contain the message identity, subject, relevant body passage, extracted amount and reference, and a source URI. The INR 9,421 neighbour can no longer win because it does not satisfy the requested identifier.

Embeddings are maps of similarity, not proof of identity. Exact identifiers, money, dates, ticket numbers, and order numbers deserve a different retrieval lane from open-ended topical questions.

11. Direct RAG and synthesized answers share evidence, not behavior

After retrieval, ThunderbirdAI can take two routes.

With Direct RAG on:

scope → local retrieval → evidence cards → deterministic rendering

No answer model is called. This is useful for exact lookups, auditing, and diagnosing whether retrieval itself works. The output may be less conversational, but endpoint synthesis cannot alter it.

With Direct RAG off:

scope → local retrieval → bounded evidence cards
      → policy-safe endpoint request → citation URI mapping → rendered answer

The endpoint sees a small evidence pack, not a Thunderbird profile or an open mailbox handle. Email bodies remain untrusted data even if they contain imperative text such as “ignore prior instructions.” The model is asked to answer from supplied evidence and acknowledge absent facts.

Citation mapping checks whether emitted source identifiers correspond to retrieved records. That is valuable, but it is not an independent fact verifier. A mapped citation can still fail to support a particular sentence. The current pipeline does not hide a second model judge or claim that every answer has passed a repair loop. Debug output should say what happened rather than upgrading citation plumbing into a stronger guarantee.

12. “All Mail” names two very different workloads

For a normal question, All Mail expands the local candidate scope. It does not expand the endpoint prompt to the entire mailbox.

6,000 eligible AI records
  → local exact, lexical, vector, sender, and entity retrieval
  → perhaps eight final evidence records
  → one bounded synthesis request

An interface may report “8 retrieved / 6,000 examined.” That means the local retrieval system considered six thousand eligible records and selected eight. It does not mean Ollama read six thousand raw email bodies.

An exhaustive digest is different. If the user explicitly asks to summarize an entire folder, account, or mailbox, the system freezes that scope, reads each available body, applies endpoint policy, splits long messages, packs bounded requests, and runs a resumable map/reduce workflow.

flowchart TD
  A[Freeze scope headers] --> B[Read available bodies]
  B --> C[Redact and split bounded chunks]
  C --> D[Map one structured node per source]
  D --> E[Reduce long-message nodes]
  E --> F[Reduce thread nodes]
  F --> G[Reduce scope digest]
  G --> H[Validate IDs and coverage]
  H --> I[Persist maps, job, digest, and citations]

The digest records coverage: total, read, summarized, cached, skipped, unavailable, failed, and truncated messages. If ten bodies could not be processed, the result must say so instead of claiming complete mailbox coverage.

Per-message maps are checkpointed and reusable. Pause and resume need not repeat finished work. A changed source body, model, or endpoint invalidates the relevant cached result. This workflow requires an explicitly configured private or loopback source because it deliberately processes a much wider body set than normal RAG.

Conflating normal All Mail search with an exhaustive digest causes both privacy confusion and bad performance estimates. They share a scope label, not an execution plan.

13. Follow the fictional transaction to its answer

We can now replay the small question from the beginning.

StageState
IntakeThunderbird reads the Book Nook message and parser sidecar
Local analysisFinance routing, deterministic summary, security and PII state
ExtractionINR 805.00 and TXN-HC-100005 retain source provenance
PersistenceCanonical record commits to mail-intelligence.sqlite
IndexingContextual passage enters FTS5 and vector storage
ScopeHuman-Banking folder identities are frozen for the turn
PlanningTXN-HC-100005 becomes an exact identifier constraint
RetrievalThe matching message is verified and locked
EvidenceOne bounded card contains the relevant passage and source URI
AnswerDirect local rendering or endpoint synthesis returns INR 805.00
CitationOpen source resolves to the original parent message

Now change one condition at a time:

  • If the banking message is outside a Selected Messages scope, it is not retrieved.
  • If the RAG index is stale, the canonical store can drive a rebuild.
  • If the optional summary endpoint fails, local analysis can still complete.
  • If Direct RAG is enabled, answer synthesis does not contact the endpoint.
  • If the exact identifier is absent, the result should preserve that uncertainty.
  • If a citation ID does not map to retrieved evidence, it must not become an openable source.

This is why end-to-end tests are more informative than asking whether “the model works.” The same final symptom can originate in parsing, policy, persistence, indexing, scope, planning, ranking, packing, synthesis, or citation mapping.

14. Debug the route, not the prose

A useful debug surface exposes the execution route and its boundaries. Relevant fields include:

route             exact lookup, direct RAG, synthesized RAG, mailbox digest
scope             selected messages, folder, account, or all mail
candidateSources  exact, lexical, vector, sender, entity, template, thread
contextBudget     included and excluded records, bytes, tokens, truncation
policy            destination, redaction decision, endpoint attempt
citations         parent messages and passage identities
storage           generations, dirty records, checkpoints, rebuild state

Suppose an answer omits the transaction. Start from the earliest failed invariant:

  1. Was the message readable and canonically analysed?
  2. Did its record commit?
  3. Is the derived index current?
  4. Was the record inside the frozen scope?
  5. Did exact parsing recognize the identifier?
  6. Was the right passage packed into evidence?
  7. Did the renderer or endpoint preserve the value?
  8. Did the citation map back to the parent message?

Reading the answer repeatedly will not reveal which stage failed. A trace should.

15. What the build and evaluations establish

The public bundle is designed to reproduce a patched Thunderbird tree from pinned Gecko and comm revisions rather than uploading two enormous upstream working trees. It contains patches, overlays for new files, deletion manifests, hashes, a tested mozconfig, and scripts that verify and apply the export.

The exported state reports a successful Thunderbird build on 6 September 2026 with Python 3.12 and zero compiler warnings. Its GitHub bundle-validation workflow also completed successfully. Those facts establish that the exported bundle verifies and that the reconstructed source compiled in the recorded environment. They do not establish production security, perfect mail compatibility, or model quality for every inbox.

The repository keeps the evidence boundaries visible:

An experiment is more trustworthy when its negative space is documented. “Built successfully” is a build claim. “Retrieved the right message in this frozen corpus” is an evaluation claim. Neither should quietly become “safe and correct for all mail.”

16. What I would reuse in another local-AI application

The Thunderbird-specific code is large, but the architecture reduces to a compact set of reusable rules:

  1. Keep source, canonical derived state, and rebuildable indexes distinct. Recovery becomes directional instead of speculative.
  2. Make scope an authorization boundary before ranking. A relevance score must never grant access.
  3. Route exact facts differently from semantic questions. Identifiers and amounts should not compete only through vector similarity.
  4. Create endpoint-safe representations deliberately. “Local model” does not remove the need to name process and destination boundaries.
  5. Bound every expanding operation. Queues, workers, chunks, candidates, prompts, retries, traces, and caches all need limits.
  6. Treat citations as mappings, not automatic verification. Preserve what they prove and what they do not.
  7. Report coverage and fallback honestly. Partial work is useful when it remains visibly partial.
  8. Debug stage transitions. Final prose is the last artifact, not the complete execution record.

These rules do not require a language model. That is partly the point. The reliability of a local-AI product depends heavily on ordinary parsing, storage, policy, scheduling, and user interface code around its models.

17. The compact mental model

ThunderbirdAI first makes a message locally searchable and auditable. It stores canonical AI records separately from its disposable retrieval index. When a question arrives, Thunderbird freezes the user’s scope, plans exact and semantic retrieval inside that boundary, and packs a small evidence set.

Direct RAG renders that evidence locally. Synthesized RAG sends only permitted, bounded evidence to the configured endpoint. An exhaustive digest is a separate, explicit and checkpointed workflow whose coverage is reported. In every route, the parent email remains the source that the user can inspect.

The result is less mysterious than “an AI that knows your inbox,” and that is a feature. A mailbox assistant should be explainable as a sequence of bounded transformations:

mail
  → canonical local record
  → rebuildable evidence index
  → frozen scope
  → exact and semantic retrieval
  → bounded evidence
  → local rendering or optional synthesis
  → source-linked answer

Once those boundaries are explicit, model improvements become upgrades to particular stages rather than excuses to blur the whole system.

Continue the ThunderbirdAI series

Diagram