PRA-only-demo/packages/rpa-core/src/capabilities/message.ts
yu caa8e19332 阶段1-rpa-core建包: engine+31个纯Node能力迁入packages/rpa-core, 领域类型单一源化, createEngine工厂收拢模块级状态
- engine/(registry/types/runtime/expression)物理迁入包, 解耦shared/types(域类型归包, 宿主type-only re-export)
- 31个纯Node能力+27个测试迁入包; 宿主留守Electron/浏览器/AI依赖能力, index.ts经@rpa/core/capabilities拉起
- 新增createEngine()/defaultEngine: excel缓存/Excel句柄/db连接/FTP客户端/OCR实例/拟真设置挂引擎实例
- remaining-core子流程依赖改为host-bridge注入, 包内不再import宿主flows
- 宿主main/index.ts启动时注入子流程源; electron-vite/vitest/tsconfig配@rpa/core源码别名
- 验证: typecheck绿, 快照909个id diff为零, 332测试过(2个桌面冒烟因会话无交互SetForegroundWindow被拒, 环境阻塞)
2026-11-04 12:36:03 +08:00

175 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import nodemailer from "nodemailer";
import { register } from "../engine/registry";
import { RuntimeError } from "../engine/types";
import { powershell } from "./desktop";
function text(args: Record<string, unknown>, keys: string[], fallback = ""): string {
for (const key of keys) if (args[key] !== undefined && args[key] !== null && args[key] !== "") return String(args[key]);
return fallback;
}
function list(value: string): string[] {
return value.split(/[;,,;\s]+/).map((item) => item.trim()).filter(Boolean);
}
function accountObject(value: unknown): Record<string, unknown> {
if (value && typeof value === "object") return value as Record<string, unknown>;
if (typeof value === "string" && value.trim().startsWith("{")) {
try { return JSON.parse(value); } catch { return {}; }
}
return {};
}
register(["Mail_Send", "Mail_Send_V1", "Mail_UnionSend", "Mail_UnionSend_v1"], async (_ctx, node, args) => {
const configured = accountObject(args.account_obj);
const account = text({ ...configured, ...args }, ["account", "from", "username", "user"]);
const password = text({ ...configured, ...args }, ["password", "pass"]);
const host = text({ ...configured, ...args }, ["host", "smtp", "smtpHost", "server"]);
if (!host || !account) throw new Error(`邮件配置不完整(${node.componentId})`);
const port = Number(text({ ...configured, ...args }, ["port", "smtpPort"], "25"));
const secure = ["true", "1", "是", "True"].includes(text({ ...configured, ...args }, ["is_ssl", "secure", "ssl"], "False"));
const transporter = nodemailer.createTransport({ host, port, secure, auth: password ? { user: account, pass: password } : undefined });
try {
const result = await transporter.sendMail({
from: account,
to: list(text(args, ["to", "receiver"])),
cc: list(text(args, ["Cc", "cc"])),
bcc: list(text(args, ["Bcc", "bcc"])),
subject: text(args, ["subject"]),
text: text(args, ["content", "body"]),
attachments: text(args, ["attachment"]) ? [{ path: text(args, ["attachment"]) }] : undefined
});
return { messageId: result.messageId, accepted: result.accepted };
} finally {
transporter.close();
}
});
export interface NormalizedMail {
uid: string; from: string; to: string[]; cc: string[]; subject: string;
text: string; html: string; date: string; messageId: string; hasAttachment: boolean;
attachmentNames: string[];
}
export function normalizeAccount(args: Record<string, unknown>): Record<string, unknown> {
return accountObject(args.account_obj ?? args.accountObj ?? args.account);
}
function addressList(value: unknown): string[] {
if (!value) return [];
if (Array.isArray(value)) return value.map((item) => String(typeof item === "object" && item && "text" in (item as Record<string, unknown>) ? (item as { text: unknown }).text : item)).filter(Boolean);
if (typeof value === "object") { const record = value as { text?: unknown; value?: unknown }; return addressList(record.text ?? record.value); }
return String(value).split(/[;,,;\s]+/).filter(Boolean);
}
export function normalizeMail(input: Record<string, unknown>, uid = ""): NormalizedMail {
const attachments = Array.isArray(input.attachments) ? input.attachments as Array<Record<string, unknown>> : [];
return {
uid: String(input.uid ?? uid),
from: String(input.from ?? input.sender ?? ""),
to: addressList(input.to), cc: addressList(input.cc),
subject: String(input.subject ?? ""),
text: String(input.text ?? input.body ?? ""),
html: String(input.html ?? ""),
date: input.date instanceof Date ? input.date.toISOString() : String(input.date ?? input.receivedTime ?? ""),
messageId: String(input.messageId ?? ""),
hasAttachment: Boolean(input.hasAttachment ?? attachments.length),
attachmentNames: attachments.map((item) => String(item.filename ?? item.name ?? "")).filter(Boolean)
};
}
function matches(mail: NormalizedMail, args: Record<string, unknown>): boolean {
const contains = (needle: unknown, haystack: string): boolean => !needle || haystack.toLowerCase().includes(String(needle).toLowerCase());
return contains(args.From_data, mail.from)
&& contains(args.To_data, mail.to.join(" "))
&& contains(args.Subject_data, mail.subject)
&& contains(args.Body_data, mail.text + "\n" + mail.html)
&& (args.attachment === undefined || String(args.attachment) === "2" || (String(args.attachment) === "1") === mail.hasAttachment);
}
export async function findImapMail(args: Record<string, unknown>, node: { id: string; componentId: string }): Promise<NormalizedMail[]> {
const configured = normalizeAccount(args);
const merged = { ...args, ...configured };
const account = text(merged, ["account", "username", "user", "from"]);
const password = text(merged, ["password", "pass", "token"]);
const host = text(merged, ["imap", "imapHost", "imap_host", "server", "host"]);
if (!host || !account) throw new RuntimeError("validation_error", "IMAP 配置不完整:需要 host 和 account", { nodeId: node.id, componentId: node.componentId });
const port = Number(text(merged, ["imapPort", "imap_port", "recvPort", "port"], "993"));
const secure = ["true", "1", "是", "True"].includes(text(merged, ["imap_ssl", "imapSsl", "is_ssl", "secure"], "True"));
const folder = text(args, ["folder_name", "email_folder", "folder"], "INBOX");
const { ImapFlow } = await import("imapflow");
const client = new ImapFlow({ host, port, secure, auth: { user: account, pass: password }, logger: false });
const mails: NormalizedMail[] = [];
try {
await client.connect();
const lock = await client.getMailboxLock(folder);
try {
const range = String(args.search_range ?? "ALL").toUpperCase();
const criteria: Record<string, unknown> = range === "UNSEEN" ? { seen: false } : range === "SEEN" ? { seen: true } : range === "FLAGGED" ? { flagged: true } : range === "ANSWERED" ? { answered: true } : range === "DELETED" ? { deleted: true } : range === "DRAFT" ? { draft: true } : { all: true };
if (args.From_data) criteria.from = String(args.From_data);
if (args.To_data) criteria.to = String(args.To_data);
if (args.Subject_data) criteria.subject = String(args.Subject_data);
const uids = await client.search(criteria as never, { uid: true }) || [];
const selected = Math.max(1, Number(args.email_num ?? 1) || 1);
for (let index = uids.length - 1; index >= 0 && mails.length < Math.max(20, selected); index -= 1) {
const download = await client.fetchOne(String(uids[index]), { uid: true, envelope: true, flags: true, bodyStructure: true, source: true }, { uid: true });
if (!download || !download.source) continue;
const { simpleParser } = await import("mailparser");
const parsed = await simpleParser(download.source);
const mail = normalizeMail(parsed as unknown as Record<string, unknown>, String(download.uid));
if (matches(mail, args)) mails.push(mail);
}
} finally { lock.release(); }
} finally { client.close(); }
const selected = Math.max(1, Number(args.email_num ?? 1) || 1);
return mails.slice(0, selected);
}
register(["Mail_FindMail", "Mail_FindMail_v1", "Mail_FindMail_v2"], async (_ctx, node, args) => findImapMail(args, node));
register(["Mail_ReplyMail", "Mail_ReplyMail_v1", "Mail_ReplyMail_v2"], async (_ctx, node, args) => {
const configured = accountObject(args.mail_obj ?? args.email_obj ?? args.account_obj);
const mail = configured && Object.keys(configured).length ? configured as unknown as NormalizedMail : args.mail as unknown as NormalizedMail;
const merged = { ...args, ...normalizeAccount(args) };
const account = text(merged, ["account", "username", "user", "from"]);
const password = text(merged, ["password", "pass"]);
const host = text(merged, ["smtp", "smtpHost", "smtp_host", "server", "host"]);
if (!host || !account) throw new RuntimeError("validation_error", "SMTP 配置不完整:需要 host 和 account", { nodeId: node.id, componentId: node.componentId });
const port = Number(text(merged, ["smtpPort", "smtp_port", "sendPort", "port"], "465"));
const secure = ["true", "1", "是", "True"].includes(text(merged, ["smtp_ssl", "smtpSsl", "is_ssl", "secure"], "True"));
const transporter = nodemailer.createTransport({ host, port, secure, auth: password ? { user: account, pass: password } : undefined });
try {
const result = await transporter.sendMail({
from: account,
to: mail?.from ? [mail.from] : list(text(args, ["to", "receiver"])),
subject: text(args, ["subject"], mail?.subject ? "Re: " + mail.subject : "Re:"),
text: text(args, ["content", "body"]),
inReplyTo: mail?.messageId, references: mail?.messageId,
attachments: text(args, ["attachment"]) ? [{ path: text(args, ["attachment"]) }] : undefined
});
return { messageId: result.messageId, accepted: result.accepted };
} finally { transporter.close(); }
});
function outlookScript(args: Record<string, unknown>): string {
const criteria = Buffer.from(JSON.stringify({ to: String(args.To_data ?? ""), from: String(args.From_data ?? ""), subject: String(args.Subject_data ?? ""), body: String(args.Body_data ?? "") }), "utf8").toString("base64");
return [
"$outlook=New-Object -ComObject Outlook.Application;$ns=$outlook.GetNamespace('MAPI')",
`$folderPath=${JSON.stringify(String(args.folder_name || args.email_folder || "INBOX"))}`,
"$folder=$ns.GetDefaultFolder(6);foreach($part in ($folderPath -split '/')){ if($part -and $part -ne 'INBOX'){ $folder=$folder.Folders.Item($part) } }",
"$folder.Items.Sort('[ReceivedTime]',$true)|Out-Null;$rows=@();$count=[Math]::Min(500,$folder.Items.Count)",
`$criteria=[Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${criteria}'))|ConvertFrom-Json`,
"for($i=1;$i -le $count;$i++){ $item=$folder.Items.Item($i); $subject=[string]$item.Subject; $body=[string]$item.Body; $to=[string]$item.To; $from=[string]$item.SenderEmailAddress; if(($criteria.to -and $to -notlike ('*'+$criteria.to+'*')) -or ($criteria.from -and $from -notlike ('*'+$criteria.from+'*')) -or ($criteria.subject -and $subject -notlike ('*'+$criteria.subject+'*')) -or ($criteria.body -and $body -notlike ('*'+$criteria.body+'*'))){continue}; $bodyB64=[Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($body)); $rows += @{uid=[string]$item.EntryID;from=$from;to=$to;subject=$subject;text=$bodyB64;html='';date=$item.ReceivedTime.ToString('o');messageId=[string]$item.EntryID;hasAttachment=($item.Attachments.Count -gt 0);attachmentNames=@()}; if($rows.Count -ge 100){break} }",
"$json=$rows|ConvertTo-Json -Compress -Depth 4;[Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($json))|Write-Output"
].join(";");
}
register(["Mail_DoGetMailMessage", "Mail_DoGetMailMessage_v1", "Mail_DoGetMailMessage_v2"], async (_ctx, node, args) => {
const output = await powershell(outlookScript(args), 120000);
if (!output.trim()) throw new RuntimeError("outlook_unavailable", "Outlook COM 未返回数据;请确认 Outlook 已安装并至少配置一个邮箱", { nodeId: node.id, componentId: node.componentId });
const rows = JSON.parse(Buffer.from(output.trim(), "base64").toString("utf8"));
const mails = (Array.isArray(rows) ? rows : [rows]).map((row: Record<string, unknown>) => normalizeMail({ ...row, text: row.text ? Buffer.from(String(row.text), "base64").toString("utf8") : "" }));
const selected = Math.max(1, Number(args.email_num ?? 1) || 1);
return mails.slice(0, selected);
});