MailCat / Technical anatomy

A live mail system, with an adaptive agent above it.

MailCat joins two layers without pretending they are equally complete. The AppForCat inbox and Resend delivery paths are live today. The folderless agent—retrieval, drafting, curation, and personal learning—is the working product architecture being built above that real substrate.

Mail substrate● Live end to end
Agent layer◆ Phase 1 prototype
InterfaceAsk / answer / correct
Control contractAI proposes / human disposes
00 / Map

Live infrastructure below. Adaptive intelligence above.

Cloudflare receives mail, the local service exposes a locked inbox, and Resend delivers outbound. A provider-neutral agent layer will turn that mailbox into something the user asks rather than organizes.

Inbound / receive into the private inbox

Outside senderAny ordinary email service
Managed edgeCloudflare Email Routing
Edge logicEmail Worker
Private pathCloudflare Tunnel
Owned computePython intake on the PC
Read mailLocked role inbox
Owned or controlled locallyExternal trust boundary

Outbound / deliver product email

AppForCat productCompose invite or notification
Scoped credentialSending-only API key
Managed deliveryResend API + queue
Domain proofDKIM + aligned Return-Path
DestinationRecipient mail server
App-controlled compositionExternal trust boundary

Agent layer / working concept

Live sourceMailCat inbox
NormalizationCanonical message model
UnderstandingPeople + thread + intent graph
RetrievalStructured + semantic + rerank
Personal learningWeighted user memory
InteractionFocused answer, draft, or proposal
Live mailbox boundaryConcept component

Trust boundary: Cloudflare processes inbound messages, and Resend processes outbound messages. TLS protects the hops; MailCat is not end-to-end encrypted against those providers.

Product boundary: MailCat learns forward from its live mailbox. Bulk-copying years of another account for archival introspection is a separate project, not a prerequisite or the product thesis. A historical source could become an adapter later without changing the core agent.

01 / Inbound

From public MX to a raw file at home.

The inbound path avoids a residential SMTP server and any open inbound port while retaining a complete local original.

Public edge

Standards-facing work is handled before the message reaches the PC.

  1. DNS advertises Cloudflare MXThe domain publishes three Email Routing MX records. Any normal sender delivers by SMTP.
  2. An explicit address rule matchesSelected roles—currently consultant, info, james, and support—route to the intake Worker. Unknown local parts are dropped.
  3. Send-only addresses are refusedThe Worker rejects noreply and no-reply with a clear SMTP failure instead of silently accepting replies.
  4. The Worker reads the raw messageThe incoming RFC 822 stream is copied into an ArrayBuffer. No reformatting occurs at this stage.

Private intake

A tunnel carries the exact message to the local service.

  1. Authenticated HTTPS POSTThe Worker posts message/rfc822 to /intake with sender, recipient, and a shared-secret header.
  2. No router port is openedA named Cloudflare Tunnel forwards the public hostname to host port 8795. The PC never accepts public SMTP or requires a public IP.
  3. The raw original is written firstThe Python service writes a uniquely named .eml into Maildir/new.
  4. A disposable index is addedHeaders, a text snippet, attachment count, size, path, and seen state enter SQLite for a fast viewer.
02 / Outbound

Compose locally; delegate deliverability.

Receiving into a private inbox and earning reliable outbound delivery are different problems. MailCat gives each path the infrastructure it needs.

Application side

The sending application chooses the recipient, subject, body, and AppForCat sender identity.

  1. A local app creates the messageInvites and notifications are ordinary HTML/text email, typically from [email protected].
  2. A sending-only key authorizes deliveryThe operational key can send mail but cannot manage the Resend account or domain configuration.
  3. A normal User-Agent is suppliedThe provider edge rejects bare Python urllib traffic, so the client declares an application User-Agent.

Delivery side

Resend handles queueing, reputation, remote SMTP negotiation, and feedback.

  1. DKIM authenticates the domainA dedicated domain key signs mail as AppForCat rather than as a generic relay identity.
  2. A branded Return-Path handles bouncesThe send subdomain has its own MX and SPF records for aligned delivery feedback.
  3. DMARC observes alignmentThe current policy is monitoring-only (p=none), ready to tighten after more operational confidence.
03 / Inventory

One purpose per component.

No framework is required in the local mail path. The intake and viewer use the Python standard library.

ComponentResponsibilityReplaceability
Domain + DNSStable identity, MX discovery, SPF, DKIM, DMARC, bounce routing.The domain is the durable layer; DNS providers can change.
Cloudflare Email RoutingInternet-facing SMTP, address matching, initial mail handling.Replaceable with another inbound SMTP/forwarding edge.
Email WorkerReject send-only roles, preserve the raw stream, authenticate the intake POST, choose optional fallback behavior.Small JavaScript module with no storage state.
Cloudflare TunnelOutbound-established route from the PC to the public intake hostname.Replaceable with another authenticated reverse tunnel or private relay.
Python intakeValidate secret and size, parse metadata, write EML, update SQLite, render a minimal inbox.Single stdlib service; storage formats remain usable without it.
MaildirOne-file-per-message RFC 822 originals for the live private inbox under new/.Readable by standard mail and mailbox tooling.
SQLiteFast inbox listing, snippets, attachment counts, and seen state.Secondary index; the EML files are the primary record.
ResendOutbound API, reputation, queueing, DKIM delivery, bounce handling.Replaceable without changing inbound storage or user addresses.
Docker ComposeRuntime packaging, persistent bind mount, restart policy, health checks.The service also has a host-process fallback.
Windows Credential ManagerSource of intake, viewer, and provider credentials.Deployment materializes only the values the process needs.
04 / Storage

The original and the index are different things.

If the interface disappears, the message should not disappear with it.

Raw RFC 822 is the durable record

Each accepted message is saved byte-for-byte as an .eml file. Headers, MIME structure, HTML, plain text, and attachment payloads remain in the original even when the current viewer only renders simplified text.

SQLite is a convenience layer

The viewer does not scan and parse the full mailbox on every page load. SQLite stores the fields needed to order and preview up to 200 recent messages. A future repair tool can rebuild this index from EML originals.

data/
├── Maildir/
│   ├── new/   ← accepted .eml originals
│   ├── cur/   ← reserved for Maildir evolution
│   └── tmp/   ← reserved for atomic delivery
└── mail.sqlite3  ← searchable metadata + seen state
SQLite fieldMeaning
idLocal integer identity used by the viewer.
received_atLocal intake timestamp, indexed descending.
dateParsed sender-provided Date header when valid.
message_idOriginal Message-ID header; currently stored, not deduplicated.
mail_from / mail_toWorker-supplied envelope hints, falling back to message headers.
subjectDecoded subject used in the inbox list.
snippetWhitespace-normalized first 280 characters of preferred text.
attachmentsCount of MIME parts with filenames.
sizeRaw message byte length.
eml_pathFilesystem pointer to the durable original.
seenViewer state; opening a message sets it to one.
05 / Agent layer

No folders is an architectural constraint, not a cosmetic choice.

The agent must turn natural language into precise retrieval, expose the evidence behind its answer, and preserve human authority over every consequential action. Phase 1—the Recall spine—is implemented and test-covered; Compose, Tend, the live-mail adapter, and the user interface remain in development.

A provider-neutral message seam

The intelligence cannot be allowed to know whether a message came from Cloudflare, Gmail, IMAP, or a generated development corpus.

  1. A source adapter normalizes mailProvider-specific IDs, folders, and flags stop at the adapter boundary.
  2. The canonical model makes meaning explicitMessages carry people, timestamps, Message-ID relationships, attachments, source references, and derived metadata in one contract.
  3. People and threads become graphsAddresses and aliases resolve into people; Message-ID, In-Reply-To, References, and subject fallback reconstruct conversations.
  4. The live MailCat adapter is the next integrationThe existing synthetic adapter proves the seam without requiring a bulk historical import.

An ask becomes a retrieval plan

“Find the conversation with Jane last week” is not one vector search.

  1. Resolve language into structure“Jane” becomes a person candidate; “last week” becomes a concrete timezone-aware range; “conversation” requests a thread.
  2. Use the right retrieval axisWho, when, and which thread are structured queries. What something was about can add semantic retrieval.
  3. Fuse and rerank when meaning is fuzzyMetadata, full text, and embeddings can fan out, merge, and rerank before the model synthesizes an answer.
  4. Return evidence, not an oracle voiceThe result includes the underlying messages or thread so “truth” stays inspectable and correctable.
Recall / implemented spine

Find the coherent exchange

Resolve the person and time window, reconstruct the thread, and return the conversation in order with a concise gist.

Compose / next hero

Draft with context and voice

Retrieve the active thread plus relevant facts from other mail, then create a draft. A human remains the final author and sender.

Tend / next hero

Learn what not to surface

Propose a generalized noise rule, show what it would affect, apply only with consent, and retain a reversible muted lane.

Interface / product rule

Conversation replaces taxonomy

No label editor or folder ritual sits above the engine. Ask, inspect, correct, and act through one conversational surface.

Agent componentCurrent stateTechnical role
Canonical domain modelImplementedProvider-neutral messages, people, threads, drafts, queries, and learned-binding types.
Source-adapter contractImplementedConformance-tested seam; deterministic synthetic corpus adapter exists, live MailCat adapter is next.
Ingest + graph spineImplementedNormalization, person resolution, thread reconstruction, embedding seam, and account-scoped repositories.
EmbeddingsLocal client implementedDeterministic test provider plus a 768-dimensional nomic-embed-text client for local Ollama.
Text modelPluggable client implementedDeterministic test client plus an OpenAI-compatible client configured for DeepInfra by default; a local text-model adapter is not wired yet.
RecallServicePhase 1 greenRule-based query planning, ambiguity handling, participant/time retrieval, ordered thread views, optional gist.
PersistenceMemory + Postgres adaptersShared conformance suite proves repository substitution; optional Postgres checks require a configured database.
ResolutionMemoryInterface definedPhrase-to-person and later preference bindings; durable personalized backend is not wired yet.
Compose + Tend servicesDesigned / not builtContextual drafting, voice memory, consented noise generalization, reversible suppression.
Conversational UINot builtOne place to ask, see evidence, disambiguate, edit drafts, consent, and correct the agent.

Verification snapshot: the agent repository currently passes 158 automated tests. Nineteen optional Postgres conformance cases skip when no test database is configured; in-memory conformance and the end-to-end Recall hero remain green.

06 / Learning

The model stays replaceable. The relationship lives in memory.

Per-user adaptation should not require fine-tuning a private model for every mailbox. Learning is a small, inspectable memory of weighted choices that a stateless model consults.

01 / Propose

Offer a result or rule

A thread, a draft, a resolved person, or a suggested class of noise.

02 / Dispose

The user chooses or corrects

Pick the right Jane, reject the rule, edit the wording, or approve the action.

03 / Record

Save a weighted binding

Store the correction with account scope, evidence, confidence, and reversible history.

04 / Reuse

Resolve confidently next time

The planner consults memory before asking again; later corrections reweight rather than erase history.

Personalization is explicit data

Bindings such as “Jane” → a specific person are only the first instance. The same seam can hold noise preferences, query vocabulary, recurring priorities, and voice guidance. Because it is data—not hidden model weights—it can be inspected, corrected, exported, expired, and scoped per account.

Inference is tiered

Cheap local work can happen for every arriving message: normalization, local Ollama embeddings, and light classification. Deeper synthesis, drafting, and memory updates happen only when the user engages. The text-model client remains behind an interface; the current concrete client is OpenAI-compatible and defaults to DeepInfra, while local text inference remains a swappable future adapter.

Action boundary: Recall may show. Compose may draft. Tend may suggest. Nothing sends, deletes, or generalizes into a lasting rule until the user explicitly disposes of the proposal.

07 / Security

Small gates, explicit boundaries.

The live substrate minimizes exposed surfaces. The agent adds a second security problem: controlling what it may retrieve, remember, generalize, and act upon.

Ingress

No public SMTP and no router port

The only connection into the PC is through an outbound-established tunnel. Residential IP reputation and port 25 are irrelevant to inbound delivery.

Intake auth

Shared-secret request gate

/intake rejects requests unless X-Mail-Secret exactly matches the injected credential. The secret is also stored as an encrypted Worker secret at the edge.

Viewer auth

Token-gated private inbox

A successful one-time login sets a year-long HttpOnly, SameSite=Lax cookie. Inbox and message routes otherwise return 401.

Size bounds

Messages have a hard ceiling

The intake rejects missing, empty, or over-30 MB bodies. The managed email edge has a slightly lower practical message limit.

Secrets

The credential vault is the source

Startup reads Windows Credential Manager and writes a gitignored Compose environment file. Secrets are not committed, printed into pages, or exposed through health checks.

Public health

Minimal but not silent

/health exposes only an OK flag and inbox message count. This helps monitoring but leaks a small activity metric; removing the count is a reasonable hardening step.

Agent policy

Read freely; act deliberately

The product contract permits retrieval and proposals, but sending, deletion, and lasting generalization require an explicit user decision and an auditable action record.

Memory scope

Learning belongs to one account

Every learned binding and preference must be account-scoped, inspectable, reversible, and excluded from another user's prompts or retrieval context.

08 / Operations

Designed to come back after a reboot.

Durability includes the boring parts: process supervision, persistent mounts, and observable health.

ConditionCurrent behaviorOperator response / future improvement
Container exits or PC rebootsDocker uses restart: unless-stopped; the bind-mounted inbox data survives replacement.Health check probes local /health every 30 seconds. Inspect container logs if restart loops.
Tunnel or PC is unreachableThe Worker throws a temporary delivery error unless an optional verified fallback address is configured.Enable fallback for continuity or add a durable edge queue before broader use.
Wrong or missing intake secretThe service returns 401 and stores nothing; the Worker treats non-2xx as failure.Reconcile the Worker secret and locally injected credential.
Message exceeds the limitThe local service returns 413; the managed edge may reject it first.Keep the documented cap or add object storage for large payloads.
Disk or SQLite write failsThe current code returns HTTP 200 with ok:false to avoid a Worker retry storm.This is a prototype tradeoff with loss risk. Replace it with an idempotent queue/dead-letter design.
Resend is unavailableOutbound API calls fail; inbound receipt and local browsing are unaffected.Retry from the calling app or swap the outbound relay.
SQLite is lostRaw EML originals remain on disk, but the current UI loses its index.Add and routinely test an EML-to-SQLite rebuild command.
09 / Decisions

Why this shape instead of a traditional mail server and client?

Managed SMTP

Deliverability is specialist infrastructure

Inbound abuse handling and outbound reputation change constantly. Delegating them is more reliable than pretending a home IP can do the same job cheaply.

Raw + index

Exact message, fast inbox

EML preserves the received message. SQLite makes the private inbox quick to list and preview. Keeping both avoids reparsing every message for every page.

Standard library

A small dependency surface

The local service uses Python's HTTP, email, SQLite, and filesystem libraries. The built image needs no runtime package installation.

Single service

Enough architecture for the proof

Intake and viewer share one process because the current inbox is small and private. A queue and separate read model become useful only when scale justifies them.

10 / Gaps

The honest path from proof to dependable product.

The mail substrate is live; the agent is a tested Phase 1 spine, not a finished product. These are known limitations, not hidden roadmap footnotes.

Recovery

No proven backup and rebuild routine yet

The bind mount survives containers, but hardware loss is different. Add encrypted versioned backup plus an automated EML index rebuild and restoration drill.

Delivery semantics

No idempotency or dead-letter queue

A storage error can currently produce 200 / ok:false. Production intake needs a stable message key, transactional acknowledgement, retries, and a dead-letter path.

Authentication

One shared viewer token

All delivered roles share one inbox and one viewer credential. Add real identities, per-recipient scopes, session rotation, expiry, and CSRF protection.

Cookie hardening

The viewer cookie lacks an explicit Secure flag

The public route is HTTPS, but production code should still declare Secure, shorten the lifetime, rotate tokens, and invalidate sessions server-side.

At-rest privacy

Raw EML is not encrypted by the app

Local disk controls are the present boundary. Add volume/file encryption and an encrypted backup strategy for stronger inbox-data protection.

Message experience

The viewer is intentionally minimal

HTML is reduced to text, attachment filenames are listed but not downloadable, threads are not grouped, and search is not yet exposed.

Domain policy

DMARC remains monitoring-only

Move from p=none toward quarantine or reject after confirming every legitimate sender aligns; add aggregate reporting.

Privacy surface

Health reveals the inbox count

The endpoint contains no mail content, but the count is unnecessary public metadata. A boolean-only health response would be tighter.

Agent integration

The live MailCat adapter is not wired

Phase 1 runs against the provider-neutral contracts and deterministic corpus. Build the adapter that tails new MailCat messages into the canonical ingest path.

Product surface

The conversational interface is not built

Recall is a service, not yet the polished ask-and-correct experience. The UI must expose evidence, ambiguity, edits, consent, reversibility, and action history.

Personal memory

Learning has a contract, not a durable backend

ResolutionMemory defines the seam, but persistent account-scoped bindings, inspection, correction, expiry, and export still need implementation.

Hero coverage

Compose and Tend remain designs

Draft persistence exists, but contextual composing, voice memory, consented noise rules, and the reversible muted lane are not complete services yet.