This commit is contained in:
kim
2026-05-05 00:51:37 +09:00
parent 9714d4b50c
commit 233b67b9a3
35 changed files with 3496 additions and 138 deletions
+39 -27
View File
@@ -19,12 +19,19 @@ interface EmailConfig {
password: string;
}
function getEmailConfig(accountId?: string): EmailConfig | null {
function getEmailConfig(accountId?: string, workspacePath?: string): EmailConfig | null {
try {
// Per-user config: look in the workspace directory first (e.g. <workspace>/.smallclaw/email-config.json)
const perUserPath = workspacePath
? path.join(workspacePath, '.smallclaw', 'email-config.json')
: null;
const globalPath = path.join(os.homedir(), '.smallclaw', 'config.json');
const projectCfg = path.join(process.cwd(), '.smallclaw', 'config.json');
const cfgPath = fs.existsSync(projectCfg)
? projectCfg
: path.join(os.homedir(), '.smallclaw', 'config.json');
const cfgPath = perUserPath && fs.existsSync(perUserPath)
? perUserPath
: fs.existsSync(projectCfg)
? projectCfg
: globalPath;
if (!fs.existsSync(cfgPath)) return null;
const data = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));
const emailCfg = data.email;
@@ -42,7 +49,7 @@ function getEmailConfig(accountId?: string): EmailConfig | null {
if (!e?.imap_host || !e?.username) return null;
// Resolve vault: references for password
// Resolve vault: references for password (vault is shared with the config dir)
let password = e.password || '';
if (password.startsWith('vault:')) {
try {
@@ -73,8 +80,8 @@ function getEmailConfig(accountId?: string): EmailConfig | null {
// ── IMAP helper ───────────────────────────────────────────────────────────────
async function withImap<T>(fn: (client: any) => Promise<T>, accountId?: string): Promise<T> {
const cfg = getEmailConfig(accountId);
async function withImap<T>(fn: (client: any) => Promise<T>, accountId?: string, workspacePath?: string): Promise<T> {
const cfg = getEmailConfig(accountId, workspacePath);
if (!cfg) throw new Error('이메일 설정이 없습니다. config.json의 email 섹션을 확인하세요.');
const { ImapFlow } = await import('imapflow');
const client = new ImapFlow({
@@ -121,14 +128,14 @@ export const emailListTool = {
schema: {
folder: '조회할 폴더 (기본: INBOX)',
limit: '가져올 메일 수 (기본: 20, 최대: 50)',
account: '계정 ID (예: applecherry, evangel. 기본: default 계정)',
account: '계정 ID (미지정 시 default)',
},
jsonSchema: {
type: 'object',
properties: {
folder: { type: 'string', description: '폴더명 (기본: INBOX)' },
limit: { type: 'number', description: '가져올 수 (기본 20, 최대 50)' },
account: { type: 'string', description: '계정 ID (예: applecherry, evangel)' },
account: { type: 'string', description: '계정 ID (미지정 시 default)' },
},
additionalProperties: false,
},
@@ -136,6 +143,7 @@ export const emailListTool = {
const folder = String(args?.folder || 'INBOX');
const limit = Math.min(Number(args?.limit) || 20, 50);
const account = args?.account ? String(args.account) : undefined;
const workspacePath = args?._workspacePath || args?._workspace || undefined;
try {
const messages = await withImap(async (client) => {
await client.mailboxOpen(folder, { readOnly: true });
@@ -162,7 +170,7 @@ export const emailListTool = {
});
}
return results.reverse();
}, account);
}, account, workspacePath);
if (messages.length === 0) {
return { success: true, stdout: `${folder}: 메일이 없습니다.`, data: [] };
@@ -190,14 +198,14 @@ export const emailReadTool = {
schema: {
uid: '읽을 메일의 UID (email_list에서 확인)',
folder: '폴더명 (기본: INBOX)',
account: '계정 ID (예: applecherry, evangel, daum. 기본: default 계정)',
account: '계정 ID (미지정 시 default)',
},
jsonSchema: {
type: 'object',
properties: {
uid: { type: 'number', description: '메일 UID' },
folder: { type: 'string', description: '폴더명 (기본: INBOX)' },
account: { type: 'string', description: '계정 ID (예: applecherry, evangel, daum)' },
account: { type: 'string', description: '계정 ID (미지정 시 default)' },
},
required: ['uid'],
additionalProperties: false,
@@ -206,6 +214,7 @@ export const emailReadTool = {
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; // always save attachments when present
if (!uid) return { success: false, error: 'uid가 필요합니다.' };
try {
@@ -221,17 +230,17 @@ export const emailReadTool = {
const savedPaths: string[] = [];
const savedRelPaths: string[] = [];
if (saveAttachments && parsed.attachments?.length) {
const injectedWorkspace = args?._workspace;
let workspacePath: string;
const injectedWorkspace = workspacePath || args?._workspacePath || args?._workspace;
let resolvedWorkspacePath: string;
if (injectedWorkspace) {
workspacePath = path.resolve(String(injectedWorkspace));
resolvedWorkspacePath = path.resolve(String(injectedWorkspace));
} else {
const cfgPath = path.join(process.cwd(), '.smallclaw', 'config.json');
const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));
workspacePath = path.resolve(cfg?.workspace?.path || path.join(process.cwd(), 'workspace'));
resolvedWorkspacePath = path.resolve(cfg?.workspace?.path || path.join(process.cwd(), 'workspace'));
}
const relDir = path.join('attachments', `uid-${uid}`);
const saveDir = path.join(workspacePath, relDir);
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}`;
@@ -259,7 +268,7 @@ export const emailReadTool = {
};
}
return null;
}, account);
}, account, workspacePath);
if (!result) return { success: false, error: `UID ${uid} 메일을 찾을 수 없습니다.` };
@@ -296,7 +305,7 @@ export const emailSendTool = {
subject: '제목',
body: '본문 (Plain text)',
cc: '참조 주소 (선택, 쉼표 구분)',
account: '계정 ID (예: applecherry, evangel. 기본: default 계정)',
account: '계정 ID (미지정 시 default)',
},
jsonSchema: {
type: 'object',
@@ -305,7 +314,7 @@ export const emailSendTool = {
subject: { type: 'string', description: '제목' },
body: { type: 'string', description: '본문' },
cc: { type: 'string', description: '참조 (선택)' },
account: { type: 'string', description: '계정 ID (예: applecherry, evangel)' },
account: { type: 'string', description: '계정 ID (미지정 시 default)' },
},
required: ['to', 'subject', 'body'],
additionalProperties: false,
@@ -321,7 +330,8 @@ export const emailSendTool = {
if (!subject) return { success: false, error: 'subject(제목)가 필요합니다.' };
if (!body) return { success: false, error: 'body(본문)가 필요합니다.' };
const cfg = getEmailConfig(account);
const workspacePath = args?._workspacePath || args?._workspace || undefined;
const cfg = getEmailConfig(account, workspacePath);
if (!cfg) return { success: false, error: '이메일 설정이 없습니다. config.json의 email 섹션을 확인하세요.' };
try {
@@ -368,7 +378,7 @@ export const emailSearchTool = {
before: '이 날짜 이전 (YYYY-MM-DD)',
folder: '검색할 폴더 (기본: INBOX)',
limit: '최대 결과 수 (기본: 20)',
account: '계정 ID (예: applecherry, evangel. 기본: default 계정)',
account: '계정 ID (미지정 시 default)',
},
jsonSchema: {
type: 'object',
@@ -380,7 +390,7 @@ export const emailSearchTool = {
before: { type: 'string', description: 'YYYY-MM-DD 이전' },
folder: { type: 'string', description: '폴더 (기본 INBOX)' },
limit: { type: 'number', description: '최대 결과 수 (기본 20)' },
account: { type: 'string', description: '계정 ID (예: applecherry, evangel)' },
account: { type: 'string', description: '계정 ID (미지정 시 default)' },
},
additionalProperties: false,
},
@@ -388,6 +398,7 @@ export const emailSearchTool = {
const folder = String(args?.folder || 'INBOX');
const limit = Math.min(Number(args?.limit) || 20, 50);
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 중 하나 이상이 필요합니다.' };
@@ -427,7 +438,7 @@ export const emailSearchTool = {
});
}
return results;
}, account);
}, account, workspacePath);
if (messages.length === 0) {
return { success: true, stdout: '검색 결과가 없습니다.', data: [] };
@@ -455,14 +466,14 @@ export const emailDeleteTool = {
schema: {
uid: '삭제할 메일의 UID',
folder: '폴더명 (기본: INBOX)',
account: '계정 ID (예: applecherry, evangel. 기본: default 계정)',
account: '계정 ID (미지정 시 default)',
},
jsonSchema: {
type: 'object',
properties: {
uid: { type: 'number', description: '삭제할 메일 UID' },
folder: { type: 'string', description: '폴더명 (기본: INBOX)' },
account: { type: 'string', description: '계정 ID (예: applecherry, evangel)' },
account: { type: 'string', description: '계정 ID (미지정 시 default)' },
},
required: ['uid'],
additionalProperties: false,
@@ -471,6 +482,7 @@ export const emailDeleteTool = {
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가 필요합니다.' };
try {
@@ -489,7 +501,7 @@ export const emailDeleteTool = {
await client.messageFlagsAdd(uid.toString(), ['\\Deleted'], { uid: true });
await client.mailboxClose();
}
}, account);
}, account, workspacePath);
return {
success: true,