#!/usr/bin/env node 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"; import { sql, Database } from "@leafac/sqlite"; import databaseMigrate from "@leafac/sqlite-migration"; import javascript from "tagged-template-noop"; 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"); fs.ensureDirSync(rootDirectory); const database = new Database( path.join(rootDirectory, "kill-the-newsletter.db") ); databaseMigrate(database, [ sql` CREATE TABLE "feeds" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, "createdAt" TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, "updatedAt" TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, "reference" TEXT NOT NULL UNIQUE, "title" TEXT NOT NULL ); CREATE TABLE "entries" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, "createdAt" TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, "reference" TEXT NOT NULL UNIQUE, "feed" INTEGER NOT NULL REFERENCES "feeds", "title" TEXT NOT NULL, "author" TEXT NOT NULL, "content" TEXT NOT NULL ); `, ]); 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`
Convert email newsletters into Atom feeds
$${logo}
Error: Missing newsletter name. Try again.
` ) ); const reference = newReference(); const created = html`
Sign up for the newsletter with
${reference}@${webApplication.get("email host")}
Subscribe to the Atom feed at
${webApplication.get("url")}/feeds/${reference}.xml
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!
`; const feedId = database.run( sql`INSERT INTO "feeds" ("reference", "title") VALUES (${reference}, ${req.body.name})` ).lastInsertRowid; database.run( sql` INSERT INTO "entries" ("reference", "feed", "title", "author", "content") VALUES (${newReference()}, ${feedId}, ${`“${req.body.name}” inbox created`}, ${"Kill the Newsletter!"}, ${created}) ` ); res.send( layout(html`“${req.body.name}” inbox created $${created}
`) ); }); webApplication.get<{ feedReference: string }, HTML, {}, {}, {}>( "/feeds/:feedReference.xml", (req, res, next) => { const feed = database.get<{ id: number; updatedAt: string; title: string; }>( sql`SELECT "id", "updatedAt", "title" FROM "feeds" WHERE "reference" = ${req.params.feedReference}` ); if (feed === undefined) return next(); const entries = database.all<{ createdAt: string; reference: string; title: string; author: string; content: string; }>( sql` SELECT "createdAt", "reference", "title", "author", "content" FROM "entries" WHERE "feed" = ${feed.id} ` ); res .contentType("application/atom+xml") .header("X-Robots-Tag", "noindex") .send( html`404 Not found
`) ); }); const emailApplication = 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( `^(?