From a28517868d1de1da4c21d32f83347495477f7ef7 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Sat, 5 Sep 2026 15:05:19 -0700 Subject: [PATCH] revert: drop the business postal address from every email footer (#1021) Reverts commit 8c56a92d. The postal address must not appear on any user-facing surface. Recipient email footers return to the signature and unsubscribe line only, the BUSINESS_POSTAL_ADDRESS constant is removed, and the design spec no longer records the constraint. Co-Authored-By: Claude Fable 5.1 --- apps/lifecycle/src/campaign/send.spec.ts | 20 +++---------------- apps/lifecycle/src/campaign/send.ts | 9 +++------ apps/lifecycle/src/campaign/templates.ts | 10 ---------- ...-threadplane-growth-lifecycle-v1-design.md | 1 - 4 files changed, 6 insertions(+), 34 deletions(-) diff --git a/apps/lifecycle/src/campaign/send.spec.ts b/apps/lifecycle/src/campaign/send.spec.ts index 377540a82..41d777163 100644 --- a/apps/lifecycle/src/campaign/send.spec.ts +++ b/apps/lifecycle/src/campaign/send.spec.ts @@ -26,7 +26,7 @@ import { type LifecycleJobDependencies, } from './send.js'; import { DeterministicLifecycleJobError } from '../job-errors.js'; -import { BUSINESS_POSTAL_ADDRESS, FOUNDER_BOOKING_URL } from './templates.js'; +import { FOUNDER_BOOKING_URL } from './templates.js'; const NOW = new Date('2026-09-01T12:03:00.000Z'); const CONTACT_ID = '00000000-0000-4000-8000-000000000002'; @@ -161,13 +161,6 @@ describe('prepareCampaignMessage', () => { if (prepared.status !== 'ready') throw new Error('expected ready'); expect(prepared.text).toContain(unsubscribeActionUrlValue(UNSUBSCRIBE)); expect(prepared.text).toContain('\n\n—\nBrian\n'); - expect( - prepared.text.endsWith( - `${unsubscribeActionUrlValue( - UNSUBSCRIBE - )}\n${BUSINESS_POSTAL_ADDRESS}` - ) - ).toBe(true); if (step === 3) expect(prepared.text).toContain('This is my last automated follow-up.'); } @@ -266,13 +259,6 @@ describe('prepareCampaignMessage', () => { '\n\n—\nBrian\n\nIs this email not relevant to you? Stop here: ' ); expect(prepared.text).toContain(unsubscribeActionUrlValue(UNSUBSCRIBE)); - expect( - prepared.text.endsWith( - `${unsubscribeActionUrlValue( - UNSUBSCRIBE - )}\n${BUSINESS_POSTAL_ADDRESS}` - ) - ).toBe(true); expect(prepared.text).not.toContain('ada@example.com'); } ); @@ -314,7 +300,7 @@ describe('prepareCampaignMessage', () => { expect(prepared.html).toContain('


Brian

'); expect( prepared.html.endsWith( - `

Is this email not relevant to you? Click here.

\n

${BUSINESS_POSTAL_ADDRESS}

` + `

Is this email not relevant to you? Click here.

` ) ).toBe(true); expect(prepared.html.split(unsubscribeUrl)).toHaveLength(2); @@ -323,7 +309,7 @@ describe('prepareCampaignMessage', () => { ); expect(prepared.html).toContain(`href="${unsubscribeUrl}">here`); expect(prepared.text).toContain( - `Is this email not relevant to you? Stop here: ${unsubscribeUrl}\n${BUSINESS_POSTAL_ADDRESS}` + `Is this email not relevant to you? Stop here: ${unsubscribeUrl}` ); }); diff --git a/apps/lifecycle/src/campaign/send.ts b/apps/lifecycle/src/campaign/send.ts index 85051546c..6aa934f7d 100644 --- a/apps/lifecycle/src/campaign/send.ts +++ b/apps/lifecycle/src/campaign/send.ts @@ -48,7 +48,6 @@ import { renderFulfillmentTemplate } from '../fulfillment/templates.js'; import { renderInternalNotificationSummary } from '../notifications/templates.js'; import { DeterministicLifecycleJobError } from '../job-errors.js'; import { - BUSINESS_POSTAL_ADDRESS, renderCampaignTemplate, renderEvidenceCampaignTemplate, type CampaignDraft, @@ -280,7 +279,7 @@ function signedText( ): string { return `${body}\n\n—\nBrian\n\nIs this email not relevant to you? Stop here: ${unsubscribeActionUrlValue( unsubscribeUrl - )}\n${BUSINESS_POSTAL_ADDRESS}`; + )}`; } const HTML_ESCAPES: Record = { @@ -316,9 +315,8 @@ function htmlLine(line: string): string { /** * A plain HTML alternative for the text part: the same paragraphs, bare HTTPS - * links as anchors, a one-word unsubscribe link instead of the long signed - * URL, and the business postal address as the final line. No layout, images, - * styles, or tracking. + * links as anchors, and a one-word unsubscribe link instead of the long signed + * URL. No layout, images, styles, or tracking. */ function signedHtml( body: string, @@ -333,7 +331,6 @@ function signedHtml( ...paragraphs, '


Brian

', `

Is this email not relevant to you? Click here.

`, - `

${escapeHtml(BUSINESS_POSTAL_ADDRESS)}

`, ].join('\n'); } diff --git a/apps/lifecycle/src/campaign/templates.ts b/apps/lifecycle/src/campaign/templates.ts index bc3a8bf2a..76c7d0aa4 100644 --- a/apps/lifecycle/src/campaign/templates.ts +++ b/apps/lifecycle/src/campaign/templates.ts @@ -21,16 +21,6 @@ const CampaignStepSchema = z.enum(['immediate', 'day-3', 'day-8']); export const FOUNDER_BOOKING_URL = 'https://calendar.app.google/nK961tWHZd21izKR6'; -/** - * The business postal address every recipient-facing email prints after its - * unsubscribe line. CAN-SPAM requires a valid physical postal address on - * commercial mail, so the footer in `send.ts` renders this single constant in - * both the text and HTML parts. Keep it on one line; the HTML part escapes it - * into a bare `

`. - */ -export const BUSINESS_POSTAL_ADDRESS = - '2843 NW Lolo Dr, Bend, OR 97703, United States'; - /** * Every link recipient copy may carry, across campaign steps and fulfillment * mail. The four PDFs are the whitepaper fulfillment deliverables. diff --git a/docs/superpowers/specs/2026-08-31-threadplane-growth-lifecycle-v1-design.md b/docs/superpowers/specs/2026-08-31-threadplane-growth-lifecycle-v1-design.md index d6f842420..0d1f70c28 100644 --- a/docs/superpowers/specs/2026-08-31-threadplane-growth-lifecycle-v1-design.md +++ b/docs/superpowers/specs/2026-08-31-threadplane-growth-lifecycle-v1-design.md @@ -674,7 +674,6 @@ apps/lifecycle should declare Dawn Core/CLI/LangGraph/Postgres Storage/SDK 0.8.2 Copy constraints: - All recipient fulfillment, welcome, acknowledgment, and campaign mail is authored as plain text. Since 2026-09-05 the sender also attaches a plain HTML alternative rendered from that text (paragraphs and HTTPS anchors only) so the unsubscribe footer can read “click here” instead of the long signed URL; the sender rejects any HTML part containing images, scripts, styles, forms, event handlers, or a missing unsubscribe anchor. -- Every recipient-facing email (campaign steps and fulfillment) ends with the business postal address on a single plain line directly after the unsubscribe line, in both the text and HTML parts, because CAN-SPAM requires a valid physical postal address on commercial mail. The address is sourced from the single exported constant `BUSINESS_POSTAL_ADDRESS` in the campaign templates; the HTML part renders it as one escaped `

` so the sender's paragraphs-and-links-only check still passes. - Internal research and operational notifications are plain text as well. - Each campaign step is at most 120 words. - At most one question and at most one useful link.