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:
1 parent
ab1c15e69a
commit
7bf0f71f86
45 files changed
+90
-68
No files matched your search
@@ -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);
|
||||
}
|
||||
Reference in new issue
Block a user