import fs from 'fs'; import path from 'path'; import { ToolResult } from '../types.js'; import { getConfig } from '../config/config.js'; import { getWorkspacePath } from '../config/paths.js'; // ── Config ──────────────────────────────────────────────────────────────────── interface EmailConfig { imap_host: string; imap_port: number; imap_tls: boolean; imap_reject_unauthorized: boolean; smtp_host: string; smtp_port: number; smtp_secure: boolean; smtp_reject_unauthorized: boolean; username: string; password: string; } interface EmailAccount { id: string; config: EmailConfig; } function parseAccount(raw: any): EmailConfig | null { if (!raw?.imap_host || !raw?.username) return null; const cm = getConfig(); let password = cm.resolveSecret(raw.password || '') || ''; return { imap_host: raw.imap_host, imap_port: Number(raw.imap_port ?? 993), imap_tls: raw.imap_tls !== false, imap_reject_unauthorized: raw.imap_reject_unauthorized !== false, smtp_host: raw.smtp_host || raw.imap_host.replace('imap.', 'smtp.'), smtp_port: Number(raw.smtp_port ?? 587), smtp_secure: raw.smtp_secure === true, smtp_reject_unauthorized: raw.smtp_reject_unauthorized !== false, username: raw.username, password, }; } function getEmailConfig(accountId?: string, workspacePath?: string): EmailConfig | null { try { const cm = getConfig(); const cfg = cm.getConfig(); const emailCfg = cfg.email; if (!emailCfg) return null; let e: any; if (Array.isArray(emailCfg.accounts)) { if (accountId) { e = emailCfg.accounts.find((a: any) => a.id === accountId); } if (!e) e = emailCfg.accounts[0]; } else { e = emailCfg; } return parseAccount(e); } catch { return null; } } function getAllEmailConfigs(workspacePath?: string): EmailAccount[] { try { const cm = getConfig(); const cfg = cm.getConfig(); const emailCfg = cfg.email; if (!emailCfg) return []; if (Array.isArray(emailCfg.accounts)) { const accounts: EmailAccount[] = []; for (const a of emailCfg.accounts) { const config = parseAccount(a); if (config) accounts.push({ id: a.id, config }); } return accounts; } const config = parseAccount(emailCfg); const id = emailCfg.username ? emailCfg.username.split('@')[0] : 'default'; return config ? [{ id, config }] : []; } catch { return []; } } // ── IMAP helper ─────────────────────────────────────────────────────────────── async function withImap(fn: (client: any) => Promise, accountId?: string, workspacePath?: string): Promise { const cfg = getEmailConfig(accountId, workspacePath); if (!cfg) throw new Error('이메일 설정이 없습니다. config.json의 email 섹션을 확인하세요.'); const { ImapFlow } = await import('imapflow'); const client = new ImapFlow({ host: cfg.imap_host, port: cfg.imap_port, secure: cfg.imap_tls, auth: { user: cfg.username, pass: cfg.password }, logger: false, connectionTimeout: 15000, greetingTimeout: 10000, socketTimeout: 20000, tls: { rejectUnauthorized: cfg.imap_reject_unauthorized }, }); client.on('error', () => {}); await client.connect(); try { return await fn(client); } finally { client.logout().catch(() => {}); } } function stripHtml(html: string): string { return html .replace(/]*>[\s\S]*?<\/style>/gi, '') .replace(/]*>[\s\S]*?<\/script>/gi, '') .replace(//gi, '\n') .replace(/<\/p>/gi, '\n') .replace(/<[^>]+>/g, '') .replace(/ /g, ' ') .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"') .replace(/\n{3,}/g, '\n\n') .trim(); } // ── email_list ──────────────────────────────────────────────────────────────── export const emailListTool = { name: 'email_list', description: '받은편지함(또는 지정 폴더)의 최근 이메일 목록을 가져옵니다. account 미지정 시 모든 계정을 순회합니다.', schema: { folder: '조회할 폴더 (기본: INBOX)', limit: '가져올 메일 수 (기본: 20, 최대: 50)', account: '계정 ID (미지정 시 모든 계정 순회)', }, jsonSchema: { type: 'object', properties: { folder: { type: 'string', description: '폴더명 (기본: INBOX)' }, limit: { type: 'number', description: '가져올 수 (기본 20, 최대 50)' }, account: { type: 'string', description: '계정 ID (미지정 시 모든 계정)' }, }, additionalProperties: false, }, execute: async (args: any): Promise => { const folder = String(args?.folder || 'INBOX'); const limit = Math.min(Number(args?.limit) || 50, 200); const account = args?.account ? String(args.account) : undefined; const workspacePath = args?._workspacePath || args?._workspace || undefined; try { // Single account mode if (account) { const messages = await withImap(async (client) => { await client.mailboxOpen(folder, { readOnly: true }); const status = await client.status(folder, { messages: true }); const total = status.messages || 0; if (total === 0) return []; const seqFrom = Math.max(1, total - limit + 1); const results: any[] = []; for await (const msg of client.fetch(`${seqFrom}:*`, { uid: true, flags: true, envelope: true, })) { const from = msg.envelope?.from?.[0]; results.push({ uid: msg.uid, seq: msg.seq, subject: msg.envelope?.subject || '(제목 없음)', from: from ? `${from.name || ''} <${from.address}>`.trim() : '', date: msg.envelope?.date ? new Date(msg.envelope.date).toLocaleString('ko-KR', { timeZone: 'Asia/Seoul' }) : '', unread: !msg.flags?.has('\\Seen'), account, }); } return results.reverse(); }, account, workspacePath); if (messages.length === 0) { return { success: true, stdout: `[${account}] ${folder}: 메일이 없습니다.`, data: [] }; } const lines = [ `📬 [${account}] ${folder} — 최근 ${messages.length}개`, '', ...messages.map((m: any, i: number) => `${i + 1}. [UID:${m.uid}] ${m.unread ? '🔵 ' : ''}${m.subject}\n 발신: ${m.from}\n 날짜: ${m.date}` ), ]; return { success: true, stdout: lines.join('\n'), data: messages }; } // All accounts mode const allAccounts = getAllEmailConfigs(workspacePath); if (allAccounts.length === 0) { return { success: false, error: '등록된 이메일 계정이 없습니다.' }; } const allMessages: any[] = []; const errors: string[] = []; for (const acct of allAccounts) { try { const messages = await withImap(async (client) => { await client.mailboxOpen(folder, { readOnly: true }); const status = await client.status(folder, { messages: true }); const total = status.messages || 0; if (total === 0) return []; const perAcctLimit = Math.max(Math.ceil(limit / allAccounts.length), 5); const seqFrom = Math.max(1, total - perAcctLimit + 1); const results: any[] = []; for await (const msg of client.fetch(`${seqFrom}:*`, { uid: true, flags: true, envelope: true, })) { const from = msg.envelope?.from?.[0]; results.push({ uid: msg.uid, seq: msg.seq, subject: msg.envelope?.subject || '(제목 없음)', from: from ? `${from.name || ''} <${from.address}>`.trim() : '', date: msg.envelope?.date ? new Date(msg.envelope.date).toLocaleString('ko-KR', { timeZone: 'Asia/Seoul' }) : '', unread: !msg.flags?.has('\\Seen'), account: acct.id, }); } return results.reverse(); }, acct.id, workspacePath); allMessages.push(...messages); } catch (err: any) { errors.push(`${acct.id}: ${err.message}`); } } // Sort all messages by date descending allMessages.sort((a, b) => { const da = a.date ? new Date(a.date).getTime() : 0; const db = b.date ? new Date(b.date).getTime() : 0; return db - da; }); const trimmed = allMessages.slice(0, limit); if (trimmed.length === 0) { const errInfo = errors.length ? `\n⚠️ 오류: ${errors.join('; ')}` : ''; return { success: true, stdout: `${folder}: 메일이 없습니다.${errInfo}`, data: [] }; } // Group by account, preserving the account order from config const grouped = new Map(); for (const acct of allAccounts) { grouped.set(acct.id, []); } for (const m of trimmed) { if (!grouped.has(m.account)) grouped.set(m.account, []); grouped.get(m.account)!.push(m); } const lines: string[] = []; let totalShown = 0; for (const [acctId, msgs] of grouped) { if (msgs.length === 0) { lines.push(`📭 [${acctId}] ${folder} — 메일 없음`, ''); continue; } lines.push(`📬 [${acctId}] ${folder} — ${msgs.length}개`, ''); for (let i = 0; i < msgs.length; i++) { const m = msgs[i]; lines.push(`${i + 1}. [UID:${m.uid}] ${m.unread ? '🔵 ' : ''}${m.subject}\n 발신: ${m.from}\n 날짜: ${m.date}`); totalShown++; } lines.push(''); } lines.unshift(`📬 ${folder} — 전체 계정에서 ${totalShown}개`, ''); if (errors.length) { lines.push(`⚠️ 일부 계정 오류: ${errors.join('; ')}`); } return { success: true, stdout: lines.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd(), data: trimmed }; } catch (err: any) { return { success: false, error: err.message }; } }, }; // ── email_read ──────────────────────────────────────────────────────────────── export const emailReadTool = { name: 'email_read', description: 'UID로 특정 이메일의 전문(발신자, 수신자, 제목, 본문)을 읽어옵니다. account 미지정 시 모든 계정에서 UID를 찾습니다. 첨부파일이 있으면 자동으로 workspace/attachments/uid-{uid}/ 에 저장하고 경로를 반환합니다.', schema: { uid: '읽을 메일의 UID (email_list에서 확인)', folder: '폴더명 (기본: INBOX)', account: '계정 ID (미지정 시 모든 계정에서 검색)', }, jsonSchema: { type: 'object', properties: { uid: { type: 'number', description: '메일 UID' }, folder: { type: 'string', description: '폴더명 (기본: INBOX)' }, account: { type: 'string', description: '계정 ID (미지정 시 모든 계정 검색)' }, }, required: ['uid'], additionalProperties: false, }, execute: async (args: any): Promise => { const uid = Number(args?.uid); const folder = String(args?.folder || 'INBOX'); const account = args?.account ? String(args.account) : undefined; const workspacePath = args?._workspacePath || args?._workspace || undefined; const saveAttachments = true; if (!uid) return { success: false, error: 'uid가 필요합니다.' }; const readMail = async (acctId: string): Promise<{ result: any; account: string } | null> => { try { const result = await withImap(async (client) => { await client.mailboxOpen(folder, { readOnly: false }); await client.messageFlagsAdd(uid.toString(), ['\\Seen'], { uid: true }).catch(() => {}); const { simpleParser } = await import('mailparser'); for await (const msg of client.fetch(uid.toString(), { uid: true, envelope: true, source: true }, { uid: true })) { const parsed = await simpleParser(msg.source); const body = parsed.text || (parsed.html ? stripHtml(parsed.html) : '(본문 없음)'); const savedPaths: string[] = []; const savedRelPaths: string[] = []; if (saveAttachments && parsed.attachments?.length) { const injectedWorkspace = workspacePath || args?._workspacePath || args?._workspace; let resolvedWorkspacePath: string; if (injectedWorkspace) { resolvedWorkspacePath = path.resolve(String(injectedWorkspace)); } else { const cfg = getConfig().getConfig(); resolvedWorkspacePath = path.resolve(getWorkspacePath(args)); } const relDir = path.join('attachments', `uid-${uid}`); const saveDir = path.join(resolvedWorkspacePath, relDir); fs.mkdirSync(saveDir, { recursive: true }); for (const att of parsed.attachments) { const filename = att.filename || `attachment-${savedPaths.length + 1}`; const safeName = filename.replace(/[/\\:*?"<>|]/g, '_').replace(/\s+/g, '_'); const dest = path.join(saveDir, safeName); fs.writeFileSync(dest, att.content); savedPaths.push(dest); savedRelPaths.push(path.join(relDir, safeName)); } } return { uid, subject: parsed.subject || '(제목 없음)', from: (Array.isArray(parsed.from) ? parsed.from[0] : parsed.from)?.text || '', to: (Array.isArray(parsed.to) ? parsed.to[0] : parsed.to )?.text || '', cc: (Array.isArray(parsed.cc) ? parsed.cc[0] : parsed.cc )?.text || '', date: parsed.date ? new Date(parsed.date).toLocaleString('ko-KR', { timeZone: 'Asia/Seoul' }) : '', body: body.slice(0, 8000), attachments: (parsed.attachments || []).map((a: any) => `${a.filename} (${a.size} bytes)`), savedPaths, savedRelPaths, }; } return null; }, acctId, workspacePath); return result ? { result, account: acctId } : null; } catch { return null; } }; try { // If account specified, try only that account if (account) { const found = await readMail(account); if (!found) return { success: false, error: `[${account}] UID ${uid} 메일을 찾을 수 없습니다.` }; const r = found.result; const lines = [ `📧 메일 상세 (UID: ${uid}, 계정: ${account})`, `제목: ${r.subject}`, `발신: ${r.from}`, `수신: ${r.to}`, r.cc ? `참조: ${r.cc}` : '', `날짜: ${r.date}`, r.attachments.length ? `첨부파일: ${r.attachments.join(', ')}` : '', r.savedRelPaths?.length ? `✅ 첨부파일 저장 완료 — pdf_read 등에서 아래 상대경로를 그대로 사용하세요:\n${r.savedRelPaths.map((p: string) => ` - [${path.basename(p)}](/api/files/${p.replace(/\\/g, '/')})`).join('\n')}` : '', '', '─── 본문 ───', r.body, ].filter((l: string) => l !== ''); const { savedPaths: _sp, ...safeData } = { ...r, account: found.account }; return { success: true, stdout: lines.join('\n'), data: safeData }; } // No account specified — try all accounts const allAccounts = getAllEmailConfigs(workspacePath); if (allAccounts.length === 0) { return { success: false, error: '등록된 이메일 계정이 없습니다.' }; } const errors: string[] = []; for (const acct of allAccounts) { const found = await readMail(acct.id); if (found) { const r = found.result; const lines = [ `📧 메일 상세 (UID: ${uid}, 계정: ${found.account})`, `제목: ${r.subject}`, `발신: ${r.from}`, `수신: ${r.to}`, r.cc ? `참조: ${r.cc}` : '', `날짜: ${r.date}`, r.attachments.length ? `첨부파일: ${r.attachments.join(', ')}` : '', r.savedRelPaths?.length ? `✅ 첨부파일 저장 완료 — pdf_read 등에서 아래 상대경로를 그대로 사용하세요:\n${r.savedRelPaths.map((p: string) => ` - [${path.basename(p)}](/api/files/${p.replace(/\\/g, '/')})`).join('\n')}` : '', '', '─── 본문 ───', r.body, ].filter((l: string) => l !== ''); const { savedPaths: _sp, ...safeData } = { ...r, account: found.account }; return { success: true, stdout: lines.join('\n'), data: safeData }; } errors.push(acct.id); } return { success: false, error: `UID ${uid} 메일을 찾을 수 없습니다. (검색한 계정: ${errors.join(', ')})` }; } catch (err: any) { return { success: false, error: err.message }; } }, }; // ── email_send ──────────────────────────────────────────────────────────────── export const emailSendTool = { name: 'email_send', description: '이메일을 발송합니다. SMTP를 통해 to/subject/body를 지정해 발송하며 cc 추가도 가능합니다. account 미지정 시 첫 번째 등록 계정으로 발송합니다.', schema: { to: '수신자 이메일 주소 (여러 명은 쉼표로 구분)', subject: '제목', body: '본문 (Plain text)', cc: '참조 주소 (선택, 쉼표 구분)', account: '계정 ID (미지정 시 첫 번째 계정)', }, jsonSchema: { type: 'object', properties: { to: { type: 'string', description: '수신자 이메일' }, subject: { type: 'string', description: '제목' }, body: { type: 'string', description: '본문' }, cc: { type: 'string', description: '참조 (선택)' }, account: { type: 'string', description: '계정 ID (미지정 시 첫 번째 계정)' }, }, required: ['to', 'subject', 'body'], additionalProperties: false, }, execute: async (args: any): Promise => { const to = String(args?.to || '').trim(); const subject = String(args?.subject || '').trim(); const body = String(args?.body || '').trim(); const cc = args?.cc ? String(args.cc).trim() : undefined; const account = args?.account ? String(args.account) : undefined; if (!to) return { success: false, error: 'to(수신자)가 필요합니다.' }; if (!subject) return { success: false, error: 'subject(제목)가 필요합니다.' }; if (!body) return { success: false, error: 'body(본문)가 필요합니다.' }; const workspacePath = args?._workspacePath || args?._workspace || undefined; const cfg = getEmailConfig(account, workspacePath); if (!cfg) return { success: false, error: '이메일 설정이 없습니다. config.json의 email 섹션을 확인하세요.' }; try { const nodemailer = await import('nodemailer'); const transporter = nodemailer.createTransport({ host: cfg.smtp_host, port: cfg.smtp_port, secure: cfg.smtp_secure, auth: { user: cfg.username, pass: cfg.password }, connectionTimeout: 15000, greetingTimeout: 10000, tls: { rejectUnauthorized: cfg.smtp_reject_unauthorized }, }); const info = await transporter.sendMail({ from: cfg.username, to, cc, subject, text: body, }); return { success: true, stdout: `✅ 메일 발송 완료\n발신: ${cfg.username}\n수신: ${to}\n제목: ${subject}\nMessage-ID: ${info.messageId}`, data: { messageId: info.messageId, from: cfg.username, to, subject }, }; } catch (err: any) { return { success: false, error: `발송 실패: ${err.message}` }; } }, }; // ── email_search ────────────────────────────────────────────────────────────── export const emailSearchTool = { name: 'email_search', description: '이메일을 검색합니다. 키워드, 발신자, 제목, 날짜 범위로 검색 가능합니다. account 미지정 시 모든 계정을 순회합니다.', schema: { query: '검색어 (제목+본문 전문 검색)', from: '발신자 이메일 주소로 필터', subject: '제목으로 필터', since: '이 날짜 이후 (YYYY-MM-DD)', before: '이 날짜 이전 (YYYY-MM-DD)', folder: '검색할 폴더 (기본: INBOX)', limit: '최대 결과 수 (기본: 20)', account: '계정 ID (미지정 시 모든 계정 순회)', }, jsonSchema: { type: 'object', properties: { query: { type: 'string', description: '전문 검색어' }, from: { type: 'string', description: '발신자 필터' }, subject: { type: 'string', description: '제목 필터' }, since: { type: 'string', description: 'YYYY-MM-DD 이후' }, before: { type: 'string', description: 'YYYY-MM-DD 이전' }, folder: { type: 'string', description: '폴더 (기본 INBOX)' }, limit: { type: 'number', description: '최대 결과 수 (기본 20)' }, account: { type: 'string', description: '계정 ID (미지정 시 모든 계정)' }, }, additionalProperties: false, }, execute: async (args: any): Promise => { const folder = String(args?.folder || 'INBOX'); const limit = Math.min(Number(args?.limit) || 50, 200); const account = args?.account ? String(args.account) : undefined; const workspacePath = args?._workspacePath || args?._workspace || undefined; if (!args?.query && !args?.from && !args?.subject && !args?.since && !args?.before) { return { success: false, error: 'query, from, subject, since, before 중 하나 이상이 필요합니다.' }; } const buildCriteria = () => { const criteria: any[] = []; if (args?.from) criteria.push({ from: String(args.from) }); if (args?.subject) criteria.push({ subject: String(args.subject) }); if (args?.query) criteria.push({ body: String(args.query) }); if (args?.since) criteria.push({ since: new Date(String(args.since)) }); if (args?.before) criteria.push({ before: new Date(String(args.before)) }); return criteria.length === 1 ? criteria[0] : { and: criteria }; }; const searchCriteria = buildCriteria(); const searchAccount = async (acctId: string): Promise => { return await withImap(async (client) => { await client.mailboxOpen(folder, { readOnly: true }); const uids: number[] = await client.search(searchCriteria, { uid: true }); const perAcctLimit = account ? limit : Math.max(Math.ceil(limit / (getAllEmailConfigs(workspacePath).length || 1)), 5); const recentUids = uids.slice(-perAcctLimit).reverse(); if (recentUids.length === 0) return []; const results: any[] = []; for await (const msg of client.fetch(recentUids.join(','), { uid: true, flags: true, envelope: true, }, { uid: true })) { const from = msg.envelope?.from?.[0]; results.push({ uid: msg.uid, subject: msg.envelope?.subject || '(제목 없음)', from: from ? `${from.name || ''} <${from.address}>`.trim() : '', date: msg.envelope?.date ? new Date(msg.envelope.date).toLocaleString('ko-KR', { timeZone: 'Asia/Seoul' }) : '', unread: !msg.flags?.has('\\Seen'), account: acctId, }); } return results; }, acctId, workspacePath); }; try { // Single account mode if (account) { const messages = await searchAccount(account); if (messages.length === 0) { return { success: true, stdout: '검색 결과가 없습니다.', data: [] }; } const lines = [ `🔍 [${account}] 검색 결과 — ${messages.length}개`, '', ...messages.map((m: any, i: number) => `${i + 1}. [UID:${m.uid}] ${m.unread ? '🔵 ' : ''}${m.subject}\n 발신: ${m.from}\n 날짜: ${m.date}` ), ]; return { success: true, stdout: lines.join('\n'), data: messages }; } // All accounts mode const allAccounts = getAllEmailConfigs(workspacePath); if (allAccounts.length === 0) { return { success: false, error: '등록된 이메일 계정이 없습니다.' }; } const allMessages: any[] = []; const errors: string[] = []; for (const acct of allAccounts) { try { const messages = await searchAccount(acct.id); allMessages.push(...messages); } catch (err: any) { errors.push(`${acct.id}: ${err.message}`); } } // Sort by date descending allMessages.sort((a, b) => { const da = a.date ? new Date(a.date).getTime() : 0; const db = b.date ? new Date(b.date).getTime() : 0; return db - da; }); const trimmed = allMessages.slice(0, limit); if (trimmed.length === 0) { const errInfo = errors.length ? `\n⚠️ 오류: ${errors.join('; ')}` : ''; return { success: true, stdout: `검색 결과가 없습니다.${errInfo}`, data: [] }; } const lines = [ `🔍 검색 결과 — 전체 계정에서 ${trimmed.length}개`, '', ...trimmed.map((m: any, i: number) => `${i + 1}. [${m.account}] [UID:${m.uid}] ${m.unread ? '🔵 ' : ''}${m.subject}\n 발신: ${m.from}\n 날짜: ${m.date}` ), ]; if (errors.length) { lines.push('', `⚠️ 일부 계정 오류: ${errors.join('; ')}`); } return { success: true, stdout: lines.join('\n'), data: trimmed }; } catch (err: any) { return { success: false, error: err.message }; } }, }; // ── email_delete ────────────────────────────────────────────────────────────── export const emailDeleteTool = { name: 'email_delete', description: 'UID로 이메일을 삭제합니다. account 미지정 시 모든 계정에서 UID를 찾아 삭제합니다. 가능한 경우 휴지통으로 이동하고, 불가능하면 \\Deleted 플래그를 설정합니다.', schema: { uid: '삭제할 메일의 UID', folder: '폴더명 (기본: INBOX)', account: '계정 ID (미지정 시 모든 계정에서 검색)', }, jsonSchema: { type: 'object', properties: { uid: { type: 'number', description: '삭제할 메일 UID' }, folder: { type: 'string', description: '폴더명 (기본: INBOX)' }, account: { type: 'string', description: '계정 ID (미지정 시 모든 계정 검색)' }, }, required: ['uid'], additionalProperties: false, }, execute: async (args: any): Promise => { const uid = Number(args?.uid); const folder = String(args?.folder || 'INBOX'); const account = args?.account ? String(args.account) : undefined; const workspacePath = args?._workspacePath || args?._workspace || undefined; if (!uid) return { success: false, error: 'uid가 필요합니다.' }; const deleteFromAccount = async (acctId: string): Promise => { try { await withImap(async (client) => { await client.mailboxOpen(folder); const mailboxes = await client.list(); const trash = mailboxes.find((m: any) => m.specialUse === '\\Trash' || /^(Trash|휴지통|Deleted Items|Deleted Messages)$/i.test(m.name) ); if (trash) { await client.messageMove(uid.toString(), trash.path, { uid: true }); } else { await client.messageFlagsAdd(uid.toString(), ['\\Deleted'], { uid: true }); await client.mailboxClose(); } }, acctId, workspacePath); return true; } catch { return false; } }; try { // Single account mode if (account) { const ok = await deleteFromAccount(account); if (!ok) return { success: false, error: `[${account}] UID ${uid} 삭제 실패` }; return { success: true, stdout: `🗑️ [${account}] UID ${uid} 메일이 삭제되었습니다.`, data: { uid, folder, account }, }; } // All accounts mode — try each account const allAccounts = getAllEmailConfigs(workspacePath); if (allAccounts.length === 0) { return { success: false, error: '등록된 이메일 계정이 없습니다.' }; } for (const acct of allAccounts) { const ok = await deleteFromAccount(acct.id); if (ok) { return { success: true, stdout: `🗑️ [${acct.id}] UID ${uid} 메일이 삭제되었습니다.`, data: { uid, folder, account: acct.id }, }; } } return { success: false, error: `UID ${uid} 메일을 찾을 수 없어 삭제하지 못했습니다.` }; } catch (err: any) { return { success: false, error: `삭제 실패: ${err.message}` }; } }, };