This commit is contained in:
Leandro Facchinetti 2020-03-20 15:08:04 -04:00
parent c72a764c93
commit d1d6a9ba82
2 changed files with 43 additions and 37 deletions

View File

@ -23,13 +23,16 @@ const webApp = express()
) )
.post("/", (req, res) => { .post("/", (req, res) => {
const name = req.body.name; const name = req.body.name;
const token = newToken(); const identifier = newIdentifier();
fs.writeFileSync(feedPath(token), renderXML(Feed({ name, token }))); fs.writeFileSync(
feedPath(identifier),
renderXML(Feed({ name, identifier }))
);
res.send( res.send(
renderHTML( renderHTML(
<Layout> <Layout>
<h1>{name} Inbox Created</h1> <h1>{name} Inbox Created</h1>
<Created token={token}></Created> <Created identifier={identifier}></Created>
</Layout> </Layout>
) )
); );
@ -41,8 +44,8 @@ const emailApp: SMTPServerOptions = {
const paths = session.envelope.rcptTo.flatMap(({ address }) => { const paths = session.envelope.rcptTo.flatMap(({ address }) => {
const match = address.match(/^(\w+)@kill-the-newsletter.com$/); const match = address.match(/^(\w+)@kill-the-newsletter.com$/);
if (match === null) return []; if (match === null) return [];
const token = match[1]; const identifier = match[1];
const path = feedPath(token); const path = feedPath(identifier);
if (!fs.existsSync(path)) return []; if (!fs.existsSync(path)) return [];
return [path]; return [path];
}); });
@ -179,23 +182,23 @@ function Form() {
); );
} }
function Created({ token }: { token: string }) { function Created({ identifier }: { identifier: string }) {
return ( return (
<> <>
<p> <p>
Sign up for the newsletter with Sign up for the newsletter with
<br /> <br />
<code>{feedEmail(token)}</code> <code>{feedEmail(identifier)}</code>
</p> </p>
<p> <p>
Subscribe to the Atom feed at Subscribe to the Atom feed at
<br /> <br />
<code>{feedURL(token)}</code> <code>{feedURL(identifier)}</code>
</p> </p>
<p> <p>
Dont share these addresses. Dont share these addresses.
<br /> <br />
They contain a security token that other people could use They contain an identifier that other people could use
<br /> <br />
to send you spam and to control your newsletter subscriptions. to send you spam and to control your newsletter subscriptions.
</p> </p>
@ -209,7 +212,7 @@ function Created({ token }: { token: string }) {
); );
} }
function Feed({ name, token }: { name: string; token: string }) { function Feed({ name, identifier }: { name: string; identifier: string }) {
return { return {
feed: { feed: {
$: { xmlns: "http://www.w3.org/2005/Atom" }, $: { xmlns: "http://www.w3.org/2005/Atom" },
@ -218,7 +221,7 @@ function Feed({ name, token }: { name: string; token: string }) {
$: { $: {
rel: "self", rel: "self",
type: "application/atom+xml", type: "application/atom+xml",
href: feedURL(token) href: feedURL(identifier)
} }
}, },
{ {
@ -229,15 +232,17 @@ function Feed({ name, token }: { name: string; token: string }) {
} }
} }
], ],
id: id(token), id: urn(identifier),
title: name, title: name,
subtitle: `Kill the Newsletter! Inbox “${feedEmail(token)}`, subtitle: `Kill the Newsletter! Inbox: ${feedEmail(
identifier
)} ${feedURL(identifier)}`,
updated: now(), updated: now(),
...Entry({ ...Entry({
title: `${name}” Inbox Created`, title: `${name}” Inbox Created`,
author: "Kill the Newsletter!", author: "Kill the Newsletter!",
content: ReactDOMServer.renderToStaticMarkup( content: ReactDOMServer.renderToStaticMarkup(
<Created token={token}></Created> <Created identifier={identifier}></Created>
) )
}) })
} }
@ -255,7 +260,7 @@ function Entry({
}) { }) {
return { return {
entry: { entry: {
id: id(newToken()), id: urn(newIdentifier()),
title, title,
author: { name: author }, author: { name: author },
updated: now(), updated: now(),
@ -264,7 +269,7 @@ function Entry({
}; };
} }
function newToken(): string { function newIdentifier(): string {
return cryptoRandomString({ return cryptoRandomString({
length: 20, length: 20,
characters: "1234567890qwertyuiopasdfghjklzxcvbnm" characters: "1234567890qwertyuiopasdfghjklzxcvbnm"
@ -275,20 +280,20 @@ function now(): string {
return new Date().toISOString(); return new Date().toISOString();
} }
function feedPath(token: string): string { function feedPath(identifier: string): string {
return `static/feeds/${token}.xml`; return `static/feeds/${identifier}.xml`;
} }
function feedURL(token: string): string { function feedURL(identifier: string): string {
return `https://www.kill-the-newsletter.com/feeds/${token}.xml`; return `https://www.kill-the-newsletter.com/feeds/${identifier}.xml`;
} }
export function feedEmail(token: string): string { export function feedEmail(identifier: string): string {
return `${token}@kill-the-newsletter.com`; return `${identifier}@kill-the-newsletter.com`;
} }
function id(token: string): string { function urn(identifier: string): string {
return `urn:kill-the-newsletter:${token}`; return `urn:kill-the-newsletter:${identifier}`;
} }
function renderHTML(component: React.ReactElement): string { function renderHTML(component: React.ReactElement): string {

View File

@ -4,9 +4,9 @@ import axios from "axios";
import qs from "qs"; import qs from "qs";
test("create feed", async () => { test("create feed", async () => {
const token = await createFeed(); const identifier = await createFeed();
expect(await readFeed(token)).toMatch("My Feed"); expect(await readFeed(identifier)).toMatch("My Feed");
}); });
describe("receive email", () => { describe("receive email", () => {
@ -17,41 +17,41 @@ describe("receive email", () => {
}); });
test("HTML content", async () => { test("HTML content", async () => {
const token = await createFeed(); const identifier = await createFeed();
await transporter.sendMail({ await transporter.sendMail({
from: "publisher@example.com", from: "publisher@example.com",
to: feedEmail(token), to: feedEmail(identifier),
subject: "New Message", subject: "New Message",
html: "<p>HTML content</p>" html: "<p>HTML content</p>"
}); });
const feed = await readFeed(token); const feed = await readFeed(identifier);
expect(feed).toMatch("publisher@example.com"); expect(feed).toMatch("publisher@example.com");
expect(feed).toMatch("New Message"); expect(feed).toMatch("New Message");
expect(feed).toMatch("HTML content"); expect(feed).toMatch("HTML content");
}); });
test("text content", async () => { test("text content", async () => {
const token = await createFeed(); const identifier = await createFeed();
await transporter.sendMail({ await transporter.sendMail({
from: "publisher@example.com", from: "publisher@example.com",
to: feedEmail(token), to: feedEmail(identifier),
subject: "New Message", subject: "New Message",
text: "TEXT content" text: "TEXT content"
}); });
const feed = await readFeed(token); const feed = await readFeed(identifier);
expect(feed).toMatch("TEXT content"); expect(feed).toMatch("TEXT content");
}); });
test("truncation", async () => { test("truncation", async () => {
const token = await createFeed(); const identifier = await createFeed();
for (const repetition of [...new Array(4).keys()]) for (const repetition of [...new Array(4).keys()])
await transporter.sendMail({ await transporter.sendMail({
from: "publisher@example.com", from: "publisher@example.com",
to: feedEmail(token), to: feedEmail(identifier),
subject: "New Message", subject: "New Message",
text: `REPETITION ${repetition} `.repeat(10_000) text: `REPETITION ${repetition} `.repeat(10_000)
}); });
const feed = await readFeed(token); const feed = await readFeed(identifier);
expect(feed).toMatch("REPETITION 3"); expect(feed).toMatch("REPETITION 3");
expect(feed).not.toMatch("REPETITION 0"); expect(feed).not.toMatch("REPETITION 0");
}, 10_000); }, 10_000);
@ -74,6 +74,7 @@ async function createFeed(): Promise<string> {
).data.match(/(\w{20}).xml/)![1]; ).data.match(/(\w{20}).xml/)![1];
} }
async function readFeed(token: string): Promise<string> { async function readFeed(identifier: string): Promise<string> {
return (await axios.get(`http://localhost:8000/feeds/${token}.xml`)).data; return (await axios.get(`http://localhost:8000/feeds/${identifier}.xml`))
.data;
} }