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
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<void> {
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<EmailCredentials | null> {
export async function getEmailCredentials(
userId: string,
accountId: string
): Promise<EmailCredentials | null> {
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<void> {
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<any | null> {
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<void> {
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<any | null> {
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<void> {
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<void> {
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<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
*/
export async function getImapConnection(userId: string): Promise<ImapFlow> {
console.log(`Getting IMAP connection for user ${userId}`);
export async function getImapConnection(
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
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<ImapFlow> {
* Get user's email credentials from database
*/
export async function getUserEmailCredentials(userId: string): Promise<EmailCredentials | null> {
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<EmailCred
color: true
}
});
if (!credentials) {
return null;
}
// Return only the fields that exist in credentials
if (!credentials) return null;
return {
email: credentials.email,
password: credentials.password,
host: credentials.host,
port: credentials.port,
...(credentials.secure !== undefined && { secure: credentials.secure }),
...(credentials.smtp_host && { smtp_host: credentials.smtp_host }),
...(credentials.smtp_port && { smtp_port: credentials.smtp_port }),
...(credentials.smtp_secure !== undefined && { smtp_secure: credentials.smtp_secure }),
...(credentials.display_name && { display_name: credentials.display_name }),
...(credentials.color && { color: credentials.color })
secure: credentials.secure,
smtp_host: credentials.smtp_host,
smtp_port: credentials.smtp_port,
smtp_secure: credentials.smtp_secure,
display_name: credentials.display_name,
color: credentials.color
};
}
@ -189,10 +191,11 @@ export async function getUserEmailCredentials(userId: string): Promise<EmailCred
* Save or update user's email credentials
*/
export async function saveUserEmailCredentials(
userId: string,
userId: string,
accountId: string,
credentials: EmailCredentials
): 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
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<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;
}
}