courrier multi account restore compose
This commit is contained in:
parent
6ab432e0ff
commit
d07492f6bc
45
lib/redis.ts
45
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<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);
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user