diff --git a/TODO/index.ts b/TODO/index.ts deleted file mode 100644 index cfc3ca9..0000000 --- a/TODO/index.ts +++ /dev/null @@ -1,517 +0,0 @@ -#!/usr/bin/env node - -import path from "path"; -import express from "express"; -import { SMTPServer } from "smtp-server"; -import mailparser from "mailparser"; -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 javascript from "tagged-template-noop"; -import { sql, Database } from "@leafac/sqlite"; -import databaseMigrate from "@leafac/sqlite-migration"; - -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", "smtp://localhost:2525"); - 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 - ); - `, - sql` - CREATE INDEX "entriesFeed" ON "entries" ("feed"); - `, - ]); - - 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! - - -
-

- Kill the Newsletter! -

-

Convert email newsletters into Atom feeds

-

- $${logo} -

-
-
$${body}
- - - - - `); - } - - webApplication.get<{}, HTML, {}, {}, {}>("/", (req, res) => { - res.send( - layout(html` -
-

- -

-

- -

-
- `) - ); - }); - - webApplication.post<{}, HTML, { name?: string }, {}, {}>("/", (req, res) => { - if ( - typeof req.body.name !== "string" || - req.body.name.trim() === "" || - req.body.name.length > 500 - ) - return res.status(422).send( - layout( - html` -

- Error: Missing newsletter name. - Try again. -

- ` - ) - ); - - const feedReference = newReference(); - const welcomeTitle = `“${req.body.name}” inbox created`; - const welcomeContent = html` -

- Sign up for the newsletter with
- ${feedReference}@${new URL(webApplication.get("email")) - .hostname} -

-

- Subscribe to the Atom feed at
- ${webApplication.get("url")}/feeds/${feedReference}.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!

-

- Create another inbox -

- `; - - database.executeTransaction(() => { - const feedId = database.run( - sql`INSERT INTO "feeds" ("reference", "title") VALUES (${feedReference}, ${req.body.name})` - ).lastInsertRowid; - database.run( - sql` - INSERT INTO "entries" ("reference", "feed", "title", "author", "content") - VALUES ( - ${newReference()}, - ${feedId}, - ${welcomeTitle}, - ${"Kill the Newsletter!"}, - ${welcomeContent} - ) - ` - ); - }); - - res.send( - layout(html` -

${welcomeTitle}

- $${welcomeContent} - `) - ); - }); - - function renderFeed(feedReference: string): HTML | undefined { - const feed = database.get<{ - id: number; - updatedAt: string; - title: string; - }>( - sql`SELECT "id", "updatedAt", "title" FROM "feeds" WHERE "reference" = ${feedReference}` - ); - if (feed === undefined) return; - - 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} - ORDER BY "id" DESC - ` - ); - - return html` - - - - - urn:kill-the-newsletter:${feedReference} - ${feed.title} - Kill the Newsletter! Inbox: - ${feedReference}@${new URL(webApplication.get("email")).hostname} → - ${webApplication.get("url")}/feeds/${feedReference}.xml - ${new Date(feed.updatedAt).toISOString()} - Kill the Newsletter! - $${entries.map( - (entry) => html` - - urn:kill-the-newsletter:${entry.reference} - ${entry.title} - ${entry.author} - ${new Date(entry.createdAt).toISOString()} - - ${entry.content} - - ` - )} - - `.trim(); - } - - webApplication.get<{ feedReference: string }, HTML, {}, {}, {}>( - "/feeds/:feedReference.xml", - (req, res, next) => { - const feed = renderFeed(req.params.feedReference); - if (feed === undefined) return next(); - res.type("atom").header("X-Robots-Tag", "noindex").send(feed); - } - ); - - webApplication.get<{ entryReference: string }, HTML, {}, {}, {}>( - "/alternates/:entryReference.html", - (req, res, next) => { - const entry = database.get<{ content: string }>( - sql`SELECT "content" FROM "entries" WHERE "reference" = ${req.params.entryReference}` - ); - if (entry === undefined) return next(); - res.header("X-Robots-Tag", "noindex").send(entry.content); - } - ); - - webApplication.use((req, res) => { - res.send( - layout(html` -

404 Not found

-

- Create a new inbox -

- `) - ); - }); - - const emailApplication = new SMTPServer({ - disabledCommands: ["AUTH", "STARTTLS"], - async onData(stream, session, callback) { - try { - const email = await mailparser.simpleParser(stream); - const from = email.from?.text ?? ""; - const subject = email.subject ?? ""; - const body = - typeof email.html === "string" ? email.html : email.textAsHtml ?? ""; - database.executeTransaction(() => { - for (const address of new Set( - session.envelope.rcptTo.map( - (smtpServerAddress) => smtpServerAddress.address - ) - )) { - const addressParts = address.split("@"); - if (addressParts.length !== 2) continue; - const [feedReference, hostname] = addressParts; - if (hostname !== new URL(webApplication.get("email")).hostname) - continue; - const feed = database.get<{ id: number }>( - sql`SELECT "id" FROM "feeds" WHERE "reference" = ${feedReference}` - ); - if (feed === undefined) continue; - database.run( - sql` - INSERT INTO "entries" ("reference", "feed", "title", "author", "content") - VALUES ( - ${newReference()}, - ${feed.id}, - ${subject}, - ${from}, - ${body} - ) - ` - ); - database.run( - sql`UPDATE "feeds" SET "updatedAt" = CURRENT_TIMESTAMP WHERE "id" = ${feed.id}` - ); - while (renderFeed(feedReference)!.length > 500_000) - database.run( - sql`DELETE FROM "entries" WHERE "feed" = ${feed.id} ORDER BY "id" ASC LIMIT 1` - ); - } - }); - 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.")); - } - }, - }); - - function newReference(): string { - return cryptoRandomString({ - length: 16, - characters: "abcdefghijklmnopqrstuvwxyz0123456789", - }); - } - - return { webApplication, emailApplication }; -} - -if (require.main === module) { - console.log(`Kill the Newsletter!/${VERSION}`); - if (process.argv[2] === undefined) { - const { webApplication, emailApplication } = killTheNewsletter( - path.join(process.cwd(), "data") - ); - webApplication.listen(new URL(webApplication.get("url")).port, () => { - console.log(`Web server started at ${webApplication.get("url")}`); - }); - emailApplication.listen(new URL(webApplication.get("email")).port, () => { - console.log(`Email server started at ${webApplication.get("email")}`); - }); - } else { - const configurationFile = path.resolve(process.argv[2]); - require(configurationFile)(require); - console.log(`Configuration loaded from ‘${configurationFile}’.`); - } -} diff --git a/server/index.mts b/server/index.mts index 242e4d8..bc27eef 100644 --- a/server/index.mts +++ b/server/index.mts @@ -291,27 +291,523 @@ await commander.program application.configuration.hstsPreload ??= false; application.configuration.caddy ??= caddyfile``; - // application.server.locals.middleware = {} as any; - // application.server.locals.helpers = {} as any; + // #!/usr/bin/env node - await logging(application); - await database(application); - await healthChecks(application); - await base(application); - // await liveUpdates(application); - // await authentication(application); - // await layouts(application); - // await about(application); - // await administration(application); - // await user(application); - // await course(application); - // await conversation(application); - // await message(application); - // await content(application); - // await email(application); - // await demonstration(application); - // await error(application); - // await helpers(application); + // import path from "path"; + // import express from "express"; + // import { SMTPServer } from "smtp-server"; + // import mailparser from "mailparser"; + // 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 javascript from "tagged-template-noop"; + // import { sql, Database } from "@leafac/sqlite"; + // import databaseMigrate from "@leafac/sqlite-migration"; + + // 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", "smtp://localhost:2525"); + // 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 + // ); + // `, + // sql` + // CREATE INDEX "entriesFeed" ON "entries" ("feed"); + // `, + // ]); + + // 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! + // + // + //
+ //

+ // Kill the Newsletter! + //

+ //

Convert email newsletters into Atom feeds

+ //

+ // $${logo} + //

+ //
+ //
$${body}
+ // + // + // + // + // `); + // } + + // webApplication.get<{}, HTML, {}, {}, {}>("/", (req, res) => { + // res.send( + // layout(html` + //
+ //

+ // + //

+ //

+ // + //

+ //
+ // `) + // ); + // }); + + // webApplication.post<{}, HTML, { name?: string }, {}, {}>("/", (req, res) => { + // if ( + // typeof req.body.name !== "string" || + // req.body.name.trim() === "" || + // req.body.name.length > 500 + // ) + // return res.status(422).send( + // layout( + // html` + //

+ // Error: Missing newsletter name. + // Try again. + //

+ // ` + // ) + // ); + + // const feedReference = newReference(); + // const welcomeTitle = `“${req.body.name}” inbox created`; + // const welcomeContent = html` + //

+ // Sign up for the newsletter with
+ // ${feedReference}@${new URL(webApplication.get("email")) + // .hostname} + //

+ //

+ // Subscribe to the Atom feed at
+ // ${webApplication.get("url")}/feeds/${feedReference}.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!

+ //

+ // Create another inbox + //

+ // `; + + // database.executeTransaction(() => { + // const feedId = database.run( + // sql`INSERT INTO "feeds" ("reference", "title") VALUES (${feedReference}, ${req.body.name})` + // ).lastInsertRowid; + // database.run( + // sql` + // INSERT INTO "entries" ("reference", "feed", "title", "author", "content") + // VALUES ( + // ${newReference()}, + // ${feedId}, + // ${welcomeTitle}, + // ${"Kill the Newsletter!"}, + // ${welcomeContent} + // ) + // ` + // ); + // }); + + // res.send( + // layout(html` + //

${welcomeTitle}

+ // $${welcomeContent} + // `) + // ); + // }); + + // function renderFeed(feedReference: string): HTML | undefined { + // const feed = database.get<{ + // id: number; + // updatedAt: string; + // title: string; + // }>( + // sql`SELECT "id", "updatedAt", "title" FROM "feeds" WHERE "reference" = ${feedReference}` + // ); + // if (feed === undefined) return; + + // 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} + // ORDER BY "id" DESC + // ` + // ); + + // return html` + // + // + // + // + // urn:kill-the-newsletter:${feedReference} + // ${feed.title} + // Kill the Newsletter! Inbox: + // ${feedReference}@${new URL(webApplication.get("email")).hostname} → + // ${webApplication.get("url")}/feeds/${feedReference}.xml + // ${new Date(feed.updatedAt).toISOString()} + // Kill the Newsletter! + // $${entries.map( + // (entry) => html` + // + // urn:kill-the-newsletter:${entry.reference} + // ${entry.title} + // ${entry.author} + // ${new Date(entry.createdAt).toISOString()} + // + // ${entry.content} + // + // ` + // )} + // + // `.trim(); + // } + + // webApplication.get<{ feedReference: string }, HTML, {}, {}, {}>( + // "/feeds/:feedReference.xml", + // (req, res, next) => { + // const feed = renderFeed(req.params.feedReference); + // if (feed === undefined) return next(); + // res.type("atom").header("X-Robots-Tag", "noindex").send(feed); + // } + // ); + + // webApplication.get<{ entryReference: string }, HTML, {}, {}, {}>( + // "/alternates/:entryReference.html", + // (req, res, next) => { + // const entry = database.get<{ content: string }>( + // sql`SELECT "content" FROM "entries" WHERE "reference" = ${req.params.entryReference}` + // ); + // if (entry === undefined) return next(); + // res.header("X-Robots-Tag", "noindex").send(entry.content); + // } + // ); + + // webApplication.use((req, res) => { + // res.send( + // layout(html` + //

404 Not found

+ //

+ // Create a new inbox + //

+ // `) + // ); + // }); + + // const emailApplication = new SMTPServer({ + // disabledCommands: ["AUTH", "STARTTLS"], + // async onData(stream, session, callback) { + // try { + // const email = await mailparser.simpleParser(stream); + // const from = email.from?.text ?? ""; + // const subject = email.subject ?? ""; + // const body = + // typeof email.html === "string" ? email.html : email.textAsHtml ?? ""; + // database.executeTransaction(() => { + // for (const address of new Set( + // session.envelope.rcptTo.map( + // (smtpServerAddress) => smtpServerAddress.address + // ) + // )) { + // const addressParts = address.split("@"); + // if (addressParts.length !== 2) continue; + // const [feedReference, hostname] = addressParts; + // if (hostname !== new URL(webApplication.get("email")).hostname) + // continue; + // const feed = database.get<{ id: number }>( + // sql`SELECT "id" FROM "feeds" WHERE "reference" = ${feedReference}` + // ); + // if (feed === undefined) continue; + // database.run( + // sql` + // INSERT INTO "entries" ("reference", "feed", "title", "author", "content") + // VALUES ( + // ${newReference()}, + // ${feed.id}, + // ${subject}, + // ${from}, + // ${body} + // ) + // ` + // ); + // database.run( + // sql`UPDATE "feeds" SET "updatedAt" = CURRENT_TIMESTAMP WHERE "id" = ${feed.id}` + // ); + // while (renderFeed(feedReference)!.length > 500_000) + // database.run( + // sql`DELETE FROM "entries" WHERE "feed" = ${feed.id} ORDER BY "id" ASC LIMIT 1` + // ); + // } + // }); + // 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.")); + // } + // }, + // }); + + // function newReference(): string { + // return cryptoRandomString({ + // length: 16, + // characters: "abcdefghijklmnopqrstuvwxyz0123456789", + // }); + // } + + // return { webApplication, emailApplication }; + // } + + // if (require.main === module) { + // console.log(`Kill the Newsletter!/${VERSION}`); + // if (process.argv[2] === undefined) { + // const { webApplication, emailApplication } = killTheNewsletter( + // path.join(process.cwd(), "data") + // ); + // webApplication.listen(new URL(webApplication.get("url")).port, () => { + // console.log(`Web server started at ${webApplication.get("url")}`); + // }); + // emailApplication.listen(new URL(webApplication.get("email")).port, () => { + // console.log(`Email server started at ${webApplication.get("email")}`); + // }); + // } else { + // const configurationFile = path.resolve(process.argv[2]); + // require(configurationFile)(require); + // console.log(`Configuration loaded from ‘${configurationFile}’.`); + // } + // } switch (application.process.type) { case "main": {