228 lines
6.5 KiB
TypeScript
228 lines
6.5 KiB
TypeScript
|
|
import Imap from 'imap';
|
||
|
|
import { getCredentialsFromSession, decryptCredentials } from './encryption';
|
||
|
|
|
||
|
|
interface ConnectionPoolItem {
|
||
|
|
connection: any;
|
||
|
|
sessionId: string;
|
||
|
|
lastUsed: number;
|
||
|
|
isConnected: boolean;
|
||
|
|
}
|
||
|
|
|
||
|
|
class IMAPConnectionPool {
|
||
|
|
private connections: Map<string, ConnectionPoolItem> = new Map();
|
||
|
|
private maxConnections = 5;
|
||
|
|
private connectionTimeout = 300000; // 5 minutes
|
||
|
|
private reconnectAttempts = 3;
|
||
|
|
private reconnectDelay = 1000; // 1 second
|
||
|
|
|
||
|
|
private cleanupInterval: NodeJS.Timeout;
|
||
|
|
|
||
|
|
constructor() {
|
||
|
|
// Cleanup expired connections every minute
|
||
|
|
this.cleanupInterval = setInterval(() => {
|
||
|
|
this.cleanupExpiredConnections();
|
||
|
|
}, 60000);
|
||
|
|
}
|
||
|
|
|
||
|
|
async getConnection(sessionId: string): Promise<any> {
|
||
|
|
// Check if we have an existing connection
|
||
|
|
const existing = this.connections.get(sessionId);
|
||
|
|
if (existing && existing.isConnected) {
|
||
|
|
// Test connection health
|
||
|
|
if (await this.testConnection(existing.connection)) {
|
||
|
|
existing.lastUsed = Date.now();
|
||
|
|
return existing.connection;
|
||
|
|
} else {
|
||
|
|
// Connection is dead, remove it
|
||
|
|
this.removeConnection(sessionId);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Create new connection
|
||
|
|
return await this.createConnection(sessionId);
|
||
|
|
}
|
||
|
|
|
||
|
|
private async createConnection(sessionId: string): Promise<any> {
|
||
|
|
const encryptedCredentials = getCredentialsFromSession(sessionId);
|
||
|
|
if (!encryptedCredentials) {
|
||
|
|
throw new Error('No credentials found for session');
|
||
|
|
}
|
||
|
|
|
||
|
|
let credentials;
|
||
|
|
try {
|
||
|
|
credentials = decryptCredentials(encryptedCredentials);
|
||
|
|
} catch (error) {
|
||
|
|
throw new Error('Failed to decrypt credentials');
|
||
|
|
}
|
||
|
|
|
||
|
|
const imapConfig = {
|
||
|
|
user: credentials.email,
|
||
|
|
password: credentials.password,
|
||
|
|
host: process.env.NUXT_EMAIL_IMAP_HOST || 'mail.portnimara.com',
|
||
|
|
port: parseInt(process.env.NUXT_EMAIL_IMAP_PORT || '993'),
|
||
|
|
tls: true,
|
||
|
|
tlsOptions: {
|
||
|
|
rejectUnauthorized: false
|
||
|
|
},
|
||
|
|
connTimeout: 15000, // 15 seconds connection timeout
|
||
|
|
authTimeout: 10000, // 10 seconds auth timeout
|
||
|
|
keepalive: {
|
||
|
|
interval: 10000, // Send keepalive every 10 seconds
|
||
|
|
idleInterval: 300000, // IDLE for 5 minutes
|
||
|
|
forceNoop: true
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
return new Promise((resolve, reject) => {
|
||
|
|
const imap = new Imap(imapConfig);
|
||
|
|
let connectionAttempts = 0;
|
||
|
|
|
||
|
|
const attemptConnection = () => {
|
||
|
|
connectionAttempts++;
|
||
|
|
|
||
|
|
imap.once('ready', () => {
|
||
|
|
console.log(`[IMAPPool] Connection established for session ${sessionId}`);
|
||
|
|
|
||
|
|
// Store connection
|
||
|
|
this.connections.set(sessionId, {
|
||
|
|
connection: imap,
|
||
|
|
sessionId,
|
||
|
|
lastUsed: Date.now(),
|
||
|
|
isConnected: true
|
||
|
|
});
|
||
|
|
|
||
|
|
// Set up error handlers
|
||
|
|
imap.on('error', (err: any) => {
|
||
|
|
console.error(`[IMAPPool] Connection error for session ${sessionId}:`, err);
|
||
|
|
this.markConnectionAsDead(sessionId);
|
||
|
|
});
|
||
|
|
|
||
|
|
imap.on('end', () => {
|
||
|
|
console.log(`[IMAPPool] Connection ended for session ${sessionId}`);
|
||
|
|
this.removeConnection(sessionId);
|
||
|
|
});
|
||
|
|
|
||
|
|
resolve(imap);
|
||
|
|
});
|
||
|
|
|
||
|
|
imap.once('error', (err: any) => {
|
||
|
|
console.error(`[IMAPPool] Connection attempt ${connectionAttempts} failed:`, err);
|
||
|
|
|
||
|
|
if (connectionAttempts < this.reconnectAttempts) {
|
||
|
|
setTimeout(() => {
|
||
|
|
try {
|
||
|
|
imap.connect();
|
||
|
|
} catch (connectError) {
|
||
|
|
console.error(`[IMAPPool] Reconnect attempt failed:`, connectError);
|
||
|
|
attemptConnection();
|
||
|
|
}
|
||
|
|
}, this.reconnectDelay * connectionAttempts);
|
||
|
|
} else {
|
||
|
|
reject(err);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
try {
|
||
|
|
imap.connect();
|
||
|
|
} catch (connectError) {
|
||
|
|
console.error(`[IMAPPool] Initial connect failed:`, connectError);
|
||
|
|
if (connectionAttempts < this.reconnectAttempts) {
|
||
|
|
setTimeout(attemptConnection, this.reconnectDelay);
|
||
|
|
} else {
|
||
|
|
reject(connectError);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
attemptConnection();
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
private async testConnection(imap: any): Promise<boolean> {
|
||
|
|
return new Promise((resolve) => {
|
||
|
|
try {
|
||
|
|
if (!imap || imap.state !== 'authenticated') {
|
||
|
|
resolve(false);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Test with a simple NOOP command
|
||
|
|
imap.seq.fetch('1:1', { bodies: 'HEADER' }, (err: any) => {
|
||
|
|
// Even if fetch fails, if no connection error then connection is likely OK
|
||
|
|
resolve(!err || err.code !== 'ECONNRESET');
|
||
|
|
});
|
||
|
|
|
||
|
|
// Timeout the test after 5 seconds
|
||
|
|
setTimeout(() => resolve(false), 5000);
|
||
|
|
} catch (error) {
|
||
|
|
resolve(false);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
private markConnectionAsDead(sessionId: string): void {
|
||
|
|
const connection = this.connections.get(sessionId);
|
||
|
|
if (connection) {
|
||
|
|
connection.isConnected = false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private removeConnection(sessionId: string): void {
|
||
|
|
const connection = this.connections.get(sessionId);
|
||
|
|
if (connection) {
|
||
|
|
try {
|
||
|
|
if (connection.connection && typeof connection.connection.end === 'function') {
|
||
|
|
connection.connection.end();
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error(`[IMAPPool] Error closing connection:`, error);
|
||
|
|
}
|
||
|
|
this.connections.delete(sessionId);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private cleanupExpiredConnections(): void {
|
||
|
|
const now = Date.now();
|
||
|
|
for (const [sessionId, connection] of this.connections.entries()) {
|
||
|
|
if (now - connection.lastUsed > this.connectionTimeout) {
|
||
|
|
console.log(`[IMAPPool] Cleaning up expired connection for session ${sessionId}`);
|
||
|
|
this.removeConnection(sessionId);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async closeConnection(sessionId: string): Promise<void> {
|
||
|
|
this.removeConnection(sessionId);
|
||
|
|
}
|
||
|
|
|
||
|
|
async closeAllConnections(): Promise<void> {
|
||
|
|
for (const sessionId of this.connections.keys()) {
|
||
|
|
this.removeConnection(sessionId);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
destroy(): void {
|
||
|
|
if (this.cleanupInterval) {
|
||
|
|
clearInterval(this.cleanupInterval);
|
||
|
|
}
|
||
|
|
this.closeAllConnections();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Global instance
|
||
|
|
let globalIMAPPool: IMAPConnectionPool | null = null;
|
||
|
|
|
||
|
|
export function getIMAPPool(): IMAPConnectionPool {
|
||
|
|
if (!globalIMAPPool) {
|
||
|
|
globalIMAPPool = new IMAPConnectionPool();
|
||
|
|
}
|
||
|
|
return globalIMAPPool;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function destroyIMAPPool(): void {
|
||
|
|
if (globalIMAPPool) {
|
||
|
|
globalIMAPPool.destroy();
|
||
|
|
globalIMAPPool = null;
|
||
|
|
}
|
||
|
|
}
|