courrier multi account restore compose

This commit is contained in:
alma 2025-04-28 12:53:47 +02:00
parent 6ab432e0ff
commit d07492f6bc
2 changed files with 73 additions and 266 deletions

View File

@ -73,12 +73,12 @@ export function decryptData(encryptedData: string): string {
// Cache key definitions // Cache key definitions
export const KEYS = { export const KEYS = {
CREDENTIALS: (userId: string) => `email:credentials:${userId}`, CREDENTIALS: (userId: string, accountId: string) => `email:credentials:${userId}:${accountId}`,
SESSION: (userId: string) => `email:session:${userId}`, SESSION: (userId: string) => `email:session:${userId}`,
EMAIL_LIST: (userId: string, folder: string, page: number, perPage: number) => EMAIL_LIST: (userId: string, accountId: string, folder: string, page: number, perPage: number) =>
`email:list:${userId}:${folder}:${page}:${perPage}`, `email:list:${userId}:${accountId}:${folder}:${page}:${perPage}`,
EMAIL_CONTENT: (userId: string, emailId: string) => EMAIL_CONTENT: (userId: string, accountId: string, emailId: string) =>
`email:content:${userId}:${emailId}` `email:content:${userId}:${accountId}:${emailId}`
}; };
// TTL constants in seconds // TTL constants in seconds
@ -115,10 +115,11 @@ interface ImapSessionData {
*/ */
export async function cacheEmailCredentials( export async function cacheEmailCredentials(
userId: string, userId: string,
accountId: string,
credentials: EmailCredentials credentials: EmailCredentials
): Promise<void> { ): Promise<void> {
const redis = getRedisClient(); const redis = getRedisClient();
const key = KEYS.CREDENTIALS(userId); const key = KEYS.CREDENTIALS(userId, accountId);
// Validate credentials before caching // Validate credentials before caching
if (!credentials.email || !credentials.host || !credentials.password) { if (!credentials.email || !credentials.host || !credentials.password) {
@ -169,9 +170,12 @@ export async function cacheEmailCredentials(
/** /**
* Get email credentials from Redis * Get email credentials from Redis
*/ */
export async function getEmailCredentials(userId: string): Promise<EmailCredentials | null> { export async function getEmailCredentials(
userId: string,
accountId: string
): Promise<EmailCredentials | null> {
const redis = getRedisClient(); const redis = getRedisClient();
const key = KEYS.CREDENTIALS(userId); const key = KEYS.CREDENTIALS(userId, accountId);
try { try {
const credStr = await redis.get(key); const credStr = await redis.get(key);
@ -251,13 +255,14 @@ export async function getCachedImapSession(
*/ */
export async function cacheEmailList( export async function cacheEmailList(
userId: string, userId: string,
accountId: string,
folder: string, folder: string,
page: number, page: number,
perPage: number, perPage: number,
data: any data: any
): Promise<void> { ): Promise<void> {
const redis = getRedisClient(); 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); await redis.set(key, JSON.stringify(data), 'EX', TTL.EMAIL_LIST);
} }
@ -267,12 +272,13 @@ export async function cacheEmailList(
*/ */
export async function getCachedEmailList( export async function getCachedEmailList(
userId: string, userId: string,
accountId: string,
folder: string, folder: string,
page: number, page: number,
perPage: number perPage: number
): Promise<any | null> { ): Promise<any | null> {
const redis = getRedisClient(); 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); const cachedData = await redis.get(key);
if (!cachedData) return null; if (!cachedData) return null;
@ -285,11 +291,12 @@ export async function getCachedEmailList(
*/ */
export async function cacheEmailContent( export async function cacheEmailContent(
userId: string, userId: string,
accountId: string,
emailId: string, emailId: string,
data: any data: any
): Promise<void> { ): Promise<void> {
const redis = getRedisClient(); 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); await redis.set(key, JSON.stringify(data), 'EX', TTL.EMAIL_CONTENT);
} }
@ -299,10 +306,11 @@ export async function cacheEmailContent(
*/ */
export async function getCachedEmailContent( export async function getCachedEmailContent(
userId: string, userId: string,
accountId: string,
emailId: string emailId: string
): Promise<any | null> { ): Promise<any | null> {
const redis = getRedisClient(); const redis = getRedisClient();
const key = KEYS.EMAIL_CONTENT(userId, emailId); const key = KEYS.EMAIL_CONTENT(userId, accountId, emailId);
const cachedData = await redis.get(key); const cachedData = await redis.get(key);
if (!cachedData) return null; if (!cachedData) return null;
@ -315,10 +323,11 @@ export async function getCachedEmailContent(
*/ */
export async function invalidateFolderCache( export async function invalidateFolderCache(
userId: string, userId: string,
accountId: string,
folder: string folder: string
): Promise<void> { ): Promise<void> {
const redis = getRedisClient(); 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 // Use SCAN to find and delete keys matching the pattern
let cursor = '0'; let cursor = '0';
@ -337,10 +346,11 @@ export async function invalidateFolderCache(
*/ */
export async function invalidateEmailContentCache( export async function invalidateEmailContentCache(
userId: string, userId: string,
accountId: string,
emailId: string emailId: string
): Promise<void> { ): Promise<void> {
const redis = getRedisClient(); const redis = getRedisClient();
const key = KEYS.EMAIL_CONTENT(userId, emailId); const key = KEYS.EMAIL_CONTENT(userId, accountId, emailId);
await redis.del(key); await redis.del(key);
} }
@ -416,7 +426,8 @@ export async function invalidateUserEmailCache(
* @deprecated Use getEmailCredentials instead * @deprecated Use getEmailCredentials instead
*/ */
export async function getCachedEmailCredentials( export async function getCachedEmailCredentials(
userId: string userId: string,
accountId: string
): Promise<EmailCredentials | null> { ): Promise<EmailCredentials | null> {
return getEmailCredentials(userId); return getEmailCredentials(userId, accountId);
} }

View File

@ -52,32 +52,37 @@ setInterval(() => {
/** /**
* Get IMAP connection for a user, reusing existing connections when possible * Get IMAP connection for a user, reusing existing connections when possible
*/ */
export async function getImapConnection(userId: string): Promise<ImapFlow> { export async function getImapConnection(
console.log(`Getting IMAP connection for user ${userId}`); userId: string,
accountId?: string
): Promise<ImapFlow> {
console.log(`Getting IMAP connection for user ${userId}${accountId ? ` account ${accountId}` : ''}`);
// First try to get credentials from Redis cache // 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 not in cache, get from database and cache them
if (!credentials) { 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); credentials = await getUserEmailCredentials(userId);
if (!credentials) { if (!credentials) {
throw new Error('No email credentials found'); throw new Error('No email credentials found');
} }
// Cache credentials for future use // Cache credentials for future use
await cacheEmailCredentials(userId, credentials); await cacheEmailCredentials(userId, accountId || 'default', credentials);
} }
// Validate credentials // Validate credentials
if (!credentials.password) { 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'); throw new Error('No password configured');
} }
if (!credentials.email || !credentials.host) { 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'); throw new Error('Invalid email credentials configuration');
} }
@ -150,7 +155,7 @@ export async function getImapConnection(userId: string): Promise<ImapFlow> {
* Get user's email credentials from database * Get user's email credentials from database
*/ */
export async function getUserEmailCredentials(userId: string): Promise<EmailCredentials | null> { export async function getUserEmailCredentials(userId: string): Promise<EmailCredentials | null> {
const credentials = await prisma.mailCredentials.findUnique({ const credentials = await prisma.mailCredentials.findFirst({
where: { userId }, where: { userId },
select: { select: {
email: true, email: true,
@ -166,22 +171,19 @@ export async function getUserEmailCredentials(userId: string): Promise<EmailCred
} }
}); });
if (!credentials) { if (!credentials) return null;
return null;
}
// Return only the fields that exist in credentials
return { return {
email: credentials.email, email: credentials.email,
password: credentials.password, password: credentials.password,
host: credentials.host, host: credentials.host,
port: credentials.port, port: credentials.port,
...(credentials.secure !== undefined && { secure: credentials.secure }), secure: credentials.secure,
...(credentials.smtp_host && { smtp_host: credentials.smtp_host }), smtp_host: credentials.smtp_host,
...(credentials.smtp_port && { smtp_port: credentials.smtp_port }), smtp_port: credentials.smtp_port,
...(credentials.smtp_secure !== undefined && { smtp_secure: credentials.smtp_secure }), smtp_secure: credentials.smtp_secure,
...(credentials.display_name && { display_name: credentials.display_name }), display_name: credentials.display_name,
...(credentials.color && { color: credentials.color }) color: credentials.color
}; };
} }
@ -190,9 +192,10 @@ export async function getUserEmailCredentials(userId: string): Promise<EmailCred
*/ */
export async function saveUserEmailCredentials( export async function saveUserEmailCredentials(
userId: string, userId: string,
accountId: string,
credentials: EmailCredentials credentials: EmailCredentials
): Promise<void> { ): Promise<void> {
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 // Extract only the fields that exist in the database schema
const dbCredentials = { const dbCredentials = {
@ -204,16 +207,20 @@ export async function saveUserEmailCredentials(
// Save to database - only using fields that exist in the schema // Save to database - only using fields that exist in the schema
await prisma.mailCredentials.upsert({ await prisma.mailCredentials.upsert({
where: { userId }, where: {
id: accountId,
userId
},
update: dbCredentials, update: dbCredentials,
create: { create: {
id: accountId,
userId, userId,
...dbCredentials ...dbCredentials
} }
}); });
// Cache the full credentials object in Redis (with all fields) // 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 // Helper type for IMAP fetch options
@ -667,7 +674,7 @@ export async function sendEmail(
// Create SMTP transporter with user's SMTP settings if available // Create SMTP transporter with user's SMTP settings if available
const transporter = nodemailer.createTransport({ 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, port: credentials.smtp_port || 587,
secure: credentials.smtp_secure || false, secure: credentials.smtp_secure || false,
auth: { auth: {
@ -680,240 +687,29 @@ export async function sendEmail(
}); });
try { try {
// Verify connection const info = await transporter.sendMail({
await transporter.verify();
// Prepare email options
const mailOptions = {
from: credentials.email, from: credentials.email,
to: emailData.to, to: emailData.to,
cc: emailData.cc || undefined, cc: emailData.cc,
bcc: emailData.bcc || undefined, bcc: emailData.bcc,
subject: emailData.subject || '(No subject)', subject: emailData.subject,
text: emailData.body,
html: emailData.body, html: emailData.body,
attachments: emailData.attachments?.map(file => ({ attachments: emailData.attachments?.map(att => ({
filename: file.name, filename: att.name,
content: file.content, content: att.content,
contentType: file.type contentType: att.type
})) || [] })),
}; });
// Send email
const info = await transporter.sendMail(mailOptions);
return { return {
success: true, success: true,
messageId: info.messageId messageId: info.messageId
}; };
} catch (error) { } catch (error) {
console.error('Error sending email:', error);
return { return {
success: false, success: false,
error: error instanceof Error ? error.message : 'Unknown error' error: error instanceof Error ? error.message : 'Unknown error'
}; };
} }
} }
/**
* Get list of mailboxes (folders)
*/
export async function getMailboxes(client: ImapFlow): Promise<string[]> {
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<boolean> {
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<boolean> {
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;
}
}