From d07492f6bc1b6f2940e16502490ff11d31ef088e Mon Sep 17 00:00:00 2001 From: alma Date: Mon, 28 Apr 2025 12:53:47 +0200 Subject: [PATCH] courrier multi account restore compose --- lib/redis.ts | 45 ++++-- lib/services/email-service.ts | 294 ++++++---------------------------- 2 files changed, 73 insertions(+), 266 deletions(-) diff --git a/lib/redis.ts b/lib/redis.ts index 3ed63293..0d7da7d3 100644 --- a/lib/redis.ts +++ b/lib/redis.ts @@ -73,12 +73,12 @@ export function decryptData(encryptedData: string): string { // Cache key definitions export const KEYS = { - CREDENTIALS: (userId: string) => `email:credentials:${userId}`, + CREDENTIALS: (userId: string, accountId: string) => `email:credentials:${userId}:${accountId}`, 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}` + EMAIL_LIST: (userId: string, accountId: string, folder: string, page: number, perPage: number) => + `email:list:${userId}:${accountId}:${folder}:${page}:${perPage}`, + EMAIL_CONTENT: (userId: string, accountId: string, emailId: string) => + `email:content:${userId}:${accountId}:${emailId}` }; // TTL constants in seconds @@ -114,11 +114,12 @@ interface ImapSessionData { * Cache email credentials in Redis */ export async function cacheEmailCredentials( - userId: string, + userId: string, + accountId: string, credentials: EmailCredentials ): Promise { const redis = getRedisClient(); - const key = KEYS.CREDENTIALS(userId); + const key = KEYS.CREDENTIALS(userId, accountId); // Validate credentials before caching if (!credentials.email || !credentials.host || !credentials.password) { @@ -169,9 +170,12 @@ export async function cacheEmailCredentials( /** * Get email credentials from Redis */ -export async function getEmailCredentials(userId: string): Promise { +export async function getEmailCredentials( + userId: string, + accountId: string +): Promise { const redis = getRedisClient(); - const key = KEYS.CREDENTIALS(userId); + const key = KEYS.CREDENTIALS(userId, accountId); try { const credStr = await redis.get(key); @@ -251,13 +255,14 @@ export async function getCachedImapSession( */ export async function cacheEmailList( userId: string, + accountId: string, folder: string, page: number, perPage: number, data: any ): Promise { const redis = getRedisClient(); - const key = KEYS.EMAIL_LIST(userId, folder, page, perPage); + const key = KEYS.EMAIL_LIST(userId, accountId, folder, page, perPage); await redis.set(key, JSON.stringify(data), 'EX', TTL.EMAIL_LIST); } @@ -267,12 +272,13 @@ export async function cacheEmailList( */ export async function getCachedEmailList( userId: string, + accountId: string, folder: string, page: number, perPage: number ): Promise { const redis = getRedisClient(); - const key = KEYS.EMAIL_LIST(userId, folder, page, perPage); + const key = KEYS.EMAIL_LIST(userId, accountId, folder, page, perPage); const cachedData = await redis.get(key); if (!cachedData) return null; @@ -285,11 +291,12 @@ export async function getCachedEmailList( */ export async function cacheEmailContent( userId: string, + accountId: string, emailId: string, data: any ): Promise { const redis = getRedisClient(); - const key = KEYS.EMAIL_CONTENT(userId, emailId); + const key = KEYS.EMAIL_CONTENT(userId, accountId, emailId); await redis.set(key, JSON.stringify(data), 'EX', TTL.EMAIL_CONTENT); } @@ -299,10 +306,11 @@ export async function cacheEmailContent( */ export async function getCachedEmailContent( userId: string, + accountId: string, emailId: string ): Promise { const redis = getRedisClient(); - const key = KEYS.EMAIL_CONTENT(userId, emailId); + const key = KEYS.EMAIL_CONTENT(userId, accountId, emailId); const cachedData = await redis.get(key); if (!cachedData) return null; @@ -315,10 +323,11 @@ export async function getCachedEmailContent( */ export async function invalidateFolderCache( userId: string, + accountId: string, folder: string ): Promise { const redis = getRedisClient(); - const pattern = `email:list:${userId}:${folder}:*`; + const pattern = `email:list:${userId}:${accountId}:${folder}:*`; // Use SCAN to find and delete keys matching the pattern let cursor = '0'; @@ -337,10 +346,11 @@ export async function invalidateFolderCache( */ export async function invalidateEmailContentCache( userId: string, + accountId: string, emailId: string ): Promise { const redis = getRedisClient(); - const key = KEYS.EMAIL_CONTENT(userId, emailId); + const key = KEYS.EMAIL_CONTENT(userId, accountId, emailId); await redis.del(key); } @@ -416,7 +426,8 @@ export async function invalidateUserEmailCache( * @deprecated Use getEmailCredentials instead */ export async function getCachedEmailCredentials( - userId: string + userId: string, + accountId: string ): Promise { - return getEmailCredentials(userId); + return getEmailCredentials(userId, accountId); } \ No newline at end of file diff --git a/lib/services/email-service.ts b/lib/services/email-service.ts index 7e9134cd..71249bb3 100644 --- a/lib/services/email-service.ts +++ b/lib/services/email-service.ts @@ -52,32 +52,37 @@ setInterval(() => { /** * Get IMAP connection for a user, reusing existing connections when possible */ -export async function getImapConnection(userId: string): Promise { - console.log(`Getting IMAP connection for user ${userId}`); +export async function getImapConnection( + userId: string, + accountId?: string +): Promise { + console.log(`Getting IMAP connection for user ${userId}${accountId ? ` account ${accountId}` : ''}`); // First try to get credentials from Redis cache - let credentials = await getCachedEmailCredentials(userId); + let credentials = accountId + ? await getCachedEmailCredentials(userId, accountId) + : await getCachedEmailCredentials(userId, 'default'); // If not in cache, get from database and cache them if (!credentials) { - console.log(`Credentials not found in cache for ${userId}, attempting database lookup`); + console.log(`Credentials not found in cache for ${userId}${accountId ? ` account ${accountId}` : ''}, attempting database lookup`); credentials = await getUserEmailCredentials(userId); if (!credentials) { throw new Error('No email credentials found'); } // Cache credentials for future use - await cacheEmailCredentials(userId, credentials); + await cacheEmailCredentials(userId, accountId || 'default', credentials); } // Validate credentials if (!credentials.password) { - console.error(`Missing password in credentials for user ${userId}`); + console.error(`Missing password in credentials for user ${userId}${accountId ? ` account ${accountId}` : ''}`); throw new Error('No password configured'); } if (!credentials.email || !credentials.host) { - console.error(`Incomplete credentials for user ${userId}`); + console.error(`Incomplete credentials for user ${userId}${accountId ? ` account ${accountId}` : ''}`); throw new Error('Invalid email credentials configuration'); } @@ -150,7 +155,7 @@ export async function getImapConnection(userId: string): Promise { * Get user's email credentials from database */ export async function getUserEmailCredentials(userId: string): Promise { - const credentials = await prisma.mailCredentials.findUnique({ + const credentials = await prisma.mailCredentials.findFirst({ where: { userId }, select: { email: true, @@ -165,23 +170,20 @@ export async function getUserEmailCredentials(userId: string): Promise { - console.log('Saving credentials for user:', userId); + console.log('Saving credentials for user:', userId, 'account:', accountId); // Extract only the fields that exist in the database schema const dbCredentials = { @@ -204,16 +207,20 @@ export async function saveUserEmailCredentials( // Save to database - only using fields that exist in the schema await prisma.mailCredentials.upsert({ - where: { userId }, + where: { + id: accountId, + userId + }, update: dbCredentials, create: { + id: accountId, userId, ...dbCredentials } }); // Cache the full credentials object in Redis (with all fields) - await cacheEmailCredentials(userId, credentials); + await cacheEmailCredentials(userId, accountId, credentials); } // Helper type for IMAP fetch options @@ -667,7 +674,7 @@ export async function sendEmail( // Create SMTP transporter with user's SMTP settings if available const transporter = nodemailer.createTransport({ - host: credentials.smtp_host || 'smtp.infomaniak.com', // Use custom SMTP or default + host: credentials.smtp_host || 'smtp.infomaniak.com', port: credentials.smtp_port || 587, secure: credentials.smtp_secure || false, auth: { @@ -678,242 +685,31 @@ export async function sendEmail( rejectUnauthorized: false } }); - + try { - // Verify connection - await transporter.verify(); - - // Prepare email options - const mailOptions = { + const info = await transporter.sendMail({ from: credentials.email, to: emailData.to, - cc: emailData.cc || undefined, - bcc: emailData.bcc || undefined, - subject: emailData.subject || '(No subject)', + cc: emailData.cc, + bcc: emailData.bcc, + subject: emailData.subject, + text: emailData.body, html: emailData.body, - attachments: emailData.attachments?.map(file => ({ - filename: file.name, - content: file.content, - contentType: file.type - })) || [] - }; - - // Send email - const info = await transporter.sendMail(mailOptions); - + attachments: emailData.attachments?.map(att => ({ + filename: att.name, + content: att.content, + contentType: att.type + })), + }); + return { success: true, messageId: info.messageId }; } catch (error) { - console.error('Error sending email:', error); - return { success: false, error: error instanceof Error ? error.message : 'Unknown error' }; } -} - -/** - * Get list of mailboxes (folders) - */ -export async function getMailboxes(client: ImapFlow): Promise { - try { - const list = await client.list(); - return list.map(mailbox => mailbox.path); - } catch (error) { - console.error('Error listing mailboxes:', error); - return []; - } -} - -/** - * Test email connections with given credentials - */ -export async function testEmailConnection(credentials: EmailCredentials): Promise<{ - imap: boolean; - smtp: boolean; - error?: string; -}> { - // Test IMAP connection - let imapSuccess = false; - let smtpSuccess = false; - let errorMessage = ''; - - // First test IMAP - const imapClient = new ImapFlow({ - host: credentials.host, - port: credentials.port, - secure: true, - auth: { - user: credentials.email, - pass: credentials.password, - }, - logger: false, - tls: { - rejectUnauthorized: false - } - }); - - try { - await imapClient.connect(); - await imapClient.mailboxOpen('INBOX'); - imapSuccess = true; - } catch (error) { - console.error('IMAP connection test failed:', error); - errorMessage = error instanceof Error ? error.message : 'Unknown IMAP error'; - return { imap: false, smtp: false, error: `IMAP connection failed: ${errorMessage}` }; - } finally { - try { - await imapClient.logout(); - } catch (e) { - // Ignore logout errors - } - } - - // If IMAP successful and SMTP details provided, test SMTP - if (credentials.smtp_host && credentials.smtp_port) { - const transporter = nodemailer.createTransport({ - host: credentials.smtp_host, - port: credentials.smtp_port, - secure: true, - auth: { - user: credentials.email, - pass: credentials.password, - }, - tls: { - rejectUnauthorized: false - } - }); - - try { - await transporter.verify(); - smtpSuccess = true; - } catch (error) { - console.error('SMTP connection test failed:', error); - errorMessage = error instanceof Error ? error.message : 'Unknown SMTP error'; - return { - imap: imapSuccess, - smtp: false, - error: `SMTP connection failed: ${errorMessage}` - }; - } - } else { - // If no SMTP details, just mark as successful - smtpSuccess = true; - } - - return { imap: imapSuccess, smtp: smtpSuccess }; -} - -// Original simplified function for backward compatibility -export async function testImapConnection(credentials: EmailCredentials): Promise { - const result = await testEmailConnection(credentials); - return result.imap; -} - -// Email formatting functions have been moved to lib/utils/email-formatter.ts -// Use those functions instead of the ones previously defined here - -/** - * Force recaching of user credentials from database - * This is a helper function to fix issues with missing credentials in Redis - */ -export async function forceRecacheUserCredentials(userId: string): Promise { - try { - console.log(`[CREDENTIAL FIX] Attempting to force recache credentials for user ${userId}`); - - // Get credentials directly from database - const dbCredentials = await prisma.mailCredentials.findUnique({ - where: { userId }, - select: { - email: true, - password: true, - host: true, - port: true, - secure: true, - smtp_host: true, - smtp_port: true, - smtp_secure: true, - display_name: true, - color: true - } - }); - - if (!dbCredentials) { - console.error(`[CREDENTIAL FIX] No credentials found in database for user ${userId}`); - return false; - } - - // Log what we found (without revealing the actual password) - console.log(`[CREDENTIAL FIX] Found database credentials for user ${userId}:`, { - email: dbCredentials.email, - hasPassword: !!dbCredentials.password, - passwordLength: dbCredentials.password?.length || 0, - host: dbCredentials.host, - port: dbCredentials.port - }); - - if (!dbCredentials.password) { - console.error(`[CREDENTIAL FIX] Password is empty in database for user ${userId}`); - return false; - } - - // Try to directly encrypt the password to see if encryption works - try { - const { encryptData } = await import('@/lib/redis'); - const encryptedPassword = encryptData(dbCredentials.password); - console.log(`[CREDENTIAL FIX] Successfully test-encrypted password for user ${userId}`); - - // If we got here, encryption works - } catch (encryptError) { - console.error(`[CREDENTIAL FIX] Encryption test failed for user ${userId}:`, encryptError); - return false; - } - - // Format credentials for caching - const credentials = { - email: dbCredentials.email, - password: dbCredentials.password, - host: dbCredentials.host, - port: dbCredentials.port, - ...(dbCredentials.secure !== undefined && { secure: dbCredentials.secure }), - ...(dbCredentials.smtp_host && { smtp_host: dbCredentials.smtp_host }), - ...(dbCredentials.smtp_port && { smtp_port: dbCredentials.smtp_port }), - ...(dbCredentials.smtp_secure !== undefined && { smtp_secure: dbCredentials.smtp_secure }), - ...(dbCredentials.display_name && { display_name: dbCredentials.display_name }), - ...(dbCredentials.color && { color: dbCredentials.color }) - }; - - // Try to cache the credentials - try { - const { cacheEmailCredentials } = await import('@/lib/redis'); - await cacheEmailCredentials(userId, credentials); - console.log(`[CREDENTIAL FIX] Successfully cached credentials for user ${userId}`); - - // Now verify the credentials were cached correctly - const { getEmailCredentials } = await import('@/lib/redis'); - const cachedCreds = await getEmailCredentials(userId); - - if (!cachedCreds) { - console.error(`[CREDENTIAL FIX] Failed to verify cached credentials for user ${userId}`); - return false; - } - - if (!cachedCreds.password) { - console.error(`[CREDENTIAL FIX] Cached credentials missing password for user ${userId}`); - return false; - } - - console.log(`[CREDENTIAL FIX] Verified cached credentials for user ${userId}`); - return true; - } catch (cacheError) { - console.error(`[CREDENTIAL FIX] Failed to cache credentials for user ${userId}:`, cacheError); - return false; - } - } catch (error) { - console.error(`[CREDENTIAL FIX] Error in force recache process for user ${userId}:`, error); - return false; - } } \ No newline at end of file