import express from "express"; import { Server } from "http"; import { SMTPServer } from "smtp-server"; import { simpleParser } from "mailparser"; import React from "react"; import ReactDOMServer from "react-dom/server"; import { Builder, Parser } from "xml2js"; import fs from "fs"; import cryptoRandomString from "crypto-random-string"; const app = express(); if (process.env.NODE_ENV === "production") app.use((req, res, next) => { if ( req.protocol !== "https" || req.hostname !== "www.kill-the-newsletter.com" ) return res.redirect( 301, `https://www.kill-the-newsletter.com${req.originalUrl}` ); next(); }); app.use(express.static("static")); app.use(express.urlencoded({ extended: true })); app.get("/", (req, res) => res.send( renderHTML(
) ) ); app.post("/", (req, res) => { const inbox: Inbox = { name: req.body.name, token: newToken() }; fs.writeFileSync(feedPath(inbox.token), renderXML(Feed(inbox))); res.send( renderHTML( ) ); }); export const emailServer = new SMTPServer({ authOptional: true, async onData(stream, session, callback) { const paths = session.envelope.rcptTo.flatMap(({ address }) => { const match = address.match(/^(\w+)@kill-the-newsletter.com$/); if (match === null) return []; const token = match[1]; const path = feedPath(token); if (!fs.existsSync(path)) return []; return [path]; }); if (paths.length === 0) return callback(); const email = await simpleParser(stream); const { entry } = Entry({ title: email.subject, author: email.from.text, content: typeof email.html !== "boolean" ? email.html : email.textAsHtml }); for (const path of paths) { const xml = await new Parser().parseStringPromise( fs.readFileSync(path).toString() ); xml.feed.updated = now(); if (xml.feed.entry === undefined) xml.feed.entry = []; xml.feed.entry.unshift(entry); while (xml.feed.entry.length > 0 && renderXML(xml).length > 500_000) xml.feed.entry.pop(); fs.writeFileSync(path, renderXML(xml)); } callback(); } }); export const webServer = app.listen( process.env.NODE_ENV === "production" ? 80 : 8000 ); if (process.env.NODE_ENV === "production") { app.listen(443); } emailServer.listen(process.env.NODE_ENV === "production" ? 25 : 2525); type Inbox = { name: string; token: string; }; function Layout({ children }: { children: React.ReactNode }) { return ( Kill the Newsletter!

Kill the Newsletter!

Convert email newsletters into Atom feeds

Convert email newsletters into Atom feeds

{children}
); } function Form() { return (

); } function Created({ inbox: { name, token } }: { inbox: Inbox }) { return ( <>

“{name}” Inbox Created

Sign up for the newsletter with
{feedEmail(token)}

Subscribe to the Atom feed at
{feedURL(token)}

Don’t share these addresses.
They contain a security token that other people could use
to send you spam and to control your newsletter subscriptions.

Enjoy your readings!

Create Another Inbox

); } // https://validator.w3.org/feed/docs/atom.html // https://validator.w3.org/feed/#validate_by_input function Feed(inbox: Inbox) { const { name, token } = inbox; return { feed: { $: { xmlns: "http://www.w3.org/2005/Atom" }, link: [ { $: { rel: "self", type: "application/atom+xml", href: feedURL(token) } }, { $: { rel: "alternate", type: "text/html", href: "https://www.kill-the-newsletter.com/" } } ], id: id(token), title: name, subtitle: `Kill the Newsletter! Inbox “${feedEmail(token)}”`, updated: now(), ...Entry({ title: `“${name}” Inbox Created`, author: "Kill the Newsletter!", content: ReactDOMServer.renderToStaticMarkup( ) }) } }; } function Entry({ title, author, content }: { title: string; author: string; content: string; }) { return { entry: { id: id(newToken()), title, author: { name: author }, updated: now(), content: { $: { type: "html" }, _: content } } }; } function newToken() { return cryptoRandomString({ length: 20, characters: "1234567890qwertyuiopasdfghjklzxcvbnm" }); } function now() { return new Date().toISOString(); } export function feedPath(token: string) { return `static/feeds/${token}.xml`; } function feedURL(token: string) { return `https://www.kill-the-newsletter.com/feeds/${token}.xml`; } export function feedEmail(token: string) { return `${token}@kill-the-newsletter.com`; } function id(token: string) { return `urn:kill-the-newsletter:${token}`; } function renderHTML(component: React.ReactElement): string { return `\n${ReactDOMServer.renderToStaticMarkup(component)}`; } function renderXML(xml: object): string { return new Builder().buildObject(xml); }