* feat: add setting to disable auto-loading external images in emails
Adds a privacy setting (default off) that blocks remote images in email
content until the user explicitly loads them per message. Blocked images
are replaced with a placeholder; a banner allows one-click loading.
Closes#1073
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(frontend): block remote content with DOMPurify and an allowlist policy
Address review on the blocking logic. The first pass matched quoted
`<img src="http...">` with a regex, which left unquoted src, srcset,
`<source>`, CSS background-image, SVG `<image href>` and entity-encoded
schemes fetching as usual, and replaced only `src` on an element that also
carried `srcset` -- so the browser still had a remote candidate to prefer
while the UI claimed the image was blocked.
Two changes rather than a wider regex:
Sanitising is delegated to DOMPurify, which is already a dependency. The
hard part here is not enumerating attributes but surviving the parser: a
hand-written pass over a DOMParser tree still missed that `<noscript>` is
parsed as markup where scripting is off and as raw text where it is on, so a
`</noscript>` smuggled into an attribute value reopens the document at
render time and revives an `<img>` the cleaner never saw. Elements that
fetch by themselves or change how relative URLs resolve -- base, meta,
script, link, iframe, object, embed, noscript -- are dropped in this mode.
`<style>` is kept so layout survives, with its url(), image-set() and
@import references filtered.
URL classification is an allowlist. Asking "does this look remote?" means
enumerating every disguise -- backslash authorities, tab/newline/control
characters the URL parser strips, CSS escapes, schemes with no slashes --
and losing to the first one not thought of. Asking "can I prove this is
local?" fails closed instead: cid:, data:image/, blob: and relative paths
are kept, everything else is blocked. Relative paths are only safe because
`<base>` is removed, which is what stopped it re-pointing them at a tracker.
The blocked URL is discarded rather than parked in a data-* attribute, so
"the cleaned body contains no remote URL at all" is directly assertable;
restoring images re-renders from the untouched source.
Also: blob: is added to the allowed schemes -- DOMPurify's default list
omits it, and email-parser rewrites cid: attachments into blob: URLs, so
without it every inline image would be stripped along with the trackers.
The policy lives in its own module with its own tests (30 attack vectors,
7 preservation cases); email-parser.js goes back to MIME parsing only. The
per-mail override no longer initialises from the global setting, and the
banner reports the blocked count as the PR description promised.
Refs #1073
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Refresh dependencies and lockfiles across frontend, Worker, Pages, and VitePress documentation packages. Update Wrangler to 4.114.0 and align Cloudflare Workers types with its peer requirements.
Validated with frontend tests/build, Worker build/lint, docs build, and repository E2E.
Sanitize HTML announcements in both the About page and startup notification through a shared DOMPurify helper. Add regression tests and bilingual changelog entries.
Co-authored-by: tuanaiseo <tuanaiseo@gmail.com>
Fix IMAP flag persistence so read/unread state survives reconnects, and align SEARCH/FETCH behavior with persisted flags.
Co-authored-by: bounce12340 <bounce12340@users.noreply.github.com>
Cover the junk-mail policy behavior fixed in #1085:
- none/neutral results for SPF/DKIM/DMARC are treated as the method
being absent and do not trigger JUNK_MAIL_CHECK_LIST rejection
- explicit fail results are still rejected
- JUNK_MAIL_FORCE_PASS_LIST only accepts an explicit pass
Run with: node --test-isolation=none --test worker/src/email/junk_mail_policy.test.mjs
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(mailbox): add list view mode
- Add a toggleable list view for the mailbox, with a settings option and back button.
- deselect mail on second click in list view
- set current mail on row click in multi-action mode
* feat(mailbox): add configurable body preview line clamp
Allow users to set the number of preview lines (0–5) for mail body in the list view via a slider in Appearance settings. Includes i18n support for the new option and its "Off" state.
* chore: clarify some i18n message in settings
include the following changes:
- The original "Mailbox Split Size" to "Left list width in two-column mailbox view"
- The description of new feature "Full-width mailbox list view"
sync all languages with the updated message
* docs: update changelog with recent UI improvements
- Added mailbox full-width list view and body preview lines settings
- Extended left panel width ratio range to 0
- Included English changelog translations
* docs: fix CHANGELOG improvements types
* fix: enable mail list preview line clamp settings on mobile
* fix: validate AI extracted link domains
* fix: validate extracted links against full email content
* refactor: simplify AI link domain guard
* refactor: keep AI domain fix prompt-only
docs: add Resend DNS-only proxy warning to send-mail config
Resend domain verification CNAME records must use DNS-only (gray
cloud) on Cloudflare. Proxied (orange cloud) records prevent
verification, and a single failed attempt can take hours before
retry. This is a recurring issue (#515) that the Resend setup
docs did not warn about.
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
* fix: add page header padding for mobile layout
* fix: limit page header padding to mobile layout
* docs: update changelog for mobile header fix
---------
Co-authored-by: dreamhunter2333 <dreamhunter2333@gmail.com>
feat: add regex fallback for verification code extraction without Workers AI
When AI email extraction is enabled but no Workers AI binding is available,
fall back to a built-in, zero-dependency regex extractor so self-hosted
deployments without Workers AI still surface verification codes in Telegram
notifications and webhooks.
- Add worker/src/email/extract_code.ts: rule-based multilingual
(English / Chinese / Japanese / Korean) verification-code extractor with
year and YYYYMMDD date rejection to avoid false positives.
- ai_extract.ts: share the allowlist check and content parsing across both
paths, extract a saveExtractMetadata helper, and use the regex fallback
when env.AI is absent.
- Reuse the existing aiExtractResult pipeline (auth_code type), so Telegram
and webhook output need no changes.
- Update bilingual CHANGELOG and AI-extract feature docs.
Random subdomain mailbox creation only generates addresses; mail delivery
depends on DNS / Cloudflare Email Routing covering *.<base-domain>.
Cloudflare Email Routing does not inherit apex configuration onto
subdomains, so a wildcard `*` MX record on the base domain is required
for random subdomains to actually receive mail.
- Add `[!IMPORTANT]` block in subdomain.md (zh/en) explaining the two
deliverable paths: DNS-only wildcard MX (recommended for random
subdomains) vs Cloudflare dashboard "Add subdomain"
- Link to Cloudflare Email Routing — Subdomains official docs from
worker-vars.md and subdomain.md
- Instruct copying apex MX records to host `*` preserving each record's
priority/target, instead of hardcoding specific MX targets
- Shorten frontend `randomSubdomainTip` for CreateAccount and Login
views (6 locales: zh/en/de/es/ja/pt-BR), drop Markdown backticks
(Vue text interpolation renders them literally), and point users to
the docs instead of embedding DNS instructions
- Trim overlap between existing `[!NOTE]` and new `[!IMPORTANT]` in
subdomain.md
- Update CHANGELOG.md / CHANGELOG_EN.md under v1.9.0(main)
Closes#1035Closes#1026
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* fix: update AI extract default model
* fix: update e2e worker node version
* fix: use node lts for e2e worker
* fix: align AI model and CI node version
* ci: allow docs deploy without GitHub release
* fix: use workflow run branch for docs tag fallback
---------
Co-authored-by: dreamhunter2333 <dreamhunter2333@gmail.com>
- bump project version metadata to v1.9.0
- refresh npm dependencies and lockfiles across frontend, worker, pages, and docs
- link .agents/skills to .claude/skills
In quick-start / worker-vars / email-routing (zh + en), explicitly
call out that a Cloudflare-hosted domain with Email Routing + Catch-all
must be set up before deploying, and that subdomains do not inherit
the parent domain's Email Routing. Closes#1004.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>