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
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—
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.