← back to NationalPaperHangers
routes/unsubscribe.js
71 lines
// Public unsubscribe endpoint. CAN-SPAM requires that opt-out be functional
// without requiring login or a per-message reply.
//
// GET /unsubscribe?token=… → render confirmation page (and pre-process the opt-out
// so the user sees the result on first paint).
// POST /unsubscribe?token=… → idempotent — adds the recipient to comms_suppression.
// Also handles RFC 8058 "List-Unsubscribe-Post" 1-click flow.
//
// Skips CSRF since the One-Click flow can't include a CSRF token (it's a
// mailclient-initiated POST). The HMAC token IS the auth.
const express = require('express');
const compliance = require('../lib/compliance');
const router = express.Router();
router.use((req, res, next) => { req.skipCsrf = true; next(); });
router.get('/', async (req, res, next) => {
try {
const token = (req.query.token || '').trim();
if (!token) {
return res.status(400).render('public/error', {
title: 'Invalid unsubscribe link',
message: 'Missing token. If you got an unwanted email from us, reply to it and we will remove you manually.'
});
}
const row = await compliance.consumeUnsubscribeToken(token);
if (!row) {
return res.status(400).render('public/error', {
title: 'Unsubscribe link invalid',
message: 'This unsubscribe link is unrecognized. Email info@nationalpaperhangers.com and we will remove you within 24 hours.'
});
}
await compliance.addSuppression({
channel: row.channel,
identifier: row.identifier,
reason: 'unsubscribe',
source: 'unsub_link',
installerId: row.installer_id,
notes: row.campaign ? `campaign=${row.campaign}` : null
});
res.render('public/unsubscribed', {
title: 'Unsubscribed · National Paper Hangers',
identifier: row.identifier,
campaign: row.campaign
});
} catch (err) { next(err); }
});
// RFC 8058 1-click: mail client POSTs with body `List-Unsubscribe=One-Click`.
router.post('/', async (req, res, next) => {
try {
const token = (req.query.token || req.body.token || '').trim();
if (!token) return res.status(400).json({ error: 'missing_token' });
const row = await compliance.consumeUnsubscribeToken(token);
if (!row) return res.status(400).json({ error: 'invalid_token' });
await compliance.addSuppression({
channel: row.channel,
identifier: row.identifier,
reason: 'unsubscribe',
source: 'list_unsubscribe_post',
installerId: row.installer_id,
notes: row.campaign ? `campaign=${row.campaign}` : null
});
return res.status(200).json({ ok: true });
} catch (err) { next(err); }
});
module.exports = router;