feat(email): open external links in a new tab (safely)#598
feat(email): open external links in a new tab (safely)#598hildebrandttk wants to merge 1 commit into
Conversation
External web links (http/https) in rendered email bodies now open in a new browser tab with target="_blank" rel="noopener noreferrer". mailto:, tel:, and in-page #anchors keep their default behavior (hand off to the mail client / navigate in place) instead of spawning a blank tab. Two root causes were fixed: 1. The iframe HTML render path set target=_blank on EVERY <a>, including mailto:/tel:. It is now scoped to http(s) links. 2. Plaintext emails (and HTML alternatives that are really plaintext) have no <a> tags in the source - they are generated by plainTextToSafeHtml and rendered in the main DOM via sanitizePlainTextRenderedHtml. Although the generator emits target/rel, DOMPurify silently stripped them: when a custom ALLOWED_URI_REGEXP is set it validates target/rel against the regexp (which "_blank" never matches) and drops them, so those links opened in the same tab. Changes: - email-sanitization: add isHttpLinkHref() and a shared applyNewTabToAnchor() helper (http/https -> target+rel; mailto/tel/#/other -> strip target/rel). - sanitizePlainTextRenderedHtml: re-apply target/rel after sanitization via an afterSanitizeAttributes hook (the established codebase pattern). - sanitizeI18nHtml: same DOMPurify strip affected the in-app docs link in settings.security.not_available (target="_blank") - keep the translator's target and harden rel="noopener noreferrer". - email-viewer: both anchor passes (DOMPurify hook + post-render walk) now use the shared applyNewTabToAnchor() helper. - tests: unit coverage for isHttpLinkHref / applyNewTabToAnchor / sanitizeI18nHtml plus an end-to-end integration suite exercising the real plaintext and HTML/iframe render pipelines so this can't regress unnoticed.
|
Sorry – I think we've collided. I wrote #594, which fixes the same thing and landed on The overlap. We diagnosed the identical root cause: DOMPurify URI-tests every attribute not on its URI-safe list, so a custom const PLAIN_TEXT_RENDERED_CONFIG = {
ALLOWED_TAGS: ['a', 'br', 'p', 'div', 'span'],
ALLOWED_ATTR: ['href', 'target', 'rel', 'class', 'style'],
// ...the strict ALLOWED_URI_REGEXP below would strip target="_blank" (and rel)
// — "_blank" is not a URI. ... Exempt the two from the URI check.
ADD_URI_SAFE_ATTR: ['target', 'rel'],
ALLOW_DATA_ATTR: false,
ALLOWED_URI_REGEXP: /^(?:https?:|mailto:|tel:|cid:|#)/i,
};So the GitHub now flags the branch as conflicting. Simulating the merge locally (
So the rebase needs a decision about how the two mechanisms divide up, not just conflict resolution. Suggestion below. What this PR fixes that #594 didn't:
One detail that might save you some work: If it helps, the smallest rebase I can see is: drop the Entirely the maintainers' call on which idiom they want, and I'm not precious about #594's approach – if the hook is preferred as the single mechanism, that's a reasonable consolidation too. Happy to help rebase, or to review, if that's welcome. |
Summary
External web links (http/https) in rendered email bodies now open in a new browser tab with target="_blank" rel="noopener noreferrer". mailto:, tel:, and in-page #anchors keep their default behavior (hand off to the mail client / navigate in place) instead of spawning a blank tab.
Two root causes were fixed:
The iframe HTML render path set target=_blank on EVERY , including mailto:/tel:. It is now scoped to http(s) links.
Plaintext emails (and HTML alternatives that are really plaintext) have no tags in the source - they are generated by plainTextToSafeHtml and rendered in the main DOM via sanitizePlainTextRenderedHtml. Although the generator emits target/rel, DOMPurify silently stripped them: when a custom ALLOWED_URI_REGEXP is set it validates target/rel against the regexp (which "_blank" never matches) and drops them, so those links opened in the same tab.
Changes
Related Issues
Closes #
Type of Change
Checklist
npm run typecheck && npm run lintand there are no errorsnpm run build)locales/) if my changes affect user-facing textScreenshots / Demo
Notes for Reviewers