Email System
Full email platform with CF Email Routing inbound, Resend outbound, R2 attachments, FTS5 search, contacts, labels, filters, autoresponder, scheduled send, templates, and keyboard shortcuts
Email System (v5.0.0)
The email system at /admin/email is a full email platform for @Arcturus-Prime.com addresses. Inbound via Cloudflare Email Routing, outbound via Resend, storage in D1 + R2, with FTS5 search, contacts, labels, filters, autoresponder, scheduled send, templates, aliases, and keyboard shortcuts.
Architecture
Inbound: Sender → MX (CF Email Routing) → Arcturus-Prime-email-receiver Worker
→ blocked sender check → D1 storage → R2 attachments
→ contacts auto-populate → filter rules → autoresponder
→ Gmail forward
Outbound: Admin/User UI → /api/{admin,user}/email → Resend API → D1
→ contacts auto-populate → delivery status via webhooks
Scheduled: Admin UI → D1 email_scheduled → CF Cron (every 1 min)
→ Worker scheduled handler → Resend API → D1 (sent folder)
Attachments: Inbound: Worker → R2 (metadata in D1)
Outbound: UI upload → API → R2 + Resend
Download: API → R2 (fallback D1 BLOB for legacy)
Search: FTS5 virtual table (auto-synced via triggers)
Mailboxes
Managed in email_mailboxes D1 table. Default set (seed via gear icon):
| Address | Display Name |
|---|---|
[email protected] | Arcturus-Prime Admin (default) |
[email protected] | Arcturus-Prime |
[email protected] | Arcturus-Prime Support |
[email protected] | daniel |
Each mailbox has: address, display name, signature, is_default, is_active. When sending, the from field uses ${display_name} <${address}> format.
Aliases
Aliases route additional addresses to a target mailbox. The worker resolves aliases on inbound: if no direct mailbox match, checks email_aliases for a matching alias_address and routes to target_mailbox_id.
Storage
D1 (Structured Data)
emails— all inbound/outbound messages with metadataemail_mailboxes— mailbox definitions with signaturesemail_attachments— attachment metadata +r2_keypointer (legacy rows may have D1 BLOBcontent)email_contacts— auto-populated address bookemail_labels+email_label_map— custom labelsemail_filter_rules— automated sorting rulesemail_autoresponders— per-mailbox out-of-officeemail_scheduled— time-delayed outbound queueemail_blocked_senders— spam/block patternsemail_templates— reusable compose templatesemail_aliases— address routing
R2 (Binary Data)
- Bucket:
Arcturus-Prime-email-attachments - Key format:
attachments/{email_id}/{attachment_id}/{filename} - Limit: 25 MB per attachment (R2 supports up to 5 GB, capped for email sanity)
- Free tier: 10 GB storage, 1M Class A ops, 10M Class B ops, zero egress
FTS5 (Search)
- Virtual table
emails_ftsindexed on: subject, body_text, from_address, to_address - Auto-synced via INSERT/UPDATE/DELETE triggers on
emailstable - Search API uses FTS5 MATCH with relevance ranking, fallback to LIKE if FTS table missing
Features
Contacts (Auto-Populated)
- Worker upserts sender into contacts on every inbound email
- API upserts recipients on every outbound send
- Tracks: email, name, last_contacted, contact_count, is_favorite
- Autocomplete in compose To/CC/BCC fields (debounced search)
- Favorite contacts shown first
Signatures
- Per-mailbox signature stored in
email_mailboxes.signature - Auto-appended to compose body below
--separator - Editable in compose modal
Labels
- Custom labels with name and color
- Assign/remove labels from emails
- Sidebar shows label list with counts, click to filter
- Label pills displayed on email list items
Filter Rules
- JSON conditions:
[{field, operator, value}]— field: from/to/subject/body, operator: contains/equals/matches (regex) - JSON actions:
[{type, params}]— type: move/label/star/mark_read/delete - Worker evaluates rules after storing inbound email, first match wins
- Priority ordering, toggle active/inactive
Autoresponder
- Per-mailbox out-of-office with subject and body
- Optional date range (start_date/end_date)
- Tracks
responded_toJSON set to avoid re-sending to same address - Sends via Resend from the mailbox’s address
Scheduled Send
- “Send Later” with datetime picker in compose modal
- Stored in
email_scheduledtable with status: pending/sent/cancelled/failed - CF Cron Trigger (every 1 minute) on worker processes due emails
- Scheduled folder in sidebar with pending count
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 — rejects with
message.setReject('Sender blocked')
Delivery Status
- Resend webhook endpoint receives: delivered/bounced/complained/delayed events
- Updates
delivery_statuscolumn on emails - Status indicators shown in sent folder (colored dots)
Templates
- Reusable email templates with name, subject, body
- “Insert Template” button in compose opens template picker
- Template body replaces compose body, subject auto-fills
Custom Folders
- User-created folders in
email_custom_folderstable - Name, color (8 presets), optional icon
- Uses existing
foldercolumn with folder UUID - Sidebar counts, rename/delete via context menu
Email Forwarding
- Forward button in reading pane (keyboard:
f) - Builds forwarded subject (
Fwd: ...) and body with original headers - Optionally includes user message above forwarded content
- Admin + user pages
Rich Text Compose
- Contenteditable editor with formatting toolbar (replaced textarea on admin)
- Commands: Bold, Italic, Underline, Strikethrough, H/H2 headings
- Lists (bullet/numbered), Blockquote, Horizontal Rule
- Link/Unlink, Clear Formatting, Font Size (4 levels), Text Color (7 colors)
- Sends both
html(innerHTML) andbody(innerText) to API - Reply pre-fills with quoted HTML from original
Dark Reader (HTML Email Body)
- Dark CSS injected directly into iframe srcdoc (not filter:invert)
- Dark bg (#0a0e1a), cyan links (#22d3ee), light text, cyan-tinted borders
- Toggle button swaps iframe internal stylesheet live
- Preference saved to localStorage, defaults ON
- Wrapper gets gradient top line (cyan→purple), outer cyan glow
CID Inline Image Resolution
- Scans iframe for
img[src^="cid:"]after load - Fetches attachment list, matches CID to
content_id - Downloads attachment as blob, replaces
img.srcwith blob URL
Draft Auto-Save
- Compose content auto-saves to localStorage every 3 seconds
- Closing modal saves immediately
- Reopening “New Email” offers to restore (within 24 hours)
- Successful send clears draft
- “Draft saved” indicator in compose footer
Keyboard Shortcuts
| Key | Action |
|---|---|
j / k | Next / previous email |
Enter | Open selected email |
Escape | Close reading pane / modal |
c | Compose new email |
r | Reply to current email |
f | Forward current email |
e | Archive |
# | Trash |
s | Toggle star |
u | Toggle read/unread |
/ | Focus search |
? | Show shortcut help overlay |
g i | Go to inbox |
g s | Go to sent |
g a | Go to archive |
Inline Image Preview
- Attachment images render in a gallery div below email body
- Fetches via API, creates blob URLs
User Email Portal
/user/email — authenticated non-admin users with owner_email on mailbox.
API: src/pages/api/user/email.ts (~30 actions, ownership-verified).
Page: src/pages/user/email.astro
Features ported from admin: labels, templates, contacts, blocked senders, custom folders, thread view, rich text compose, dark reader, CID images, forwarding, draft auto-save, empty trash. NOT ported (admin-only): AI compose, scheduled send, filter rules, autoresponder, aliases, permanent delete.
Three-Phase Deletion
- Trash (
folder = 'trash',deleted_atset) — recoverable via “Move to Inbox” - Purge (
purged_atset) — visible only in “Purged” folder, recoverable via “Restore” - Permanent Delete (hard
DELETE) — only from purge area, requires confirmation
API Reference
GET /api/admin/email
Returns service status and folder stats.
POST /api/admin/email
| Action | Description |
|---|---|
list | List emails by folder (+ optional mailbox_id) |
read | Get single email, marks as read |
thread | Get all emails in a thread |
send | Send via Resend + save to D1 sent folder |
move | Move to folder (inbox/sent/archive/trash) |
star | Toggle starred |
search | FTS5 search on subject/from/to/body |
stats | Folder counts + unread + purged |
mark-read | Mark email as read |
mark-unread | Mark email as unread |
attachments | List attachment metadata for an email |
download-attachment | Return attachment content (R2 first, D1 fallback) |
delete-attachment | Delete from R2 + D1 |
poll | Lightweight polling for new emails since timestamp |
delete | Purge from trash |
purge-all-trash | Purge all trashed emails |
purged | List purged emails |
permanent-delete | Hard delete from purge |
restore | Restore from purge to inbox |
mailboxes | List mailboxes |
create-mailbox | Create new mailbox |
update-mailbox | Update mailbox settings |
seed-mailboxes | Create default mailboxes |
update-signature | Set mailbox signature |
compose | AI draft (SSE stream via brain-chat) |
summarize | AI thread summary (SSE stream) |
contacts | List/search contacts |
contact-delete | Remove contact |
contact-favorite | Toggle favorite |
labels | CRUD labels |
label-email | Add/remove label from email |
list-by-label | Emails filtered by label |
filter-rules | CRUD filter rules |
autoresponder | Get/set autoresponder per mailbox |
schedule-send | Create scheduled email |
list-scheduled | List pending scheduled emails |
cancel-scheduled | Cancel scheduled email |
blocked-senders | List blocked senders |
block-sender | Block email/domain pattern |
unblock-sender | Remove block |
mark-spam | Trash + auto-block sender |
webhook-resend | Delivery status webhook |
templates | List templates |
create-template | Create template |
update-template | Update template |
delete-template | Delete template |
aliases | List aliases |
create-alias | Create alias |
delete-alias | Delete alias |
CF Email Worker
Lives at workers/email-receiver/, deploys separately from main site.
Inbound flow:
- Check sender against
email_blocked_senders— reject if blocked - Parse MIME with
postal-mime - Extract headers (From, To, Subject, Message-ID, In-Reply-To, CC, Date)
- Resolve threading via In-Reply-To lookup
- Match
toaddress to mailbox (direct match → alias fallback) - Store email in D1
- Store attachments in R2 (metadata in D1)
- Auto-populate sender into contacts
- Evaluate filter rules (first match wins)
- Check autoresponder (send via Resend if active)
- Forward to Gmail
Cron handler (every 1 min):
- Query
email_scheduledwherestatus = 'pending'andscheduled_at <= now() - Resolve From address from mailbox
- Send via Resend API
- Save to
emailstable as outbound - Update scheduled status to sent/failed
Files
| File | Purpose |
|---|---|
src/pages/api/admin/email.ts | API route (~45 actions) |
src/pages/admin/email.astro | Admin inbox UI |
src/lib/admin-db.ts | D1 email methods (~55 methods) |
src/lib/email-client.ts | Browser-side typed API client (~45 functions) |
workers/email-receiver/src/index.ts | CF Email Worker (inbound + cron) |
workers/email-receiver/wrangler.toml | Worker config (D1 + R2 + cron) |
src/pages/user/email.astro | User email portal UI |
src/pages/api/user/email.ts | User email API (~30 actions) |
src/config/modules/email.ts | Module manifest |
migrations/0002-0019 | D1 migrations (18 files) |
Environment Variables
| Variable | Where | Purpose |
|---|---|---|
RESEND_API_KEY | CF Pages env + Worker env | Resend API for sending |
RESEND_FROM_EMAIL | CF Pages env | Default from address fallback |
ADMIN_DB | wrangler.toml D1 binding | D1 database (Pages + Worker) |
EMAIL_ATTACHMENTS | wrangler.toml R2 binding | R2 bucket (Pages + Worker) |
FORWARD_TO | Worker env (optional) | Gmail forwarding address |
Migrations
| Migration | Purpose |
|---|---|
0002 | emails table with indexes |
0004 | email_mailboxes table, adds mailbox_id |
0005 | deleted_at and purged_at columns |
0006 | email_attachments table (D1 BLOB) |
0007 | email_attachments content_id column |
0008 | email_attachments r2_key column |
0009 | email_contacts table |
0010 | email_mailboxes signature column |
0011 | emails_fts FTS5 virtual table + triggers |
0012 | email_labels + email_label_map tables |
0013 | email_filter_rules table |
0014 | email_autoresponders table |
0015 | email_scheduled + email_blocked_senders + delivery_status |
0016 | email_templates + email_aliases tables |
0017 | email_attachments nullable content column |
0018 | owner_email column on email_mailboxes |
0019 | email_custom_folders table |