This commit is contained in:
parent
c72a764c93
commit
d1d6a9ba82
|
@ -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>
|
||||||
Don’t share these addresses.
|
Don’t 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 {
|
||||||
|
|
27
src/test.ts
27
src/test.ts
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue