Skip to content

1.9.0: coupons@profullstack.com intake (Forward Email webhook) - #76

Merged
ralyodio merged 1 commit into
masterfrom
feat/inbound-email-coupons
Oct 6, 2026
Merged

ralyodio merged 1 commit into
masterfrom
feat/inbound-email-coupons

Conversation

@ralyodio

@ralyodio ralyodio commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

What

Mail to coupons@profullstack.com becomes coupons on c0upons.com.

POST /api/webhooks/email receives Forward Email's webhook JSON (the parsed message plus DKIM/SPF/DMARC verdicts) for the alias. For each message it:

  1. Refuses bounces and auto-replies, mail where neither DKIM nor SPF passed (so nobody can forge a "store" with fake codes), and Message-IDs it has already processed (Forward Email retries until it gets a 200).
  2. Confirms double opt-in signups: if the subject or text reads as a subscription confirmation, it GETs the confirm/verify/activate link once (at most two tries). Unsubscribe, preferences and view-in-browser links are never clicked, and neither is any host that resolves to a private address.
  3. Extracts offers with claude-haiku-4-5 structured output, falling back to the Reddit source's rules (extractCode, parseDiscount) when there is no key or the model refuses. A code only counts if it appears verbatim in the mail. An offer is usable if it has a code, or a discount and a link.
  4. Dedupes a code against existing coupons at the same store (matched by slug or website, any source, any case, so BBW26/PROOF50 posted by hand count). A sale with no code is a duplicate if the same title was posted in the last 30 days.
  5. Posts with source 'email', source_id '<message hash>:<n>'.
  6. Logs every message in the new inbound_emails table with its outcome: posted, duplicate, confirmed, confirm-failed, no-offer, unauthenticated, ignored or error.

Auth is ?key=$INBOUND_EMAIL_SECRET in the alias's webhook URL, or X-Webhook-Signature when FORWARDEMAIL_WEBHOOK_KEY is set. With neither set, every request gets a 401, so this is safe to deploy before the secret exists.

Surfaces: API (GET /api/webhooks/email with Bearer secret lists the log), the shipped shell CLI and the TS CLI (c0upons inbox).

Ops still needed

  • INBOUND_EMAIL_SECRET in ~/www/c0upons.com/app.env on dev2 + vault c0upons--prod
  • Forward Email alias coupons@profullstack.com → https://c0upons.com/api/webhooks/email?key=<secret> (dashboard only: no API token exists)

Tests

test/inbound-email.test.mjs (11 cases): signature check, rules, a code the model made up gets dropped, post then reprocess then duplicate, a cross-store duplicate of a hand-posted code, no-offer, forged mail, opt-in click (and not the unsubscribe link), promo "verify" link not clicked, private-address refusal. Full suite: 70 pass; typecheck clean.

🤖 Generated with Claude Code

POST /api/webhooks/email takes Forward Email's parsed-message JSON for the
coupons@ alias (auth: ?key=INBOUND_EMAIL_SECRET, or X-Webhook-Signature when
FORWARDEMAIL_WEBHOOK_KEY is set). Each message is refused when neither DKIM
nor SPF passed, its subscription-confirm link is followed when it is a
double opt-in mail (never unsubscribe/preferences links, never private
addresses), or its offers are extracted (claude-haiku-4-5 with structured
output, rules fallback), checked (a code must appear verbatim in the mail),
deduped against existing codes at the same store, and posted with source
'email'. Every message is logged in inbound_emails with its outcome;
GET on the same route and `c0upons inbox` read the log.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
const key = messageKey(mail);
const result: InboundResult = { ok: true, outcome: 'no-offer', message_key: key, posted: [], duplicates: [], skipped: [], clicked: [] };

const prior = await db.sql`SELECT outcome FROM inbound_emails WHERE message_key = ${key} LIMIT 1`;
test('a code already posted by hand under a differently named store is a duplicate', async () => {
await db.sql`INSERT INTO stores (name, slug, website) VALUES ('The Proof Peptides', 'the-proof-peptides', 'https://theproof.com')`;
const [{ id }] = await db.sql`SELECT id FROM stores WHERE slug = 'the-proof-peptides'`;
await db.sql`INSERT INTO coupons (store_id, code, title) VALUES (${id}, 'PROOF50', 'PROOF50')`;
@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown

ThreatCrush Security Scan

31 finding(s)

MEDIUM: 28 | LOW: 3

Severity Rule Location
MEDIUM sql-template-interpolation apps/web/app/api/bounties/[id]/claim/route.ts:18
MEDIUM sql-template-interpolation apps/web/app/api/bounties/[id]/claim/route.ts:34
MEDIUM sql-template-interpolation apps/web/app/api/bounties/[id]/claim/route.ts:43
MEDIUM sql-template-interpolation apps/web/app/api/bounties/route.ts:81
MEDIUM sql-template-interpolation apps/web/app/api/coupons/route.ts:68
MEDIUM sql-template-interpolation apps/web/app/api/coupons/route.ts:75
MEDIUM sql-template-interpolation apps/web/app/api/coupons/vote/route.ts:31
MEDIUM sql-template-interpolation apps/web/app/api/coupons/vote/route.ts:33
MEDIUM sql-template-interpolation apps/web/app/api/stores/[slug]/route.ts:9
MEDIUM sql-template-interpolation apps/web/app/api/webhooks/crawlproof/route.ts:83
MEDIUM sql-template-interpolation apps/web/app/api/webhooks/crawlproof/route.ts:90
MEDIUM js-open-redirect apps/web/app/bounties/new/page.tsx:57
MEDIUM js-unescaped-html-sink apps/web/app/coupons/[id]/page.tsx:57
MEDIUM js-unescaped-html-sink apps/web/app/layout.tsx:81
MEDIUM sql-template-interpolation apps/web/app/stores/[slug]/page.tsx:15
MEDIUM sql-template-interpolation apps/web/lib/flipp-sync.ts:445
MEDIUM sql-template-interpolation apps/web/lib/inbound-email.ts:570
MEDIUM sql-template-interpolation apps/web/lib/mcp-tools.ts:81
MEDIUM sql-template-interpolation apps/web/lib/nichedb-sync.ts:186
MEDIUM sql-template-interpolation apps/web/lib/nichedb-sync.ts:206
MEDIUM sql-template-interpolation apps/web/lib/reddit-sync.ts:336
MEDIUM sql-template-interpolation apps/web/lib/reddit-sync.ts:355
MEDIUM sql-template-interpolation apps/web/lib/reddit-sync.ts:361
MEDIUM sql-template-interpolation apps/web/lib/reveal-coupon.ts:115
MEDIUM sql-template-interpolation apps/web/lib/reveal-coupon.ts:170
MEDIUM sh-predictable-temp-path apps/web/public/install.sh:37
MEDIUM sh-predictable-temp-path apps/web/public/install.sh:39
MEDIUM sql-template-interpolation apps/web/scripts/migrate.mjs:192
LOW sql-template-interpolation test/inbound-email.test.mjs:139
LOW sql-template-interpolation test/reveal-sweep.test.mjs:69
LOW sql-template-interpolation test/reveal-sweep.test.mjs:112

Snippets are redacted; ThreatCrush never prints matched credential material.

@ralyodio
ralyodio merged commit f1bc6ea into master Oct 6, 2026
5 checks passed
@ralyodio
ralyodio deleted the feat/inbound-email-coupons branch October 6, 2026 05:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants