diff --git a/apps/server/src/lib/email-utils.test.ts b/apps/server/src/lib/email-utils.test.ts new file mode 100644 index 0000000000..86fe1134bf --- /dev/null +++ b/apps/server/src/lib/email-utils.test.ts @@ -0,0 +1,19 @@ +import { describe, it, expect } from 'vitest'; +import { getListUnsubscribeAction } from './email-utils'; + +describe('getListUnsubscribeAction', () => { + it('parses a valid http List-Unsubscribe URL', () => { + expect( + getListUnsubscribeAction({ listUnsubscribe: '' }), + ).toEqual({ type: 'get', url: 'https://example.com/u?id=1', host: 'example.com' }); + }); + + it('returns null for an angle-bracketed value that is not a valid URL', () => { + // Regression: previously threw `TypeError: Invalid URL` because the primary + // `new URL(match[1])` path had no try/catch, unlike the fallback path. + expect(getListUnsubscribeAction({ listUnsubscribe: '' })).toBeNull(); + expect( + getListUnsubscribeAction({ listUnsubscribe: 'Click to unsubscribe' }), + ).toBeNull(); + }); +}); diff --git a/apps/server/src/lib/email-utils.ts b/apps/server/src/lib/email-utils.ts index 2ea009ef15..c0a305239a 100644 --- a/apps/server/src/lib/email-utils.ts +++ b/apps/server/src/lib/email-utils.ts @@ -45,7 +45,13 @@ export const getListUnsubscribeAction = ({ } // NOTE: List-Unsubscribe can contain multiple URLs, but the spec says to process the first one we can. - const url = new URL(match[1]); + let url: URL; + try { + url = new URL(match[1]); + } catch { + // The angle-bracket content is not a valid URL (malformed or placeholder header). + return null; + } if (url.protocol.startsWith('http')) { return processHttpUrl(url, listUnsubscribePost);