123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191 |
- import { Email, PostalMime } from "./email.ts";
- import { handleSmtp } from "./smtp.ts";
- import { SqliteStore } from "./store.ts";
- function escapeHTML(unsafe: string): string {
- return unsafe
- .replace(/&/g, "&")
- .replace(/</g, "<")
- .replace(/>/g, ">")
- .replace(/"/g, """)
- .replace(/'/g, "'");
- }
- const CONTENT_SECURITY_POLICY = [
- "default-src 'self'",
- "img-src *",
- "style-src 'unsafe-inline' *",
- "frame-src data:",
- ].join("; ");
- class Server {
- constructor(readonly store: SqliteStore) {}
- async getLatestEmails(
- recipient: string,
- interval: number = 600,
- ): Promise<Array<{ id: string; receivedAt: Date; email: Email }>> {
- const newerThan = new Date();
- newerThan.setSeconds(newerThan.getSeconds() - interval);
- const emails = this.store.getLatestEmails(recipient, newerThan);
- console.time("parseEmail");
- const result = Promise.all(emails.map(async ({ data, ...rest }) => {
- return {
- email: await new PostalMime().parse(data),
- ...rest,
- };
- }));
- console.timeEnd("parseEmail");
- return result;
- }
- async start() {
- this.startSmtp();
- this.startHttp();
- }
- async startSmtp() {
- const listener = Deno.listen({ port: 2525 });
- for await (const conn of listener) {
- handleSmtp(conn, (recipient, sender, data) => {
- this.store.saveEmail(recipient, sender, data);
- });
- }
- }
- async startHttp() {
- const listener = Deno.listen({ port: 8000 });
- for await (const conn of listener) {
- this.serveHttp(conn);
- }
- }
- async serveHttp(conn: Deno.Conn) {
- const httpConn = Deno.serveHttp(conn);
- for await (const requestEvent of httpConn) {
- this.handle(requestEvent)
- .then((response) => requestEvent.respondWith(response))
- .catch((err) => console.error(err));
- }
- }
- async handle(requestEvent: Deno.RequestEvent): Promise<Response> {
- const url = new URL(requestEvent.request.url);
- const path = url.pathname;
- if (path.startsWith("/html/")) {
- const recipient = path.substring(6);
- return this.handleHtml(recipient);
- } else if (path.startsWith("/json/")) {
- const recipient = path.substring(6);
- return this.handleJson(recipient);
- } else {
- return new Response("Not found", {
- status: 404,
- });
- }
- }
- async handleHtml(recipient: string): Promise<Response> {
- const emails = await this.getLatestEmails(recipient, 24 * 60 * 60);
- console.time("renderHTML");
- const dateOptions = {
- weekday: "short",
- year: "numeric",
- month: "2-digit",
- day: "2-digit",
- hour: "2-digit",
- minute: "2-digit",
- timeZoneName: "short",
- } as const;
- const body = emails.map(({ email, receivedAt }) => {
- let result = "<div>";
- result += `<h2>${escapeHTML(email.subject ?? "<No Subject>")}</h2>`;
- if (email.from) {
- result += `<strong>${escapeHTML(email.from.name)} <${
- escapeHTML(email.from.address)
- }></strong>`;
- }
- result += "<br>";
- result += (email.date ? new Date(email.date) : receivedAt)
- .toLocaleString(undefined, dateOptions);
- if (email.html) {
- result += `<iframe src="data:text/html;base64,${
- btoa(email.html)
- }"></iframe>`;
- } else {
- result += `<pre>${email.text}</pre>`;
- }
- result += "</div>";
- return result;
- }).join("\n");
- const html = `
- <html>
- <head>
- <title>smtp2rss</title>
- <style>
- body > div {
- margin: 10px;
- padding: 10px;
- border: solid 1px gray;
- border-radius: 10px;
- max-width: 800px;
- }
- iframe {
- width: 100%;
- height: 500px;
- }
- </style>
- </head>
- <body>${body}</body>
- </html>
- `;
- console.timeEnd("renderHTML");
- return new Response(html, {
- status: 200,
- headers: {
- "Content-Type": "text/html; charset=utf-8",
- "Content-Security-Policy": CONTENT_SECURITY_POLICY,
- },
- });
- }
- async handleJson(recipient: string): Promise<Response> {
- const emails = await this.getLatestEmails(recipient, 24 * 60 * 60);
- console.time("renderJSON");
- const items = emails.map(({ id, email, receivedAt }) => {
- const author = email.from
- ? { name: `${email.from.name} <${email.from.address}>` }
- : undefined;
- return {
- id,
- title: email.subject,
- content_text: email.text ?? "<No Content>",
- content_html: email.html,
- date_published: email.date ?? receivedAt.toString(),
- author,
- };
- });
- const json = JSON.stringify({
- version: "1.1",
- title: `Mail to ${recipient}`,
- items,
- });
- console.timeEnd("renderJSON");
- return new Response(json, {
- status: 200,
- headers: {
- "Content-Type": "application/feed+json; charset=utf-8",
- },
- });
- }
- }
- const configFile = Deno.args.length === 0 ? "config.json" : Deno.args[0];
- const config = JSON.parse(Deno.readTextFileSync(configFile));
- const server = new Server(new SqliteStore(config.store));
- server.start();
|