import express from "express"; import { SMTPServer } from "smtp-server"; import mailparser from "mailparser"; import React from "react"; import ReactDOMServer from "react-dom/server"; import xml2js from "xml2js"; import fs from "fs"; import cryptoRandomString from "crypto-random-string"; export const webServer = express() .use(express.static("static")) .use(express.urlencoded({ extended: true })) .get("/", (req, res) => res.send( renderHTML(
) ) ) .post("/", (req, res) => { const name = req.body.name; const identifier = newIdentifier(); fs.writeFileSync( feedPath(identifier), renderXML(Feed({ name, identifier })) ); res.send( renderHTML(

“{name}” Inbox Created

) ); }) .listen(8000, "localhost"); export const emailServer = new SMTPServer({ disabledCommands: ["AUTH", "STARTTLS"], 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 identifier = match[1]; const path = feedPath(identifier); if (!fs.existsSync(path)) return []; return [path]; }); if (paths.length === 0) return callback(); const email = await mailparser.simpleParser(stream); const { entry } = Entry({ title: email.subject, author: email.from.text, // FIXME: https://github.com/DefinitelyTyped/DefinitelyTyped/pull/43234 / typeof email.html !== "boolean" => email.html !== false content: typeof email.html !== "boolean" ? email.html : email.textAsHtml }); for (const path of paths) { const xml = await new xml2js.Parser().parseStringPromise( fs.readFileSync(path, "utf8") ); 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(); } }).listen(process.env.NODE_ENV === "production" ? 25 : 2525); 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({ identifier }: { identifier: string }) { return ( <>

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

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

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

Enjoy your readings!

Create Another Inbox

); } function Feed({ name, identifier }: { name: string; identifier: string }) { return { feed: { $: { xmlns: "http://www.w3.org/2005/Atom" }, link: [ { $: { rel: "self", type: "application/atom+xml", href: feedURL(identifier) } }, { $: { rel: "alternate", type: "text/html", href: "https://www.kill-the-newsletter.com/" } } ], id: urn(identifier), title: name, subtitle: `Kill the Newsletter! Inbox: ${feedEmail( identifier )} → ${feedURL(identifier)}`, 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: urn(newIdentifier()), title, author: { name: author }, updated: now(), content: { $: { type: "html" }, _: content } } }; } function newIdentifier(): string { return cryptoRandomString({ length: 20, characters: "1234567890qwertyuiopasdfghjklzxcvbnm" }); } function now(): string { return new Date().toISOString(); } function feedPath(identifier: string): string { return `static/feeds/${identifier}.xml`; } function feedURL(identifier: string): string { return `https://www.kill-the-newsletter.com/feeds/${identifier}.xml`; } function feedEmail(identifier: string): string { return `${identifier}@kill-the-newsletter.com`; } function urn(identifier: string): string { return `urn:kill-the-newsletter:${identifier}`; } function renderHTML(component: React.ReactElement): string { return `\n${ReactDOMServer.renderToStaticMarkup(component)}`; } function renderXML(xml: object): string { return new xml2js.Builder().buildObject(xml); }