95 lines
3.9 KiB
TypeScript
95 lines
3.9 KiB
TypeScript
import { NextResponse } from "next/server";
|
|
import { getPayload } from "payload";
|
|
import config from "@payload-config";
|
|
|
|
export const dynamic = "force-dynamic";
|
|
|
|
type InboundBody = {
|
|
from?: unknown;
|
|
sender?: unknown;
|
|
email?: unknown;
|
|
name?: unknown;
|
|
subject?: unknown;
|
|
text?: unknown;
|
|
body?: unknown;
|
|
html?: unknown;
|
|
messageId?: unknown;
|
|
inReplyTo?: unknown;
|
|
};
|
|
|
|
function stringValue(value: unknown): string {
|
|
return typeof value === "string" ? value.trim() : "";
|
|
}
|
|
|
|
function extractEmail(value: unknown): string {
|
|
if (value && typeof value === "object" && "email" in value) return extractEmail((value as { email?: unknown }).email);
|
|
const raw = stringValue(value);
|
|
const match = raw.match(/<([^>]+)>/) || raw.match(/([\w.+-]+@[\w.-]+\.[A-Za-z]{2,})/);
|
|
return (match?.[1] || raw).trim().toLowerCase();
|
|
}
|
|
|
|
function stripHtml(value: string): string {
|
|
return value.replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
|
|
}
|
|
|
|
function findThreadKey(subject: string): string | null {
|
|
return subject.match(/\[ANOUMA-([^\]]+)\]/i)?.[1] || null;
|
|
}
|
|
|
|
export async function POST(request: Request) {
|
|
const secret = process.env.INBOUND_EMAIL_SECRET;
|
|
const supplied = request.headers.get("x-inbound-secret") || request.headers.get("authorization")?.replace(/^Bearer\s+/i, "");
|
|
if (!secret || supplied !== secret) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
|
|
let body: InboundBody;
|
|
try {
|
|
body = await request.json();
|
|
} catch {
|
|
return NextResponse.json({ error: "JSON erwartet." }, { status: 400 });
|
|
}
|
|
|
|
const fromRaw = body.from ?? body.sender ?? body.email;
|
|
const email = extractEmail(fromRaw);
|
|
const fromName = fromRaw && typeof fromRaw === "object" && "name" in fromRaw ? stringValue((fromRaw as { name?: unknown }).name) : "";
|
|
const name = stringValue(body.name) || fromName || stringValue(fromRaw).replace(/<[^>]+>/, "").trim() || email;
|
|
const subject = stringValue(body.subject) || "Antwort auf deine Anfrage";
|
|
const messageId = stringValue(body.messageId);
|
|
const text = stringValue(body.text) || stringValue(body.body) || stripHtml(stringValue(body.html));
|
|
if (!email || !messageId || !text) return NextResponse.json({ error: "Absender, messageId und Inhalt sind erforderlich." }, { status: 400 });
|
|
|
|
try {
|
|
const payload = await getPayload({ config });
|
|
if (messageId) {
|
|
const existing = await payload.find({ collection: "conversation-messages", where: { externalMessageId: { equals: messageId } }, limit: 1, overrideAccess: true });
|
|
if (existing.docs[0]) return NextResponse.json({ ok: true, duplicate: true });
|
|
}
|
|
|
|
const threadKey = findThreadKey(subject);
|
|
let conversation = threadKey
|
|
? (await payload.find({ collection: "conversations", where: { threadKey: { equals: threadKey } }, limit: 1, overrideAccess: true })).docs[0]
|
|
: undefined;
|
|
if (!conversation) {
|
|
conversation = (await payload.find({ collection: "conversations", where: { email: { equals: email }, status: { not_equals: "closed" } }, sort: "-lastMessageAt", limit: 1, overrideAccess: true })).docs[0];
|
|
}
|
|
if (!conversation) return NextResponse.json({ error: "Keine passende Konversation gefunden." }, { status: 404 });
|
|
|
|
await payload.create({
|
|
collection: "conversation-messages",
|
|
overrideAccess: true,
|
|
data: {
|
|
conversation: conversation.id,
|
|
senderType: "visitor",
|
|
senderName: name,
|
|
senderEmail: email,
|
|
body: text.slice(0, 10000),
|
|
externalMessageId: messageId,
|
|
},
|
|
});
|
|
await payload.update({ collection: "conversations", id: conversation.id, overrideAccess: true, data: { status: "open", lastMessageAt: new Date().toISOString() } });
|
|
return NextResponse.json({ ok: true });
|
|
} catch (error) {
|
|
console.error("inbound email failed", error);
|
|
return NextResponse.json({ error: "E-Mail konnte nicht übernommen werden." }, { status: 500 });
|
|
}
|
|
}
|