* 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>
* 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: 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>
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>
- 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
Stale localStorage credentials (`jwt` / `auth` / `adminAuth` / `userJwt` /
`access_token`) can be the empty string, the literal string `"undefined"`,
or carry a stray newline / control character left over from an older
build. axios + undici reject these eagerly with `Invalid character in
header content ["Authorization"]`, so every API call crashes client-side
before reaching the worker.
This adds two tiny helpers in `frontend/src/utils/headers.js`:
- `safeHeaderValue(v)` returns the trimmed value when it is a non-empty
string with no control chars (per RFC 7230) and no `"undefined"` /
`"null"` sentinel; otherwise `undefined`.
- `safeBearerHeader(jwt)` wraps a safe JWT with `Bearer `, otherwise
`undefined`.
`apiFetch` builds the headers object incrementally and only sets each
auth header when its value is safe. Missing/unsafe credentials now drop
out cleanly and the worker returns a normal 401, which the existing
`response.status === 401` flow already handles by surfacing the auth
prompt — the same UX users see on a fresh session.
Tests: `frontend/src/utils/__tests__/headers.test.js` adds 9 vitest
cases covering safe input, sentinel strings, control chars (\\n / \\r /
\\t / NUL / 0x1F / DEL), trimming, and `Bearer` construction. Build
(`pnpm build`) and tests (`pnpm test`) both pass.
Co-authored-by: voidborne-d <voidborne.d@agentmail.to>
Co-authored-by: Dream Hunter <dreamhunter2333@gmail.com>
* feat(i18n): enhance locale handling and routing
- Implemented dynamic locale aliases in router configuration.
- Added support for preferred locale storage in global state.
- Improved locale resolution logic in router beforeEach guard.
- Created utility functions for locale management and path manipulation.
- Added tests for locale matching and message extraction.
- Updated Header component to allow language selection.
- Refactored getRouterPathWithLang to utilize new locale utilities.
- Updated Vite configuration to support aliasing for vue-i18n.
- Bumped version numbers across various packages to 1.9.0.
* feat(i18n): update version to 1.8.0 and enhance locale handling
- Updated version numbers across all package.json files to 1.8.0.
- Enhanced locale handling in App.vue by centralizing locale configurations.
- Improved Turnstile component to support dynamic language rendering.
- Refactored i18n utilities to include initial locale setup and empty locale messages.
- Updated i18n.ts to utilize the new locale management structure.
- Added naive-locale.ts for better integration with Naive UI's locale handling.
- Adjusted Header.vue to streamline language selection and locale changes.
- Fixed translations in multiple locale files for consistency and accuracy.
* fix(i18n): address review feedback
* feat(i18n): update default locale to English and enhance language handling in components
* fix(i18n): switch locale selector to dropdown
* docs: add topbar language and github order design spec
* fix(i18n): 修复 Header 语言切换器相关问题,恢复为独立控件并调整样式
* Refactor locale handling in router and add locale-guard utility functions
- Improved locale resolution logic in router by introducing utility functions for better readability and maintainability.
- Added `locale-guard.js` to encapsulate locale-related functions such as getting route locale, resolving locale for navigation, and applying locale navigation state.
- Updated JWT synchronization logic to streamline the handling of JWT from query parameters.
- Modified i18n messages test to check for coverage of registered locale message keys instead of extracting English source messages.
* 删除顶部栏语言和GitHub顺序设计文档
* fix: 修复前端设置初始化时未返回 domains 数组导致的 undefined 错误
* refactor(i18n): consolidate locale infrastructure
* fix(i18n): stabilize locale route switching
* fix(i18n): persist default locale selection
* fix(i18n): 修复前端设置初始化时未返回 domains 数组导致的 undefined 错误,统一按空数组兜底处理
feat(i18n): 添加 locale 别名处理,支持默认语言的重定向
test(i18n): 增加对默认语言别名重定向的测试用例
* refactor: replace useAppI18n with useScopedI18n in multiple components for improved localization management
* fix(tests): 移除不必要的 URL 断言以简化 Passkey 测试
* fix(i18n): 更新语言切换逻辑,确保使用当前语言设置进行路由导航
* fix(i18n): 强制路由切换以确保语言切换后正确导航
* refactor(i18n): 优化消息注册和路由本地化逻辑,移除冗余代码
* refactor(i18n): 拆分 API 文件以优化路由管理,更新语言处理逻辑
* fix: align i18n release notes and frontend test script
- Upgrade version to 1.8.0 in all package.json files
- Add cf-temp-mail-release-notify skill with MarkdownV2 Telegram posting
- Optimize docs_deploy.yml to auto-trigger on Tag Build CI completion
- Add v1.8.0 placeholder in CHANGELOG.md and CHANGELOG_EN.md
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* fix: auto initialize default send balance
* fix: tighten send access auto init flow
* refactor: centralize send balance state
* fix: separate legacy repair from admin control in send balance
Add an `address_sender.source` column to distinguish legacy / auto /
user / admin rows. `ensureDefaultSendBalance` now only repairs rows
with `source IS NULL`, so admin-disabled and user-requested rows are
never overwritten. Admin POST writes tag `source = 'admin'`; new
auto-init inserts tag `'auto'`; `requestSendMailAccess` inserts tag
`'user'`.
Bumps DB_VERSION to v0.0.8 with the usual `PRAGMA table_info` guarded
ALTER, plus a standalone SQL patch under db/.
Adds E2E regressions: legacy repair path, admin-disabled rows stay
disabled across settings and send, send after admin deletion
auto-initializes a fresh row.
* fix: drop runtime legacy repair; backfill source='legacy' on migrate
Pre-v0.0.8 schema cannot distinguish legacy request-send-access
remnants from admin-disabled rows — both share `balance = 0,
enabled = 0`. Letting ensureDefaultSendBalance repair that shape on
upgrade could silently re-enable an admin-disabled row.
Remove the runtime repair path entirely:
- `ensureDefaultSendBalance` now uses `ON CONFLICT(address) DO NOTHING`;
existing rows are never touched.
- The v0.0.8 migration (and the matching SQL patch) backfills every
pre-existing row with `source = 'legacy'`, making pre-migration
state explicitly off-limits to runtime auto-init.
- E2E: flip the legacy test to the negative direction — a
`source='legacy'` zero-balance row stays untouched by settings
reads and send attempts. Harden `resetSenderToLegacy` to return
404 when `meta.changes < 1`.
- Update changelog and docs: legacy/admin-disabled rows must be
restored manually via the admin UI.
* refactor: collapse send balance auto-init to missing-row insert
Per review feedback: the runtime guarantee we actually need is
"create an address_sender row when one is missing, leave existing
rows alone". Once `ensureDefaultSendBalance` switched to
`ON CONFLICT DO NOTHING`, the `source` column, the v0.0.8 migration,
and the `resetSenderToLegacy` test endpoint became dead weight —
the DO NOTHING path already protects admin-disabled and admin-edited
rows without any provenance metadata.
- Drop `address_sender.source` and the v0.0.8 migration; revert
DB_VERSION to v0.0.7. No schema change ships with this PR.
- Strip the `source` field from `ensureDefaultSendBalance`,
`requestSendMailAccess`, and the admin-update path.
- Remove the `/admin/test/reset_sender_to_legacy` test endpoint and
its E2E helper; the negative legacy-repair test it served is no
longer needed because the runtime no longer touches existing rows.
- E2E coverage stays focused on the three guardrails: missing-row
auto-init, admin-disabled rows stay disabled, admin deletion
triggers a fresh re-insert.
- Tighten changelog and docs to "auto-initialize missing rows".
* docs: align common-issues with missing-row-only auto-init
The FAQ entries for "DEFAULT_SEND_BALANCE set but still No balance"
still described the old behaviour of repairing legacy
`balance = 0 && enabled = 0` rows. Rewrite both zh and en rows to
match the current runtime: only addresses with no existing
`address_sender` row get auto-initialised; legacy, admin-disabled,
and admin-edited rows must be restored manually through the admin
console.
* fix: harden send mail form validation
* fix: tighten send mail content checks
* fix: refine send mail empty-content checks
* fix: reset send mail preview state
- Upgrade deps across frontend/worker/pages/vitepress-docs (wrangler 4.82.2, dompurify 3.4.0, resend 6.11.0, etc.)
- Bump version to v1.7.0 in all package.json and worker constants
- Add v1.7.0 CHANGELOG placeholder; move #978/#930 Bug Fixes from v1.6.0 to v1.7.0 (merged after v1.6.0 tag)
- Add upgrade-dependencies skill; translate version-upgrade skill to English
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: respect user mail deletion toggle in user center
Hide user mailbox delete actions and block /user_api/mails deletion when ENABLE_USER_DELETE_EMAIL is disabled. Add an e2e regression test and changelog entries for issue #978.
* test: hash user password in mail deletion e2e
Use the same SHA-256 pre-hashed password format as the frontend for the user register/login flow in the mail deletion regression test.
* feat(admin): add IP whitelist (strict allowlist mode) (#920)
- Add enableWhitelist/whitelist fields to IpBlacklistSettings
- Implement three-layer access control: whitelist → blacklist → daily limit
- Whitelist uses exact match for IPv4/IPv6, regex for patterns
- Whitelisted IPs skip blacklist checks (trusted)
- Fail-closed when cf-connecting-ip missing under whitelist mode
- Frontend: independent whitelist toggle + empty list protection
- Backend: backward compatible (old frontends get defaults)
- E2E tests: config validation + runtime behavior
- Docs: CHANGELOG zh/en updated
Closes#920
* fix(admin): address PR review feedback on IP whitelist
- Add IPv4-mapped IPv6 (::ffff:x.x.x.x) exact match in isWhitelisted
- Include error.message in whitelist regex parse failure log
- Include actual/max size in whitelist size limit error message
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(admin): validate whitelist regex on save and preserve existing whitelist on partial update
- Reject invalid regex patterns in whitelist at save time to prevent runtime lockout
- Preserve existing enableWhitelist/whitelist from DB when older clients omit these fields
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(admin): revert P2 - keep simple ?? defaults for backward compat
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(admin): validate whitelist elements are strings before trimming
Prevents 500 error when whitelist contains non-string elements (e.g. numbers, null)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs(admin): add IP blacklist/whitelist documentation (zh + en)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(admin): fix fingerprint blacklist bypass when cf-connecting-ip absent, improve e2e tests
- Split checkBlacklist into checkFingerprintBlacklist (IP-independent) and checkIpAsnBlacklist
- Fingerprint check now runs before the !reqIp early-return to prevent bypass
- Add afterEach reset to config test group, extract RESET_SETTINGS constant
- Strengthen whitelist-blocks test to deterministic 403 assertion
- Add e2e tests: invalid regex rejection, non-string element rejection, fingerprint-blocks-without-IP
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(admin): suppress no-useless-escape lint warning in whitelist regex check
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat: upgrade version to v1.6.0
- Update version number to 1.6.0 in all package.json files
- Add v1.6.0 placeholder in CHANGELOG.md and CHANGELOG_EN.md
* docs: update release skill to use bilingual format (zh + en collapsed)
* chore: upgrade dependencies
* fix: correct CHANGELOG placeholder position and update version-upgrade skill
* docs: update version-upgrade skill with correct CHANGELOG placeholder position
* feat(admin): add column sorting and reset pagination on search (#918)
- Add server-side column sorting for admin address list (ID, name, created_at, updated_at, mail_count, send_count)
- Reset pagination to page 1 when searching or changing sort order
- Add optional orderBy parameter to handleListQuery with whitelist validation
Closes#918
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: add JSDoc warning for orderBy parameter in handleListQuery
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address code review findings
- Fix count not resetting to 0 when search returns empty results
- Add source_meta column sorting support
- Use Object.hasOwn to prevent prototype pollution in sort column lookup
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add localStorage fallback for OAuth2 session state on mobile browsers
Some mobile browsers (Safari ITP, WebViews) lose sessionStorage during
cross-origin OAuth2 redirects. Add localStorage fallback via computed
wrapper that dual-writes on set and reads sessionStorage-first on get.
Also cleanup state in finally block to ensure one-time consumption.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: i18n for 'code not found' in OAuth2 callback
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>