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!
{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);
}