From f94508994f5fcc4a7663ce80f1f9fa37eb6fa109 Mon Sep 17 00:00:00 2001 From: nesquena-hermes Date: Fri, 19 Jun 2026 01:58:28 +0000 Subject: [PATCH] =?UTF-8?q?release:=20v0.51.505=20=E2=80=94=20allow=20macO?= =?UTF-8?q?S=20Mail=20message:=20links=20in=20chat=20markdown=20(#4420,=20?= =?UTF-8?q?#4319)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: bergeouss Closes #4420 Closes #4319 --- CHANGELOG.md | 6 ++++++ static/messages.js | 2 +- static/ui.js | 6 +++--- tests/test_issue2768_workspace_links.py | 18 ++++++++++++++++-- 4 files changed, 26 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 83f50fd4..734f227c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ ## [Unreleased] +## [v0.51.505] — 2026-06-18 — Release RP (allow macOS Mail `message:` links in chat markdown) + +### Fixed + +- **`message:` links (macOS Mail message URLs) now render as clickable links in chat instead of being blocked (#4319).** The markdown renderer's link allowlists treated `message:` like an unknown scheme and dropped it; it's now allowed alongside the other OS-delegated schemes (`mailto:`/`tel:`) on every render path (streamed markdown + the two `renderMd` link passes + the anchor-safety check). The `javascript:`/`data:`/`vbscript:` denylist still fires first, so this adds no script-execution surface. Useful for RAG/notes workflows that link back to source emails. Thanks @bergeouss. + ## [v0.51.504] — 2026-06-18 — Release RO (group OpenRouter / Nous models by vendor in the model picker) ### Added diff --git a/static/messages.js b/static/messages.js index b5b7a7de..f0fba24f 100644 --- a/static/messages.js +++ b/static/messages.js @@ -2219,7 +2219,7 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){ } // Allowed URL schemes for anchors and images rendered from agent-streamed markdown. // Raw file:// anchors are rewritten to /api/media before the user can click them. - const _SMD_SAFE_URL_RE=/^(?:https?:|mailto:|tel:|\/|#|\?|\.|api|session\/)/i; + const _SMD_SAFE_URL_RE=/^(?:https?:|mailto:|tel:|message:|\/|#|\?|\.|api|session\/)/i; const _SMD_SAFE_IMG_URL_RE=/^(?:https?:|mailto:|tel:|\/|#|\?|\.)/i; function _smdLinkHref(raw){ const href=String(raw||''); diff --git a/static/ui.js b/static/ui.js index 983db3be..d139a65a 100644 --- a/static/ui.js +++ b/static/ui.js @@ -4692,7 +4692,7 @@ function renderMd(raw){ t=t.replace(/\x00C(\d+)\x00/g,(_,i)=>_code_stash[+i]); // Stash [label](url) links before autolink so the URL in href= is not re-linked const _link_stash=[]; - t=t.replace(/\[([^\]]+)\]\(((?:https?:\/\/|file:\/\/|workspace:\/\/|session:\/\/|mailto:|tel:)[^\s\)]+)\)/g,(_,lb,u)=>{_link_stash.push(_markdownAnchor(lb,u));return `\x00L${_link_stash.length-1}\x00`;}); + t=t.replace(/\[([^\]]+)\]\(((?:https?:\/\/|file:\/\/|workspace:\/\/|session:\/\/|mailto:|tel:|message:)[^\s\)]+)\)/g,(_,lb,u)=>{_link_stash.push(_markdownAnchor(lb,u));return `\x00L${_link_stash.length-1}\x00`;}); t=t.replace(/(https?:\/\/[^\s<>"')\]]+)/g,(url)=>{const trail=url.match(/[.,;:!?)]$/)?url.slice(-1):'';const clean=trail?url.slice(0,-1):url;return `${esc(clean)}${trail}`;}); t=t.replace(/\x00L(\d+)\x00/g,(_,i)=>_link_stash[+i]); t=t.replace(/\x00G(\d+)\x00/g,(_,i)=>_img_stash[+i]); @@ -4833,7 +4833,7 @@ function renderMd(raw){ // Stash existing tags first to avoid re-linking already-linked URLs. const _a_stash=[]; s=s.replace(/(]*>[\s\S]*?<\/a>)/g,m=>{_a_stash.push(m);return `\x00A${_a_stash.length-1}\x00`;}); - s=s.replace(/\[([^\]]+)\]\(((?:https?:\/\/|file:\/\/|workspace:\/\/|session:\/\/|mailto:|tel:)[^\s\)]+)\)/g,(_,label,url)=>_markdownAnchor(label,url)); + s=s.replace(/\[([^\]]+)\]\(((?:https?:\/\/|file:\/\/|workspace:\/\/|session:\/\/|mailto:|tel:|message:)[^\s\)]+)\)/g,(_,label,url)=>_markdownAnchor(label,url)); s=s.replace(/\x00A(\d+)\x00/g,(_,i)=>_a_stash[+i]); // Restore raw
 only after markdown rewrites so literal preformatted
   // content stays placeholder-protected, then let the sanitizer normalize tags.
@@ -4919,7 +4919,7 @@ function renderMd(raw){
     if(!compact) return false;
     if(/^(javascript|data|vbscript):/i.test(compact)) return false;
     if(/^https?:\/\//i.test(raw)) return true;
-    if(/^(mailto:|tel:)/i.test(raw)) return true;
+    if(/^(mailto:|tel:|message:)/i.test(raw)) return true;
     if(img && /^api\//i.test(raw)) return true;
     if(!img && (/^api\//i.test(raw) || /^#/.test(raw) || _isInternalSessionHref(raw))) return true;
     return false;
diff --git a/tests/test_issue2768_workspace_links.py b/tests/test_issue2768_workspace_links.py
index 0733e200..0d759d5b 100644
--- a/tests/test_issue2768_workspace_links.py
+++ b/tests/test_issue2768_workspace_links.py
@@ -63,10 +63,24 @@ def _render(markdown: str) -> str:
 
 
 def test_workspace_markdown_renders_mailto_and_tel_links():
-    html = _render("[email](mailto:foo@example.test) and [phone](tel:+15551212)")
+    html = _render("[email](mailto:foo@example.test) and [phone](tel:+155****1212)")
 
     assert 'email' in html
-    assert 'phone' in html
+    assert 'phone' in html
+
+
+def test_workspace_markdown_renders_message_scheme_links():
+    """macOS Mail `message:` links must render clickable (#4319) — same OS-handled
+    class as mailto:/tel: — while `javascript:` stays blocked even when a
+    `message:`-style label is attached."""
+    html = _render(
+        "[mail](message://%3Cabc@example.test%3E) "
+        "and [x](javascript:alert(1))"
+    )
+
+    assert 'mail' in html
+    # The javascript: scheme must NOT produce an executable href.
+    assert "javascript:alert(1)" not in html or 'href="javascript:' not in html
 
 
 def test_workspace_html_iframe_allows_links_to_escape_sandbox():