apps/mail is the standalone Tuturuuu mailbox app. It runs on
https://mail.tuturuuu.com in production and port 7820 locally, delegates
auth to apps/web, and preserves Tuturuuu workspace and mailbox-role checks.
Platform operators may configure additional managed domains; a mailbox address
must match its linked private.mail_domains row.
Architecture
apps/mailowns the mailbox UI and protected app-local APIs under/api/v1/workspaces/:wsId/mail/*.- The mailbox mirror is stored in service-only
private.mail_*tables. Browser code never reads those tables directly; routes verify workspace membership and mailbox roles before using an admin client. - SES and Cloudflare Email Service are permanent outbound transports. The
domain default comes from
mail_domains.outbound_provider; an optional mailbox override wins. Shared abuse, rate-limit, and audit controls in@tuturuuu/email-servicestill run for both providers. - Inbound transport is domain-wide. SES receipt/S3/SNS ingestion remains
supported, while Cloudflare Email Routing invokes the Worker at
apps/mail/src/email-worker/index.ts. - Supabase is authoritative for domains, authorization, audit, threads, search, AI state, and MCP credentials. The private R2 bucket contains raw MIME, body objects, and attachment bytes; callers receive authorized short-lived access, never raw keys or R2 credentials.
- Thread resolution checks
Message-ID,In-Reply-To, andReferencesfirst. Normalized subject is only a recent-reply fallback and is not unique.
/verify-token in the proxy before centralized auth handling so
local Portless handoffs set cookies on mail.tuturuuu.localhost. The app keeps
token refresh same-origin through /api/auth/refresh-app-session; a valid
refresh cookie should rotate the Mail-local and Web-issued app-session cookies
without sending the browser back through apps/web login.
Authenticated root and personal workspace entry requests redirect directly to
/personal/inbox in the proxy, preserving query state and locale. Mailbox discovery
uses bootstrap ?view=mailboxes; unread badges load separately through
?view=counts and stay unknown until available. Both paths retain session and
membership checks. The default bootstrap response still includes counts for other
clients. Thread scans fetch previews rather than full bodies; missing legacy
snippets are loaded only for the visible page. State scans exclude read-only rows
unless the requested search needs read state.
Reading and attachment previews
Settings → Reading stores the archive navigation and email appearance preferences in the current browser. Archiving an open inbox conversation selects the next visible conversation by default, falling back to the previous one at the end of the loaded list. Users can choose to return to the list instead. Bulk archive skips all selected conversations when choosing the next message. Loaded unread conversations are marked read even when opened through restored URLs or archive navigation. Read updates wait for pending archive/trash operations to settle. Archive controls track the affected conversation, so the next conversation remains actionable while earlier requests finish. Its header remains available from the inbox summary while the message body loads. Failed operations restore only their own conversations and counters; mailbox reconciliation waits for overlapping operations to finish. Catch-all headers fall back to the observed delivery recipient, then the envelope recipient when parsed To recipients are absent. Sender and recipient summaries clamp long addresses and expose full text in tooltips. Email frames remain hidden until the saved appearance is restored and contrast adjustments finish. Dark mode matches neutral paper surfaces to the reader’s background, including black and gray panels and neutral gradients, and softens bright neutral borders while preserving colored panels and images. Compact Dark/Original icon controls live on the floating action toolbar with tooltips. Original mode retains the sender’s colors. Neither mode enables scripts inside email HTML. Message headers use a single truncated sender-to-recipient row. The participant button opens full address details; the separate chevron collapses the message. The reader collapses recognized Gmail, Outlook, cite-style, and plain-text reply history into a native disclosure. Quote-only and unrecognized messages remain visible. Quote detection runs on the sanitized isolated document before it is revealed; it never modifies stored message content or outgoing reply headers. The protected attachment endpoint accepts?preview=1 for a passive-format
allowlist: raster images, supported video/audio types, and plain text (including
.txt files stored as application/octet-stream). Preview access uses the same
mailbox and message authorization as downloads and preserves byte-range support.
HTML, SVG, and other unsupported types remain downloads. Responses are private,
uncached, MIME-sniffing-disabled, and sandboxed. The reader exposes previews from
both the conversation and Attachments tab, with a separate download action. Text
previews use the internal API client, render escaped text in the reader theme, and
fall back to download on authorization failures or files larger than 1 MiB.
Composing and draft recovery
Close saves the latest draft before dismissing the composer; offline or failed saves keep the editor open. Untouched empty composers close without creating a draft. Drafts exposes Continue editing and deletion, and reopening keeps the original draft identity, recipients, reply headers and attachments. Switching composers waits for the previous draft to save before starting a new editor session. The reader header omits its back button; narrow layouts retain a message-list action on the floating toolbar. The composer keeps recognized trailing Gmail/Outlook history outside the rich-text editor, collapsed by default, preserving its HTML when saving or sending. Bounded reply blocks are recognized; ambiguous Outlook tails and authored quotations stay editable. Inline quoted images retain their attachment metadata. Selecting text exposes Enhance selection. AI previews replace only that passage after review; a document change invalidates the captured selection. Plain-text reading caps runs of blank lines without changing stored text or meaningful indentation.SES Receiving Setup
Do not change DNS from code or migrations. The current public MX fortuturuuu.com is Google-routed, so real @tuturuuu.com receiving requires an explicit staged MX cutover or a pilot subdomain first.
- Verify the domain or pilot subdomain in the SES receiving region.
- Create an S3 bucket for raw MIME objects.
- Create an SNS topic for receipt notifications and subscribe the web webhook:
POST /api/v1/webhooks/mail/ses. - Create an SES receipt rule that stores raw MIME in S3 and publishes the SNS notification.
- Configure
MAIL_SES_INBOUND_TOPIC_ARN,MAIL_SES_INBOUND_BUCKET,MAIL_SES_INBOUND_KEY_PREFIX, andMAIL_SES_REGION. - Only after validation, stage the MX/DNS change outside the app repository.
MAIL_SES_SNS_SIGNATURE_VERIFICATION=disabled. Do not use that setting in production.
Cloudflare onboarding
Cloudflare must already manage DNS for an onboarded domain. Arbitrary-recipient sending also requires Email Sending to be enabled for the account. Configure a staging domain before changing a production domain.- Create a private R2 bucket (the checked-in Worker configuration uses
tuturuuu-mail) and bind it asMAIL_R2_BUCKETinapps/mail/wrangler.email-routing.jsonc. - Configure the Mail app server with
MAIL_R2_ACCOUNT_ID,MAIL_R2_ACCESS_KEY_ID,MAIL_R2_SECRET_ACCESS_KEY, andMAIL_R2_BUCKET_NAME. The bucket name must match the Worker binding.MAIL_R2_ENDPOINTis only needed for an R2-compatible development or test endpoint. Object keys are private implementation details and must not be returned to clients. Seeapps/mail/.env.examplefor the complete contract. - Set the Worker secret with
bunx wrangler secret put MAIL_INGEST_SECRET --config apps/mail/wrangler.email-routing.jsonc. - Configure the same value as
MAIL_CLOUDFLARE_INGEST_SECRETin the Mail Vercel environment. Signed events include the request body and a timestamp; the API rejects invalid or older-than-five-minute signatures. - Set
MAIL_INGEST_URLto the deployed Mail endpoint/api/v1/webhooks/mail/cloudflare, then deploy withbunx wrangler deploy --config apps/mail/wrangler.email-routing.jsonc. - In Cloudflare Email Routing, onboard the domain and route its intended
address patterns to
tuturuuu-mail-email-routing. - Configure the domain row through
GET/PUT /api/v1/mail/domains. Only root workspace operators may use this endpoint. Move the domain fromverifyingtoactiveonly after DNS and routing checks pass. - For outbound Cloudflare sends, set
MAIL_CLOUDFLARE_API_TOKENwith Email Sending permission and either store the managed account ID on the domain or setMAIL_CLOUDFLARE_ACCOUNT_IDas the fallback.
postal-mime, stores deterministic R2 objects, and submits a signed
idempotent delivery event. Malformed or spam/virus-signaled deliveries are
recorded as quarantined. Transient API failures are thrown so Email Routing can
retry without creating duplicate messages.
Cloudflare controls the outbound Message-ID header and rejects clients that
set it. Mail therefore sends only In-Reply-To and References through the
Cloudflare API. SES raw MIME sends retain a deterministic Message-ID. Store
Cloudflare’s returned provider identifier separately; do not substitute it for
an RFC message identifier unless the provider explicitly returns one in that
format.
Mailbox API foundation
Mailbox routes require workspace membership and a mailbox role on every request. The API provides chronological thread retrieval and thread-level state changes, label and custom-folder CRUD, bulk message mutations, and private attachment upload/download/delete routes. Attachment downloads stream through an authorized route with byte-range support; clients never receive an R2 object key. Message listing accepts the structured search operatorsfrom:, to:,
cc:, bcc:, subject:, is:, has:attachment, before:, after:, and
label:. Quote values containing spaces. Structured filters are combined with
remaining free text and mailbox/folder state filters. Client applications
should call these routes through packages/internal-api/src/mail.ts.
Cloudflare currently permits 50 combined to/cc/bcc recipients and a
normal outbound size of 5 MiB including attachments. Email Routing accepts up
to 25 MiB inbound. The provider enforces outbound limits before making the API
request; the Worker rejects inbound events above the routing limit. Reconfirm
current quotas in the
Cloudflare Email Service limits
before changing these constants.
The managed staging baseline uses tutur3u.com for Email Routing and Email
Sending and the private tuturuuu-mail R2 bucket. Do not attach a routing rule
to the inbound Worker until the Mail deployment has the matching ingestion
secret and Supabase has an enabled ingest.tutur3u.com domain row linked to
the canonical tuturuuu.com row. Email Sending and R2 can be verified
independently before that inbound cutover.
Google Workspace shadow-ingestion migration
Do not onboardtuturuuu.com into Cloudflare Email Routing while Google
Workspace still owns its apex MX records. Email Routing is enabled at the zone
level before Cloudflare allows routing subdomains, so onboarding the production
zone would replace and lock the Google MX records too early. Use the already
onboarded staging zone as the shadow bridge instead:
- Cloudflare Email Routing is enabled for
ingest.tutur3u.com; its routing DNS records are managed and locked by Cloudflare. - A temporary exact-address routing rule may forward a pilot shadow address to a verified test inbox. Replace this action with the deployed ingestion Worker before starting a parity run.
- Google Workspace has a recipient-address-map setting named
Cloudflare shadow ingestion pilot. Keep it disabled between tests. It must map each selected@tuturuuu.comaddress to the same local part at@ingest.tutur3u.com, retain the original Gmail destination, and addX-Gm-Original-To. - The public
tuturuuu.comMX records remain Google-only throughout the shadow phase. Never publish Google and Cloudflare MX records together as a substitute for dual delivery.
Activation gates
Complete all of these before changing the temporary Cloudflare forwarding rule to the ingestion Worker or enabling the Google pilot:- Canonicalize a trusted shadow recipient such as
user@ingest.tutur3u.comtouser@tuturuuu.com. Preserve both addresses in the ingestion event and acceptX-Gm-Original-Toonly on the configured shadow domain. - Configure the same HMAC secret as
MAIL_INGEST_SECRETon the Worker andMAIL_CLOUDFLARE_INGEST_SECRETon the Mail deployment. - Deploy the Worker with the private
tuturuuu-mailR2 binding and confirm a signed domain check and ingestion event reach the Mail webhook. - Ensure Supabase has enabled domain metadata for the shadow and canonical domains, including their explicit relationship. Do not infer an arbitrary production domain from an inbound subdomain.
- Prove direct shadow delivery, raw MIME storage, body and attachment storage, quarantine behavior, and duplicate retry before enabling Google delivery.
add_mail_domain_canonical_relationship migration installs this
exact staging relationship. Apply it through the normal database release path;
do not push it ad hoc from a workstation. The Worker derives the canonical
recipient only after a signed domain check, and the webhook independently
validates the ingress domain, canonical domain, observed recipient, and local
part. Duplicate transport deliveries with the same mailbox and RFC
Message-ID reuse the existing message instead of incrementing thread counts.
Rollout plan
- Single-address pilot: point one exact Cloudflare shadow rule at the Worker, enable only the matching Google address-map entry, and send external and internal test messages. The original Gmail delivery must remain enabled.
- Shadow parity: expand the explicit map to the active user, group, and
alias inventory. Run for at least three days and preferably seven. Compare
Google Email Log Search with Supabase ingestion records by authoritative
Message-ID, recipient, timestamp, raw MIME hash, attachment count, and quarantine result. - Cutover readiness: require no unexplained missing messages, idempotent duplicate handling, correct canonical recipients, attachment parity, and an alertable ingestion-latency baseline. Snapshot the Google MX records and lower or verify their DNS TTL at least 24 hours before the change.
- Apex cutover: during a low-traffic window, onboard
tuturuuu.comin Cloudflare and route its intended addresses to the same Worker. Keep Google Workspace and the shadow address map available for at least seven days so senders using cached Google MX records still feed the Cloudflare ingestion path. - Stabilization: remove the temporary test forwarding destination only after the Worker route is verified. Retire the Google shadow map after the cached-MX window and parity checks are complete; migrate outbound transport separately.
Rollback
Restore the saved Google MX records first and wait for DNS confirmation. Keep the Google shadow map and Cloudflare staging subdomain available during rollback so messages delivered through either cached MX path still reach the same idempotent ingestion boundary. Change the Supabase inbound-provider flag only after DNS is serving the intended provider. A rollback must not delete R2 objects, mail metadata, Google accounts, or the disabled pilot configuration. The initial transport smoke test used distinct subjects for Google outbound and Google-to-Cloudflare shadow delivery. Google delivered the original inbound message to the Workspace inbox, and Cloudflare recorded the mapped shadow copy as forwarded. After the test, the Google pilot setting was returned to its disabled state.Catch-all delivery
Catch-all routing is platform-operated because it affects an entire inbound domain. The destination is relational metadata onprivate.mail_domains, not a
workspace secret: catch_all_mailbox_id must reference an active mailbox on the
canonical domain, and catch_all_enabled defaults to false. Automatic drafts
for catch-all deliveries have a separate opt-in and remain disabled unless a
platform operator explicitly enables them.
Activate the bridge in this order:
- Apply the additive
mail_catch_all_deliverymigration through the normal database release process and deploy the matching Mail app and Email Routing Worker. - Open Mail settings as a root workspace operator, select
ingest.tutur3u.com, choose the destination mailbox, and enable the logical catch-all route. The initial pilot destination isphucvo@tuturuuu.comwhen that mailbox exists. - In Cloudflare Email Routing, select the already-onboarded
ingest.tutur3u.comsubdomain and set its catch-all action to thetuturuuu-mail-email-routingWorker. Keep explicit rules enabled; they take precedence over catch-all. - Send a unique random local part directly to the ingress subdomain. Confirm the original recipient is visible in Mail, raw MIME and attachments are in R2, and a retry does not create another message.
- In Google Admin, add a rule named
Tuturuuu Mail catch-all bridgefor inbound Unrecognized/Catch-all recipients only. Replace only the recipient domain withingest.tutur3u.com, addX-Gm-Original-To, and leave Users and Groups unchecked. This keeps recognized Google Workspace delivery unchanged while preserving the unknown local part for Mail.
tuturuuu.com apex while Google
owns its MX records. To roll back, disable the Google unrecognized-recipient
rule first, disable the Cloudflare subdomain catch-all second, and disable the
logical Mail route last. Do not delete ingested metadata or R2 objects.
Provider rollout and rollback
Provider selection is independent in each direction. Change only one direction at a time on the staging domain, complete inbound delivery, outbound delivery, attachment, threading, duplicate retry, and bounce/throttle smoke tests, then enable the production domain. A mailbox override may be used for a narrow outbound canary. To roll back outbound delivery, clear the mailbox override and set the domain outbound provider toses. To roll back inbound delivery, restore the domain’s
SES MX/receipt-rule configuration first, then set inbound_provider to ses.
Do not change the database flag before DNS is serving the intended provider.
Existing SES jobs, raw S3 metadata, and credentials remain supported throughout
the rollback.
Operations
Mail settings and account settings
Mailbox settings use themailSettings=open query parameter. The shared
satellite account settings use settingsDialog=open and settingsTab; keep
these separate so the guidance dialog cannot cover the mailbox delivery,
membership, and sender controls. Closing either dialog must preserve the
other dialog’s query state.
Mail sanitizes message HTML and signatures with sanitize-html in both the
server ingestion/draft path and browser previews. Its allowlist retains email
tables, typography, and cid: image references while excluding active content
and CSS resource URLs. Avoid reintroducing an externalized jsdom-based sanitizer:
an incompatible CommonJS/ESM dependency can fail module initialization and make
even the mailbox bootstrap route return HTTP 500. Validate sanitizer security
fixtures and a real Mail build when changing this dependency boundary.
Full Google mail migration
Inventory active and suspended accounts, aliases, group membership, delegated mailbox access, and existing routing before importing data. Preserve suspended account access restrictions. Google groups can also authorize access to Google Cloud resources; migrating their email does not authorize deleting those groups or changing their IAM membership. For the Tuturuuu migration, suspended accounts stay suspended while incoming mail for their addresses follows the domain catch-all tophucvo@tuturuuu.com.
An existing inactive mailbox must not be recreated by automatic provisioning.
This delivery policy does not grant the suspended user mailbox access.
Google Admin’s available organization export may include all services rather
than Gmail alone. Review the actual scope before starting it, obtain approval
for non-mail data, and use only the mail portion for this migration. A scheduled
or in-progress export is not an imported archive. Keep the source service until
the completed export has been downloaded, reconciled, and imported with original
dates, folders/labels, read state, attachments, and mailbox ownership intact.
The live Cloudflare webhook imports new inbound deliveries; it is not a
historical Gmail importer and must not be used to silently flatten sent mail,
drafts, or labels into the inbox.
Use the resumable Google Takeout importer after extracting only each account’s
Takeout/Mail directory into one directory per email address. Run it without
--apply first and reconcile its report with an independent MBOX inventory:
--apply. The
importer writes raw MIME and attachments to the configured Mail R2 bucket,
uses deterministic IDs, and skips existing provider or Internet Message-ID
records so an interrupted run can resume safely. It processes Deleted MBOX
files first so duplicates retain their trash state. It preserves Gmail thread
IDs, original timestamps, sent/draft/spam/trash/read/star/archive state, system
labels, and custom labels. State rows may be recorded for a suspended user’s
existing profile, but the importer does not activate a mailbox or create a
mailbox membership.
After the apply run, compare its report with counts from
private.mail_messages, private.mail_raw_messages,
private.mail_attachments, and private.mail_stored_objects. Re-run the
importer with the same arguments to verify that every message is reported as
already present. Keep the source export until database counts, R2 objects, and
an authenticated Mail reader sample all agree.
Onboard Cloudflare Email Sending separately from Email Routing. Sending
uses its bounce subdomain and DKIM records and can be prepared while the apex
MX remains on Google. Verify the existing DMARC policy and reporting addresses
after onboarding. Check the account’s actual daily quota and the outbound
message-size limit before switching the application provider; domain activation
alone does not prove application sends or recipient delivery.
For tuturuuu.com, the requested catch-all destination is
phucvo@tuturuuu.com. Configure the logical Mail destination first, route the
Cloudflare catch-all to the ingestion Worker, and retain explicit recipient
routes. Prove delivery to a new random local part, the preserved original
recipient, attachment storage, and duplicate handling before enabling the Google
catch-all bridge or changing the apex MX. Never use SMTP forwarding back to the
same domain as a substitute for internal mailbox ingestion.
Smart labels and AI-assisted drafting
Apply themail_smart_labels migration before deploying the matching settings
UI. It adds a description, mailbox-scoped AI instructions, an enable flag, and
an auto-apply flag to each private custom label. The migration is additive and
defaults every AI option to disabled. Label CRUD remains limited to mailbox
owners and admins; senders may apply configured labels but cannot redefine the
taxonomy.
Mail exposes AI drafting and smart-label classification only through
mailbox-authorized app routes and packages/internal-api. Drafting supports new
messages, rewrites, and follow-ups with bounded thread context. Message content
is treated as untrusted reference material so instructions embedded in an
email cannot override the system prompt. Generated text is returned to the
composer as an editable draft; the AI route has no send, schedule, or transport
tool.
Smart-label suggestions help owners/admins create a taxonomy from recent
mailbox patterns. Classification accepts explicit thread IDs, validates every
thread and label against the active mailbox, and applies labels only after an
authorized user invokes the workflow. ai_auto_apply records whether a label
may be applied by an authorized automatic workflow; it does not grant AI any
additional mailbox role or send capability.
The repair_mail_thread_subjects migration backfills blank legacy thread
subjects from the newest meaningful message. Runtime thread hydration also
falls back to the newest message so sent mail remains accurate when application
deployment precedes the migration.
- Run
bun sb:uplocally after mail schema changes, thenbun sb:typegen. - Keep new mail route access checks in
apps/mail; do not add direct client Supabase reads. - Use
packages/internal-api/src/mail.tsfor client helpers and TanStack Query in the app UI. - Unknown inbound recipients are retained as
quarantinedjobs for administrator review instead of being delivered to a user inbox. - Keep generated public assets such as
/manifest.webmanifest,/sw.js, and offline worker files out of the auth proxy matcher. Redirecting those files to central Web login breaks standalone Mail startup and PWA registration. - Keep
apps/mail/src/proxy.tsconfig.matcherentries as inline string literals. Next.js statically parses proxy matcher config during Vercel builds and rejects imported constants even when they resolve to strings.
CI and deployment
Mail has dedicated Vercel workflows:.github/workflows/vercel-preview-mail.yaml.github/workflows/vercel-production-mail.yaml
tuturuuu.ts and use the shared
ci-check.yml switchboard. They require environment-scoped Vercel credentials
plus VERCEL_MAIL_PROJECT_ID; production Supabase and SES values should live in
the Vercel project environment rather than GitHub Actions.
The preview workflow builds and deploys prebuilt artifacts through Vercel CLI.
The production workflow also uses workflow concurrency so stale production runs
are canceled instead of deploying after a newer production commit is pushed.
Deployment credentials must stay environment-scoped in GitHub Actions. The
preview job is bound to the vercel-preview-mail GitHub Environment, and the
production job is bound to vercel-production-mail. Store VERCEL_TOKEN,
VERCEL_ORG_ID, and VERCEL_MAIL_PROJECT_ID in those environments instead of
repository-wide or organization-wide secrets. Do not add TURBO_TOKEN,
TURBO_TEAM, production Supabase service keys, or SES credentials to
workflow-level env; Mail deploys should rely on Vercel project environment
variables pulled by vercel pull. Manual production dispatch is only valid from
refs/heads/production.
apps/mail/vercel.json disables Vercel Git deployments and GitHub integration
so preview and production deploys only happen through the CI workflows.
Mail client navigation and recovery
Mail owns one settings entry in its sidebar. Its shell disables the shared settings utility withshowSettingsButton={false}; other satellite apps keep
the default. Mail settings continue using the mailSettings URL state.
Unread and attachment quick filters compose with the search query. The selection
checkbox selects loaded conversations, and selection resets when the mailbox,
folder, label, or search changes. Message loading failures show an explicit retry
state instead of appearing as an empty mailbox. Sending, draft autosave, protected
attachments, and delivery settings remain behind the existing Mail APIs.
The conversation pane constrains message width and keeps message details and
attachments available without expanding them by default. Small screens show one
pane at a time, with a back action returning to the message list.
Distribution groups
A shared address can be provisioned as a distribution group by settingprivate.mail_mailboxes.metadata.mail_group to:
Internal forwarding and smart labels
Mailbox owners and administrators can use Settings → Automation to forward new incoming messages to another accessible, active mailbox in the same domain, or to that domain’s current enabled catch-all mailbox. Delivery retains the source copy and preserves original content and attachments. Forwarding is one internal hop; forwarded copies never trigger another forwarding rule. Self, inactive, cross-domain and distribution-group targets are rejected. Distribution groups continue to use member delivery. Disabling catch-all suspends a catch-all-based forward until an eligible target is configured again. Smart inbox usesgemini-3.5-flash-lite, Google’s latest stable Flash-Lite model
verified on September 7, 2026. Enable it separately per mailbox. New incoming mail
is classified against custom labels; only labels with both AI and automatic
application enabled are applied. Gemini may suggest up to three new categories;
owners and administrators can review recent suggestions under Settings → Labels.
Suggested labels are never created automatically. Subjects, sender addresses and
up to 6,000 text characters are processed by the configured Google AI service.
Attachments are not sent to the classifier. The existing AI memory/usage wrapper
continues to apply its configured policies.
Classification is bounded to twelve seconds without provider retries. Failures
are logged and never reject accepted mail. The existing manual classification
and suggestion controls remain available for recovery and older messages.
Completed classification events prevent repeat work on normal delivery retries.
Concurrent duplicate deliveries can still produce duplicate AI requests; label
assignment itself is idempotent. Forwarding errors remain retryable through the
inbound delivery job. Configuration is stored in mailbox metadata; this feature
requires no schema migration.
The reader defaults to dark appearance, with original newsletter colors available
from the floating toolbar or Reading settings. Stylesheets are retained only inside a sandboxed iframe with
scripts, forms, remote stylesheets/fonts and network APIs blocked by CSP. The
iframe grants same-origin access to the parent solely for height measurement and
protected inline images; it never grants script execution. External links open
separately with no referrer. Raster CID images resolve through authorized
attachment endpoints. Signature sanitization still removes stylesheets. Wide
messages scroll within the reader rather than expanding the app. Rendering aims
for common Gmail-style newsletters; it is not a claim of pixel-identical Gmail
compatibility for every email template.
Thread lists always display both the message date and delivery time. The mobile
reader uses constrained native scrolling to prevent wide tables, subjects or
attachment names from widening the application viewport.
The reader preserves hidden newsletter preheaders and isolated inline CSS. Dark
appearance adapts neutral surfaces and borders while preserving branded colors. Message content uses the full pane
width; reply actions float over the bottom edge with reserved scroll space so the
last line remains reachable. The collapsed sidebar contains folder actions only.
Expanded navigation groups shared mailboxes by repeated address prefixes (for
example GCP), while personal mailboxes remain directly accessible.
Keyboard navigation
Use the keyboard button beside the mailbox filters, or? while single-key shortcuts are enabled, to open the shortcut
reference. Reading settings and the reference both let users disable single-key
shortcuts; Tab, Shift+Tab and arrow navigation remain available.
Arrow keys move focus between loaded conversations, Home/End reach the list
boundaries, and Enter opens the focused row. j/k also switch an already-open
reader. x selects a conversation, Shift+X toggles all loaded conversations, and
e, #, and Shift+I apply archive, trash, and mark-read to the checked selection.
u or Escape clears a checked selection; when no selection remains, it returns
to the list. / focuses search,
c composes, and r/a/f reply, reply all, or forward the loaded message.
g followed by a folder key navigates while preserving the active mailbox.
Composer shortcuts use Ctrl on Windows/Linux or Command on macOS: Enter sends
through the normal review flow, S saves, and J opens AI assistance. Escape saves
and closes. Focus moves to recipients for a new draft or the body for a reply,
and returns to the opener after closing. Mailbox shortcuts pause during typing,
IME composition, composing, and dialogs. Same-origin email-frame key listeners
retain the script-disabled sandbox.