[{"data":1,"prerenderedAt":1879},["ShallowReactive",2],{"search":3,"content-developer\u002Fpostbox-architecture":442,"surround-\u002Fdeveloper\u002Fpostbox-architecture":1874},[4,8,12,16,20,24,28,32,36,40,44,48,52,56,60,64,68,72,76,80,84,88,92,96,100,104,108,112,116,120,124,128,132,136,140,144,148,152,156,160,164,168,172,176,180,184,188,192,196,200,204,208,212,216,220,224,228,232,236,240,244,248,252,256,260,264,268,272,276,280,284,288,292,296,300,304,308,312,316,320,323,327,331,335,339,343,347,351,355,359,363,367,371,375,379,383,387,391,395,399,403,407,411,415,419,423,426,430,434,438],{"path":5,"title":6,"description":7},"\u002Fguide","Guide","Product guides for Owlat — a modular, self-hosted email platform. Learn how to send campaigns, run a personal mailbox, manage a team inbox, and more.",{"path":9,"title":10,"description":11},"\u002Fguide\u002Fgetting-started","Welcome to Owlat","Set up your Owlat workspace and send your first email — from deploying the stack to verifying a domain, building your audience, and launching a campaign.",{"path":13,"title":14,"description":15},"\u002Fguide\u002Fcontact-properties","Contact Properties","Custom fields that extend built-in contact data with your own values for segmentation.",{"path":17,"title":18,"description":19},"\u002Fguide\u002Ftopics","Topics","Topics are explicit audience groups you manage by hand — ideal for opt-in subscribers, imported cohorts, and organized contact buckets you target with campaigns.",{"path":21,"title":22,"description":23},"\u002Fguide\u002Fsegments","Segments","Build dynamic, rule-based contact groups from properties, email activity, and topic membership, re-evaluated from current data each time they're used.",{"path":25,"title":26,"description":27},"\u002Fguide\u002Fforms","Forms","Form Endpoints collect new contacts from your website or landing pages by exposing a public endpoint that accepts submissions and feeds them into a topic.",{"path":29,"title":30,"description":31},"\u002Fguide\u002Fcampaigns","Campaigns & Reporting","Build and send marketing campaigns to a topic or segment with the five-step wizard, optional A\u002FB testing, and full delivery reporting.",{"path":33,"title":34,"description":35},"\u002Fguide\u002Fab-testing","A\u002FB Testing","Compare two variants of a campaign on a test group, then automatically or manually send the winning version to the rest of your audience.",{"path":37,"title":38,"description":39},"\u002Fguide\u002Fautomations","Automations","Send emails automatically based on triggers, delays, and conditions — build welcome series, trial flows, and follow-ups once and let Owlat run them.",{"path":41,"title":42,"description":43},"\u002Fguide\u002Ftransactional","Transactional Emails","One-to-one emails your application triggers in response to a user action — password resets, order confirmations, welcome emails, and similar notifications.",{"path":45,"title":46,"description":47},"\u002Fguide\u002Fcreate-campaign","Create a Campaign","Walk through Owlat's five-step campaign wizard: Basics, Audience, Content, A\u002FB Test, and Review & Send.",{"path":49,"title":50,"description":51},"\u002Fguide\u002Fsend-campaign","Send & Monitor a Campaign","How to send your campaign and track its performance with real-time metrics.",{"path":53,"title":54,"description":55},"\u002Fguide\u002Fquick-start","Quick Start","The fastest path from a blank Owlat workspace to a live email campaign, from your first template through sending and reviewing results.",{"path":57,"title":58,"description":59},"\u002Fguide\u002Ftransactional-setup","Transactional Email Setup","Set up and send transactional emails like password resets and order confirmations via the Owlat API and SDKs.",{"path":61,"title":62,"description":63},"\u002Fguide\u002Fdeliverability","Deliverability","Verify sending domains, manage your blocklist, monitor sending reputation, and stay compliant so your emails reach the inbox.",{"path":65,"title":66,"description":67},"\u002Fguide\u002Fapi-keys-webhooks","API Keys & Webhooks","Create API keys for programmatic access and set up outbound webhooks to receive real-time notifications for email and contact events.",{"path":69,"title":70,"description":71},"\u002Fguide\u002Ffeature-flags","Feature flags","Owlat is modular — every feature listed in this guide can be turned on or off. This page is the user-facing overview of how to do it.",{"path":73,"title":74,"description":75},"\u002Fguide\u002Fteam-permissions","Team & Permissions","Use role-based access to control what each member of your organization can do, with Owner, Admin, and Editor roles.",{"path":77,"title":78,"description":79},"\u002Fguide\u002Faudit-logs","Audit Logs","A chronological record of significant actions in your Owlat organization, so you can see who did what and when.",{"path":81,"title":82,"description":83},"\u002Fguide\u002Fshare-links","Share Links","Create temporary preview links to share email designs with stakeholders who don't have dashboard access.",{"path":85,"title":86,"description":87},"\u002Fguide\u002Fpostbox","Postbox — Personal Email","Per-user mailboxes with a webmail interface and native IMAP\u002FSMTP support. Run your own Gmail-equivalent personal mailbox on your Owlat instance.",{"path":89,"title":90,"description":91},"\u002Fguide\u002Fmigrate-from-google","Migrate from Google","Import your full Gmail history into Owlat over IMAP, and let your AI assistant learn from every imported conversation.",{"path":93,"title":94,"description":95},"\u002Fguide\u002Fteam-inbox","Team Inbox","Triage inbound email as a team: read AI-classified threads, approve, edit or reject agent drafts, work the review queue, and manage quarantine.",{"path":97,"title":98,"description":99},"\u002Fguide\u002Femail-editor","Email Editor","A block-based visual editor for building responsive emails that render consistently across desktop, mobile, Outlook, Gmail, and Apple Mail.",{"path":101,"title":102,"description":103},"\u002Fguide\u002Fai-agent","AI Agent & Autonomy","Configure the AI agent that classifies and drafts replies to inbound mail: auto-reply settings, the health dashboard, circuit breakers, autonomy rules, and the knowledge backfill.",{"path":105,"title":106,"description":107},"\u002Fguide\u002Fknowledge-graph","Knowledge Graph","Browse, search, and manage Owlat's typed organizational knowledge — the 7 entry types, source attribution, confidence decay, relations, and how entries are extracted from mail.",{"path":109,"title":110,"description":111},"\u002Fguide\u002Ffiles","Files","Upload, browse, search, tag, and version documents in the file library.",{"path":113,"title":114,"description":115},"\u002Fguide\u002Fchat","Team Chat","Use Owlat's built-in team chat: public and private channels, direct messages, mentions, attachments, and channels linked to an inbox conversation.",{"path":117,"title":118,"description":119},"\u002Fguide\u002Fcode-tasks","Code Tasks","Queue coding-agent tasks, watch them move from queued through review, and run the code-worker sidecar that opens the pull requests.",{"path":121,"title":122,"description":123},"\u002Fguide\u002Faudience-data","Audience Data: Identities, Relationships & Timeline","Unify a contact across email, phone, and messaging channels, merge duplicates, map relationships, and read the cross-channel interaction timeline.",{"path":125,"title":126,"description":127},"\u002Fguide\u002Fimporting-contacts","Importing & Exporting Contacts","Bring contacts into Owlat from a CSV or from Mailchimp and Stripe, export them back out, and run bulk operations on your audience.",{"path":129,"title":130,"description":131},"\u002Fguide\u002Faccount","Your Account & Data","Export your data as JSON or CSV, request account deletion with a 30-day grace period, and use the onboarding checklist and the public preference center.",{"path":133,"title":134,"description":135},"\u002Fguide\u002Fchannels","Communication Channels","Configure SMS, WhatsApp, and generic-webhook channels, monitor channel health, and understand which channels are fully live today.",{"path":137,"title":138,"description":139},"\u002Fguide\u002Fdesktop-app","Desktop App","Install the Owlat desktop app, connect one or more workspaces, switch between them, and use native notifications, tray badges, shortcuts, and deep links.",{"path":141,"title":142,"description":143},"\u002Fguide\u002Femail-templates","Email Templates","Reusable email designs that define the structure, content, and personalization of every campaign and transactional message you send in Owlat.",{"path":145,"title":146,"description":147},"\u002Fguide\u002Fai-assistant","AI Assistant","Owlat's multi-turn, streaming, tool-calling AI assistant — a private chat surface that can search your workspace and draft copy, plus @assistant replies inside team chat.",{"path":149,"title":150,"description":151},"\u002Fguide\u002Fsecurity-scanning","Sending Security & Scanning","Owlat's security scanning: a content check for spam and phishing, an attachment scan for malware, and a Google Safe Browsing URL check. Suspicious content goes to a review queue.",{"path":153,"title":154,"description":155},"\u002Fguide\u002Fsystem-updates","System & Updates","The owner-only System & Updates screen: your current Owlat version, container health, LLM spend, and the in-app one-click updater with history.",{"path":157,"title":158,"description":159},"\u002Fguide\u002Foperating-modes","Operating Modes","The different ways to run Owlat at a company — read external mailboxes over IMAP, send transactional or marketing email through a delivery provider, host your own mail server, or run a team inbox with AI — and the rules that keep each combination coherent.",{"path":161,"title":162,"description":163},"\u002Fguide\u002Fsaved-blocks","Saved Blocks","Create reusable, linked content blocks you can drop into any email — edit one and every email that uses it updates automatically.",{"path":165,"title":166,"description":167},"\u002Fguide\u002Fmedia-library","Media Library","Manage, organize, search, and reuse images and files across your emails from one centralized hub.",{"path":169,"title":170,"description":171},"\u002Fguide\u002Femail-theme","Email Theme","Set your organization's default colors, font, and email width so every new template starts from a consistent baseline.",{"path":173,"title":174,"description":175},"\u002Fguide\u002Ftranslations","Translations","Send one email in multiple languages: add per-language translations to a single template and Owlat picks the right version for each recipient.",{"path":177,"title":178,"description":179},"\u002Fguide\u002Fcontacts","Contacts","How to add, view, organize, and manage contacts in Owlat, including sources, the contact detail tabs, and subscription compliance.",{"path":181,"title":182,"description":183},"\u002Fapi","API Overview","Owlat exposes authenticated API endpoints under your Convex site URL.",{"path":185,"title":186,"description":187},"\u002Fapi\u002Fwebhooks","Webhooks","Owlat supports both outbound customer webhooks and inbound provider webhooks.",{"path":189,"title":190,"description":191},"\u002Fapi\u002Fpublic-endpoints","Public Endpoints","These routes are public-facing and usually accessed from email links or embedded forms.",{"path":193,"title":194,"description":195},"\u002Fapi\u002Fwebhook-payloads","Webhook Payloads","The authoritative wire contract for outbound webhooks: envelope, signature headers, per-event data shapes, and payload versioning.",{"path":197,"title":198,"description":199},"\u002Fapi\u002Finbound-channels","Inbound Channel Webhooks","Provider webhook reference for inbound SMS, WhatsApp, and generic-channel messages, plus the MTA mailbox and credential callbacks.",{"path":201,"title":202,"description":203},"\u002Fapi\u002Fauthentication","Authentication","Secure API access with organization-scoped API keys.",{"path":205,"title":206,"description":207},"\u002Fapi\u002Fsdk","TypeScript SDK","Typed client for the Owlat API, usable from Node.js, Bun, Deno, or any server-side JavaScript runtime.",{"path":209,"title":210,"description":211},"\u002Fapi\u002Fsdk-java","Java SDK","The official `owlat-sdk` package provides a typed client for interacting with the Owlat API from any JVM application. Requires Java 11+.",{"path":213,"title":214,"description":215},"\u002Fapi\u002Fcontacts","Contacts API","Manage contacts for your organization.",{"path":217,"title":218,"description":219},"\u002Fapi\u002Ftopics","Topics API","Manage topic membership through authenticated endpoints.",{"path":221,"title":222,"description":223},"\u002Fapi\u002Fevents","Events API","Send contact events to drive segmentation and automation triggers.",{"path":225,"title":226,"description":227},"\u002Fapi\u002Ftransactional","Transactional API","Send published transactional templates to a recipient.",{"path":229,"title":230,"description":231},"\u002Fapi\u002Fforms","Forms API","Capture subscribers through public form endpoints.",{"path":233,"title":234,"description":235},"\u002Fdeveloper","Developer Guide","Technical architecture, feature-flag model, and provider abstractions used by Owlat.",{"path":237,"title":238,"description":239},"\u002Fdeveloper\u002Fmta-system","MTA System","Owlat's custom Mail Transfer Agent for direct SMTP delivery with intelligent rate limiting, bounce processing, and IP warming.",{"path":241,"title":242,"description":243},"\u002Fdeveloper\u002Ffeature-flags","Feature flags — developer reference","How the Owlat feature flag system works: single source of truth, dependency resolution, docker profile mapping, and how to add a new flag.",{"path":245,"title":246,"description":247},"\u002Fdeveloper\u002Fhow-email-works","How Email Works","A technical deep-dive into how email actually works — from SMTP and DNS to authentication, deliverability, and the differences between marketing and private email.",{"path":249,"title":250,"description":251},"\u002Fdeveloper\u002Femail-security","Email Security","Content scanning, attachment validation, URL reputation checking, and malware detection for outbound emails.",{"path":253,"title":254,"description":255},"\u002Fdeveloper\u002Fpostbox-architecture","Postbox Architecture","How the Postbox personal-mail feature is wired — schema, IMAP server, app-password auth, outbound relay, inbound delivery, and external mailboxes.",{"path":257,"title":258,"description":259},"\u002Fdeveloper\u002Fproviders","Providers","Pluggable provider abstractions for LLM, email sending, notifications, vector stores, and analytics, selected per-deployment so self-hosters can swap implementations without code changes.",{"path":261,"title":262,"description":263},"\u002Fdeveloper\u002Fcampaign-internals","Campaign Internals","How the campaign backend works: two status machines, send pre-flight, the send orchestrator, emailSends records, and the priority workpools.",{"path":265,"title":266,"description":267},"\u002Fdeveloper\u002Faudience-internals","Audience Internals","Backend reference for contact resolution, the double opt-in lifecycle, topic subscription, the conditions registry, and segment evaluation.",{"path":269,"title":270,"description":271},"\u002Fdeveloper\u002Fautomation-internals","Automation Internals","How the automation run engine works: the step walker, the lifecycle state machine, trigger fanout, the three step types, and the resilience cron.",{"path":273,"title":274,"description":275},"\u002Fdeveloper\u002Fdeliverability-infrastructure","Deliverability Infrastructure","The Convex-side deliverability backend: provider routing, health-aware failover, sending reputation with auto-enforcement, IP warming cache, the blocklist, and the content-scan gate.",{"path":277,"title":278,"description":279},"\u002Fdeveloper\u002Farchitecture","Architecture Overview","Owlat follows a modern serverless architecture with real-time capabilities.",{"path":281,"title":282,"description":283},"\u002Fdeveloper\u002Fplatform-operations","Platform Operations","Operator reference for abuse status and the sending gate, the platform-admin roster, content review, org deletion, in-app self-update, dev endpoints, crons, and migrations.",{"path":285,"title":286,"description":287},"\u002Fdeveloper\u002Fscopes","Scopes","What each app and package in the Owlat monorepo is responsible for.",{"path":289,"title":290,"description":291},"\u002Fdeveloper\u002Fself-hosting","Self-Hosting","Deploy Owlat on your own infrastructure with Docker Compose. Complete guide from first boot to production.",{"path":293,"title":294,"description":295},"\u002Fdeveloper\u002Fself-hosting-config","Self-Hosting Configuration","Complete reference for Docker environment variables, Convex backend variables, service topology, and volume persistence.",{"path":297,"title":298,"description":299},"\u002Fdeveloper\u002Fself-hosting-dns-email","DNS & Email Setup","Configure DNS records, DKIM signing, SPF, DMARC, and bounce handling for reliable email delivery.",{"path":301,"title":302,"description":303},"\u002Fdeveloper\u002Fself-hosting-production","Production Deployment","Secure your self-hosted Owlat instance with TLS, firewall rules, backups, and monitoring.",{"path":305,"title":306,"description":307},"\u002Fdeveloper\u002Fself-hosting-maintenance","Maintenance & Updates","Keep your self-hosted Owlat instance up to date, manage backups, scale performance, and troubleshoot common issues.",{"path":309,"title":310,"description":311},"\u002Fdeveloper\u002Fself-hosting-desktop","Desktop Installer","Install Owlat on a bare Linux VPS straight from the desktop app over SSH — no terminal — with a live, animated provisioning timeline.",{"path":313,"title":314,"description":315},"\u002Fdeveloper\u002Fsetup-cli","Setup CLI & Installer","Operator reference for the Owlat self-host tooling: the install.sh one-liner, the owlat-setup CLI, the convex-deploy flow, and admin bootstrap.",{"path":317,"title":318,"description":319},"\u002Fdeveloper\u002Fconvex","Convex Backend","Owlat uses Convex as its serverless backend, providing real-time subscriptions, ACID transactions, and TypeScript-first development.",{"path":321,"title":202,"description":322},"\u002Fdeveloper\u002Fauthentication","Owlat uses BetterAuth with the Convex adapter for authentication and organization (team) management.",{"path":324,"title":325,"description":326},"\u002Fdeveloper\u002Femail-system","Email System","Owlat's email system consists of a visual editor, template management, and multi-provider sending infrastructure.",{"path":328,"title":329,"description":330},"\u002Fdeveloper\u002Femail-renderer","Email Renderer","The @owlat\u002Femail-renderer package converts editor JSON blocks into production-ready HTML emails with cross-client compatibility, CSS inlining, dark mode, and Outlook VML fallbacks.",{"path":332,"title":333,"description":334},"\u002Fdeveloper\u002Fenvironment-variables","Environment Variables","Reference for every environment variable Owlat reads across the Convex backend, web app, MTA, IMAP server, and mail-sync worker.",{"path":336,"title":337,"description":338},"\u002Fdeveloper\u002Fcomponents","Component Library","Reference for the reusable, auto-imported Vue UI components shipped in the packages\u002Fui layer.",{"path":340,"title":341,"description":342},"\u002Fdeveloper\u002Fdecisions","Architectural Decision Records","The architectural decision records for the Owlat project, each capturing the context, the decision, and the trade-offs involved.",{"path":344,"title":345,"description":346},"\u002Fdeveloper\u002Fdecisions\u002F009-model-routing","ADR-009: Task-Based Model Routing","Why Owlat supports per-task LLM model selection instead of using a single model for all pipeline steps.",{"path":348,"title":349,"description":350},"\u002Fdeveloper\u002Fdecisions\u002F010-listing-engine","ADR-010: Listing Engine","Why Owlat replaced four incompatible list-query contracts with one generic listing engine driven by per-entity descriptors.",{"path":352,"title":353,"description":354},"\u002Fdeveloper\u002Fdecisions\u002F001-custom-email-renderer","ADR-001: Custom Email Renderer Over MJML","Why Owlat built a custom table-based HTML email renderer instead of using MJML, gaining full control over VML, dark mode, and per-client rendering.",{"path":356,"title":357,"description":358},"\u002Fdeveloper\u002Fdecisions\u002F002-convex-backend","ADR-002: Convex as Backend","Why Owlat chose Convex over PostgreSQL and Firebase for real-time reactivity, co-located TypeScript logic, and zero-config scaling.",{"path":360,"title":361,"description":362},"\u002Fdeveloper\u002Fdecisions\u002F003-notion-like-builder","ADR-003: Notion-like Email Builder","Why Owlat replaced the traditional 3-panel email editor with a Notion-like single-column canvas for inline WYSIWYG editing.",{"path":364,"title":365,"description":366},"\u002Fdeveloper\u002Fdecisions\u002F004-monorepo-bun-workspaces","ADR-004: Monorepo with Bun Workspaces","Why Owlat uses a monorepo with Bun workspaces and Turborepo for fast installs, atomic cross-package changes, and cached CI.",{"path":368,"title":369,"description":370},"\u002Fdeveloper\u002Fdecisions\u002F005-custom-mta","ADR-005: Custom MTA","Why Owlat built a custom Mail Transfer Agent instead of relying solely on third-party email providers.",{"path":372,"title":373,"description":374},"\u002Fdeveloper\u002Fdecisions\u002F006-self-hosted-convex","ADR-006: Self-Hosted Convex","Why Owlat uses the open-source Convex backend for self-hosting instead of migrating to a different database.",{"path":376,"title":377,"description":378},"\u002Fdeveloper\u002Fdecisions\u002F007-pluggable-llm","ADR-007: Pluggable LLM Provider","Why Owlat uses the Vercel AI SDK with a provider abstraction layer instead of hardcoding a single LLM vendor.",{"path":380,"title":381,"description":382},"\u002Fdeveloper\u002Fdecisions\u002F008-process-architecture","ADR-008: Agent Process Architecture","Why Owlat processes inbound messages with a self-scheduling step walker plus a lifecycle coordinator instead of one sequential function.",{"path":384,"title":385,"description":386},"\u002Fexamples","Examples","Copy-pasteable integration patterns for common Owlat use cases.",{"path":388,"title":389,"description":390},"\u002Fexamples\u002Fwelcome-email","Welcome Email","Send a personalized welcome email when a new user signs up.",{"path":392,"title":393,"description":394},"\u002Fexamples\u002Fbilling-email","Billing Email","Send a billing receipt with an invoice PDF attached after a successful payment.",{"path":396,"title":397,"description":398},"\u002Fexamples\u002Fevent-automation","Event Automation","Trigger automations with custom events for trial lifecycle, feature adoption, and more.",{"path":400,"title":401,"description":402},"\u002Fexamples\u002Fcontact-sync","Contact Sync","Sync contacts from your database to Owlat using upsert patterns and bulk operations.",{"path":404,"title":405,"description":406},"\u002Fexamples\u002Fwebhook-handler","Webhook Handler","Handle Owlat delivery webhooks with signature verification and event routing.",{"path":408,"title":409,"description":410},"\u002Fexamples\u002Fmultilingual-email","Multilingual Email","Send emails in the recipient's preferred language using template translations.",{"path":412,"title":413,"description":414},"\u002Fvision","Vision","Where Owlat is heading — from email platform to unified communication intelligence powered by AI agents.",{"path":416,"title":417,"description":418},"\u002Fvision\u002Fself-hosting","Self-Hosting Architecture","How Owlat runs as a fully self-hosted stack using Docker Compose — open-source Convex backend, custom MTA, and a pluggable LLM provider.",{"path":420,"title":421,"description":422},"\u002Fvision\u002Fagent-pipeline","Agent Pipeline","Technical architecture for the inbound email agent pipeline — step modules, the walker, security scanning, threading, and human review.",{"path":424,"title":106,"description":425},"\u002Fvision\u002Fknowledge-graph","Technical architecture for Owlat's typed knowledge storage — how organizational knowledge is stored, searched, decayed, and maintained.",{"path":427,"title":428,"description":429},"\u002Fvision\u002Fmulti-channel","Multi-Channel & CRM","Technical architecture for channel adapters, unified messaging, contact identity unification, and the CRM hub.",{"path":431,"title":432,"description":433},"\u002Fvision\u002Ffile-system","Semantic File System","Technical architecture for Owlat's semantic file storage — version tracking with provenance today, plus the planned embedding-based retrieval and auto-tagging layer.",{"path":435,"title":436,"description":437},"\u002Fvision\u002Fdesktop-app","Desktop App & Advanced Agents","Architecture of the Owlat desktop shell, visualization agent, adaptive dashboard, agent health, graduated autonomy, and coding agents.",{"path":439,"title":440,"description":441},"\u002Fvision\u002Froadmap","Roadmap","What's planned next for Owlat — the documented-but-unbuilt pieces still being wired, and the enhancements on our radar.",{"id":443,"title":254,"body":444,"description":255,"extension":1868,"meta":1869,"navigation":1870,"path":253,"seo":1871,"stem":1872,"__hash__":1873},"content\u002F3.developer\u002F14.postbox-architecture.md",{"type":445,"value":446,"toc":1853},"minimark",[447,456,484,489,499,503,522,528,580,583,680,684,690,1007,1011,1025,1037,1043,1052,1058,1067,1073,1076,1224,1227,1233,1258,1282,1322,1326,1329,1454,1483,1492,1496,1502,1533,1541,1547,1576,1601,1604,1680,1698,1702,1799,1803],[448,449,450,451,455],"p",{},"Postbox is Owlat's personal-mail feature: per-user mailboxes with a webmail UI and native IMAP4rev1 \u002F SMTP submission support. This page covers the implementation. For the user-facing guide see ",[452,453,454],"a",{"href":85},"Postbox in the product guide",".",[457,458,461],"callout",{"title":459,"type":460},"Feature flag","info",[448,462,463,464,468,469,472,473,476,477,480,481,483],{},"Hosted Postbox is gated by the ",[465,466,467],"code",{},"postbox"," flag (off by default). Enabling it activates the ",[465,470,471],{},"personal-mail"," Docker Compose profile, which starts the ",[465,474,475],{},"apps\u002Fimap"," service. The separate ",[465,478,479],{},"mail.external"," flag (covered below) is independent of ",[465,482,467],{}," — it lets a user connect an existing external mailbox without registering a sending domain.",[485,486,488],"h2",{"id":487},"component-map","Component map",[490,491,496],"pre",{"className":492,"code":494,"language":495},[493],"language-text","                        ┌────────────────────────────────────┐\n   ┌──────────────────► │ apps\u002Fapi (Convex)                  │ ◄── webmail UI\n   │                    │  ├── mail\u002Fmailbox \u002F mail\u002Fimap      │     \u002Fdashboard\u002Fpostbox\u002F*\n   │                    │  ├── mail\u002Ffolders \u002F mail\u002Flabels    │\n   │                    │  ├── mail\u002Fdrafts + draftLifecycle  │\n   │                    │  ├── mail\u002Foutbound (+ lifecycle)   │\n   │                    │  ├── mail\u002Ffilters \u002F mail\u002Faliases   │\n   │                    │  ├── mail\u002FappPasswords (PBKDF2)    │\n   │                    │  ├── mail\u002FauthHttp (HMAC verify)   │\n   │                    │  └── mail\u002Fdelivery (inbound route) │\n   │                    └────────────────────────────────────┘\n   │                              ▲              ▲\n   │                              │ webhook      │ verify-cred\n   │                              │ (delivery)   │ (HMAC)\n   │                              │              │\n┌──┴───────────┐     SMTP    ┌────┴──────────────┴────┐    IMAP\u002FSMTP    ┌──────────────┐\n│ apps\u002Fweb     │ submission  │ apps\u002Fmta               │    submission   │ native       │\n│ (composer)   │────────────►│  outbound queue        │◄────────────────│ clients      │\n└──────────────┘             │  inbound routing       │                 │ (Apple Mail, │\n                             └────────┬───────────────┘                 │  Thunderbird,│\n                                      │  bind to                        │  mobile)     │\n                                      │  mailboxResolver                └──────┬───────┘\n                                      ▼                                        │\n                             ┌────────────────────────┐                        │\n                             │ apps\u002Fimap              │ ◄──────────────────────┘\n                             │  IMAP4rev1 server      │     IMAP fetch \u002F store\n                             │  port 993 implicit TLS │\n                             └────────────────────────┘\n                                      │\n                                      └─── reads\u002Fwrites via Convex client\n                                           (uses CONVEX_URL + CONVEX_ADMIN_KEY)\n","text",[465,497,494],{"__ignoreMap":498},"",[485,500,502],{"id":501},"schema","Schema",[448,504,505,506,509,510,513,514,517,518,521],{},"The mail tables are defined in ",[465,507,508],{},"apps\u002Fapi\u002Fconvex\u002Fschema\u002Fmail.ts"," (re-exported into ",[465,511,512],{},"schema.ts"," as ",[465,515,516],{},"mailTables"," and spread into ",[465,519,520],{},"defineSchema()","). Key relationships:",[490,523,526],{"className":524,"code":525,"language":495},[493],"mailboxes (1)──(*) mailFolders (*)──(*) mailMessages (*)──(1) mailThreads\n         ├────(*) mailAliases\n         ├────(*) mailAppPasswords\n         ├────(*) mailFilters\n         ├────(*) mailLabels\n         ├────(*) mailDrafts\n         ├────(*) mailSignatures\n         ├────(*) mailForwarding\n         ├────(*) mailVacationResponders \u002F mailVacationLog\n         ├────(*) mailContacts\n         └────(*) mailAuditLog \u002F mailAuthFailures\n\nexternalMailAccounts (1)──(1) mailboxes (kind='external')\n                     └────(*) externalMailFolderSync\npendingMailboxes        — reserved-mailbox intent attached to an invitation\n",[465,527,525],{"__ignoreMap":498},[448,529,530,531,535,536,539,540,543,544,535,546,549,550,553,554,557,558,535,560,563,564,567,568,571,572,575,576,579],{},"There is ",[532,533,534],"strong",{},"no"," ",[465,537,538],{},"mailOutbound"," table — outbound state lives in the embedded ",[465,541,542],{},"mailMessages.outbound"," object (a denormalized aggregate plus a per-recipient array). There is ",[532,545,534],{},[465,547,548],{},"mailIdentities"," table — sending identities are a module (",[465,551,552],{},"mail\u002Fidentities.ts",") layered over ",[465,555,556],{},"mailSignatures"," and the mailbox's allowed-from set. There is ",[532,559,534],{},[465,561,562],{},"mailSnooze"," table — snooze is the ",[465,565,566],{},"snoozedUntil"," \u002F ",[465,569,570],{},"snoozedFromFolderId"," fields on ",[465,573,574],{},"mailMessages",", swept by a 1-minute cron via the ",[465,577,578],{},"by_snoozed_until"," index.",[448,581,582],{},"Index notes:",[584,585,586,630,648],"ul",{},[587,588,589,591,592,595,596,599,600,595,603,606,607,610,611,614,615,617,618,621,622,625,626,629],"li",{},[465,590,574],{}," inbox sorting uses ",[465,593,594],{},"by_mailbox_and_received"," on ",[465,597,598],{},"[mailboxId, receivedAt]",". IMAP UID ranges use ",[465,601,602],{},"by_folder_and_uid",[465,604,605],{},"[folderId, uid]","; CONDSTORE fast-resync uses ",[465,608,609],{},"by_folder_and_modseq",". Thread reads use ",[465,612,613],{},"by_thread","; the snooze cron uses ",[465,616,578],{},". There is also a ",[465,619,620],{},"search_messages"," full-text search index on ",[465,623,624],{},"snippet",". (There is no ",[465,627,628],{},"internalDate"," index.)",[587,631,632,635,636,639,640,643,644,647],{},[465,633,634],{},"mailAppPasswords"," stores PBKDF2-SHA256 hashes (100k iterations, encoded ",[465,637,638],{},"\u003Csalt-hex>:\u003Chash-hex>",") — the cleartext is shown to the user exactly once on creation. A separate ",[465,641,642],{},"passwordPrefix"," (first 4 chars) narrows the candidate set via ",[465,645,646],{},"by_prefix"," before the deliberately slow hash compare.",[587,649,650,653,654,657,658,595,661,664,665,668,669,672,673,676,677,679],{},[465,651,652],{},"mailAliases"," stores the canonical lowercase address in the ",[465,655,656],{},"alias"," field and is indexed by ",[465,659,660],{},"by_alias",[465,662,663],{},"[alias]"," (plus ",[465,666,667],{},"by_target","). ",[465,670,671],{},"organizationId"," is stored on the row but is ",[532,674,675],{},"not"," part of the lookup index — the inbound router resolves a recipient by the ",[465,678,656],{}," field alone.",[485,681,683],{"id":682},"module-layout-convex","Module layout (Convex)",[448,685,686,687,455],{},"All mail modules live under ",[465,688,689],{},"apps\u002Fapi\u002Fconvex\u002Fmail\u002F",[691,692,693,706],"table",{},[694,695,696],"thead",{},[697,698,699,703],"tr",{},[700,701,702],"th",{},"Module",[700,704,705],{},"Responsibility",[707,708,709,726,736,746,765,775,788,807,817,830,840,853,863,877,887,900,910,931,941,951,965,977,987],"tbody",{},[697,710,711,723],{},[712,713,714,567,717,567,720],"td",{},[465,715,716],{},"mailbox.ts",[465,718,719],{},"mailboxActions.ts",[465,721,722],{},"mailboxQueries.ts",[712,724,725],{},"Mailbox lifecycle, reads, and message reads\u002Findex",[697,727,728,733],{},[712,729,730],{},[465,731,732],{},"pendingMailbox.ts",[712,734,735],{},"Reserved-mailbox intent on a BetterAuth invitation, claimed on accept",[697,737,738,743],{},[712,739,740],{},[465,741,742],{},"folders.ts",[712,744,745],{},"System folder initialization, folder CRUD",[697,747,748,753],{},[712,749,750],{},[465,751,752],{},"messageActions.ts",[712,754,755,756,759,760,567,762],{},"Message mutations (read\u002Funread, label ops, moves) — there is no ",[465,757,758],{},"messages.ts","; reads live in ",[465,761,716],{},[465,763,764],{},"imap.ts",[697,766,767,772],{},[712,768,769],{},[465,770,771],{},"labels.ts",[712,773,774],{},"User-defined labels",[697,776,777,785],{},[712,778,779,567,782],{},[465,780,781],{},"drafts.ts",[465,783,784],{},"draftLifecycle.ts",[712,786,787],{},"Compose drafts with autosave; the draft state machine + send cascade",[697,789,790,804],{},[712,791,792,567,795,567,798,567,801],{},[465,793,794],{},"outbound.ts",[465,796,797],{},"outboundCron.ts",[465,799,800],{},"outboundQueries.ts",[465,802,803],{},"postboxOutboundLifecycle.ts",[712,805,806],{},"Outbound dispatch action, scheduled-send cron, query helpers, and the per-recipient outbound state machine",[697,808,809,814],{},[712,810,811],{},[465,812,813],{},"filters.ts",[712,815,816],{},"Sieve-like rule engine",[697,818,819,827],{},[712,820,821,567,824],{},[465,822,823],{},"aliases.ts",[465,825,826],{},"aliasesActions.ts",[712,828,829],{},"Aliases routing into a mailbox",[697,831,832,837],{},[712,833,834],{},[465,835,836],{},"forwarding.ts",[712,838,839],{},"Outbound forwarding rules",[697,841,842,850],{},[712,843,844,567,847],{},[465,845,846],{},"signatures.ts",[465,848,849],{},"identities.ts",[712,851,852],{},"Signatures + the allowed-from \u002F sending-identity resolution",[697,854,855,860],{},[712,856,857],{},[465,858,859],{},"appPasswords.ts",[712,861,862],{},"PBKDF2-SHA256 credential storage + verification",[697,864,865,870],{},[712,866,867],{},[465,868,869],{},"authHttp.ts",[712,871,872,873,876],{},"HMAC-signed ",[465,874,875],{},"verify-credential"," endpoint for the MTA\u002FIMAP",[697,878,879,884],{},[712,880,881],{},[465,882,883],{},"authRateLimit.ts",[712,885,886],{},"Per-address auth throttle",[697,888,889,897],{},[712,890,891,567,894],{},[465,892,893],{},"snooze.ts",[465,895,896],{},"vacation.ts",[712,898,899],{},"Snooze sweep + RFC 3834 vacation auto-responder",[697,901,902,907],{},[712,903,904],{},[465,905,906],{},"contacts.ts",[712,908,909],{},"Per-mailbox address book",[697,911,912,920],{},[712,913,914,567,917],{},[465,915,916],{},"ai.ts",[465,918,919],{},"aiGate.ts",[712,921,922,923,926,927,930],{},"In-inbox AI (thread summarize + suggest replies) on the shared LLM seam; ",[465,924,925],{},"aiGate"," enforces the ",[465,928,929],{},"ai"," flag + per-user rate limit before each call",[697,932,933,938],{},[712,934,935],{},[465,936,937],{},"delivery.ts",[712,939,940],{},"Inbound mail acceptance, folder routing, filter application",[697,942,943,948],{},[712,944,945],{},[465,946,947],{},"deliveryHooks.ts",[712,949,950],{},"Post-delivery hooks (notifications, triggers)",[697,952,953,958],{},[712,954,955],{},[465,956,957],{},"webhook.ts",[712,959,960,961,964],{},"Inbound delivery webhook handler from the MTA (",[465,962,963],{},"handleMailWebhook",")",[697,966,967,971],{},[712,968,969],{},[465,970,764],{},[712,972,973,974,976],{},"Convex-side helpers for the IMAP server (fetch slices, store\u002Fcopy\u002Fmove, folder state); full-text search lives in ",[465,975,716],{}," for the webmail UI",[697,978,979,984],{},[712,980,981],{},[465,982,983],{},"permissions.ts",[712,985,986],{},"Mailbox-level permission checks",[697,988,989,1000],{},[712,990,991,567,994,567,997],{},[465,992,993],{},"externalAccounts.ts",[465,995,996],{},"externalAccountsActions.ts",[465,998,999],{},"externalDelivery.ts",[712,1001,1002,1003,964],{},"External-mailbox connect\u002Ftest\u002Fsync (see ",[452,1004,1006],{"href":1005},"#external-mailboxes-mailexternal","External mailboxes",[485,1008,1010],{"id":1009},"app-password-auth-flow","App-password auth flow",[448,1012,1013,1014,1017,1018,1020,1021,1024],{},"Native IMAP\u002FSMTP clients can't use the dashboard session. They authenticate with ",[532,1015,1016],{},"app passwords"," — single-mailbox, revocable tokens stored as PBKDF2-SHA256 hashes in ",[465,1019,634],{},". Both verification paths terminate in the same internal Convex action, ",[465,1022,1023],{},"internal.mail.appPasswords.verify",", but they reach it differently.",[448,1026,1027,1032,1033,1036],{},[532,1028,1029,1030,964],{},"IMAP (",[465,1031,475],{}," — the IMAP server holds ",[465,1034,1035],{},"CONVEX_ADMIN_KEY"," and calls the action over the Convex client directly:",[490,1038,1041],{"className":1039,"code":1040,"language":495},[493],"1.  user opens \u002Fdashboard\u002Fpostbox\u002Fsettings\u002Fapp-passwords\n2.  mail\u002FappPasswords.generate creates a row with PBKDF2(password); returns plaintext once\n3.  user pastes plaintext into Apple Mail; client connects to apps\u002Fimap (port 993)\n4.  commands\u002Flogin calls the Convex action `mail\u002FappPasswords:verify`\n        ({ address, password }) over the admin-key client\n5.  appPasswords.verify narrows candidates by passwordPrefix, runs PBKDF2(password),\n        and returns { ok: true, mailboxId, appPasswordId, ... } on success\n6.  apps\u002Fimap binds the IMAP session to that mailboxId\n        — every subsequent IMAP command runs against Convex as that mailbox\n",[465,1042,1040],{"__ignoreMap":498},[448,1044,1045,1051],{},[532,1046,1047,1048,964],{},"SMTP submission (via ",[465,1049,1050],{},"apps\u002Fmta"," — the MTA does not hold the admin key, so it posts to the HMAC-signed HTTP endpoint instead:",[490,1053,1056],{"className":1054,"code":1055,"language":495},[493],"1.  desktop client submits over SMTP to apps\u002Fmta's submission port\n2.  apps\u002Fmta POSTs \u002Fwebhooks\u002Fmta-verify-credential\n        body:    { address, password, scope: 'imap' | 'smtp' }\n        headers: x-mta-signature: HMAC-SHA256(`${timestamp}.${body}`, MTA_WEBHOOK_SECRET)\n                 x-mta-timestamp: \u003Cunix-seconds>\n3.  mail\u002FauthHttp.ts verifies the HMAC + freshness, then delegates to\n        internal.mail.appPasswords.verify, returning { ok, mailboxId, appPasswordId, ... }\n",[465,1057,1055],{"__ignoreMap":498},[448,1059,1060,1061,1064,1065,455],{},"The HMAC pattern means the MTA never needs the Convex admin key for credential checks — only the shared ",[465,1062,1063],{},"MTA_WEBHOOK_SECRET",". The IMAP server, which runs the full command loop against Convex, does hold ",[465,1066,1035],{},[485,1068,1070,1071,964],{"id":1069},"imap-server-appsimap","IMAP server (",[465,1072,475],{},[448,1074,1075],{},"Configuration is read from env on startup:",[691,1077,1078,1091],{},[694,1079,1080],{},[697,1081,1082,1085,1088],{},[700,1083,1084],{},"Env var",[700,1086,1087],{},"Default",[700,1089,1090],{},"Purpose",[707,1092,1093,1108,1123,1142,1158,1173,1188,1201,1212],{},[697,1094,1095,1100,1105],{},[712,1096,1097],{},[465,1098,1099],{},"IMAP_PORT",[712,1101,1102],{},[465,1103,1104],{},"993",[712,1106,1107],{},"TCP listen port",[697,1109,1110,1115,1120],{},[712,1111,1112],{},[465,1113,1114],{},"IMAP_LISTEN",[712,1116,1117],{},[465,1118,1119],{},"0.0.0.0",[712,1121,1122],{},"Bind address",[697,1124,1125,1130,1135],{},[712,1126,1127],{},[465,1128,1129],{},"IMAP_GREETING_HOST",[712,1131,1132],{},[465,1133,1134],{},"hostname()",[712,1136,1137,1138,1141],{},"Hostname in ",[465,1139,1140],{},"* OK"," greeting",[697,1143,1144,1152,1155],{},[712,1145,1146,567,1149],{},[465,1147,1148],{},"IMAP_TLS_CERT",[465,1150,1151],{},"IMAP_TLS_CERT_FILE",[712,1153,1154],{},"—",[712,1156,1157],{},"TLS cert (inline or path)",[697,1159,1160,1168,1170],{},[712,1161,1162,567,1165],{},[465,1163,1164],{},"IMAP_TLS_KEY",[465,1166,1167],{},"IMAP_TLS_KEY_FILE",[712,1169,1154],{},[712,1171,1172],{},"TLS private key",[697,1174,1175,1180,1185],{},[712,1176,1177],{},[465,1178,1179],{},"TLS_CERT_DIR",[712,1181,1182],{},[465,1183,1184],{},"\u002Fopt\u002Fowlat\u002Fcerts",[712,1186,1187],{},"Shared cert dir as fallback",[697,1189,1190,1195,1198],{},[712,1191,1192],{},[465,1193,1194],{},"CONVEX_URL",[712,1196,1197],{},"— required",[712,1199,1200],{},"Convex backend URL",[697,1202,1203,1207,1209],{},[712,1204,1205],{},[465,1206,1035],{},[712,1208,1197],{},[712,1210,1211],{},"Admin key for the IMAP→Convex client",[697,1213,1214,1219,1221],{},[712,1215,1216],{},[465,1217,1218],{},"REDIS_URL",[712,1220,1154],{},[712,1222,1223],{},"Optional rate-limit backend",[448,1225,1226],{},"Source layout:",[490,1228,1231],{"className":1229,"code":1230,"language":495},[493],"apps\u002Fimap\u002Fsrc\u002F\n├── index.ts        # process entry, signal handling\n├── server.ts       # net.createServer + TLS upgrade\n├── connection.ts   # IMAP pump: socket lifecycle, line\u002Fliteral buffering\n├── parser.ts       # IMAP command parser\n├── mime.ts         # RFC 5322 \u002F 2045 parser\n├── convex.ts       # Convex client wrapper used by command modules\n├── rateLimit.ts    # per-IP and per-credential throttling\n├── config.ts       # env loading\n├── logger.ts\n└── commands\u002F       # one module per IMAP verb (ADR-0016)\n    ├── walker.ts   # typed dispatch registry + CAPABILITY-line assembly\n    ├── types.ts    # ImapVerb, CommandModule, session\u002Fdeps types\n    ├── helpers\u002F    # shared session helpers\n    └── \u003Cverb>\u002Findex.ts   # login, select, fetch, store, copy, move, idle, …\n",[465,1232,1230],{"__ignoreMap":498},[448,1234,1235,1238,1239,1242,1243,1246,1247,1250,1251,1253,1254,1257],{},[465,1236,1237],{},"connection.ts"," is the ",[532,1240,1241],{},"pump"," — it owns the socket, line buffering, and literal absorption, and knows nothing about IMAP verbs. Command handling was extracted into per-verb modules under ",[465,1244,1245],{},"commands\u002F\u003Cverb>\u002Findex.ts",", dispatched through ",[465,1248,1249],{},"commands\u002Fwalker.ts"," (see ADR-0016 and the header comment in ",[465,1252,1237],{},"). Multi-verb modules (LIST + LSUB, SELECT + EXAMINE, UNSELECT + CLOSE) register themselves under each verb they declare; the walker also assembles the ",[465,1255,1256],{},"CAPABILITY"," line from each module's declared atoms.",[457,1259,1261],{"title":1260,"type":460},"Octet-accurate literal framing",[448,1262,1263,1265,1266,1269,1270,1273,1274,1277,1278,1281],{},[465,1264,1237],{}," leaves the socket in its default binary mode (no ",[465,1267,1268],{},"setEncoding",") and buffers raw octets in a ",[465,1271,1272],{},"Buffer",", so literal absorption (RFC 3501 §4.3) counts ",[532,1275,1276],{},"bytes, not decoded characters",". 8-bit\u002Fbinary MIME bodies and ",[465,1279,1280],{},"{N}"," octet declarations frame correctly; command text is decoded as UTF-8 only once a full CRLF-terminated line has been sliced off.",[448,1283,1284,1285,1288,1289,1292,1293,1296,1297,1300,1301,1304,1305,1308,1309,1308,1312,1308,1315,1308,1318,1321],{},"Each connection lifecycle: ",[465,1286,1287],{},"* OK greeting"," → ",[465,1290,1291],{},"LOGIN","\u002F",[465,1294,1295],{},"AUTHENTICATE"," (→ ",[465,1298,1299],{},"mail\u002FauthHttp",") → ",[465,1302,1303],{},"SELECT inbox"," → command loop (",[465,1306,1307],{},"FETCH",", ",[465,1310,1311],{},"STORE",[465,1313,1314],{},"COPY",[465,1316,1317],{},"MOVE",[465,1319,1320],{},"IDLE",", …).",[485,1323,1325],{"id":1324},"outbound-composing-smtp","Outbound: composing → SMTP",[448,1327,1328],{},"Sending mail from Postbox goes through the existing MTA, not the IMAP server. The flow is:",[1330,1331,1332,1347,1376,1415],"ol",{},[587,1333,1334,1335,1338,1339,1342,1343,1346],{},"User clicks Send. The web app moves the draft into ",[465,1336,1337],{},"pending_send"," (undo-send window) and schedules ",[465,1340,1341],{},"internal.mail.outbound.dispatchDraft"," (",[465,1344,1345],{},"apps\u002Fapi\u002Fconvex\u002Fmail\u002Foutbound.ts",").",[587,1348,1349,1352,1353,1356,1357,1360,1361,1364,1365,1368,1369,1371,1372,1375],{},[465,1350,1351],{},"dispatchDraft"," (a Node action) validates the draft state and undo token, scans each attachment through the MTA's ClamAV endpoint (fail-open on outage), renders the final HTML + plain-text bodies through ",[465,1354,1355],{},"@owlat\u002Femail-renderer",", builds an RFC 5322 multipart message, and stores the raw ",[465,1358,1359],{},".eml"," in ",[465,1362,1363],{},"ctx.storage",". It then hands off to ",[465,1366,1367],{},"internal.mail.draftLifecycle.transition({ to: 'sent' })",", which atomically inserts the Sent-folder ",[465,1370,574],{}," row with ",[465,1373,1374],{},"outbound.state='queued'"," and deletes the draft. See ADR-0028.",[587,1377,1378,1379,1382,1383,1386,1387,1390,1391,1394,1395,1398,1399,1402,1403,1406,1407,1410,1411,1414],{},"For a ",[532,1380,1381],{},"hosted"," mailbox it POSTs one MTA ",[465,1384,1385],{},"\u002Fsend"," per recipient, prefixing the MTA message id with ",[465,1388,1389],{},"pb-\u003CmailMessageId>-\u003Cidx>"," so the bounce\u002Fsent webhook can look the row back up. A synchronous ",[465,1392,1393],{},"5xx"," transitions that recipient to ",[465,1396,1397],{},"bounced","; a network error transitions it to ",[465,1400,1401],{},"failed"," — both via ",[465,1404,1405],{},"internal.mail.postboxOutboundLifecycle.transition",". (For an ",[532,1408,1409],{},"external"," mailbox it takes the single-POST ",[465,1412,1413],{},"dispatchViaExternalWorker"," path described below.)",[587,1416,1417,1418,1421,1422,1425,1426,1429,1430,1433,1434,1437,1438,1441,1442,1445,1446,1449,1450,1453],{},"Asynchronous MTA delivery events arrive at ",[465,1419,1420],{},"POST \u002Fwebhooks\u002Fmta",". The webhook ceremony (rate limit, signature check, audit, parse, dispatch) lives in ",[465,1423,1424],{},"webhooks\u002Fpipeline.ts"," + ",[465,1427,1428],{},"webhooks\u002Fadapters\u002Fmta.ts","; ",[465,1431,1432],{},"mtaWebhook.ts"," is just a thin ",[465,1435,1436],{},"httpAction"," entry that delegates to ",[465,1439,1440],{},"runInboundPipeline",". For events whose provider message id has the ",[465,1443,1444],{},"pb-"," prefix, the dispatcher (",[465,1447,1448],{},"webhooks\u002Fdispatcher.ts",") calls ",[465,1451,1452],{},"internal.mail.postboxOutboundLifecycle.transitionByMtaMessageId",", which parses the id and applies the per-recipient transition.",[448,1455,1456,1238,1458,1461,1462,1465,1466,1469,1470,1429,1473,1475,1476,1478,1479,1482],{},[465,1457,803],{},[532,1459,1460],{},"sole writer"," of every ",[465,1463,1464],{},"mailMessages.outbound.recipients[].state"," and the only producer of the derived ",[465,1467,1468],{},"mailMessages.outbound.state"," aggregate (see ADR-0012). The per-recipient states are ",[465,1471,1472],{},"queued | sent | bounced | failed",[465,1474,1397],{}," and ",[465,1477,1401],{}," are terminal. The aggregate column adds one extra literal, ",[465,1480,1481],{},"partial",", when the recipients are in a mix of states. (Personal mail discards the hard\u002Fsoft bounce classification — that is a campaign-side concern.)",[448,1484,1485,1486,1488,1489,1491],{},"Native SMTP submission from a desktop client takes the same path: the MTA accepts SMTP on its submission port, calls ",[465,1487,1299],{}," to verify the credential against ",[465,1490,634],{},", then enqueues the message normally.",[485,1493,1495],{"id":1494},"inbound-mx-mailbox","Inbound: MX → mailbox",[490,1497,1500],{"className":1498,"code":1499,"language":495},[493],"inbound SMTP ──► apps\u002Fmta ──► mailboxResolver (apps\u002Fmta\u002Fsrc\u002Finbound)\n                                  │\n                                  │ POST \u002Fwebhooks\u002Fmta-mailbox\n                                  ▼\n                         mail\u002Fwebhook.ts ──► mail\u002Fdelivery.ts\n                                              │\n                                              ├── resolve alias → mailbox\n                                              ├── apply mailFilters\n                                              ├── insert mailMessages row\n                                              ├── run mail\u002FdeliveryHooks\n                                              └── notify subscribers (Convex realtime)\n",[465,1501,1499],{"__ignoreMap":498},[448,1503,1504,1505,1508,1509,1512,1513,1515,1516,1519,1520,1342,1523,1288,1526,1529,1530,1346],{},"The MTA routes personal-mailbox deliveries to the dedicated ",[465,1506,1507],{},"POST \u002Fwebhooks\u002Fmta-mailbox"," route (",[465,1510,1511],{},"apps\u002Fapi\u002Fconvex\u002Fhttp.ts",", handled by ",[465,1514,963],{}," from ",[465,1517,1518],{},"mail\u002Fwebhook.ts","); the routing decision is in ",[465,1521,1522],{},"apps\u002Fmta\u002Fsrc\u002Fwebhooks\u002FconvexNotifier.ts",[465,1524,1525],{},"inbound.mailbox.received",[465,1527,1528],{},"\u002Fwebhooks\u002Fmta-mailbox",", everything else → ",[465,1531,1532],{},"\u002Fwebhooks\u002Fmta",[448,1534,1535,1537,1538,1540],{},[465,1536,652],{}," is the lookup table — every accepted recipient address must match an ",[465,1539,656],{}," row before delivery. Spam and ClamAV checks (when enabled) run before insert.",[485,1542,1544,1545,964],{"id":1543},"external-mailboxes-mailexternal","External mailboxes (",[465,1546,479],{},[448,1548,1549,1550,1552,1553,1556,1557,1563,1564,1567,1568,1571,1572,1575],{},"Separate from hosted Postbox, the ",[465,1551,479],{}," flag (label ",[532,1554,1555],{},"\"Connect external mailbox\"",") lets each user connect their own existing Gmail \u002F Fastmail \u002F company mailbox over IMAP+SMTP — personal mail without registering a sending domain. It is ",[532,1558,1559,1560,1562],{},"independent of the hosted ",[465,1561,467],{}," flag"," (intentionally not ",[465,1565,1566],{},"requires: ['postbox']","), activates the ",[465,1569,1570],{},"external-mail"," Docker Compose profile, and runs the ",[465,1573,1574],{},"apps\u002Fmail-sync"," worker.",[457,1577,1579],{"title":1578,"type":460},"Configuration",[448,1580,1581,1582,1425,1585,1588,1589,1592,1593,1596,1597,1600],{},"The mail-sync worker is reached via ",[465,1583,1584],{},"MAIL_SYNC_API_URL",[465,1586,1587],{},"MAIL_SYNC_API_KEY",". Credentials for the connected account are stored encrypted at rest (AES-256-GCM) on ",[465,1590,1591],{},"externalMailAccounts","; read queries never return the ciphertext. The Convex backend holds ",[465,1594,1595],{},"INSTANCE_SECRET"," and decrypts (in the internal ",[465,1598,1599],{},"getCredentialsForWorker"," action), then hands the plaintext credentials to the mail-sync worker over the admin-key channel.",[448,1602,1603],{},"How it differs from a hosted mailbox:",[584,1605,1606,1619,1645,1657],{},[587,1607,1608,1609,1371,1612,1615,1616,1618],{},"A connected account creates a ",[465,1610,1611],{},"mailboxes",[465,1613,1614],{},"kind='external'"," linked 1:1 to an ",[465,1617,1591],{}," row (host\u002Fport\u002FTLS, auth method, encrypted credential envelope, connection status).",[587,1620,1621,1622,1625,1626,1629,1630,1308,1633,1636,1637,1640,1641,1644],{},"The worker is the ",[532,1623,1624],{},"client"," of the remote IMAP server. ",[465,1627,1628],{},"externalMailFolderSync"," tracks per-(account, folder) incremental fetch cursors (",[465,1631,1632],{},"remoteUidValidity",[465,1634,1635],{},"lastSeenUid",", optional CONDSTORE ",[465,1638,1639],{},"lastSeenModseq","), distinct from ",[465,1642,1643],{},"mailFolders","' own UID state (which tracks Owlat-as-IMAP-server).",[587,1646,1647,1648,1342,1651,567,1654,1346],{},"Inbound messages fetched from the remote server are ingested via ",[465,1649,1650],{},"mail\u002FexternalDelivery.ts",[465,1652,1653],{},"ingestExternalRaw",[465,1655,1656],{},"ingestExternalMessage",[587,1658,1659,1660,1662,1663,1666,1667,1669,1670,1672,1673,1675,1676,1679],{},"Outbound for an external mailbox skips the per-recipient MTA path: ",[465,1661,1351],{}," checks ",[465,1664,1665],{},"internal.mail.externalAccounts.resolveOutboundTransport",", and for ",[465,1668,1614],{}," calls ",[465,1671,1413],{}," — a single POST to the worker's ",[465,1674,1385],{},", which sends through the user's own SMTP and APPENDs the sent copy to the remote Sent folder. SMTP is synchronous, so the worker's per-recipient result maps straight onto ",[465,1677,1678],{},"postboxOutboundLifecycle"," with no webhook.",[448,1681,1682,1683,1685,1686,1342,1688,567,1691,1694,1695,1697],{},"The connect\u002Ftest\u002Fcredential modules are ",[465,1684,993],{}," (queries + internal mutations), ",[465,1687,996],{},[465,1689,1690],{},"connect",[465,1692,1693],{},"testConnection"," \u002F credential handling), and ",[465,1696,999],{}," (ingest).",[485,1699,1701],{"id":1700},"integration-points","Integration points",[691,1703,1704,1714],{},[694,1705,1706],{},[697,1707,1708,1711],{},[700,1709,1710],{},"Other feature",[700,1712,1713],{},"How it interacts with Postbox",[707,1715,1716,1737,1750,1761,1772,1783],{},[697,1717,1718,1723],{},[712,1719,1720,1721,964],{},"MTA (",[465,1722,1050],{},[712,1724,1725,1726,1728,1729,1342,1731,1734,1735],{},"Submits inbound mail via ",[465,1727,1528],{},"; verifies SMTP-submission credentials via ",[465,1730,1299],{},[465,1732,1733],{},"\u002Fwebhooks\u002Fmta-verify-credential","); receives outbound sends; posts delivery events to ",[465,1736,1532],{},[697,1738,1739,1744],{},[712,1740,1741,1742,964],{},"mail-sync worker (",[465,1743,1574],{},[712,1745,1746,1747,1749],{},"For ",[465,1748,479],{},": connects to remote IMAP servers, ingests inbound, and sends outbound via the user's own SMTP",[697,1751,1752,1758],{},[712,1753,1754,1755,964],{},"Content scanner (",[465,1756,1757],{},"scan.content",[712,1759,1760],{},"Runs on inbound mail before insert",[697,1762,1763,1769],{},[712,1764,1765,1766,964],{},"ClamAV (",[465,1767,1768],{},"scan.files",[712,1770,1771],{},"Scans attachments during inbound delivery and on outbound dispatch",[697,1773,1774,1777],{},[712,1775,1776],{},"Notification provider",[712,1778,1779,1782],{},[465,1780,1781],{},"mail\u002FdeliveryHooks"," calls the notification provider for new-mail alerts",[697,1784,1785,1791],{},[712,1786,1787,1788,964],{},"AI agent (",[465,1789,1790],{},"ai.agent",[712,1792,1793,1794,1798],{},"Listens to inbound events on the ",[1795,1796,1797],"em",{},"shared"," support inbox feature, not Postbox — Postbox is intentionally personal-only",[485,1800,1802],{"id":1801},"where-to-start-when-extending","Where to start when extending",[584,1804,1805,1815,1825,1835,1842],{},[587,1806,1807,1808,1811,1812],{},"New folder type \u002F system folder → ",[465,1809,1810],{},"mail\u002Ffolders.ts"," + dashboard sidebar in ",[465,1813,1814],{},"apps\u002Fweb\u002Fapp\u002Fcomponents\u002Fpostbox\u002F",[587,1816,1817,1818,1821,1822],{},"New filter action → ",[465,1819,1820],{},"mail\u002Ffilters.ts"," rule executor + UI in ",[465,1823,1824],{},"\u002Fdashboard\u002Fpostbox\u002Fsettings\u002Ffilters",[587,1826,1827,1828,1831,1832,1834],{},"New IMAP command → add a ",[465,1829,1830],{},"apps\u002Fimap\u002Fsrc\u002Fcommands\u002F\u003Cverb>\u002Findex.ts"," module and register it in ",[465,1833,1249],{}," (see ADR-0016)",[587,1836,1837,1838,1841],{},"New outbound state transition → extend ",[465,1839,1840],{},"mail\u002FpostboxOutboundLifecycle.ts"," (the single writer of outbound state)",[587,1843,1844,1845,1848,1849,1852],{},"Server-side enforcement of the feature flag → check ",[465,1846,1847],{},"isFlagEnabled(stored, 'postbox')"," (or ",[465,1850,1851],{},"'mail.external'",") early in every mutation",{"title":498,"searchDepth":1854,"depth":1854,"links":1855},2,[1856,1857,1858,1859,1860,1862,1863,1864,1866,1867],{"id":487,"depth":1854,"text":488},{"id":501,"depth":1854,"text":502},{"id":682,"depth":1854,"text":683},{"id":1009,"depth":1854,"text":1010},{"id":1069,"depth":1854,"text":1861},"IMAP server (apps\u002Fimap)",{"id":1324,"depth":1854,"text":1325},{"id":1494,"depth":1854,"text":1495},{"id":1543,"depth":1854,"text":1865},"External mailboxes (mail.external)",{"id":1700,"depth":1854,"text":1701},{"id":1801,"depth":1854,"text":1802},"md",{},true,{"title":254,"description":255},"3.developer\u002F14.postbox-architecture","CtY8gevWp7uPtjC1DxCXqqxVfCKMfFQvsw92q3wbyro",[1875,1877],{"title":250,"path":249,"stem":1876,"children":-1},"3.developer\u002F13.email-security",{"title":258,"path":257,"stem":1878,"children":-1},"3.developer\u002F15.providers",1782846428454]