Files
vexplor_dev/backend-node/src/services/imapConnectionPool.ts
syc0123 4c42cc7b53 feat: 사용자 메일 관리 IMAP 구현
- IMAP 계정 등록/수정/삭제/연결테스트
- SSE 스트리밍으로 메일 목록 로드 (폴더별 지원)
- 메일 상세 조회, 읽음 처리, 삭제(휴지통 이동), 폴더 이동
- 첨부파일 다운로드 (ReadableStream 진행바)
- SMTP 발송, 답장, 전달
- imapConnectionPool, mailCache 서비스
- encryptionService Node 22+ 호환 수정
- authMiddleware query token 지원 추가

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 17:17:20 +09:00

110 lines
3.1 KiB
TypeScript

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<any>; resolve: (v: any) => void; reject: (e: any) => void }>;
}
class ImapConnectionPool {
private pool = new Map<number, PoolEntry>();
private readonly maxIdleMs = 300_000;
constructor() {
setInterval(() => this.cleanupIdle(), 60_000);
process.on('SIGTERM', () => this.destroyAll());
process.on('SIGINT', () => this.destroyAll());
}
async execute<T>(account: UserMailAccount, fn: (client: ImapFlow) => Promise<T>): Promise<T> {
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<T>((resolve, reject) => {
entry!.queue.push({ fn: fn as any, resolve, reject });
});
}
return this.runWithEntry(entry, fn);
}
private async runWithEntry<T>(entry: PoolEntry, fn: (client: ImapFlow) => Promise<T>): Promise<T> {
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();