import path from "path";
import express from "express";
import { SMTPServer } from "smtp-server";
import mailparser from "mailparser";
import escapeStringRegexp from "escape-string-regexp";
import fs from "fs-extra";
import cryptoRandomString from "crypto-random-string";
import { html, HTML } from "@leafac/html";
import { css, process as processCSS } from "@leafac/css";
const VERSION = require("../package.json").version;
export default function killTheNewsletter(
rootDirectory: string
): { webApplication: express.Express; emailApplication: SMTPServer } {
const webApplication = express();
webApplication.set("url", "http://localhost:4000");
webApplication.set("email port", 2525);
webApplication.set("email host", "localhost");
webApplication.set("administrator", "mailto:kill-the-newsletter@leafac.com");
webApplication.use(express.static(path.join(__dirname, "../public")));
webApplication.use(express.urlencoded({ extended: true }));
const logo = fs.readFileSync(path.join(__dirname, "../public/logo.svg"));
function layout(body: HTML): HTML {
return processCSS(html`
Kill the Newsletter!
$${body}
`);
}
webApplication.get<{}, HTML, {}, {}, {}>("/", (req, res) => {
res.send(
layout(html`
`)
);
});
const emailApplication = new SMTPServer();
return { webApplication, emailApplication };
}
if (require.main === module) {
console.log(`Kill the Newsletter!/${VERSION}`);
const configurationFile = path.resolve(
process.argv[2] ?? path.join(process.cwd(), "configuration.js")
);
require(configurationFile)(require);
console.log(`Configuration file loaded from ‘${configurationFile}’.`);
}
/*
export const webServer = express()
.use(["/feeds", "/alternate"], (req, res, next) => {
res.header("X-Robots-Tag", "noindex");
next();
})
.get("/", (req, res) => res.send(layout(newInbox())))
.post("/", async (req, res, next) => {
try {
const { name } = req.body;
const identifier = createIdentifier();
const renderedCreated = created(identifier);
await writeFileAtomic(
feedFilePath(identifier),
feed(
identifier,
X(name),
entry(
identifier,
createIdentifier(),
`“${X(name)}” Inbox Created`,
"Kill the Newsletter!",
X(renderedCreated)
)
)
);
res.send(
layout(html`
“${H(name)}” Inbox Created
${renderedCreated}
`)
);
} catch (error) {
console.error(error);
next(error);
}
})
.get(
alternatePath(":feedIdentifier", ":entryIdentifier"),
async (req, res, next) => {
try {
const { feedIdentifier, entryIdentifier } = req.params;
const path = feedFilePath(feedIdentifier);
let text;
try {
text = await fs.readFile(path, "utf8");
} catch {
return res.sendStatus(404);
}
const feed = new JSDOM(text, { contentType: "text/xml" });
const document = feed.window.document;
const link = document.querySelector(
`link[href="${alternateURL(feedIdentifier, entryIdentifier)}"]`
);
if (link === null) return res.sendStatus(404);
res.send(
entities.decodeXML(
link.parentElement!.querySelector("content")!.textContent!
)
);
} catch (error) {
console.error(error);
next(error);
}
}
)
.listen(WEB_PORT, () => console.log(`Server started: ${webApplication.get("url")}`));
export const emailServer = new SMTPServer({
disabledCommands: ["AUTH", "STARTTLS"],
async onData(stream, session, callback) {
try {
const email = await mailparser.simpleParser(stream);
const content =
typeof email.html === "string" ? email.html : email.textAsHtml ?? "";
for (const address of new Set(
session.envelope.rcptTo.map(({ address }) => address)
)) {
const match = address.match(
new RegExp(
`^(?\\w+)@${escapeStringRegexp(EMAIL_DOMAIN)}$`
)
);
if (match?.groups === undefined) continue;
const identifier = match.groups.identifier.toLowerCase();
const path = feedFilePath(identifier);
let text;
try {
text = await fs.readFile(path, "utf8");
} catch {
continue;
}
const feed = new JSDOM(text, { contentType: "text/xml" });
const document = feed.window.document;
const updated = document.querySelector("feed > updated");
if (updated === null) {
console.error(`Field ‘updated’ not found: ‘${path}’`);
continue;
}
updated.textContent = now();
const renderedEntry = entry(
identifier,
createIdentifier(),
X(email.subject ?? ""),
X(email.from?.text ?? ""),
X(content)
);
const firstEntry = document.querySelector("feed > entry:first-of-type");
if (firstEntry === null)
document
.querySelector("feed")!
.insertAdjacentHTML("beforeend", renderedEntry);
else firstEntry.insertAdjacentHTML("beforebegin", renderedEntry);
while (feed.serialize().length > 500_000) {
const lastEntry = document.querySelector("feed > entry:last-of-type");
if (lastEntry === null) break;
lastEntry.remove();
}
await writeFileAtomic(
path,
html`${feed.serialize()}`.trim()
);
}
callback();
} catch (error) {
console.error(
`Failed to receive message: ‘${JSON.stringify(session, null, 2)}’`
);
console.error(error);
stream.resume();
callback(new Error("Failed to receive message. Please try again."));
}
},
}).listen(EMAIL_PORT);
function layout(content: string): string {
return html`
Kill the Newsletter!
${content}
`.trim();
}
function newInbox(): string {
return html`
`;
}
function created(identifier: string): string {
return html`
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
`.trim();
}
function feed(identifier: string, name: string, initialEntry: string): string {
return html`
${urn(identifier)}
${name}
Kill the Newsletter! Inbox: ${feedEmail(identifier)} →
${feedURL(identifier)}
${now()}
Kill the Newsletter!
${initialEntry}
`.trim();
}
function entry(
feedIdentifier: string,
entryIdentifier: string,
title: string,
author: string,
content: string
): string {
return html`
${urn(entryIdentifier)}
${title}
${author}
${now()}
${content}
`.trim();
}
function createIdentifier(): string {
return cryptoRandomString({
length: 16,
characters: "1234567890qwertyuiopasdfghjklzxcvbnm",
});
}
function now(): string {
return new Date().toISOString();
}
function feedFilePath(identifier: string): string {
return `static/feeds/${identifier}.xml`;
}
function feedURL(identifier: string): string {
return `${webApplication.get("url")}/feeds/${identifier}.xml`;
}
function feedEmail(identifier: string): string {
return `${identifier}@${EMAIL_DOMAIN}`;
}
function alternatePath(
feedIdentifier: string,
entryIdentifier: string
): string {
return `/alternate/${feedIdentifier}/${entryIdentifier}.html`;
}
function alternateURL(feedIdentifier: string, entryIdentifier: string): string {
return `${webApplication.get("url")}${alternatePath(feedIdentifier, entryIdentifier)}`;
}
function urn(identifier: string): string {
return `urn:kill-the-newsletter:${identifier}`;
}
function X(string: string): string {
return entities.encodeXML(sanitizeXMLString.sanitize(string));
}
function H(string: string): string {
return entities.encodeHTML(sanitizeXMLString.sanitize(string));
}
*/