This commit is contained in:
parent
c72a764c93
commit
d1d6a9ba82
|
@ -23,13 +23,16 @@ const webApp = express()
|
|||
)
|
||||
.post("/", (req, res) => {
|
||||
const name = req.body.name;
|
||||
const token = newToken();
|
||||
fs.writeFileSync(feedPath(token), renderXML(Feed({ name, token })));
|
||||
const identifier = newIdentifier();
|
||||
fs.writeFileSync(
|
||||
feedPath(identifier),
|
||||
renderXML(Feed({ name, identifier }))
|
||||
);
|
||||
res.send(
|
||||
renderHTML(
|
||||
<Layout>
|
||||
<h1>“{name}” Inbox Created</h1>
|
||||
<Created token={token}></Created>
|
||||
<Created identifier={identifier}></Created>
|
||||
</Layout>
|
||||
)
|
||||
);
|
||||
|
@ -41,8 +44,8 @@ const emailApp: SMTPServerOptions = {
|
|||
const paths = session.envelope.rcptTo.flatMap(({ address }) => {
|
||||
const match = address.match(/^(\w+)@kill-the-newsletter.com$/);
|
||||
if (match === null) return [];
|
||||
const token = match[1];
|
||||
const path = feedPath(token);
|
||||
const identifier = match[1];
|
||||
const path = feedPath(identifier);
|
||||
if (!fs.existsSync(path)) return [];
|
||||
return [path];
|
||||
});
|
||||
|
@ -179,23 +182,23 @@ function Form() {
|
|||
);
|
||||
}
|
||||
|
||||
function Created({ token }: { token: string }) {
|
||||
function Created({ identifier }: { identifier: string }) {
|
||||
return (
|
||||
<>
|
||||
<p>
|
||||
Sign up for the newsletter with
|
||||
<br />
|
||||
<code>{feedEmail(token)}</code>
|
||||
<code>{feedEmail(identifier)}</code>
|
||||
</p>
|
||||
<p>
|
||||
Subscribe to the Atom feed at
|
||||
<br />
|
||||
<code>{feedURL(token)}</code>
|
||||
<code>{feedURL(identifier)}</code>
|
||||
</p>
|
||||
<p>
|
||||
Don’t share these addresses.
|
||||
<br />
|
||||
They contain a security token that other people could use
|
||||
They contain an identifier that other people could use
|
||||
<br />
|
||||
to send you spam and to control your newsletter subscriptions.
|
||||
</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 {
|
||||
feed: {
|
||||
$: { xmlns: "http://www.w3.org/2005/Atom" },
|
||||
|
@ -218,7 +221,7 @@ function Feed({ name, token }: { name: string; token: string }) {
|
|||
$: {
|
||||
rel: "self",
|
||||
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,
|
||||
subtitle: `Kill the Newsletter! Inbox “${feedEmail(token)}”`,
|
||||
subtitle: `Kill the Newsletter! Inbox: ${feedEmail(
|
||||
identifier
|
||||
)} → ${feedURL(identifier)}`,
|
||||
updated: now(),
|
||||
...Entry({
|
||||
title: `“${name}” Inbox Created`,
|
||||
author: "Kill the Newsletter!",
|
||||
content: ReactDOMServer.renderToStaticMarkup(
|
||||
<Created token={token}></Created>
|
||||
<Created identifier={identifier}></Created>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
@ -255,7 +260,7 @@ function Entry({
|
|||
}) {
|
||||
return {
|
||||
entry: {
|
||||
id: id(newToken()),
|
||||
id: urn(newIdentifier()),
|
||||
title,
|
||||
author: { name: author },
|
||||
updated: now(),
|
||||
|
@ -264,7 +269,7 @@ function Entry({
|
|||
};
|
||||
}
|
||||
|
||||
function newToken(): string {
|
||||
function newIdentifier(): string {
|
||||
return cryptoRandomString({
|
||||
length: 20,
|
||||
characters: "1234567890qwertyuiopasdfghjklzxcvbnm"
|
||||
|
@ -275,20 +280,20 @@ function now(): string {
|
|||
return new Date().toISOString();
|
||||
}
|
||||
|
||||
function feedPath(token: string): string {
|
||||
return `static/feeds/${token}.xml`;
|
||||
function feedPath(identifier: string): string {
|
||||
return `static/feeds/${identifier}.xml`;
|
||||
}
|
||||
|
||||
function feedURL(token: string): string {
|
||||
return `https://www.kill-the-newsletter.com/feeds/${token}.xml`;
|
||||
function feedURL(identifier: string): string {
|
||||
return `https://www.kill-the-newsletter.com/feeds/${identifier}.xml`;
|
||||
}
|
||||
|
||||
export function feedEmail(token: string): string {
|
||||
return `${token}@kill-the-newsletter.com`;
|
||||
export function feedEmail(identifier: string): string {
|
||||
return `${identifier}@kill-the-newsletter.com`;
|
||||
}
|
||||
|
||||
function id(token: string): string {
|
||||
return `urn:kill-the-newsletter:${token}`;
|
||||
function urn(identifier: string): string {
|
||||
return `urn:kill-the-newsletter:${identifier}`;
|
||||
}
|
||||
|
||||
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";
|
||||
|
||||
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", () => {
|
||||
|
@ -17,41 +17,41 @@ describe("receive email", () => {
|
|||
});
|
||||
|
||||
test("HTML content", async () => {
|
||||
const token = await createFeed();
|
||||
const identifier = await createFeed();
|
||||
await transporter.sendMail({
|
||||
from: "publisher@example.com",
|
||||
to: feedEmail(token),
|
||||
to: feedEmail(identifier),
|
||||
subject: "New Message",
|
||||
html: "<p>HTML content</p>"
|
||||
});
|
||||
const feed = await readFeed(token);
|
||||
const feed = await readFeed(identifier);
|
||||
expect(feed).toMatch("publisher@example.com");
|
||||
expect(feed).toMatch("New Message");
|
||||
expect(feed).toMatch("HTML content");
|
||||
});
|
||||
|
||||
test("text content", async () => {
|
||||
const token = await createFeed();
|
||||
const identifier = await createFeed();
|
||||
await transporter.sendMail({
|
||||
from: "publisher@example.com",
|
||||
to: feedEmail(token),
|
||||
to: feedEmail(identifier),
|
||||
subject: "New Message",
|
||||
text: "TEXT content"
|
||||
});
|
||||
const feed = await readFeed(token);
|
||||
const feed = await readFeed(identifier);
|
||||
expect(feed).toMatch("TEXT content");
|
||||
});
|
||||
|
||||
test("truncation", async () => {
|
||||
const token = await createFeed();
|
||||
const identifier = await createFeed();
|
||||
for (const repetition of [...new Array(4).keys()])
|
||||
await transporter.sendMail({
|
||||
from: "publisher@example.com",
|
||||
to: feedEmail(token),
|
||||
to: feedEmail(identifier),
|
||||
subject: "New Message",
|
||||
text: `REPETITION ${repetition} `.repeat(10_000)
|
||||
});
|
||||
const feed = await readFeed(token);
|
||||
const feed = await readFeed(identifier);
|
||||
expect(feed).toMatch("REPETITION 3");
|
||||
expect(feed).not.toMatch("REPETITION 0");
|
||||
}, 10_000);
|
||||
|
@ -74,6 +74,7 @@ async function createFeed(): Promise<string> {
|
|||
).data.match(/(\w{20}).xml/)![1];
|
||||
}
|
||||
|
||||
async function readFeed(token: string): Promise<string> {
|
||||
return (await axios.get(`http://localhost:8000/feeds/${token}.xml`)).data;
|
||||
async function readFeed(identifier: string): Promise<string> {
|
||||
return (await axios.get(`http://localhost:8000/feeds/${identifier}.xml`))
|
||||
.data;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue