diff --git a/scripts/newsletter-lib.mjs b/scripts/newsletter-lib.mjs index 4bcf0474382..083ca44c9b6 100644 --- a/scripts/newsletter-lib.mjs +++ b/scripts/newsletter-lib.mjs @@ -113,11 +113,13 @@ const send = async (test = true) => { for (let sub of subscribers[lang]) { if (l > 0) { + const unsubGet = `https://freesewing.org${ + lang === 'en' ? '/' : '/' + lang + '/' + }newsletter/unsubscribe?x=${sub.ehash}` + const unsubPost = `https://backend3.freesewing.org/ocunsub/${sub.ehash}` const body = mustache.render(template, { ...i18n[lang], - unsubscribe: `https://freesewing.org${ - lang === 'en' ? '/' : '/' + lang + '/' - }newsletter/unsubscribe?x=${sub.ehash}`, + unsubscribe: unsubGet, content, }) console.log(`[${lang}] ${l}/${subs} (${i}) Sending to ${sub.email}`) @@ -141,6 +143,16 @@ const send = async (test = true) => { Charset: 'utf-8', Data: i18n[lang].title, }, + Headers: [ + { + Name: 'List-Unsubscribe', + Value: unsubPost, + }, + { + Name: 'List-Unsubscribe-Post', + Value: 'List-Unsubscribe=One-Click', + }, + ], }, }, Destination: { diff --git a/sites/backend/src/controllers/subscribers.mjs b/sites/backend/src/controllers/subscribers.mjs index 827783180f1..88f2adf065b 100644 --- a/sites/backend/src/controllers/subscribers.mjs +++ b/sites/backend/src/controllers/subscribers.mjs @@ -1,4 +1,7 @@ import { SubscriberModel } from '../models/subscriber.mjs' +// Catch-all page +import { html as ocunsubOk } from '../html/ocunsub-ok.mjs' +import { html as ocunsubKo } from '../html/ocunsub-ko.mjs' export function SubscribersController() {} @@ -47,12 +50,16 @@ SubscribersController.prototype.confirm = async (req, res, tools) => { } /* - * Unsubscribe from the newsletter + * One-Click unsubscribe from the newsletter * See: https://freesewing.dev/reference/backend/api */ -SubscribersController.unsubscribe = async (req, res, tools) => { - const Subscriber = new SubscriberModel(tools) - await Subscriber.unsubscribe(req) +SubscribersController.prototype.ocunsub = async (req, res, tools) => { + if (!res.params?.ehash) return res.set('Content-Type', 'text/html').status(200).send(ocunsubKo) - return Subscriber.sendResponse(res) + const Subscriber = new SubscriberModel(tools) + const result = await Subscriber.ocunsub(req) + + if (result) return res.set('Content-Type', 'text/html').status(200).send(ocunsubOk) + + return res.set('Content-Type', 'text/html').status(200).send(okunsubKo) } diff --git a/sites/backend/src/html/ocunsub-ko.mjs b/sites/backend/src/html/ocunsub-ko.mjs new file mode 100644 index 00000000000..ac9b6932335 --- /dev/null +++ b/sites/backend/src/html/ocunsub-ko.mjs @@ -0,0 +1,9 @@ +import { wrapper } from './shared.mjs' + +export const html = wrapper({ + content: ` +
Whatever you intended to do, it did not work.
+ +`, +}) diff --git a/sites/backend/src/html/ocunsub-ok.mjs b/sites/backend/src/html/ocunsub-ok.mjs new file mode 100644 index 00000000000..160f0110fb7 --- /dev/null +++ b/sites/backend/src/html/ocunsub-ok.mjs @@ -0,0 +1,8 @@ +import { wrapper } from './shared.mjs' + +export const html = wrapper({ + content: ` +You have been unsubscribed.
+`, +}) diff --git a/sites/backend/src/models/subscriber.mjs b/sites/backend/src/models/subscriber.mjs index 458335508d6..4be6968f0eb 100644 --- a/sites/backend/src/models/subscriber.mjs +++ b/sites/backend/src/models/subscriber.mjs @@ -156,9 +156,9 @@ SubscriberModel.prototype.unsubscribe = async function ({ params }) { return this.setResponse(204) } else { - /* - * If not, perhaps it's an account ehash rather than subscriber ehash - */ + /* + * If not, perhaps it's an account ehash rather than subscriber ehash + */ await this.User.read({ ehash }) if (this.User.record) { await this.User.update({ newsletter: false }) @@ -173,6 +173,43 @@ SubscriberModel.prototype.unsubscribe = async function ({ params }) { return this.setResponse(404) } +/* + * One-click unsubscribe a user + * This is an unauthenticated route (has to for newsletter subscribers might not be users) + * + * @param {body} object - The request body + * @returns {SubscriberModal} object - The SubscriberModel + */ +SubscriberModel.prototype.ocunsub = async function ({ params }) { + const { ehash } = params + + /* + * Find the subscription record + */ + await this.read({ ehash }) + + /* + * If found, remove the record + */ + if (this.record) { + await this.delete({ id: this.record.id }) + + return true + } else { + /* + * If not, perhaps it's an account ehash rather than subscriber ehash + */ + await this.User.read({ ehash }) + if (this.User.record) { + await this.User.update({ newsletter: false }) + + return true + } + } + + return false +} + /* * A helper method to validate input and load the subscription record * diff --git a/sites/backend/src/routes/subscribers.mjs b/sites/backend/src/routes/subscribers.mjs index 4381eb474f8..2950d36217b 100644 --- a/sites/backend/src/routes/subscribers.mjs +++ b/sites/backend/src/routes/subscribers.mjs @@ -17,4 +17,13 @@ export function subscribersRoutes(tools) { // Unsubscribe from newsletter app.delete('/subscriber/:ehash', (req, res) => Subscriber.unsubscribe(req, res, tools)) + + // One-Click unsubscribe (ocunsub) from newsletter needs to be a POST request. + // See https://datatracker.ietf.org/doc/html/rfc8058 + app.post('/ocunsub/:ehash', (req, res) => Subscriber.ocunsub(req, res, tools)) + + // Just in case somebody lands here with a GET request + app.get('/ocunsub/:ehash', (req, res) => + res.redirect(`https://freesewing.org/newsletter/unsubscribe?i=${req.params.ehash}`) + ) }