v2.4.1
This commit is contained in:
+39
-27
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user