Files
vexplor/backend-node/src/services/imapConnectionPool.ts

110 lines
3.1 KiB
TypeScript
Raw Normal View History

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();