This commit is contained in:
Leandro Facchinetti 2021-03-11 21:52:54 +00:00
parent 1c9bcb750f
commit dc22ea9d74
1 changed files with 99 additions and 120 deletions

View File

@ -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."));
} }
*/
}, },
}); });