Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot]
2026-04-18 01:00:08 +00:00
co-authored by renee-png
parent 41f2299fe2
commit c8f250715f
5 changed files with 740 additions and 1 deletions
+157
View File
@@ -0,0 +1,157 @@
// Server route that connects to the configured IMAP mailbox, fetches new
// messages since the last polled UID, and stores them in incoming_emails.
// Triggered by pg_cron and by the "Poll now" button in settings.
import { createFileRoute } from "@tanstack/react-router";
import { supabaseAdmin } from "@/integrations/supabase/client.server";
import { ImapFlow } from "imapflow";
import { simpleParser } from "mailparser";
const MAX_MESSAGES_PER_RUN = 50;
export const Route = createFileRoute("/hooks/poll-imap")({
server: {
handlers: {
POST: async () => {
try {
const { data: settings, error: setErr } = await supabaseAdmin
.from("imap_settings")
.select("*")
.eq("enabled", true)
.order("updated_at", { ascending: false })
.limit(1)
.maybeSingle();
if (setErr) return json({ error: setErr.message }, 500);
if (!settings) return json({ error: "No IMAP settings configured" }, 400);
const password = process.env.IMAP_PASSWORD;
if (!password) {
return json({ error: "IMAP_PASSWORD secret is not set" }, 400);
}
const client = new ImapFlow({
host: settings.host,
port: settings.port,
secure: !!settings.secure,
auth: { user: settings.username, pass: password },
logger: false,
});
let imported = 0;
let highestUid = settings.last_uid ?? 0;
let errorMessage: string | null = null;
try {
await client.connect();
const lock = await client.getMailboxLock(settings.folder ?? "INBOX");
try {
const sinceUid = (settings.last_uid ?? 0) + 1;
const range = `${sinceUid}:*`;
for await (const msg of client.fetch(
range,
{ uid: true, source: true, envelope: true, size: true },
{ uid: true },
)) {
if (imported >= MAX_MESSAGES_PER_RUN) break;
if (msg.uid <= (settings.last_uid ?? 0)) continue;
let parsed;
try {
parsed = await simpleParser(msg.source as Buffer);
} catch (e) {
console.error("Failed to parse message uid", msg.uid, e);
continue;
}
const fromAddr = parsed.from?.value?.[0]?.address ?? null;
const fromName = parsed.from?.value?.[0]?.name ?? null;
const toAddrs =
Array.isArray(parsed.to)
? parsed.to.flatMap((a: any) => a.value.map((v: any) => v.address).filter(Boolean))
: (parsed.to?.value?.map((v: any) => v.address).filter(Boolean) ?? []);
const ccAddrs =
Array.isArray(parsed.cc)
? parsed.cc.flatMap((a: any) => a.value.map((v: any) => v.address).filter(Boolean))
: (parsed.cc?.value?.map((v: any) => v.address).filter(Boolean) ?? []);
const text = parsed.text ?? null;
const snippet = text
? text.replace(/\s+/g, " ").trim().slice(0, 280)
: null;
const attachments = parsed.attachments ?? [];
const insertRow = {
message_id: parsed.messageId ?? null,
imap_uid: msg.uid,
received_at: (parsed.date ?? new Date()).toISOString(),
from_address: fromAddr,
from_name: fromName,
to_addresses: toAddrs,
cc_addresses: ccAddrs,
subject: parsed.subject ?? null,
body_text: text,
body_html: parsed.html || null,
snippet,
has_attachments: attachments.length > 0,
attachment_count: attachments.length,
raw_size_bytes: msg.size ?? null,
};
// Upsert by message_id to avoid duplicates if mailbox is re-polled
const { error: insErr } = await supabaseAdmin
.from("incoming_emails")
.upsert(insertRow, { onConflict: "message_id", ignoreDuplicates: true });
if (insErr && !insErr.message.includes("duplicate")) {
console.error("Insert failed for uid", msg.uid, insErr.message);
} else {
imported++;
}
if (msg.uid > highestUid) highestUid = msg.uid;
}
} finally {
lock.release();
}
await client.logout();
} catch (e) {
errorMessage = e instanceof Error ? e.message : String(e);
console.error("IMAP poll error:", errorMessage);
try {
await client.close();
} catch (_) {
/* ignore */
}
}
await supabaseAdmin
.from("imap_settings")
.update({
last_uid: highestUid,
last_polled_at: new Date().toISOString(),
last_error: errorMessage,
})
.eq("id", settings.id);
if (errorMessage) {
return json({ ok: false, imported, error: errorMessage }, 500);
}
return json({ ok: true, imported, last_uid: highestUid });
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
console.error("poll-imap fatal", message);
return json({ error: message }, 500);
}
},
},
},
});
function json(data: unknown, status = 200) {
return new Response(JSON.stringify(data), {
status,
headers: { "Content-Type": "application/json" },
});
}