This commit is contained in:
parent
1c9bcb750f
commit
dc22ea9d74
219
src/index.ts
219
src/index.ts
|
@ -325,82 +325,83 @@ export default function killTheNewsletter(
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
webApplication.get<{ feedReference: string }, HTML, {}, {}, {}>(
|
function renderFeed(feedReference: string): HTML | undefined {
|
||||||
"/feeds/:feedReference.xml",
|
const feed = database.get<{
|
||||||
(req, res, next) => {
|
id: number;
|
||||||
const feed = database.get<{
|
updatedAt: string;
|
||||||
id: number;
|
title: string;
|
||||||
updatedAt: string;
|
}>(
|
||||||
title: string;
|
sql`SELECT "id", "updatedAt", "title" FROM "feeds" WHERE "reference" = ${feedReference}`
|
||||||
}>(
|
);
|
||||||
sql`SELECT "id", "updatedAt", "title" FROM "feeds" WHERE "reference" = ${req.params.feedReference}`
|
if (feed === undefined) return;
|
||||||
);
|
|
||||||
if (feed === undefined) return next();
|
const entries = database.all<{
|
||||||
const entries = database.all<{
|
createdAt: string;
|
||||||
createdAt: string;
|
reference: string;
|
||||||
reference: string;
|
title: string;
|
||||||
title: string;
|
author: string;
|
||||||
author: string;
|
content: string;
|
||||||
content: string;
|
}>(
|
||||||
}>(
|
sql`
|
||||||
sql`
|
SELECT "createdAt", "reference", "title", "author", "content"
|
||||||
SELECT "createdAt", "reference", "title", "author", "content"
|
FROM "entries"
|
||||||
FROM "entries"
|
WHERE "feed" = ${feed.id}
|
||||||
WHERE "feed" = ${feed.id}
|
`
|
||||||
`
|
);
|
||||||
);
|
|
||||||
res
|
return html`
|
||||||
.contentType("application/atom+xml")
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
.header("X-Robots-Tag", "noindex")
|
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||||
.send(
|
<link
|
||||||
html`
|
rel="self"
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
type="application/atom+xml"
|
||||||
<feed xmlns="http://www.w3.org/2005/Atom">
|
href="${webApplication.get("url")}/feeds/${feedReference}.xml"
|
||||||
<link
|
/>
|
||||||
rel="self"
|
<link
|
||||||
type="application/atom+xml"
|
rel="alternate"
|
||||||
href="${webApplication.get("url")}/feeds/${req.params
|
type="text/html"
|
||||||
.feedReference}.xml"
|
href="${webApplication.get("url")}/"
|
||||||
/>
|
/>
|
||||||
|
<id>urn:kill-the-newsletter:${feedReference}</id>
|
||||||
|
<title>${feed.title}</title>
|
||||||
|
<subtitle
|
||||||
|
>Kill the Newsletter! Inbox:
|
||||||
|
${feedReference}@${webApplication.get("email host")} →
|
||||||
|
${webApplication.get("url")}/feeds/${feedReference}.xml</subtitle
|
||||||
|
>
|
||||||
|
<updated>${new Date(feed.updatedAt).toISOString()}</updated>
|
||||||
|
<author><name>Kill the Newsletter!</name></author>
|
||||||
|
$${entries.map(
|
||||||
|
(entry) => html`
|
||||||
|
<entry>
|
||||||
|
<id>urn:kill-the-newsletter:${entry.reference}</id>
|
||||||
|
<title>${entry.title}</title>
|
||||||
|
<author><name>${entry.author}</name></author>
|
||||||
|
<updated>${new Date(entry.createdAt).toISOString()}</updated>
|
||||||
<link
|
<link
|
||||||
rel="alternate"
|
rel="alternate"
|
||||||
type="text/html"
|
type="text/html"
|
||||||
href="${webApplication.get("url")}/"
|
href="${webApplication.get(
|
||||||
|
"url"
|
||||||
|
)}/alternates/${entry.reference}.html"
|
||||||
/>
|
/>
|
||||||
<id>urn:kill-the-newsletter:${req.params.feedReference}</id>
|
<content type="html">${entry.content}</content>
|
||||||
<title>${feed.title}</title>
|
</entry>
|
||||||
<subtitle
|
`
|
||||||
>Kill the Newsletter! Inbox:
|
)}
|
||||||
${req.params.feedReference}@${webApplication.get("email host")}
|
</feed>
|
||||||
→
|
`.trim();
|
||||||
${webApplication.get("url")}/feeds/${req.params
|
}
|
||||||
.feedReference}.xml</subtitle
|
|
||||||
>
|
webApplication.get<{ feedReference: string }, HTML, {}, {}, {}>(
|
||||||
<updated>${new Date(feed.updatedAt).toISOString()}</updated>
|
"/feeds/:feedReference.xml",
|
||||||
<author><name>Kill the Newsletter!</name></author>
|
(req, res, next) => {
|
||||||
$${entries.map(
|
const feed = renderFeed(req.params.feedReference);
|
||||||
(entry) => html`
|
if (feed === undefined) return next();
|
||||||
<entry>
|
res
|
||||||
<id>urn:kill-the-newsletter:${entry.reference}</id>
|
.contentType("application/atom+xml")
|
||||||
<title>${entry.title}</title>
|
.header("X-Robots-Tag", "noindex")
|
||||||
<author><name>${entry.author}</name></author>
|
.send(feed);
|
||||||
<updated
|
|
||||||
>${new Date(entry.createdAt).toISOString()}</updated
|
|
||||||
>
|
|
||||||
<link
|
|
||||||
rel="alternate"
|
|
||||||
type="text/html"
|
|
||||||
href="${webApplication.get(
|
|
||||||
"url"
|
|
||||||
)}/alternates/${entry.reference}.html"
|
|
||||||
/>
|
|
||||||
<content type="html">${entry.content}</content>
|
|
||||||
</entry>
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
</feed>
|
|
||||||
`.trim()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -431,62 +432,41 @@ export default function killTheNewsletter(
|
||||||
const emailApplication = new SMTPServer({
|
const emailApplication = new SMTPServer({
|
||||||
disabledCommands: ["AUTH", "STARTTLS"],
|
disabledCommands: ["AUTH", "STARTTLS"],
|
||||||
async onData(stream, session, callback) {
|
async onData(stream, session, callback) {
|
||||||
/*
|
|
||||||
try {
|
try {
|
||||||
|
const atHost = "@" + webApplication.get("email host");
|
||||||
const email = await mailparser.simpleParser(stream);
|
const email = await mailparser.simpleParser(stream);
|
||||||
const content =
|
const from = email.from?.text ?? "";
|
||||||
|
const subject = email.subject ?? "";
|
||||||
|
const body =
|
||||||
typeof email.html === "string" ? email.html : email.textAsHtml ?? "";
|
typeof email.html === "string" ? email.html : email.textAsHtml ?? "";
|
||||||
for (const address of new Set(
|
for (const address of new Set(
|
||||||
session.envelope.rcptTo.map(({ address }) => address)
|
session.envelope.rcptTo.map(
|
||||||
|
(smtpServerAddress) => smtpServerAddress.address
|
||||||
|
)
|
||||||
)) {
|
)) {
|
||||||
const match = address.match(
|
if (!address.endsWith(atHost)) continue;
|
||||||
new RegExp(
|
const feedReference = address.slice(0, -atHost.length);
|
||||||
`^(?<identifier>\\w+)@${escapeStringRegexp(EMAIL_DOMAIN)}$`
|
const feed = database.get<{ id: number }>(
|
||||||
)
|
sql`SELECT "id" FROM "feeds" WHERE "reference" = ${feedReference}`
|
||||||
);
|
);
|
||||||
if (match?.groups === undefined) continue;
|
if (feed === undefined) continue;
|
||||||
const identifier = match.groups.identifier.toLowerCase();
|
database.run(
|
||||||
const path = feedFilePath(identifier);
|
sql`
|
||||||
let text;
|
INSERT INTO "entries" ("reference", "feed", "title", "author", "content")
|
||||||
try {
|
VALUES (
|
||||||
text = await fs.readFile(path, "utf8");
|
${newReference()}, ${feed.id}, ${subject}, ${from}, ${body}
|
||||||
} 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(
|
// TODO: Do this with a trigger.
|
||||||
"feed > entry:first-of-type"
|
database.run(
|
||||||
|
sql`UPDATE "feeds" SET "updatedAt" = datetime('now') WHERE "id" = ${feed.id}`
|
||||||
);
|
);
|
||||||
if (firstEntry === null)
|
|
||||||
document
|
while (renderFeed(feedReference)!.length > 500_00)
|
||||||
.querySelector("feed")!
|
database.run(
|
||||||
.insertAdjacentHTML("beforeend", renderedEntry);
|
sql`DELETE FROM "entries" WHERE "feed" = ${feed.id} ORDER BY "createdAt" ASC LIMIT 1`
|
||||||
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`<?xml version="1.0" encoding="utf-8"?>${feed.serialize()}`.trim()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
callback();
|
callback();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -497,7 +477,6 @@ export default function killTheNewsletter(
|
||||||
stream.resume();
|
stream.resume();
|
||||||
callback(new Error("Failed to receive message. Please try again."));
|
callback(new Error("Failed to receive message. Please try again."));
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue