import { ImapFlow } from 'imapflow'; import { encryptionService } from './encryptionService'; import { UserMailAccount } from './userMailAccountService'; interface PoolEntry { client: ImapFlow; accountId: number; lastUsed: number; busy: boolean; queue: Array<{ fn: (client: ImapFlow) => Promise; resolve: (v: any) => void; reject: (e: any) => void }>; } class ImapConnectionPool { private pool = new Map(); private readonly maxIdleMs = 300_000; constructor() { setInterval(() => this.cleanupIdle(), 60_000); process.on('SIGTERM', () => this.destroyAll()); process.on('SIGINT', () => this.destroyAll()); } async execute(account: UserMailAccount, fn: (client: ImapFlow) => Promise): Promise { const decryptedPassword = encryptionService.decrypt(account.password); let entry = this.pool.get(account.id); if (entry && !entry.client.usable) { this.pool.delete(account.id); entry = undefined; } if (!entry) { const client = new ImapFlow({ host: account.host, port: account.port, secure: account.useTls, auth: { user: account.username, pass: decryptedPassword }, logger: false as any, tls: { rejectUnauthorized: false }, }); await client.connect(); entry = { client, accountId: account.id, lastUsed: Date.now(), busy: false, queue: [] }; this.pool.set(account.id, entry); client.on('close', () => { const e = this.pool.get(account.id); if (e && e.client === client) { this.pool.delete(account.id); for (const pending of e.queue) pending.reject(new Error('IMAP 연결이 끊겼습니다')); e.queue = []; } }); } if (entry.busy) { return new Promise((resolve, reject) => { entry!.queue.push({ fn: fn as any, resolve, reject }); }); } return this.runWithEntry(entry, fn); } private async runWithEntry(entry: PoolEntry, fn: (client: ImapFlow) => Promise): Promise { entry.busy = true; entry.lastUsed = Date.now(); try { return await fn(entry.client); } catch (err) { if (!entry.client.usable) { this.pool.delete(entry.accountId); } throw err; } finally { entry.busy = false; if (entry.queue.length > 0) { const next = entry.queue.shift()!; this.runWithEntry(entry, next.fn).then(next.resolve).catch(next.reject); } } } private cleanupIdle() { const now = Date.now(); for (const [id, entry] of this.pool.entries()) { if (!entry.busy && entry.queue.length === 0 && now - entry.lastUsed > this.maxIdleMs) { try { entry.client.logout(); } catch {} this.pool.delete(id); } } } destroyByAccount(accountId: number) { const entry = this.pool.get(accountId); if (entry) { try { entry.client.logout(); } catch {} this.pool.delete(accountId); } } destroyAll() { for (const entry of this.pool.values()) { try { entry.client.logout(); } catch {} } this.pool.clear(); } } export const imapConnectionPool = new ImapConnectionPool();