chore: apply Prettier formatting to entire codebase

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Julien Herr
2026-05-20 22:01:53 +02:00
parent 1d8cffb119
commit 3ed9d2ee22
18 changed files with 1008 additions and 319 deletions
+47 -41
View File
@@ -1,4 +1,4 @@
import { EmailData } from '../types';
import { EmailData } from "../types";
/**
* Simple email parser specialized for ForwardEmail.net's webhook format
@@ -14,59 +14,62 @@ export class EmailParser {
const match = emailAddress.match(/^([a-z]+\.[a-z]+\.\d{2})@/i);
return match ? match[1] : null;
}
/**
* Parse email data from ForwardEmail.net's webhook payload
* @param payload ForwardEmail.net webhook payload
*/
static parseForwardEmailPayload(payload: any): EmailData {
if (!payload) {
throw new Error('Missing or invalid webhook payload');
throw new Error("Missing or invalid webhook payload");
}
// Extract the "to" address
const toAddress = payload.recipients?.[0] || '';
const toAddress = payload.recipients?.[0] || "";
// Extract the sender information using ForwardEmail's structure
const fromAddress = payload.from?.text ||
(payload.from?.value?.[0]?.address ?
`${payload.from.value[0].name || ''} <${payload.from.value[0].address}>` :
'Unknown Sender');
const fromAddress =
payload.from?.text ||
(payload.from?.value?.[0]?.address
? `${payload.from.value[0].name || ""} <${payload.from.value[0].address}>`
: "Unknown Sender");
// Extract subject
let subject = payload.subject || 'No Subject';
let subject = payload.subject || "No Subject";
// Decode any encoded words in the subject
subject = this.decodeEncodedWords(subject);
// Get content, preferring HTML over plain text
const content = payload.html || payload.text || '';
const content = payload.html || payload.text || "";
// Create simple email data object
return {
subject,
from: fromAddress,
content,
receivedAt: payload.date ? new Date(payload.date).getTime() : Date.now(),
headers: this.extractHeaders(payload)
headers: this.extractHeaders(payload),
};
}
/**
* Extract headers from ForwardEmail payload
*/
private static extractHeaders(payload: any): Record<string, string> {
const headers: Record<string, string> = {};
// Extract headers from headerLines if available
if (payload.headerLines && Array.isArray(payload.headerLines)) {
payload.headerLines.forEach((h: {key: string; line: string}) => {
payload.headerLines.forEach((h: { key: string; line: string }) => {
const key = h.key.toLowerCase();
const value = h.line.replace(new RegExp(`^${h.key}:\\s*`, 'i'), '').trim();
const value = h.line
.replace(new RegExp(`^${h.key}:\\s*`, "i"), "")
.trim();
headers[key] = value;
});
}
// Or from headers string if provided
else if (typeof payload.headers === 'string') {
else if (typeof payload.headers === "string") {
payload.headers.split(/\r?\n/).forEach((line: string) => {
const match = line.match(/^([^:]+):\s*(.*)$/);
if (match) {
@@ -74,35 +77,38 @@ export class EmailParser {
}
});
}
return headers;
}
/**
* Decode RFC 2047 encoded words in headers
* @param text Text that may contain encoded words like =?UTF-8?Q?Hello_World?=
*/
static decodeEncodedWords(text: string): string {
if (!text) return '';
if (!text) return "";
// Simple RFC 2047 encoded-word decoder
return text.replace(/=\?([^?]+)\?([BQ])\?([^?]+)\?=/gi, (_, charset, encoding, text) => {
if (encoding.toUpperCase() === 'B') {
// Base64 encoding
try {
const decoded = atob(text);
return decoded;
} catch (e) {
return text;
return text.replace(
/=\?([^?]+)\?([BQ])\?([^?]+)\?=/gi,
(_, charset, encoding, text) => {
if (encoding.toUpperCase() === "B") {
// Base64 encoding
try {
const decoded = atob(text);
return decoded;
} catch (e) {
return text;
}
} else if (encoding.toUpperCase() === "Q") {
// Quoted-printable encoding
return this.decodeQuotedPrintable(text.replace(/_/g, " "));
}
} else if (encoding.toUpperCase() === 'Q') {
// Quoted-printable encoding
return this.decodeQuotedPrintable(text.replace(/_/g, ' '));
}
return text;
});
return text;
},
);
}
/**
* Decode quoted-printable encoded text
* @param text Quoted-printable encoded text
@@ -112,4 +118,4 @@ export class EmailParser {
return String.fromCharCode(parseInt(hex, 16));
});
}
}
}
+16 -14
View File
@@ -1,38 +1,40 @@
import { Feed } from 'feed';
import { FeedConfig, EmailData } from '../types';
import { Feed } from "feed";
import { FeedConfig, EmailData } from "../types";
/**
* Generate an RSS feed from a list of emails
*/
export function generateRssFeed(
feedConfig: FeedConfig,
feedConfig: FeedConfig,
emails: EmailData[],
baseUrl: string
baseUrl: string,
): string {
// Create a new feed
const feed = new Feed({
title: feedConfig.title,
description: feedConfig.description || '',
description: feedConfig.description || "",
id: feedConfig.feed_url,
link: feedConfig.site_url,
language: feedConfig.language,
updated: new Date(),
generator: 'Email-to-RSS',
generator: "Email-to-RSS",
copyright: `Copyright © ${new Date().getFullYear()} ${feedConfig.title}`,
feedLinks: {
rss: feedConfig.feed_url
rss: feedConfig.feed_url,
},
author: feedConfig.author ? {
name: feedConfig.author,
email: `noreply@${new URL(feedConfig.site_url).hostname}`
} : undefined
author: feedConfig.author
? {
name: feedConfig.author,
email: `noreply@${new URL(feedConfig.site_url).hostname}`,
}
: undefined,
});
// Add each email as a feed item
for (const email of emails) {
const date = new Date(email.receivedAt);
const uniqueId = `${email.receivedAt}-${Buffer.from(email.subject).toString('base64').substring(0, 10)}`;
const uniqueId = `${email.receivedAt}-${Buffer.from(email.subject).toString("base64").substring(0, 10)}`;
feed.addItem({
title: email.subject,
id: uniqueId,
@@ -50,4 +52,4 @@ export function generateRssFeed(
// Return the RSS feed as XML
return feed.rss2();
}
}
+4 -4
View File
@@ -1,4 +1,4 @@
import { nouns } from '../data/nouns';
import { nouns } from "../data/nouns";
/**
* Generates a random feed ID in the format noun1.noun2.XY
@@ -8,10 +8,10 @@ export function generateFeedId(): string {
// Select two random nouns
const noun1 = nouns[Math.floor(Math.random() * nouns.length)];
const noun2 = nouns[Math.floor(Math.random() * nouns.length)];
// Generate a random 2-digit number between 10 and 99
const number = Math.floor(Math.random() * 90) + 10;
// Combine to create the ID with dots as separators
return `${noun1}.${noun2}.${number}`;
}
}
+52 -35
View File
@@ -1,4 +1,10 @@
import { EmailData, FeedConfig, FeedMetadata, FeedList, EmailMetadata } from '../types';
import {
EmailData,
FeedConfig,
FeedMetadata,
FeedList,
EmailMetadata,
} from "../types";
/**
* Store email data in KV
@@ -6,22 +12,22 @@ import { EmailData, FeedConfig, FeedMetadata, FeedList, EmailMetadata } from '..
export async function storeEmail(
kv: KVNamespace,
feedId: string,
emailData: EmailData
emailData: EmailData,
): Promise<string> {
// Generate a unique key for this email
const timestamp = Date.now();
const key = `feed:${feedId}:email:${timestamp}`;
// Store the email content
await kv.put(key, JSON.stringify(emailData));
// Update the feed's metadata (list of emails)
await updateFeedMetadata(kv, feedId, {
key,
subject: emailData.subject,
receivedAt: timestamp
receivedAt: timestamp,
});
return key;
}
@@ -31,21 +37,23 @@ export async function storeEmail(
async function updateFeedMetadata(
kv: KVNamespace,
feedId: string,
emailMetadata: EmailMetadata
emailMetadata: EmailMetadata,
): Promise<void> {
const feedMetadataKey = `feed:${feedId}:metadata`;
const existingMetadata = await kv.get(feedMetadataKey, { type: 'json' }) as FeedMetadata | null;
const existingMetadata = (await kv.get(feedMetadataKey, {
type: "json",
})) as FeedMetadata | null;
const metadata: FeedMetadata = existingMetadata || { emails: [] };
// Add new email to the beginning of the list
metadata.emails.unshift(emailMetadata);
// Keep only the last 50 emails in the metadata
if (metadata.emails.length > 50) {
metadata.emails = metadata.emails.slice(0, 50);
}
// Store updated metadata
await kv.put(feedMetadataKey, JSON.stringify(metadata));
}
@@ -55,10 +63,12 @@ async function updateFeedMetadata(
*/
export async function getFeedMetadata(
kv: KVNamespace,
feedId: string
feedId: string,
): Promise<FeedMetadata | null> {
const feedMetadataKey = `feed:${feedId}:metadata`;
return await kv.get(feedMetadataKey, { type: 'json' }) as FeedMetadata | null;
return (await kv.get(feedMetadataKey, {
type: "json",
})) as FeedMetadata | null;
}
/**
@@ -66,10 +76,10 @@ export async function getFeedMetadata(
*/
export async function getFeedConfig(
kv: KVNamespace,
feedId: string
feedId: string,
): Promise<FeedConfig | null> {
const feedConfigKey = `feed:${feedId}:config`;
return await kv.get(feedConfigKey, { type: 'json' }) as FeedConfig | null;
return (await kv.get(feedConfigKey, { type: "json" })) as FeedConfig | null;
}
/**
@@ -77,9 +87,9 @@ export async function getFeedConfig(
*/
export async function getEmailData(
kv: KVNamespace,
key: string
key: string,
): Promise<EmailData | null> {
return await kv.get(key, { type: 'json' }) as EmailData | null;
return (await kv.get(key, { type: "json" })) as EmailData | null;
}
/**
@@ -88,18 +98,21 @@ export async function getEmailData(
export async function createFeed(
kv: KVNamespace,
feedId: string,
feedConfig: FeedConfig
feedConfig: FeedConfig,
): Promise<void> {
// Store feed configuration
const feedConfigKey = `feed:${feedId}:config`;
await kv.put(feedConfigKey, JSON.stringify(feedConfig));
// Create empty metadata for the feed
const feedMetadataKey = `feed:${feedId}:metadata`;
await kv.put(feedMetadataKey, JSON.stringify({
emails: []
}));
await kv.put(
feedMetadataKey,
JSON.stringify({
emails: [],
}),
);
// Add feed to the list of all feeds
await addFeedToList(kv, feedId, feedConfig.title, feedConfig.description);
}
@@ -111,19 +124,21 @@ export async function addFeedToList(
kv: KVNamespace,
feedId: string,
title: string,
description?: string
description?: string,
): Promise<void> {
const feedListKey = 'feeds:list';
const existingList = await kv.get(feedListKey, { type: 'json' }) as FeedList | null;
const feedListKey = "feeds:list";
const existingList = (await kv.get(feedListKey, {
type: "json",
})) as FeedList | null;
const feedList: FeedList = existingList || { feeds: [] };
feedList.feeds.push({
id: feedId,
title,
description
description,
});
await kv.put(feedListKey, JSON.stringify(feedList));
}
@@ -131,8 +146,10 @@ export async function addFeedToList(
* Get all feeds
*/
export async function getAllFeeds(kv: KVNamespace): Promise<FeedList> {
const feedListKey = 'feeds:list';
const feedList = await kv.get(feedListKey, { type: 'json' }) as FeedList | null;
const feedListKey = "feeds:list";
const feedList = (await kv.get(feedListKey, {
type: "json",
})) as FeedList | null;
return feedList || { feeds: [] };
}
}