refactor: split src into domain / application / infrastructure layers

Replace the history-driven lib/ + utils/ split with DDD layers:
- domain/: aggregate, repositories, value objects, pure parsers/format
- application/: feed-service, email-processor, feed-fetcher, stats
- infrastructure/: logging, auth, KV/R2 adapters, HTTP, framework glue

Pure file relocation; imports updated mechanically. Behaviour unchanged.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
This commit is contained in:
Julien HerrandClaude Opus 4.7 committed 2026-05-24 00:46:56 +02:00
1 parent ab1c15e69a
commit 7bf0f71f86
45 files changed
+90 -68

No files matched your search

+127
View File
@@ -0,0 +1,127 @@
import { EmailParser } from "../domain/email-parser";
import { Env } from "../types";
import {
processEmail,
IngestResult,
RawAttachment,
} from "../application/email-processor";
import { normalizeCid } from "../infrastructure/html-processor";
/** Map an ingestion result to the HTTP response ForwardEmail expects. */
export function ingestResultToResponse(result: IngestResult): Response {
if (result.ok) {
return new Response("Email processed successfully", { status: 200 });
}
switch (result.reason) {
case "invalid_address":
return new Response("Invalid email address format", { status: 400 });
case "feed_not_found":
return new Response("Feed does not exist", { status: 404 });
case "feed_expired":
return new Response("Feed has expired", { status: 410 });
case "sender_blocked":
return new Response("Sender not allowed for this feed", { status: 403 });
}
}
export interface ForwardEmailAttachment {
filename?: string;
contentType?: string;
size?: number;
cid?: string;
contentId?: string;
content?: { type: "Buffer"; data: number[] } | ArrayBuffer | ArrayBufferView;
}
export interface ForwardEmailPayload {
recipients?: string[];
from?: {
value?: Array<{ address?: string; name?: string }>;
text?: string;
html?: string;
};
subject?: string;
text?: string;
html?: string;
date?: string;
messageId?: string;
headerLines?: Array<{ key: string; line: string }>;
headers?: string;
raw?: string;
attachments?: ForwardEmailAttachment[];
}
function normalizeEmail(value: string): string {
return value.trim().toLowerCase();
}
function extractSenderAddresses(payload: ForwardEmailPayload): string[] {
const valueEntries = payload.from?.value || [];
const structuredAddresses = valueEntries
.map((entry) => entry.address || "")
.map(normalizeEmail)
.filter(Boolean);
if (structuredAddresses.length > 0) {
return Array.from(new Set(structuredAddresses));
}
const fromText = payload.from?.text || "";
const matches =
fromText.match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi) || [];
return Array.from(new Set(matches.map(normalizeEmail)));
}
function toArrayBuffer(
content: ForwardEmailAttachment["content"],
): ArrayBuffer | null {
if (!content) return null;
if (content instanceof ArrayBuffer) return content;
if (ArrayBuffer.isView(content))
return (content as ArrayBufferView).buffer as ArrayBuffer;
if (
typeof content === "object" &&
content.type === "Buffer" &&
Array.isArray(content.data)
) {
return Uint8Array.from(content.data).buffer as ArrayBuffer;
}
return null;
}
export async function handleForwardEmail(
payload: ForwardEmailPayload,
env: Env,
ctx?: ExecutionContext,
): Promise<Response> {
const emailData = EmailParser.parseForwardEmailPayload(payload);
const rawAttachments: RawAttachment[] = (payload.attachments ?? [])
.map((a): RawAttachment | null => {
const buffer = toArrayBuffer(a.content);
if (!buffer) return null;
return {
filename: a.filename || "attachment",
contentType: a.contentType || "application/octet-stream",
content: buffer,
contentId: normalizeCid(a.cid ?? a.contentId),
};
})
.filter((a): a is RawAttachment => a !== null);
const result = await processEmail(
{
toAddress: payload.recipients?.[0] || "",
from: emailData.from,
senders: extractSenderAddresses(payload),
subject: emailData.subject,
content: emailData.content,
receivedAt: emailData.receivedAt,
headers: emailData.headers,
attachments: rawAttachments,
},
env,
ctx,
);
return ingestResultToResponse(result);
}