import Redis from 'ioredis'; import CryptoJS from 'crypto-js'; // Initialize Redis client let redisClient: Redis | null = null; /** * Get a Redis client instance (singleton pattern) */ export function getRedisClient(): Redis { if (!redisClient) { // Set Redis connection parameters from environment variables only const redisOptions = { host: process.env.REDIS_HOST, port: process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT) : undefined, password: process.env.REDIS_PASSWORD, retryStrategy: (times: number) => { const delay = Math.min(times * 100, 5000); return delay; }, maxRetriesPerRequest: 5, enableOfflineQueue: true }; console.log('Connecting to Redis using environment variables'); redisClient = new Redis(redisOptions); redisClient.on('error', (err) => { console.error('Redis connection error:', err); }); redisClient.on('connect', () => { console.log('Successfully connected to Redis'); }); redisClient.on('reconnecting', () => { console.log('Reconnecting to Redis...'); }); } return redisClient; } /** * Close Redis connection (useful for serverless environments) */ export async function closeRedisConnection(): Promise { if (redisClient) { await redisClient.quit(); redisClient = null; } } // Encryption key from environment variable or fallback const getEncryptionKey = () => { return process.env.REDIS_ENCRYPTION_KEY || 'default-encryption-key-change-in-production'; }; /** * Encrypt sensitive data before storing in Redis */ export function encryptData(data: string): string { return CryptoJS.AES.encrypt(data, getEncryptionKey()).toString(); } /** * Decrypt sensitive data retrieved from Redis */ export function decryptData(encryptedData: string): string { const bytes = CryptoJS.AES.decrypt(encryptedData, getEncryptionKey()); return bytes.toString(CryptoJS.enc.Utf8); } // Cache key definitions export const KEYS = { CREDENTIALS: (userId: string) => `email:credentials:${userId}`, SESSION: (userId: string) => `email:session:${userId}`, EMAIL_LIST: (userId: string, folder: string, page: number, perPage: number) => `email:list:${userId}:${folder}:${page}:${perPage}`, EMAIL_CONTENT: (userId: string, emailId: string) => `email:content:${userId}:${emailId}` }; // TTL constants in seconds export const TTL = { CREDENTIALS: 60 * 60 * 24, // 24 hours SESSION: 60 * 30, // 30 minutes EMAIL_LIST: 60 * 5, // 5 minutes EMAIL_CONTENT: 60 * 15 // 15 minutes }; interface EmailCredentials { email: string; password?: string; host: string; port: number; secure?: boolean; encryptedPassword?: string; } interface ImapSessionData { connectionId?: string; lastActive: number; mailboxes?: string[]; } /** * Cache email credentials in Redis */ export async function cacheEmailCredentials( userId: string, credentials: EmailCredentials ): Promise { const redis = getRedisClient(); const key = KEYS.CREDENTIALS(userId); // Validate credentials before caching if (!credentials.email || !credentials.host || !credentials.password) { console.error(`Cannot cache incomplete credentials for user ${userId}`); return; } try { console.log(`Caching credentials for user ${userId}`); // Create a copy without the password to store const secureCredentials: EmailCredentials = { email: credentials.email, host: credentials.host, port: credentials.port, secure: credentials.secure ?? true }; // Encrypt password if (credentials.password) { try { const encrypted = encryptData(credentials.password); console.log(`Successfully encrypted password for user ${userId}`); secureCredentials.encryptedPassword = encrypted; } catch (encryptError) { console.error(`Failed to encrypt password for user ${userId}:`, encryptError); // Don't proceed with caching if encryption fails return; } } else { console.warn(`No password provided for user ${userId}, skipping credential caching`); return; } await redis.set(key, JSON.stringify(secureCredentials), 'EX', TTL.CREDENTIALS); console.log(`Credentials cached for user ${userId}`); } catch (error) { console.error(`Error caching credentials for user ${userId}:`, error); } } /** * Get cached email credentials from Redis */ export async function getCachedEmailCredentials( userId: string ): Promise { const redis = getRedisClient(); const key = KEYS.CREDENTIALS(userId); try { const cachedData = await redis.get(key); if (!cachedData) { console.log(`No cached credentials found for user ${userId}`); return null; } console.log(`Found cached credentials for user ${userId}, attempting to decrypt`); const credentials = JSON.parse(cachedData) as EmailCredentials; // Check if we have encrypted password if (!credentials.encryptedPassword) { console.warn(`Cached credentials for user ${userId} missing encrypted password`); return null; } try { // Decrypt password with error handling credentials.password = decryptData(credentials.encryptedPassword); delete credentials.encryptedPassword; // Validate the credentials to ensure they're complete if (!credentials.password || !credentials.email || !credentials.host) { console.warn(`Incomplete credentials in cache for user ${userId}, missing required fields`); return null; } console.log(`Successfully retrieved and decrypted credentials for ${userId}`); return credentials; } catch (decryptError) { console.error(`Failed to decrypt password for user ${userId}:`, decryptError); return null; } } catch (error) { console.error(`Error retrieving credentials from Redis for user ${userId}:`, error); return null; } } /** * Cache IMAP session data for quick reconnection */ export async function cacheImapSession( userId: string, sessionData: ImapSessionData ): Promise { const redis = getRedisClient(); const key = KEYS.SESSION(userId); // Always update the lastActive timestamp sessionData.lastActive = Date.now(); await redis.set(key, JSON.stringify(sessionData), 'EX', TTL.SESSION); } /** * Get cached IMAP session data */ export async function getCachedImapSession( userId: string ): Promise { const redis = getRedisClient(); const key = KEYS.SESSION(userId); const cachedData = await redis.get(key); if (!cachedData) return null; return JSON.parse(cachedData) as ImapSessionData; } /** * Cache email list in Redis */ export async function cacheEmailList( userId: string, folder: string, page: number, perPage: number, data: any ): Promise { const redis = getRedisClient(); const key = KEYS.EMAIL_LIST(userId, folder, page, perPage); await redis.set(key, JSON.stringify(data), 'EX', TTL.EMAIL_LIST); } /** * Get cached email list from Redis */ export async function getCachedEmailList( userId: string, folder: string, page: number, perPage: number ): Promise { const redis = getRedisClient(); const key = KEYS.EMAIL_LIST(userId, folder, page, perPage); const cachedData = await redis.get(key); if (!cachedData) return null; return JSON.parse(cachedData); } /** * Cache email content in Redis */ export async function cacheEmailContent( userId: string, emailId: string, data: any ): Promise { const redis = getRedisClient(); const key = KEYS.EMAIL_CONTENT(userId, emailId); await redis.set(key, JSON.stringify(data), 'EX', TTL.EMAIL_CONTENT); } /** * Get cached email content from Redis */ export async function getCachedEmailContent( userId: string, emailId: string ): Promise { const redis = getRedisClient(); const key = KEYS.EMAIL_CONTENT(userId, emailId); const cachedData = await redis.get(key); if (!cachedData) return null; return JSON.parse(cachedData); } /** * Invalidate all email caches for a folder */ export async function invalidateFolderCache( userId: string, folder: string ): Promise { const redis = getRedisClient(); const pattern = `email:list:${userId}:${folder}:*`; // Use SCAN to find and delete keys matching the pattern let cursor = '0'; do { const [nextCursor, keys] = await redis.scan(cursor, 'MATCH', pattern, 'COUNT', 100); cursor = nextCursor; if (keys.length > 0) { await redis.del(...keys); } } while (cursor !== '0'); } /** * Invalidate email content cache */ export async function invalidateEmailContentCache( userId: string, emailId: string ): Promise { const redis = getRedisClient(); const key = KEYS.EMAIL_CONTENT(userId, emailId); await redis.del(key); } /** * Invalidate all user email caches (email lists and content) */ export async function invalidateUserEmailCache( userId: string ): Promise { const redis = getRedisClient(); // Patterns to delete const patterns = [ `email:list:${userId}:*`, `email:content:${userId}:*` ]; for (const pattern of patterns) { let cursor = '0'; do { const [nextCursor, keys] = await redis.scan(cursor, 'MATCH', pattern, 'COUNT', 100); cursor = nextCursor; if (keys.length > 0) { await redis.del(...keys); } } while (cursor !== '0'); } }