max / goingson
1 file changed,
+14 insertions,
-12 deletions
| @@ -295,19 +295,21 @@ function formatEmailBody(body) { | |||
| 295 | 295 | // Escape any remaining HTML for XSS protection | |
| 296 | 296 | let escaped = escapeHtml(text); | |
| 297 | 297 | ||
| 298 | - | // Convert [url] patterns to clickable links | |
| 298 | + | // Convert [url] patterns to clickable links, then bare URLs. Both routes | |
| 299 | + | // share one anchor builder so their char classes and escaping can't drift | |
| 300 | + | // apart again (the bracketed matcher previously used `[^\]]+`, which let `"` | |
| 301 | + | // and spaces through and broke out of the href attribute — a sibling of the | |
| 302 | + | // Run #28 bare-URL hardening that this consolidation closes for good). | |
| 303 | + | // `url` is already escapeHtml'd (safe as link text); the href is run through | |
| 304 | + | // safeUrl (scheme allow-list) then escapeAttrValue (encodes `"`/`&`). | |
| 305 | + | const linkAnchor = (_match, url) => | |
| 306 | + | `<a href="${escapeAttrValue(safeUrl(url))}" class="email-link" target="_blank" rel="noopener noreferrer">${url}</a>`; | |
| 307 | + | ||
| 299 | 308 | // Pattern: text [https://...] or text [http://...] | |
| 300 | - | escaped = escaped.replace( | |
| 301 | - | /\[((https?:\/\/)[^\]]+)\]/g, | |
| 302 | - | '<a href="$1" class="email-link" target="_blank" rel="noopener noreferrer">$1</a>' | |
| 303 | - | ); | |
| 304 | - | ||
| 305 | - | // Also detect bare URLs that weren't wrapped | |
| 306 | - | // Match URLs that aren't already inside an href or anchor tag | |
| 307 | - | escaped = escaped.replace( | |
| 308 | - | /(?<!href="|">)(https?:\/\/[^\s<>\[\]"]+)/g, | |
| 309 | - | '<a href="$1" class="email-link" target="_blank" rel="noopener noreferrer">$1</a>' | |
| 310 | - | ); | |
| 309 | + | escaped = escaped.replace(/\[((https?:\/\/)[^\]\s"]+)\]/g, linkAnchor); | |
| 310 | + | ||
| 311 | + | // Also detect bare URLs that weren't wrapped (not already inside an href/anchor) | |
| 312 | + | escaped = escaped.replace(/(?<!href="|">)(https?:\/\/[^\s<>\[\]"]+)/g, linkAnchor); | |
| 311 | 313 | ||
| 312 | 314 | // Collapse quoted blocks (> lines and "On ... wrote:" attribution) | |
| 313 | 315 | const lines = escaped.split('\n'); |