Skip to main content
Admin Modules

Email

Full email platform with CF Email Routing inbound, Resend outbound, R2 attachments, FTS5 search, contacts, labels, filters, autoresponder, scheduled send, templates, aliases, and keyboard shortcuts

February 25, 2026 Updated February 28, 2026

Email (v5.0.0)

The Email page at /admin/email is a full email platform built into the admin area. It receives emails via Cloudflare Email Routing, sends via Resend, stores messages in D1 with attachments in R2, and includes: FTS5 search, auto-populated contacts, per-mailbox signatures, custom labels, automated filter rules, autoresponder, scheduled send, blocked senders, email templates, address aliases, keyboard shortcuts, and AI-powered compose/summarize, custom folders, email forwarding, CID inline image resolution, dark reader for HTML email body, rich text compose with formatting toolbar, and draft auto-save.

Architecture

Inbound:      CF Email Routing → CF Worker (email-receiver)
              → blocked sender check → D1 + R2 attachments
              → contacts auto-populate → filter rules → autoresponder
              → Gmail forward

Outbound:     Admin UI → /api/admin/email → Resend API → D1 (sent folder)

Scheduled:    Admin UI → D1 email_scheduled → CF Cron (1 min) → Resend

Attachments:  Inbound: Worker → R2 + D1 BLOB (up to 25 MB each)
              Outbound: UI → API → R2 + D1 BLOB + Resend
              Download: API → R2 first, fallback D1 BLOB

Search:       FTS5 virtual table with auto-sync triggers

Polling:      Admin UI → poll action (10s) → D1 → browser notifications

All emails (inbound and outbound) are stored in the same D1 emails table, distinguished by the direction column (inbound or outbound). Attachments are stored in both R2 (primary) and D1 BLOB (fallback), with metadata rows in D1. The D1 fallback ensures downloads work from CF Pages where the R2 binding may not be available.

Page Layout — 3-Column Design

The UI uses a professional 3-column split-pane layout with glass-morphism design:

┌─────────────┬──────────────────┬────────────────────────────────┐
│  Sidebar    │  Email List      │  Reading Pane                  │
│  (220px)    │  (360px)         │  (flex)                        │
│             │                  │                                │
│             │ Mailbox▼ ⚙ [Search] [↻]│  Subject                │
│             │                  │  ┌─────────────────────────┐   │
│             │  ● Sender    2h  │  │ [A] Sender Name          │  │
│  📥 Inbox 5 │    Subject line   │  │     <email@addr>    Date │  │
│  📤 Sent  3 │    Preview text   │  │ To: recipient@addr       │  │
│  📦 Archive │  ─────────────── │  │ [Reply] [Unread] [Block] │  │
│  🗑 Trash   │  ● Sender    5h  │  │ [Spam] [Archive] [Trash] │  │
│  📅 Sched.  │    Subject line   │  └─────────────────────────┘   │
│  🏷 Labels  │    Preview text   │                                │
│             │                  │  Email body content...         │
│  ┌────────┐ │                  │                                │
│  │Stats   │ │                  │  ┌─ Inline Images ─────────┐   │
│  │grid    │ │                  │  │ [img1] [img2] [img3]     │   │
│  └────────┘ │                  │  └──────────────────────────┘   │
│  [✏ Compose]│                  │  ┌─ Attachments ────────────┐   │
│             │                  │  │ 📎 file.pdf (1.2 MB)     │   │
└─────────────┴──────────────────┴──────────────────────────────────┘

Responsive behavior:

  • < 900px: Reading pane hidden; selecting an email shows pane with back button
  • < 640px: Sidebar collapses to horizontal bar; folders become scrollable icon tabs
  • Folder nav — Inbox, Sent, Archive, Trash, Scheduled, Purged. Each shows live count.

List Toolbar

  • Mailbox dropdown — filter all views by mailbox address. “All Mailboxes” shows everything.
  • Manage button (gear icon) — opens Mailbox Settings modal (signatures, aliases, autoresponder)
  • Search — FTS5 full-text search across subject, from, to, body
  • Refresh — manual poll for new emails
  • Labels section — custom labels with email counts, click to filter
  • Stats grid — 2×2 grid showing Inbox, Unread, Sent, Archive counts
  • Compose button — opens compose modal

Email List Column

  • Search bar — FTS5 full-text search across subject, sender, body, recipient
  • Refresh button — manual data refresh
  • Email rows — each shows:
    • Color-coded avatar circle (hue derived from email address hash)
    • Sender name (bold if unread), unread indicator dot (cyan, glowing)
    • Attachment paperclip icon, sent badge for outbound
    • Delivery status indicator (sent folder: delivered/bounced/complained)
    • Label pills (colored)
    • Timestamp (relative), subject line, star toggle, preview text

Reading Pane

  • Header card — subject, sender card, To/CC line
  • Action buttons: Reply, Mark Read/Unread, Archive, Trash, Block Sender, Mark as Spam, Purge/Delete Forever/Restore (context-dependent)
  • Email body — plain text (pre-wrap) or HTML (sandboxed <iframe>)
  • Inline images — attachment images rendered in gallery div
  • Attachments — pill-shaped chips with icon, filename, size, click-to-download
  • Dark Reader — HTML emails render in sandboxed iframe with dark mode toggle. When active: dark background (#0a0e1a), cyan links, light text, cyan-tinted borders. Styles injected directly into iframe srcdoc (not CSS filter invert). Preference saved to localStorage, defaults to ON.
  • CID Inline Images — HTML emails with embedded CID images (e.g., <img src="cid:image001.png">) are resolved by fetching the email’s attachments, matching content_id, and replacing src with blob URLs.
  • Email Forwarding — Forward button in reading pane (and keyboard shortcut f). Opens compose with forwarded content including original headers (From, Date, Subject, To) and quoted body.

Compose Modal

  • From dropdown — select sending mailbox
  • To, CC, BCC — with contact autocomplete (debounced)
  • Body — Rich text editor (contenteditable) with formatting toolbar: Bold, Italic, Underline, Strikethrough, Headings (H/H2), Bullet/Numbered Lists, Blockquote, Horizontal Rule, Link/Unlink, Clear Formatting, Font Size (Small/Normal/Large/Huge), Text Color (7 colors). Auto-saves draft to localStorage every 3 seconds.
  • Attachments — file input with drag-and-drop, 25 MB per file
  • Insert Template — template picker dropdown
  • Send — delivers via Resend API, saves to D1 sent folder
  • Send Later — datetime picker for scheduled send
  • AI Draft — streams AI-composed body via brain-chat SSE
  • Draft Auto-Save — Compose content automatically saves to localStorage every 3 seconds. Closing the modal saves immediately. Reopening “New Email” offers to restore unsaved drafts (within 24 hours). Successful send clears the draft. “Draft saved” indicator shown in footer.

CSS Architecture

Uses <style is:global> because Astro scoped styles don’t apply to innerHTML-generated content. All selectors prefixed with .email-app to prevent leakage. Glass-morphism design with dark translucent backgrounds, subtle borders, backdrop blur, cyan/purple accents.

Multi-Mailbox System

Each @Arcturus-Prime.com address is a separate mailbox. Default set (seed via “Seed Default Mailboxes”):

AddressDisplay Name
[email protected]Arcturus-Prime Admin (default)
[email protected]Arcturus-Prime
[email protected]Arcturus-Prime Support
[email protected]daniel

Address-Based Filtering

When a mailbox is selected, all queries filter by address:

  • Inbound: to_address matches the mailbox address
  • Outbound: from_address matches the mailbox address
  • Fallback: mailbox_id explicitly set to the mailbox

Works for legacy emails without mailbox_id. “All Mailboxes” shows everything unfiltered.

Mailbox Settings

  • Add — create a new mailbox (address + display name)
  • Seed Default Mailboxes — creates the 4 defaults
  • Set Default — star icon sets default from-address
  • Enable/Disable — toggle visibility
  • Signature — per-mailbox signature text, auto-appended to compose
  • Aliases — route additional addresses to this mailbox
  • Autoresponder — out-of-office with subject, body, date range

Important: Creating a mailbox in admin only creates a D1 record. For inbound delivery, the address must also be configured in Cloudflare Email Routing to route to the worker.

Contacts (Auto-Populated)

  • Worker upserts sender into email_contacts on every inbound email
  • API upserts recipients on every outbound send
  • Contacts panel accessible from sidebar
  • Autocomplete in compose To/CC/BCC fields (debounced search)
  • Toggle favorites, favorites shown first
  • Tracks: email, name, last_contacted, contact_count, is_favorite

Full-Text Search (FTS5)

The emails_fts virtual table is indexed on subject, body_text, from_address, to_address. Auto-synced via triggers on INSERT/UPDATE/DELETE. Search API uses FTS5 MATCH with relevance ranking. Falls back to LIKE queries if FTS table doesn’t exist.

Custom Labels

  • Create labels with name and custom color
  • Assign/remove labels from emails (reading pane or context menu)
  • Sidebar shows label list with email counts
  • Click a label to filter the email list
  • Label pills displayed on email list items and reading pane

Custom Folders

User-created folders stored in email_custom_folders D1 table:

  • Create folder with name, color (8 preset colors), and optional icon
  • Move emails to custom folders (uses existing folder column with folder UUID)
  • Sidebar shows custom folders with email counts
  • Right-click to rename/delete (delete moves emails back to inbox)
  • Migration 0019 adds the table

Filter Rules

Automated email sorting evaluated by the worker on every inbound email:

  • Conditions: [{field, operator, value}]
    • Fields: from, to, subject, body
    • Operators: contains, equals, matches (regex)
  • Actions: [{type, params}]
    • Types: move (to folder), label, star, mark_read, delete
  • Priority ordering, first matching rule wins
  • Toggle active/inactive per rule

Autoresponder

Per-mailbox out-of-office replies:

  • Subject and body text
  • Optional date range (start/end)
  • Tracks responded_to set — won’t re-send to same address
  • Sends via Resend from the mailbox’s address
  • Worker checks after storing each inbound email

Scheduled Send

  • “Send Later” button in compose opens datetime picker
  • Stored in email_scheduled with status: pending/sent/cancelled/failed
  • CF Cron Trigger on worker runs every 1 minute, processes due emails via Resend
  • Scheduled folder in sidebar shows pending count
  • Cancel button on pending scheduled emails

Blocked Senders / Spam

  • Block by exact address ([email protected]) or wildcard domain (*@domain.com)
  • “Mark as Spam” = move to trash + auto-block sender
  • Worker checks inbound against blocked list before processing — silently rejects
  • Blocked senders management panel

Delivery Status (Resend Webhooks)

  • Webhook endpoint receives Resend events: delivered, bounced, complained, delayed
  • Updates delivery_status column on matching email (by resend_id)
  • Colored status indicators in sent folder

Templates

  • CRUD for reusable email templates (name, subject, body)
  • “Insert Template” button in compose opens template picker
  • Template body replaces compose body, subject auto-fills

Aliases

  • Map alias addresses to target mailboxes
  • Worker resolves aliases on inbound: if no direct mailbox match, checks email_aliases
  • Manage aliases in mailbox settings

Keyboard Shortcuts

KeyAction
j / kNext / previous email in list
EnterOpen selected email
EscapeClose reading pane / modal
cOpen compose
rReply to current email
eArchive current email
#Trash current email
sToggle star
uToggle read/unread
/Focus search
?Show shortcut help overlay
g iGo to inbox
g sGo to sent
g aGo to archive

Disabled when focus is in input/textarea. Two-key combos use gPending state machine.

Inline Attachment Preview

  • Images render in a gallery div below the email body
  • Fetches attachment content via API, creates blob URLs

Real-Time Polling

Polls every 10 seconds via the poll API action:

  1. POST { action: 'poll', since: lastPollTs }
  2. If new emails found: refresh list + stats, show in-app notification
  3. Browser Notification if tab not focused and permission granted
  4. Poll timer cleaned up on astro:before-swap

Read/Unread Toggle

  • Unread: cyan glowing dot, bold sender/subject
  • “Mark Unread”/“Mark Read” button in reading pane
  • Opening an email auto-marks as read

Three-Phase Deletion

  1. Trash (folder = 'trash', deleted_at set): Recoverable via “Move to Inbox”
  2. Purge (purged_at set): Visible only in “Purged” folder, recoverable via “Restore”
  3. Permanent Delete (hard DELETE): Only from purge area, requires confirmation

“Empty Trash” purges all trashed emails at once.

Email Threading

Uses RFC 822 headers:

  1. Outbound replies set In-Reply-To with original Message-ID
  2. Worker looks up In-Reply-To to find parent’s thread_id
  3. All emails in a thread share the same thread_id

Security

  • HTML emails: Sandboxed <iframe>, scripts and on* attributes stripped
  • Input validation: CRLF injection check, subject 998 chars, body 500KB
  • Auth: All API routes require validateAdmin()
  • Attachment limits: 25 MB per file (R2 storage)
  • Blocked senders: Worker rejects before processing

API Actions

All actions go through POST /api/admin/email with an action field.

Core

ActionPurposeKey Params
listList emails by folder (paginated, mailbox-filtered)folder, limit, offset, mailbox_id
readGet single email, marks as readid
threadGet all emails in a threadthread_id
sendSend via Resend + save to D1to, subject, body, mailbox_id, in_reply_to, attachments
moveMove between foldersid, folder
starToggle starredid
searchFTS5 searchquery, folder, mailbox_id
statsFolder counts + unreadmailbox_id
pollNew emails since timestampsince, mailbox_id

Read/Unread & Deletion

ActionPurpose
mark-readMark email as read
mark-unreadMark email as unread
deletePurge from trash
purge-all-trashPurge all trashed emails
purgedList purged emails
permanent-deleteHard delete from purge
restoreRestore from purge to inbox

Attachments

ActionPurpose
attachmentsList metadata for an email
download-attachmentDownload (R2 first, D1 fallback)
delete-attachmentDelete from R2 + D1

Mailboxes

ActionPurpose
mailboxesList mailboxes
create-mailboxCreate new mailbox
update-mailboxUpdate settings
seed-mailboxesCreate defaults
update-signatureSet mailbox signature

Contacts

ActionPurpose
contactsList/search contacts
contact-deleteRemove contact
contact-favoriteToggle favorite

Labels

ActionPurpose
labelsCRUD labels
label-emailAdd/remove label from email
list-by-labelEmails filtered by label

Filter Rules & Autoresponder

ActionPurpose
filter-rulesCRUD filter rules
autoresponderGet/set autoresponder per mailbox

Scheduled Send

ActionPurpose
schedule-sendCreate scheduled email
list-scheduledList pending
cancel-scheduledCancel scheduled email

Spam/Blocking

ActionPurpose
blocked-sendersList blocked
block-senderBlock email/domain
unblock-senderRemove block
mark-spamTrash + auto-block
webhook-resendDelivery status webhook

Templates

ActionPurpose
templatesList templates
create-templateCreate
update-templateUpdate
delete-templateDelete

Aliases

ActionPurpose
aliasesList aliases
create-aliasCreate
delete-aliasDelete

AI

ActionPurpose
composeAI draft (SSE stream)
summarizeAI thread summary (SSE)

GET /api/admin/email returns service status and folder stats.

CF Email Worker

Lives at workers/email-receiver/, deploys separately.

Inbound handler:

  1. Check blocked senders → reject if blocked
  2. Parse MIME with postal-mime
  3. Extract headers, resolve threading
  4. Match to address: direct mailbox → alias fallback
  5. Store in D1, attachments in R2
  6. Auto-populate contacts
  7. Evaluate filter rules (first match wins)
  8. Check autoresponder → send via Resend if active
  9. Forward to Gmail

Cron handler (every 1 min):

  1. Query due scheduled emails
  2. Resolve from-address from mailbox
  3. Send via Resend
  4. Save to emails table as outbound
  5. Update scheduled status

Deploy:

cd workers/email-receiver
npm install
npx wrangler deploy

Client Library

src/lib/email-client.ts — ~45 typed browser-side functions for all API actions. Exports types: Email, EmailMailbox, EmailAttachment, EmailContact, EmailLabel, EmailFilterRule, ScheduledEmail, BlockedSender, EmailTemplate, EmailAlias.

D1 Schema

Core Tables

  • emails — id, message_id, in_reply_to, thread_id, direction, from_address, from_name, to_address, cc, subject, body_text, body_html, folder, is_read, is_starred, resend_id, delivery_status, raw_size, mailbox_id, deleted_at, purged_at, created_at, updated_at
  • email_mailboxes — id, address, display_name, signature, is_default, is_active, created_at, updated_at
  • email_attachments — id, email_id, filename, content_type, size, content (BLOB, legacy), content_id, r2_key, created_at

Feature Tables

  • email_contacts — id, email, name, last_contacted, contact_count, is_favorite, created_at
  • email_labels — id, name, color, created_at
  • email_label_map — email_id, label_id (junction)
  • email_filter_rules — id, name, priority, conditions (JSON), actions (JSON), is_active, created_at
  • email_autoresponders — id, mailbox_id, is_active, subject, body, start_date, end_date, exclude_contacts, responded_to (JSON), created_at
  • email_scheduled — id, mailbox_id, to_address, cc, bcc, subject, body, reply_to_id, scheduled_at, status, created_at
  • email_blocked_senders — id, email_pattern, reason, created_at
  • email_templates — id, name, subject, body, created_at, updated_at
  • email_aliases — id, alias_address, target_mailbox_id, is_active, created_at

FTS5

  • emails_fts — virtual table on (subject, body_text, from_address, to_address) with auto-sync triggers

Migrations

MigrationPurpose
0002emails table
0004email_mailboxes, mailbox_id column
0005deleted_at, purged_at columns
0006email_attachments (D1 BLOB)
0007content_id column
0008r2_key column (R2 migration)
0009email_contacts table
0010signature column on mailboxes
0011emails_fts FTS5 + triggers
0012email_labels + email_label_map
0013email_filter_rules
0014email_autoresponders
0015email_scheduled + email_blocked_senders + delivery_status
0016email_templates + email_aliases
0017email_attachments: nullable content column (fixes R2-backed inserts)
0018owner_email column on email_mailboxes (user portal support)
0019email_custom_folders table

Apply all: npx wrangler d1 migrations apply Arcturus-Prime-admin --remote

File Map

FilePurpose
src/pages/admin/email.astroFull page: HTML, global CSS, inline JS client
src/pages/api/admin/email.tsAPI endpoint: ~45 actions
src/lib/admin-db.tsDatabase layer: ~55 methods
src/lib/email-client.tsBrowser client: ~45 typed functions
src/config/modules/email.tsModule manifest
workers/email-receiver/src/index.tsCF Worker: inbound + cron
workers/email-receiver/wrangler.tomlWorker config: D1 + R2 + cron
wrangler.tomlPages config: D1 + R2 bindings
src/pages/user/email.astroUser email portal UI
src/pages/api/user/email.tsUser email API (~30 actions)
migrations/0002-001918 D1 migration files

Setup Steps

  1. Apply D1 migrations: npx wrangler d1 migrations apply Arcturus-Prime-admin --remote
  2. Create R2 bucket: npx wrangler r2 bucket create Arcturus-Prime-email-attachments
  3. Deploy email worker: cd workers/email-receiver && npx wrangler deploy
  4. CF Dashboard → Email Routing: Enable for Arcturus-Prime.com, add routes to worker
  5. Verify DNS: CF auto-adds MX records; confirm Resend SPF/DKIM
  6. Seed mailboxes: /admin/email → Manage → “Seed Default Mailboxes”
  7. Set up Resend webhook: Point to /api/admin/email with action=webhook-resend
  8. Add addresses: For each new address, create mailbox in admin AND add CF Email Routing rule

Required Environment Variables

VariableWherePurpose
RESEND_API_KEYCF Pages + Worker envResend API for sending
RESEND_FROM_EMAILCF Pages env (optional)Default from fallback
ADMIN_DBwrangler.toml D1 bindingD1 database
EMAIL_ATTACHMENTSwrangler.toml R2 bindingR2 attachment bucket
FORWARD_TOWorker env (optional)Gmail forwarding address

Module Registration

Module ID:     email
Version:       5.0.0
Nav Item:      Email (group: ai)
Icon:          fa-envelope
Pages:         /admin/email
API Routes:    /api/admin/email
Required Env:  RESEND_API_KEY

Site-Wide Mail Notification

An unread mail badge in the site header (next to user avatar) on every page when authenticated. Implementation in src/components/Header.astro:

  • Polls email stats every 15 seconds (auth-gated via /api/auth/me)
  • Admin polls /api/admin/email, non-admin polls /api/user/email
  • Unread > 0: icon glows cyan with mail-glow animation (pulsing drop-shadow)
  • New mail detected (count increased): mail-shake animation (rotation + scale), badge pulses
  • No unread: icon reverts to default gray
  • All animations use <style is:global> keyframes + inline cssText (bypasses Astro scoped style renaming)
  • Red gradient badge shows unread count, hidden when empty

User Email Portal

/user/email provides authenticated non-admin users with their own email interface. Users must have a mailbox with owner_email matching their login email.

Feature parity with admin (subset):

  • Labels (create, assign, filter)
  • Templates (create, insert in compose)
  • Contacts (autocomplete, favorites)
  • Blocked senders / mark spam
  • Custom folders (create, move, manage)
  • Thread view
  • Rich text compose with full formatting toolbar
  • Dark reader toggle for HTML emails
  • CID inline image resolution
  • Email forwarding
  • Draft auto-save
  • Empty trash (but NOT permanent delete — admin-only security boundary)

Files: src/pages/user/email.astro, src/pages/api/user/email.ts API: ~30 POST actions (ownership-verified subset of admin actions)

Known Issues & Gotchas

  • CSS must be is:global: Astro scoped styles don’t apply to innerHTML-generated content. All selectors prefixed with .email-app.
  • Iframe console warnings: Blocked script execution in 'about:srcdoc' is expected — sandboxed iframe blocks scripts.
  • New mailbox routing: Creating a mailbox in admin only creates a D1 record. Inbound delivery requires a matching CF Email Routing rule.
  • Gmail forwarding: Worker always forwards to a single Gmail address (FORWARD_TO or default), not per-mailbox.
  • Resend webhook setup: Delivery tracking requires configuring the webhook URL in the Resend dashboard.
  • Cron trigger: Worker must be deployed with cron trigger for scheduled sends to process.
  • Worker RESEND_API_KEY: Worker needs its own RESEND_API_KEY env var for autoresponder and scheduled sends.
  • Migration 0017 required for attachments: Original migration 0006 had content BLOB NOT NULL. R2-backed inserts (0008+) pass content=NULL, which violated the constraint and silently failed. Migration 0017 fixes this by making content nullable.
adminemailcloudflareresendd1r2inboxcomposemailboxattachmentscontactslabelsfilterstemplatesfts5