import { getHandler, register } from "../engine/registry"; import { RuntimeError, type CapabilityHandler } from "../engine/types"; import { findImapMail, type NormalizedMail } from "./message"; function text(args: Record, keys: string[], fallback = ""): string { for (const key of keys) { const value = args[key]; if (value !== undefined && value !== null && value !== "") return String(value); } return fallback; } function number(value: unknown, fallback = 1): number { const parsed = Number(value); return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : fallback; } function record(value: unknown): Record { return value && typeof value === "object" ? value as Record : (() => { try { return JSON.parse(String(value || "{}")); } catch { return {}; } })(); } function list(value: unknown): string[] { if (Array.isArray(value)) return value.map(String); try { const parsed = JSON.parse(String(value || "[]")); return Array.isArray(parsed) ? parsed.map(String) : [String(parsed)]; } catch { return String(value || "").split(/[,,;;\s]+/).filter(Boolean); } } function mergeAccount(args: Record): Record { return { ...record(args.account_obj || args.recvObject || args.target_obj), ...args }; } async function imapClient(args: Record) { const { ImapFlow } = await import("imapflow"); const config = mergeAccount(args); const client = new ImapFlow({ host: text(config, ["host", "hostname", "server_address", "server"]), port: number(config.port, 993), secure: ["true", "1", "True"].includes(text(config, ["ssl", "is_ssl", "secure"], "True")), auth: { user: text(config, ["account", "username", "user"]), pass: text(config, ["password", "pass"]) }, logger: false }); await client.connect(); return client; } async function withMailbox(args: Record, callback: (client: any) => Promise): Promise { const client = await imapClient(args); const lock = await client.getMailboxLock(text(args, ["folder", "email_folder", "inbox_type", "folder_name"], "INBOX")); try { return await callback(client); } finally { lock.release(); client.close(); } } async function searchMails(args: Record): Promise { const normalized: Record = { ...args, From_data: text(args, ["From_data", "From", "sender_key", "recipient_key"]), Subject_data: text(args, ["Subject_data", "Subject", "subject_key"]), Body_data: text(args, ["Body_data", "body_key"]) }; return await findImapMail(normalized, { id: "mail-remaining", componentId: "mail-remaining" }); } const connect: CapabilityHandler = async (_ctx, _node, args) => { const config = mergeAccount(args); await imapClient(config).then((client) => client.close()); return { kind: "imap", ...config }; }; register(["Mail_RecvConnect", "Mail_UnionConnect", "Mail_ExConnect"], connect); register("Mail_RecvClose", async () => ({ closed: true })); register("Mail_RecvGetList", async (_ctx, _node, args) => await searchMails(args)); register("Mail_ExSearchMail", async (_ctx, _node, args) => await searchMails(args)); register("Mail_RecvMailGet", async (_ctx, _node, args) => { const mails = await searchMails(args); const wanted = number(args.limit || args.index, 1); const mail = mails[wanted - 1]; if (!mail) throw new RuntimeError("not_found", "未找到第 " + wanted + " 封邮件"); return mail; }); register("Mail_RecvCount", async (_ctx, _node, args) => await withMailbox(args, async (client) => client.mailbox.exists)); async function setSeen(args: Record, seen: boolean) { return await withMailbox(args, async (client) => { const sequence = String(number(args.index || args.limit, 1)); const flag = String.fromCharCode(92) + "Seen"; return seen ? await client.messageFlagsAdd(sequence, [flag]) : await client.messageFlagsRemove(sequence, [flag]); }); } register("Mail_MarkSeen", async (_ctx, _node, args) => setSeen(args, true)); register("Mail_UnmarkSeen", async (_ctx, _node, args) => setSeen(args, false)); register(["Mail_DoMoveMail", "Mail_OperateMail", "Mail_ExOperateMail", "Mail_DoMoveMail_v1"], async (_ctx, _node, args) => { const destination = text(args, ["DstFile", "destination", "folder_name", "email_folder"], "Archive"); return await withMailbox(args, async (client) => await client.messageMove(String(number(args.index || args.limit, 1)), destination)); }); async function invoke(id: string, args: Record) { const handler = getHandler(id); if (!handler) throw new RuntimeError("capability_not_implemented", id + " 不可用"); return handler; } register("Mail_DoSendMail", async (ctx, node, args) => { const handler = await invoke("Mail_Send", args); const config = mergeAccount(args); return await handler(ctx, node, { ...args, host: text(config, ["host", "server_address"]), account: text(config, ["Account", "account", "username"]), password: text(config, ["password"]), to: list(args.RecvByEmail), cc: list(args.CC), bcc: list(args.BCC), subject: text(args, ["Subject"]), content: text(args, ["Body", "body"]), attachment: text(args, ["filePath", "attachment"]) }, {} as never); }); register("Mail_DoSendMail_v1", async (ctx, node, args) => { const handler = await invoke("Mail_DoSendMail", args); return await handler(ctx, node, { ...args, account_obj: mergeAccount(args), RecvByEmail: args.RecvByEmail || args.target_email, CC: args.CC || args.cc, Subject: args.Subject || args.subject, Body: args.Body || args.body || args.content, filePath: args.filePath || args.attachment }, {} as never); }); register("Mail_ExSendMail", async (ctx, node, args) => { const handler = await invoke("Mail_Send", args); return await handler(ctx, node, { ...args, to: list(args.target_email), cc: list(args.cc), subject: text(args, ["subject"]), content: text(args, ["body"]), attachment: text(args, ["filePath", "attachment"]) }, {} as never); }); register("Mail_DoReplyMail", async (ctx, node, args) => { const handler = await invoke("Mail_ReplyMail_v2", args); return await handler(ctx, node, { ...args, subject: text(args, ["Subject"], "Re:"), content: text(args, ["ReplyBody", "content"]), attachment: text(args, ["filePath", "attachment"]) }, {} as never); }); register("Mail_DoReplyMail_v1", async (ctx, node, args) => { const handler = await invoke("Mail_ReplyMail_v2", args); return await handler(ctx, node, { ...args, subject: text(args, ["subject"], "Re:"), content: text(args, ["content", "ReplyBody"]), attachment: text(args, ["attachment"]) }, {} as never); });