courrier multi account restore compose
This commit is contained in:
parent
4bfb348082
commit
6ab432e0ff
@ -131,8 +131,6 @@ export async function GET() {
|
|||||||
|
|
||||||
// Get additional fields that might be in the database but not in the type
|
// Get additional fields that might be in the database but not in the type
|
||||||
const accountsWithMetadata = await Promise.all(allAccounts.map(async (account) => {
|
const accountsWithMetadata = await Promise.all(allAccounts.map(async (account) => {
|
||||||
console.log(`[DEBUG] Processing metadata for account ${account.email}`);
|
|
||||||
|
|
||||||
// Get the raw account data to access fields not in the type
|
// Get the raw account data to access fields not in the type
|
||||||
const rawAccount = await prisma.$queryRaw`
|
const rawAccount = await prisma.$queryRaw`
|
||||||
SELECT display_name, color
|
SELECT display_name, color
|
||||||
@ -142,7 +140,6 @@ export async function GET() {
|
|||||||
|
|
||||||
// Cast the raw result to an array and get the first item
|
// Cast the raw result to an array and get the first item
|
||||||
const metadata = Array.isArray(rawAccount) ? rawAccount[0] : rawAccount;
|
const metadata = Array.isArray(rawAccount) ? rawAccount[0] : rawAccount;
|
||||||
console.log(`[DEBUG] Metadata for ${account.email}:`, metadata);
|
|
||||||
|
|
||||||
// Get folders for this specific account
|
// Get folders for this specific account
|
||||||
const accountFolders = await getAccountFolders(account.id, {
|
const accountFolders = await getAccountFolders(account.id, {
|
||||||
@ -150,8 +147,6 @@ export async function GET() {
|
|||||||
...metadata
|
...metadata
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`[DEBUG] Found ${accountFolders.length} folders for ${account.email}`);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...account,
|
...account,
|
||||||
display_name: metadata?.display_name || account.email,
|
display_name: metadata?.display_name || account.email,
|
||||||
@ -160,12 +155,7 @@ export async function GET() {
|
|||||||
};
|
};
|
||||||
}));
|
}));
|
||||||
|
|
||||||
console.log(`[DEBUG] Final processed accounts:`, accountsWithMetadata.map(acc => ({
|
console.log(`Found ${allAccounts.length} email accounts for user ${userId}`);
|
||||||
id: acc.id,
|
|
||||||
email: acc.email,
|
|
||||||
display_name: acc.display_name,
|
|
||||||
folderCount: acc.folders?.length || 0
|
|
||||||
})));
|
|
||||||
|
|
||||||
// If not in cache and no accounts found, check database for single account (backward compatibility)
|
// If not in cache and no accounts found, check database for single account (backward compatibility)
|
||||||
if (!credentials && allAccounts.length === 0) {
|
if (!credentials && allAccounts.length === 0) {
|
||||||
|
|||||||
@ -125,7 +125,8 @@ export default function CourrierPage() {
|
|||||||
|
|
||||||
// Email accounts for the sidebar
|
// Email accounts for the sidebar
|
||||||
const [accounts, setAccounts] = useState<Account[]>([
|
const [accounts, setAccounts] = useState<Account[]>([
|
||||||
{ id: 'all-accounts', name: 'All', email: '', color: 'bg-gray-500' }
|
{ id: 'all-accounts', name: 'All', email: '', color: 'bg-gray-500' },
|
||||||
|
{ id: 'loading-account', name: 'Loading...', email: '', color: 'bg-blue-500', folders: [] }
|
||||||
]);
|
]);
|
||||||
const [selectedAccount, setSelectedAccount] = useState<Account | null>(null);
|
const [selectedAccount, setSelectedAccount] = useState<Account | null>(null);
|
||||||
|
|
||||||
@ -136,16 +137,17 @@ export default function CourrierPage() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('Mailboxes updated:', mailboxes);
|
console.log('Mailboxes updated:', mailboxes);
|
||||||
setAccounts(prev => {
|
setAccounts(prev => {
|
||||||
// Keep the "All" account and update the actual email account
|
const updated = [...prev];
|
||||||
const updated = [prev[0]]; // Keep the "All" account
|
if (updated.length > 1) {
|
||||||
if (prev.length > 1) {
|
// Only update folders, preserve other properties including ID
|
||||||
// Update the existing email account
|
if (updated[1]) {
|
||||||
updated.push({
|
updated[1] = {
|
||||||
...prev[1],
|
...updated[1],
|
||||||
folders: mailboxes
|
folders: mailboxes
|
||||||
});
|
};
|
||||||
|
}
|
||||||
|
console.log('Updated accounts with new mailboxes:', updated);
|
||||||
}
|
}
|
||||||
console.log('Updated accounts with new mailboxes:', updated);
|
|
||||||
return updated;
|
return updated;
|
||||||
});
|
});
|
||||||
}, [mailboxes]);
|
}, [mailboxes]);
|
||||||
@ -194,10 +196,11 @@ export default function CourrierPage() {
|
|||||||
if (!accounts || accounts.length === 0) {
|
if (!accounts || accounts.length === 0) {
|
||||||
console.warn('Accounts array is empty, restoring defaults');
|
console.warn('Accounts array is empty, restoring defaults');
|
||||||
setAccounts([
|
setAccounts([
|
||||||
{ id: 'all-accounts', name: 'All', email: '', color: 'bg-gray-500' }
|
{ id: 'all-accounts', name: 'All', email: '', color: 'bg-gray-500' },
|
||||||
|
{ id: 'loading-account', name: 'Loading...', email: '', color: 'bg-blue-500', folders: mailboxes }
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}, [accounts]);
|
}, [accounts, mailboxes]);
|
||||||
|
|
||||||
// Initialize session and start prefetching
|
// Initialize session and start prefetching
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -245,52 +248,100 @@ export default function CourrierPage() {
|
|||||||
console.log('allAccounts is array:', Array.isArray(data.allAccounts));
|
console.log('allAccounts is array:', Array.isArray(data.allAccounts));
|
||||||
console.log('allAccounts length:', data.allAccounts?.length || 0);
|
console.log('allAccounts length:', data.allAccounts?.length || 0);
|
||||||
|
|
||||||
|
// Inspect each account's structure
|
||||||
|
if (data.allAccounts && Array.isArray(data.allAccounts)) {
|
||||||
|
data.allAccounts.forEach((account: any, idx: number) => {
|
||||||
|
console.log(`Account ${idx + 1}:`, {
|
||||||
|
id: account.id,
|
||||||
|
email: account.email,
|
||||||
|
display_name: account.display_name,
|
||||||
|
foldersExist: !!account.folders,
|
||||||
|
foldersIsArray: Array.isArray(account.folders),
|
||||||
|
foldersLength: account.folders?.length || 0,
|
||||||
|
folders: account.folders
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isMounted) return;
|
||||||
|
|
||||||
if (data.authenticated) {
|
if (data.authenticated) {
|
||||||
if (data.hasEmailCredentials) {
|
if (data.hasEmailCredentials) {
|
||||||
console.log('Session initialized, prefetch status:', data.prefetchStarted ? 'running' : 'not started');
|
console.log('Session initialized, prefetch status:', data.prefetchStarted ? 'running' : 'not started');
|
||||||
setPrefetchStarted(Boolean(data.prefetchStarted));
|
setPrefetchStarted(Boolean(data.prefetchStarted));
|
||||||
|
|
||||||
// Create a copy of the current accounts to update
|
// Create a copy of the current accounts to update
|
||||||
let updatedAccounts: Account[] = [{ id: 'all-accounts', name: 'All', email: '', color: 'bg-gray-500' }];
|
const updatedAccounts = [...accounts];
|
||||||
|
|
||||||
// Check if we have multiple accounts returned
|
// Check if we have multiple accounts returned
|
||||||
if (data.allAccounts && Array.isArray(data.allAccounts) && data.allAccounts.length > 0) {
|
if (data.allAccounts && Array.isArray(data.allAccounts) && data.allAccounts.length > 0) {
|
||||||
console.log('[DEBUG] Multiple accounts found:', data.allAccounts.length);
|
console.log('[DEBUG] Multiple accounts found:', data.allAccounts.length);
|
||||||
|
|
||||||
// Process each account
|
// First, validate the structure of each account
|
||||||
data.allAccounts.forEach((account: any, index: number) => {
|
data.allAccounts.forEach((account: any, index: number) => {
|
||||||
console.log(`[DEBUG] Processing account ${index + 1}:`, {
|
console.log(`[DEBUG] Account ${index+1} structure check:`, {
|
||||||
id: account.id,
|
id: account.id,
|
||||||
email: account.email,
|
email: account.email,
|
||||||
display_name: account.display_name,
|
display_name: account.display_name,
|
||||||
hasFolders: !!account.folders,
|
hasFolders: !!account.folders,
|
||||||
foldersIsArray: Array.isArray(account.folders),
|
foldersIsArray: Array.isArray(account.folders),
|
||||||
foldersCount: Array.isArray(account.folders) ? account.folders.length : 0
|
foldersCount: Array.isArray(account.folders) ? account.folders.length : 0,
|
||||||
|
folders: account.folders || []
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Keep the All account at position 0
|
||||||
|
if (updatedAccounts.length > 0) {
|
||||||
|
// Replace the loading account with the real one
|
||||||
|
data.allAccounts.forEach((account: any, index: number) => {
|
||||||
|
// Ensure folders are valid
|
||||||
|
const accountFolders = (account.folders && Array.isArray(account.folders))
|
||||||
|
? account.folders
|
||||||
|
: (data.mailboxes && Array.isArray(data.mailboxes))
|
||||||
|
? data.mailboxes
|
||||||
|
: ['INBOX', 'Sent', 'Drafts', 'Trash', 'Junk'];
|
||||||
|
|
||||||
|
// If we're updating the first real account (index 0 in API, position 1 in our array)
|
||||||
|
if (index === 0 && updatedAccounts.length > 1) {
|
||||||
|
// Update the loading account in place to maintain references
|
||||||
|
updatedAccounts[1] = {
|
||||||
|
id: account.id, // Use the real account ID
|
||||||
|
name: account.display_name || account.email,
|
||||||
|
email: account.email,
|
||||||
|
color: account.color || 'bg-blue-500',
|
||||||
|
folders: accountFolders
|
||||||
|
};
|
||||||
|
console.log(`[DEBUG] Updated loading account to real account: ${account.email} with ID ${account.id}`);
|
||||||
|
} else {
|
||||||
|
// Add additional accounts as new entries
|
||||||
|
updatedAccounts.push({
|
||||||
|
id: account.id || `account-${index}`,
|
||||||
|
name: account.display_name || account.email,
|
||||||
|
email: account.email,
|
||||||
|
color: account.color || 'bg-blue-500',
|
||||||
|
folders: accountFolders
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Fallback if accounts array is empty for some reason
|
||||||
|
updatedAccounts.push({ id: 'all-accounts', name: 'All', email: '', color: 'bg-gray-500' });
|
||||||
|
|
||||||
// Ensure folders are valid
|
const firstAccount = data.allAccounts[0];
|
||||||
const accountFolders = (account.folders && Array.isArray(account.folders))
|
const accountFolders = (firstAccount.folders && Array.isArray(firstAccount.folders))
|
||||||
? account.folders
|
? firstAccount.folders
|
||||||
: (data.mailboxes && Array.isArray(data.mailboxes))
|
: (data.mailboxes && Array.isArray(data.mailboxes))
|
||||||
? data.mailboxes
|
? data.mailboxes
|
||||||
: ['INBOX', 'Sent', 'Drafts', 'Trash', 'Junk'];
|
: ['INBOX', 'Sent', 'Drafts', 'Trash', 'Junk'];
|
||||||
|
|
||||||
// Add the account to the list
|
|
||||||
updatedAccounts.push({
|
updatedAccounts.push({
|
||||||
id: account.id || `account-${index}`,
|
id: firstAccount.id,
|
||||||
name: account.display_name || account.email,
|
name: firstAccount.display_name || firstAccount.email,
|
||||||
email: account.email,
|
email: firstAccount.email,
|
||||||
color: account.color || 'bg-blue-500',
|
color: firstAccount.color || 'bg-blue-500',
|
||||||
folders: accountFolders
|
folders: accountFolders
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
console.log('[DEBUG] Final accounts array:', updatedAccounts.map(acc => ({
|
|
||||||
id: acc.id,
|
|
||||||
name: acc.name,
|
|
||||||
email: acc.email,
|
|
||||||
folderCount: acc.folders?.length || 0
|
|
||||||
})));
|
|
||||||
} else if (data.email) {
|
} else if (data.email) {
|
||||||
// Fallback to single account if allAccounts is not available
|
// Fallback to single account if allAccounts is not available
|
||||||
console.log(`[DEBUG] Fallback to single account: ${data.email}`);
|
console.log(`[DEBUG] Fallback to single account: ${data.email}`);
|
||||||
@ -302,14 +353,26 @@ export default function CourrierPage() {
|
|||||||
const folderList = (data.mailboxes && data.mailboxes.length > 0) ?
|
const folderList = (data.mailboxes && data.mailboxes.length > 0) ?
|
||||||
data.mailboxes : fallbackFolders;
|
data.mailboxes : fallbackFolders;
|
||||||
|
|
||||||
// Add the single account
|
// Update the loading account if it exists
|
||||||
updatedAccounts.push({
|
if (updatedAccounts.length > 1) {
|
||||||
id: 'default-account',
|
updatedAccounts[1] = {
|
||||||
name: data.displayName || data.email,
|
id: 'default-account', // Use consistent ID
|
||||||
email: data.email,
|
name: data.displayName || data.email,
|
||||||
color: 'bg-blue-500',
|
email: data.email,
|
||||||
folders: folderList
|
color: 'bg-blue-500',
|
||||||
});
|
folders: folderList
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// Fallback if accounts array is empty
|
||||||
|
updatedAccounts.push({ id: 'all-accounts', name: 'All', email: '', color: 'bg-gray-500' });
|
||||||
|
updatedAccounts.push({
|
||||||
|
id: 'default-account',
|
||||||
|
name: data.displayName || data.email,
|
||||||
|
email: data.email,
|
||||||
|
color: 'bg-blue-500',
|
||||||
|
folders: folderList
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Setting accounts:', updatedAccounts);
|
console.log('Setting accounts:', updatedAccounts);
|
||||||
@ -329,25 +392,25 @@ export default function CourrierPage() {
|
|||||||
console.log('Auto-selecting account:', updatedAccounts[1]);
|
console.log('Auto-selecting account:', updatedAccounts[1]);
|
||||||
setSelectedAccount(updatedAccounts[1]);
|
setSelectedAccount(updatedAccounts[1]);
|
||||||
setShowFolders(true);
|
setShowFolders(true);
|
||||||
|
|
||||||
// Load emails for the selected account
|
|
||||||
if (session?.user?.id) {
|
|
||||||
await loadEmails();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the user hasn't opened this page recently, trigger a background refresh
|
// Preload first page of emails for faster initial rendering
|
||||||
if (data.lastVisit && Date.now() - data.lastVisit > 5 * 60 * 1000) {
|
if (session?.user?.id) {
|
||||||
// It's been more than 5 minutes, refresh in background
|
await loadEmails();
|
||||||
try {
|
|
||||||
const refreshResponse = await fetch('/api/courrier/refresh', {
|
// If the user hasn't opened this page recently, trigger a background refresh
|
||||||
method: 'POST',
|
if (data.lastVisit && Date.now() - data.lastVisit > 5 * 60 * 1000) {
|
||||||
headers: { 'Content-Type': 'application/json' },
|
// It's been more than 5 minutes, refresh in background
|
||||||
body: JSON.stringify({ folder: currentFolder })
|
try {
|
||||||
});
|
const refreshResponse = await fetch('/api/courrier/refresh', {
|
||||||
console.log('Background refresh triggered');
|
method: 'POST',
|
||||||
} catch (error) {
|
headers: { 'Content-Type': 'application/json' },
|
||||||
console.error('Failed to trigger background refresh', error);
|
body: JSON.stringify({ folder: currentFolder })
|
||||||
|
});
|
||||||
|
console.log('Background refresh triggered');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to trigger background refresh', error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
171
lib/redis.ts
171
lib/redis.ts
@ -1,8 +1,5 @@
|
|||||||
import Redis from 'ioredis';
|
import Redis from 'ioredis';
|
||||||
import CryptoJS from 'crypto-js';
|
import CryptoJS from 'crypto-js';
|
||||||
import { PrismaClient } from '@prisma/client';
|
|
||||||
|
|
||||||
const prisma = new PrismaClient();
|
|
||||||
|
|
||||||
// Initialize Redis client
|
// Initialize Redis client
|
||||||
let redisClient: Redis | null = null;
|
let redisClient: Redis | null = null;
|
||||||
@ -93,20 +90,17 @@ export const TTL = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
interface EmailCredentials {
|
interface EmailCredentials {
|
||||||
id: string;
|
|
||||||
userId: string;
|
|
||||||
email: string;
|
email: string;
|
||||||
password: string;
|
password?: string;
|
||||||
host: string;
|
host: string;
|
||||||
port: number;
|
port: number;
|
||||||
secure: boolean;
|
secure?: boolean;
|
||||||
smtp_host: string | null;
|
encryptedPassword?: string;
|
||||||
smtp_port: number | null;
|
smtp_host?: string;
|
||||||
smtp_secure: boolean | null;
|
smtp_port?: number;
|
||||||
display_name: string | null;
|
smtp_secure?: boolean;
|
||||||
color: string | null;
|
display_name?: string;
|
||||||
createdAt: Date;
|
color?: string;
|
||||||
updatedAt: Date;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ImapSessionData {
|
interface ImapSessionData {
|
||||||
@ -119,61 +113,106 @@ interface ImapSessionData {
|
|||||||
/**
|
/**
|
||||||
* Cache email credentials in Redis
|
* Cache email credentials in Redis
|
||||||
*/
|
*/
|
||||||
export async function cacheEmailCredentials(userId: string, credentials: EmailCredentials): Promise<void> {
|
export async function cacheEmailCredentials(
|
||||||
const client = await getRedisClient();
|
userId: string,
|
||||||
const key = `email_credentials:${userId}`;
|
credentials: EmailCredentials
|
||||||
await client.set(key, JSON.stringify(credentials), 'EX', 3600); // 1 hour expiry
|
): Promise<void> {
|
||||||
|
const redis = getRedisClient();
|
||||||
|
const key = KEYS.CREDENTIALS(userId);
|
||||||
|
|
||||||
|
// Validate credentials before caching
|
||||||
|
if (!credentials.email || !credentials.host || !credentials.password) {
|
||||||
|
console.error(`Cannot cache incomplete credentials for user ${userId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log(`Caching credentials for user ${userId}`);
|
||||||
|
|
||||||
|
// Create a copy without the password to store
|
||||||
|
const secureCredentials: EmailCredentials = {
|
||||||
|
email: credentials.email,
|
||||||
|
host: credentials.host,
|
||||||
|
port: credentials.port,
|
||||||
|
secure: credentials.secure ?? true,
|
||||||
|
// Include the extended fields
|
||||||
|
...(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 })
|
||||||
|
};
|
||||||
|
|
||||||
|
// Encrypt password
|
||||||
|
if (credentials.password) {
|
||||||
|
try {
|
||||||
|
const encrypted = encryptData(credentials.password);
|
||||||
|
console.log(`Successfully encrypted password for user ${userId}`);
|
||||||
|
secureCredentials.encryptedPassword = encrypted;
|
||||||
|
} catch (encryptError) {
|
||||||
|
console.error(`Failed to encrypt password for user ${userId}:`, encryptError);
|
||||||
|
// Don't proceed with caching if encryption fails
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn(`No password provided for user ${userId}, skipping credential caching`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await redis.set(key, JSON.stringify(secureCredentials), 'EX', TTL.CREDENTIALS);
|
||||||
|
console.log(`Credentials cached for user ${userId}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error caching credentials for user ${userId}:`, error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get cached email credentials from Redis
|
* Get 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> {
|
export async function getEmailCredentials(userId: string): Promise<EmailCredentials | null> {
|
||||||
// Try to get from cache first
|
const redis = getRedisClient();
|
||||||
const cached = await getCachedEmailCredentials(userId);
|
const key = KEYS.CREDENTIALS(userId);
|
||||||
if (cached) {
|
|
||||||
return cached;
|
try {
|
||||||
}
|
const credStr = await redis.get(key);
|
||||||
|
|
||||||
// If not in cache, get from database
|
if (!credStr) {
|
||||||
const credentials = await prisma.mailCredentials.findFirst({
|
return null;
|
||||||
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
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
const creds = JSON.parse(credStr) as EmailCredentials;
|
||||||
if (!credentials) {
|
|
||||||
|
if (!creds.encryptedPassword) {
|
||||||
|
console.warn(`No encrypted password found for user ${userId}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Decrypt the password
|
||||||
|
const password = decryptData(creds.encryptedPassword);
|
||||||
|
|
||||||
|
// Return the full credentials with decrypted password
|
||||||
|
return {
|
||||||
|
email: creds.email,
|
||||||
|
password,
|
||||||
|
host: creds.host,
|
||||||
|
port: creds.port,
|
||||||
|
secure: creds.secure ?? true,
|
||||||
|
// Include the extended fields if they exist in the cache
|
||||||
|
...(creds.smtp_host && { smtp_host: creds.smtp_host }),
|
||||||
|
...(creds.smtp_port && { smtp_port: creds.smtp_port }),
|
||||||
|
...(creds.smtp_secure !== undefined && { smtp_secure: creds.smtp_secure }),
|
||||||
|
...(creds.display_name && { display_name: creds.display_name }),
|
||||||
|
...(creds.color && { color: creds.color })
|
||||||
|
};
|
||||||
|
} catch (decryptError) {
|
||||||
|
console.error(`Failed to decrypt password for user ${userId}:`, decryptError);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error retrieving credentials for user ${userId}:`, error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cache the credentials
|
|
||||||
await cacheEmailCredentials(userId, credentials);
|
|
||||||
|
|
||||||
return credentials;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -370,4 +409,14 @@ export async function invalidateUserEmailCache(
|
|||||||
}
|
}
|
||||||
} while (cursor !== '0');
|
} while (cursor !== '0');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cached email credentials from Redis
|
||||||
|
* @deprecated Use getEmailCredentials instead
|
||||||
|
*/
|
||||||
|
export async function getCachedEmailCredentials(
|
||||||
|
userId: string
|
||||||
|
): Promise<EmailCredentials | null> {
|
||||||
|
return getEmailCredentials(userId);
|
||||||
}
|
}
|
||||||
@ -18,7 +18,6 @@ import {
|
|||||||
invalidateEmailContentCache
|
invalidateEmailContentCache
|
||||||
} from '@/lib/redis';
|
} from '@/lib/redis';
|
||||||
import { EmailCredentials, EmailMessage, EmailAddress, EmailAttachment } from '@/lib/types';
|
import { EmailCredentials, EmailMessage, EmailAddress, EmailAttachment } from '@/lib/types';
|
||||||
import { PrismaClient } from '@prisma/client';
|
|
||||||
|
|
||||||
// Types specific to this service
|
// Types specific to this service
|
||||||
export interface EmailListResult {
|
export interface EmailListResult {
|
||||||
@ -68,7 +67,7 @@ export async function getImapConnection(userId: string): Promise<ImapFlow> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Cache credentials for future use
|
// Cache credentials for future use
|
||||||
await cacheEmailCredentials(userId, [credentials]);
|
await cacheEmailCredentials(userId, credentials);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate credentials
|
// Validate credentials
|
||||||
@ -151,11 +150,9 @@ 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.findFirst({
|
const credentials = await prisma.mailCredentials.findUnique({
|
||||||
where: { userId },
|
where: { userId },
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
|
||||||
userId: true,
|
|
||||||
email: true,
|
email: true,
|
||||||
password: true,
|
password: true,
|
||||||
host: true,
|
host: true,
|
||||||
@ -165,20 +162,27 @@ export async function getUserEmailCredentials(userId: string): Promise<EmailCred
|
|||||||
smtp_port: true,
|
smtp_port: true,
|
||||||
smtp_secure: true,
|
smtp_secure: true,
|
||||||
display_name: true,
|
display_name: true,
|
||||||
color: true,
|
color: true
|
||||||
createdAt: true,
|
|
||||||
updatedAt: true
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!credentials) {
|
if (!credentials) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cache the credentials
|
// Return only the fields that exist in credentials
|
||||||
await cacheEmailCredentials(userId, credentials);
|
return {
|
||||||
|
email: credentials.email,
|
||||||
return credentials;
|
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 })
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -209,7 +213,7 @@ export async function saveUserEmailCredentials(
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 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, credentials);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper type for IMAP fetch options
|
// Helper type for IMAP fetch options
|
||||||
@ -885,7 +889,7 @@ export async function forceRecacheUserCredentials(userId: string): Promise<boole
|
|||||||
// Try to cache the credentials
|
// Try to cache the credentials
|
||||||
try {
|
try {
|
||||||
const { cacheEmailCredentials } = await import('@/lib/redis');
|
const { cacheEmailCredentials } = await import('@/lib/redis');
|
||||||
await cacheEmailCredentials(userId, [credentials]);
|
await cacheEmailCredentials(userId, credentials);
|
||||||
console.log(`[CREDENTIAL FIX] Successfully cached credentials for user ${userId}`);
|
console.log(`[CREDENTIAL FIX] Successfully cached credentials for user ${userId}`);
|
||||||
|
|
||||||
// Now verify the credentials were cached correctly
|
// Now verify the credentials were cached correctly
|
||||||
|
|||||||
24
lib/types.ts
24
lib/types.ts
@ -1,18 +1,20 @@
|
|||||||
export interface EmailCredentials {
|
export interface EmailCredentials {
|
||||||
id: string;
|
// IMAP Settings
|
||||||
userId: string;
|
|
||||||
email: string;
|
email: string;
|
||||||
password: string;
|
password?: string;
|
||||||
host: string;
|
host: string;
|
||||||
port: number;
|
port: number;
|
||||||
secure: boolean;
|
secure?: boolean;
|
||||||
smtp_host: string | null;
|
encryptedPassword?: string;
|
||||||
smtp_port: number | null;
|
|
||||||
smtp_secure: boolean | null;
|
// SMTP Settings
|
||||||
display_name: string | null;
|
smtp_host?: string;
|
||||||
color: string | null;
|
smtp_port?: number;
|
||||||
createdAt: Date;
|
smtp_secure?: boolean; // true for SSL, false for TLS
|
||||||
updatedAt: Date;
|
|
||||||
|
// Display Settings
|
||||||
|
display_name?: string;
|
||||||
|
color?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EmailAddress {
|
export interface EmailAddress {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user