From 346b766b7f5f958add054ebe2fc7500b842ccaa2 Mon Sep 17 00:00:00 2001 From: alma Date: Wed, 30 Apr 2025 12:37:10 +0200 Subject: [PATCH] courrier multi account restore compose --- app/api/courrier/route.ts | 2 +- components/email/EmailSidebar.tsx | 10 +- lib/services/email-service.ts | 245 ++++++++++++++++-------------- 3 files changed, 144 insertions(+), 113 deletions(-) diff --git a/app/api/courrier/route.ts b/app/api/courrier/route.ts index 57c605b7..b3795dfc 100644 --- a/app/api/courrier/route.ts +++ b/app/api/courrier/route.ts @@ -41,7 +41,7 @@ export async function GET(request: Request) { const folderAccountId = folder.includes(':') ? folder.split(':')[0] : accountId; // Use the most specific account ID available - const effectiveAccountId = folderAccountId || accountId || 'default'; + let effectiveAccountId = folderAccountId || accountId || 'default'; // Normalize folder name by removing account prefix if present const normalizedFolder = folder.includes(':') ? folder.split(':')[1] : folder; diff --git a/components/email/EmailSidebar.tsx b/components/email/EmailSidebar.tsx index 355ea64a..074da157 100644 --- a/components/email/EmailSidebar.tsx +++ b/components/email/EmailSidebar.tsx @@ -190,8 +190,14 @@ export default function EmailSidebar({ variant="ghost" className={`w-full justify-start text-xs py-1 h-7 ${isSelected ? 'bg-gray-100' : ''}`} onClick={() => { - console.log(`Folder button clicked: ${prefixedFolder}`); - onFolderChange(prefixedFolder, accountId); + console.log(`Folder button clicked: folder=${prefixedFolder}, accountId=${accountId}, normalized=${baseFolderName}`); + + // Always ensure the folder name includes the account ID prefix + const fullyPrefixedFolder = folder.includes(':') ? folder : `${accountId}:${folder}`; + + // Make sure we pass the EXACT accountId parameter here, not the folder's extracted account ID + console.log(`Calling onFolderChange with folder=${fullyPrefixedFolder}, accountId=${accountId}`); + onFolderChange(fullyPrefixedFolder, accountId); }} >
diff --git a/lib/services/email-service.ts b/lib/services/email-service.ts index 640214a4..5cb08eaa 100644 --- a/lib/services/email-service.ts +++ b/lib/services/email-service.ts @@ -65,10 +65,28 @@ export async function getImapConnection( ): Promise { console.log(`Getting IMAP connection for user ${userId}${accountId ? ` account ${accountId}` : ''}`); + // Special handling for 'default' accountId - find the first available account + if (!accountId || accountId === 'default') { + console.log(`No specific account provided or 'default' requested, trying to find first account for user ${userId}`); + + // Query to find all accounts for this user + const accounts = await prisma.mailCredentials.findMany({ + where: { userId }, + orderBy: { createdAt: 'asc' }, + take: 1 + }); + + if (accounts && accounts.length > 0) { + const firstAccount = accounts[0]; + console.log(`Using first available account: ${firstAccount.id} (${firstAccount.email})`); + accountId = firstAccount.id; + } else { + throw new Error('No email accounts configured for this user'); + } + } + // First try to get credentials from Redis cache - let credentials = accountId - ? await getCachedEmailCredentials(userId, accountId) - : await getCachedEmailCredentials(userId, 'default'); + let credentials = await getCachedEmailCredentials(userId, accountId); // If not in cache, get from database and cache them if (!credentials) { @@ -79,7 +97,7 @@ export async function getImapConnection( } // Cache credentials for future use - await cacheEmailCredentials(userId, accountId || 'default', credentials); + await cacheEmailCredentials(userId, accountId, credentials); } // Validate credentials @@ -94,7 +112,7 @@ export async function getImapConnection( } // Use accountId in connection key to ensure different accounts get different connections - const connectionKey = `${userId}:${accountId || 'default'}`; + const connectionKey = `${userId}:${accountId}`; const existingConnection = connectionPool[connectionKey]; // Try to get session data from Redis @@ -279,127 +297,134 @@ export async function getEmails( perPage: number = 20, accountId?: string ): Promise { - let client: ImapFlow | undefined; + // Normalize folder name and handle account ID + console.log(`[getEmails] Processing request for folder: ${folder}, normalized to ${folder}, account: ${accountId || 'default'}`); try { - // Extract account ID from folder name if present and none was explicitly provided - const folderAccountId = folder.includes(':') ? folder.split(':')[0] : accountId; + // The getImapConnection function already handles 'default' accountId by finding the first available account + const client = await getImapConnection(userId, accountId); - // Use the most specific account ID available - const effectiveAccountId = folderAccountId || accountId || 'default'; + // At this point, accountId has been resolved to an actual account ID by getImapConnection + // Store the resolved accountId in a variable that is guaranteed to be a string + const resolvedAccountId = accountId || 'default'; - // Normalize folder name by removing account prefix if present - const normalizedFolder = folder.includes(':') ? folder.split(':')[1] : folder; - - console.log(`[getEmails] Processing request for folder: ${folder}, normalized to ${normalizedFolder}, account: ${effectiveAccountId}`); + // Attempt to select the mailbox + try { + const mailboxInfo = await client.mailboxOpen(folder); + console.log(`Opened mailbox ${folder} with ${mailboxInfo.exists} messages`); + + // Get list of all mailboxes for UI + const mailboxes = await getMailboxes(client, resolvedAccountId); + + // Calculate pagination + const totalEmails = mailboxInfo.exists || 0; + const totalPages = Math.ceil(totalEmails / perPage); + + // Check if mailbox is empty + if (totalEmails === 0) { + // Cache the empty result + const emptyResult = { + emails: [], + totalEmails: 0, + page, + perPage, + totalPages: 0, + folder, + mailboxes + }; + + await cacheEmailList( + userId, + resolvedAccountId, // Use the guaranteed string account ID + folder, + page, + perPage, + emptyResult + ); + + return emptyResult; + } - // Get IMAP connection using the effective account ID - client = await getImapConnection(userId, effectiveAccountId); - if (!client) { - throw new Error('Failed to establish IMAP connection'); - } + // Calculate message range for pagination + const start = Math.max(1, totalEmails - (page * perPage) + 1); + const end = Math.max(1, totalEmails - ((page - 1) * perPage)); + console.log(`Fetching messages ${start}:${end} from ${folder} for account ${resolvedAccountId}`); - // Open mailbox with the normalized folder name - await client.mailboxOpen(normalizedFolder); - const mailbox = client.mailbox; - if (!mailbox || typeof mailbox === 'boolean') { - throw new Error(`Failed to open mailbox: ${normalizedFolder}`); - } + // Fetch messages + const messages = await client.fetch(`${start}:${end}`, { + envelope: true, + flags: true, + bodyStructure: true + }); - // Get total messages - const total = mailbox.exists || 0; - console.log(`Total messages in ${normalizedFolder} for account ${effectiveAccountId}: ${total}`); + const emails: EmailMessage[] = []; + for await (const message of messages) { + const email: EmailMessage = { + id: message.uid.toString(), + from: message.envelope.from?.map(addr => ({ + name: addr.name || '', + address: addr.address || '' + })) || [], + to: message.envelope.to?.map(addr => ({ + name: addr.name || '', + address: addr.address || '' + })) || [], + subject: message.envelope.subject || '', + date: message.envelope.date || new Date(), + flags: { + seen: message.flags.has('\\Seen'), + flagged: message.flags.has('\\Flagged'), + answered: message.flags.has('\\Answered'), + draft: message.flags.has('\\Draft'), + deleted: message.flags.has('\\Deleted') + }, + size: message.size || 0, + hasAttachments: message.bodyStructure?.childNodes?.some(node => node.disposition === 'attachment') || false, + folder: folder, + contentFetched: false, + accountId: resolvedAccountId, + content: { + text: '', + html: '' + } + }; + emails.push(email); + } - // If no messages, return empty result - if (total === 0) { - return { - emails: [], - totalEmails: 0, + // Cache the result with the effective account ID + await cacheEmailList( + userId, + resolvedAccountId, // Use the guaranteed string account ID + folder, page, perPage, - totalPages: 0, - folder: normalizedFolder, - mailboxes: [] - }; - } - - // Calculate message range for pagination - const start = Math.max(1, total - (page * perPage) + 1); - const end = Math.max(1, total - ((page - 1) * perPage)); - console.log(`Fetching messages ${start}:${end} from ${normalizedFolder} for account ${effectiveAccountId}`); - - // Fetch messages - const messages = await client.fetch(`${start}:${end}`, { - envelope: true, - flags: true, - bodyStructure: true - }); - - const emails: EmailMessage[] = []; - for await (const message of messages) { - const email: EmailMessage = { - id: message.uid.toString(), - from: message.envelope.from?.map(addr => ({ - name: addr.name || '', - address: addr.address || '' - })) || [], - to: message.envelope.to?.map(addr => ({ - name: addr.name || '', - address: addr.address || '' - })) || [], - subject: message.envelope.subject || '', - date: message.envelope.date || new Date(), - flags: { - seen: message.flags.has('\\Seen'), - flagged: message.flags.has('\\Flagged'), - answered: message.flags.has('\\Answered'), - draft: message.flags.has('\\Draft'), - deleted: message.flags.has('\\Deleted') - }, - size: message.size || 0, - hasAttachments: message.bodyStructure?.childNodes?.some(node => node.disposition === 'attachment') || false, - folder: normalizedFolder, - contentFetched: false, - accountId: effectiveAccountId, - content: { - text: '', - html: '' + { + emails, + totalEmails: totalEmails, + page, + perPage, + totalPages: Math.ceil(totalEmails / perPage), + folder: folder, + mailboxes: mailboxes } + ); + + return { + emails, + totalEmails: totalEmails, + page, + perPage, + totalPages: Math.ceil(totalEmails / perPage), + folder: folder, + mailboxes: mailboxes }; - emails.push(email); + } catch (error) { + console.error('Error fetching emails:', error); + throw error; } - - // Cache the result with the effective account ID - await cacheEmailList(userId, effectiveAccountId, normalizedFolder, page, perPage, { - emails, - totalEmails: total, - page, - perPage, - totalPages: Math.ceil(total / perPage), - folder: normalizedFolder, - mailboxes: [] - }); - - return { - emails, - totalEmails: total, - page, - perPage, - totalPages: Math.ceil(total / perPage), - folder: normalizedFolder, - mailboxes: [] - }; } catch (error) { console.error('Error fetching emails:', error); throw error; - } finally { - if (client) { - try { - await client.mailboxClose(); - } catch (error) { - console.error('Error closing mailbox:', error); - } - } } }