diff --git a/src/lib/__tests__/emailHtml.test.ts b/src/lib/__tests__/emailHtml.test.ts index 0f9b9b6..e56ebeb 100644 --- a/src/lib/__tests__/emailHtml.test.ts +++ b/src/lib/__tests__/emailHtml.test.ts @@ -392,4 +392,42 @@ describe("linkifyHtmlText (via prepareHtml)", () => { const result = prepareHtml(wrap("

hello

")); assert.ok(result.includes("a{cursor:pointer}"), "cursor:pointer must be in the injected style"); }); + + it("unwraps Google Calendar redirect URLs in href attributes", () => { + const zoomUrl = "https://coreweave.zoom.us/j/86296412870"; + const googleRedirectUrl = `https://www.google.com/url?q=${encodeURIComponent(zoomUrl)}&sa=D&source=calendar&ust=1780956240000000&usg=AOvVaw2hpQdEyAOVox_UyoWKRPB1`; + const email = wrap(`Join Zoom Meeting`); + const result = prepareHtml(email); + assert.ok( + result.includes(`href="${zoomUrl}"`), + "Google Calendar redirect should be unwrapped to the direct Zoom URL" + ); + assert.ok( + !result.includes("google.com/url"), + "Google redirect wrapper should be removed" + ); + }); + + it("preserves non-Google URLs unchanged", () => { + const regularUrl = "https://example.com/meeting"; + const email = wrap(`Click here`); + const result = prepareHtml(email); + assert.ok( + result.includes(`href="${regularUrl}"`), + "regular URLs should not be modified" + ); + }); + + it("handles HTML-escaped characters in Google redirect URLs", () => { + const zoomUrl = "https://zoom.us/j/123456?pwd=xyz"; + const googleRedirectUrl = `https://www.google.com/url?q=${encodeURIComponent(zoomUrl)}&sa=D`; + // HTML-escaped version + const escapedUrl = googleRedirectUrl.replace(/&/g, "&"); + const email = wrap(`Join`); + const result = prepareHtml(email); + assert.ok( + result.includes("zoom.us/j/123456"), + "Google redirect should be unwrapped even with HTML-escaped ampersands" + ); + }); }); diff --git a/src/lib/emailHtml.ts b/src/lib/emailHtml.ts index b963411..ed889ac 100644 --- a/src/lib/emailHtml.ts +++ b/src/lib/emailHtml.ts @@ -25,6 +25,40 @@ */ import { defaultEmailRenderTheme, type EmailRenderTheme } from "./emailRenderTheme"; +// Unwrap Google Calendar redirect URLs to their actual destination. +// Google Calendar wraps meeting links (especially Zoom) like: +// https://www.google.com/url?q=&sa=D&source=calendar&... +// This function extracts the real URL so users can click directly to join meetings. +function unwrapGoogleCalendarUrl(href: string): string { + try { + // Match Google's URL wrapper pattern - the q parameter contains the actual URL + const googleUrlMatch = href.match(/[?&]q=([^&]+)/); + if (googleUrlMatch && googleUrlMatch[1]) { + const decoded = decodeURIComponent(googleUrlMatch[1]); + // Only unwrap if it's a valid http(s) URL (avoid security issues) + // and it came from a Google domain + if (/^https?:\/\//.test(decoded) && /(?:www\.)?google\.com\/url/.test(href)) { + return decoded; + } + } + } catch { + // If URL parsing fails, return original + } + return href; +} + +// Unwrap Google redirect URLs in href attributes +function unwrapGoogleUrlsInHtml(html: string): string { + // Match href="..." and href='...' attributes and unwrap their URLs + return html.replace(/\bhref=(['"])(.*?)\1/gi, (_match, quote, href) => { + const unescaped = href.replace(/"/g, '"').replace(/'/g, "'").replace(/&/g, "&"); + const unwrapped = unwrapGoogleCalendarUrl(unescaped); + // Re-escape for HTML attribute + const reescaped = unwrapped.replace(/&/g, "&").replace(/"/g, """).replace(/'/g, "'"); + return `href=${quote}${reescaped}${quote}`; + }); +} + // Linkify bare URLs in HTML text content at the string level (server-side), // so no browser DOM manipulation is needed. Skips content inside ,