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 { promises as 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, next) => {
(async () => {
const name = req.body.name;
const identifier = createIdentifier();
await fs.writeFile(
feedPath(identifier),
renderXML(Feed({ name, identifier }))
);
res.send(
renderHTML(
“{name}” Inbox Created
)
);
})().catch(next);
})
.listen(8000);
export const emailServer = new SMTPServer({
disabledCommands: ["AUTH", "STARTTLS"],
onData(stream, session, callback) {
(async () => {
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
...(typeof email.html === "boolean"
? { html: false, content: email.text ?? "" }
: { content: email.html })
});
for (const { address } of session.envelope.rcptTo) {
const match = address.match(/^(\w+)@kill-the-newsletter.com$/);
if (match === null) continue;
const identifier = match[1];
const path = feedPath(identifier);
const xmlText = await fs.readFile(path, "utf8").catch(() => null);
if (xmlText === null) continue;
const xml = await new xml2js.Parser().parseStringPromise(xmlText);
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();
await fs.writeFile(path, renderXML(xml));
}
callback();
})().catch(error => {
console.error(error);
stream.resume();
callback(error);
});
}
}).listen(process.env.NODE_ENV === "production" ? 25 : 2525);
function Layout({ children }: { children: React.ReactNode }) {
return (
Kill the Newsletter!
{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,
html
}: {
title: string;
author: string;
content: string;
html?: boolean;
}) {
return {
entry: {
id: urn(createIdentifier()),
title,
author: { name: author },
updated: now(),
content: {
...(html === false ? {} : { $: { type: "html" } }),
_: content
}
}
};
}
function createIdentifier(): 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);
}