Neah/lib/redis.ts

373 lines
8.9 KiB
TypeScript

import Redis from 'ioredis';
import CryptoJS from 'crypto-js';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
// 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<void> {
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 * 60 * 4, // 4 hours (increased from 30 minutes)
EMAIL_LIST: 60 * 5, // 5 minutes
EMAIL_CONTENT: 60 * 15 // 15 minutes
};
interface EmailCredentials {
id: string;
userId: string;
email: string;
password: string;
host: string;
port: number;
secure: boolean;
smtp_host: string | null;
smtp_port: number | null;
smtp_secure: boolean | null;
display_name: string | null;
color: string | null;
createdAt: Date;
updatedAt: Date;
}
interface ImapSessionData {
connectionId?: string;
lastActive: number;
mailboxes?: string[];
lastVisit?: number;
}
/**
* Cache email credentials in Redis
*/
export async function cacheEmailCredentials(userId: string, credentials: EmailCredentials): Promise<void> {
const client = await getRedisClient();
const key = `email_credentials:${userId}`;
await client.set(key, JSON.stringify(credentials), 'EX', 3600); // 1 hour expiry
}
/**
* Get cached email credentials from Redis
*/
export async function getCachedEmailCredentials(userId: string): Promise<EmailCredentials | null> {
const client = await getRedisClient();
const key = `email_credentials:${userId}`;
const data = await client.get(key);
return data ? JSON.parse(data) : null;
}
/**
* Get email credentials from Redis or database
*/
export async function getEmailCredentials(userId: string): Promise<EmailCredentials | null> {
// Try to get from cache first
const cached = await getCachedEmailCredentials(userId);
if (cached) {
return cached;
}
// If not in cache, get from database
const credentials = await prisma.mailCredentials.findFirst({
where: { userId },
select: {
id: true,
userId: true,
email: true,
password: true,
host: true,
port: true,
secure: true,
smtp_host: true,
smtp_port: true,
smtp_secure: true,
display_name: true,
color: true,
createdAt: true,
updatedAt: true
}
});
if (!credentials) {
return null;
}
// Cache the credentials
await cacheEmailCredentials(userId, credentials);
return credentials;
}
/**
* Cache IMAP session data for quick reconnection
*/
export async function cacheImapSession(
userId: string,
sessionData: ImapSessionData
): Promise<void> {
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<ImapSessionData | null> {
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<void> {
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<any | null> {
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<void> {
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<any | null> {
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<void> {
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<void> {
const redis = getRedisClient();
const key = KEYS.EMAIL_CONTENT(userId, emailId);
await redis.del(key);
}
/**
* Warm up Redis connection to avoid cold starts
*/
export async function warmupRedisCache(): Promise<boolean> {
try {
// Ping Redis to establish connection early
const redis = getRedisClient();
await redis.ping();
console.log('Redis connection warmed up');
return true;
} catch (error) {
console.error('Error warming up Redis:', error);
return false;
}
}
/**
* Get Redis connection status
*/
export async function getRedisStatus(): Promise<{
status: 'connected' | 'error';
ping?: string;
error?: string;
}> {
try {
const redis = getRedisClient();
const pong = await redis.ping();
return {
status: 'connected',
ping: pong
};
} catch (error) {
return {
status: 'error',
error: error instanceof Error ? error.message : String(error)
};
}
}
/**
* Invalidate all user email caches (email lists and content)
*/
export async function invalidateUserEmailCache(
userId: string
): Promise<void> {
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');
}
}