courrier multi account restore compose

This commit is contained in:
alma 2025-04-28 14:34:24 +02:00
parent d4b28b7974
commit 1506cc7390

View File

@ -159,7 +159,7 @@ export async function getUserEmailCredentials(userId: string, accountId?: string
where: {
AND: [
{ userId },
{ email: accountId }
accountId ? { id: accountId } : {}
]
}
});
@ -272,265 +272,90 @@ export async function getEmails(
searchQuery: string = '',
accountId?: string
): Promise<EmailListResult> {
console.log(`Fetching emails for user ${userId}${accountId ? ` account ${accountId}` : ''} in folder ${folder}`);
// Try to get from cache first
if (!searchQuery) {
const cacheKey = accountId ? `${userId}:${accountId}:${folder}` : `${userId}:${folder}`;
const cachedResult = await getCachedEmailList(userId, accountId || 'default', folder, page, perPage);
if (cachedResult) {
console.log(`Using cached email list for ${cacheKey}:${page}:${perPage}`);
return cachedResult;
}
const cacheKey = accountId
? `email:list:${userId}:${accountId}:${folder}:${page}:${perPage}:${searchQuery}`
: `email:list:${userId}:${folder}:${page}:${perPage}:${searchQuery}`;
const cached = await getCachedEmailList(cacheKey);
if (cached) {
console.log(`Using cached email list for ${cacheKey}`);
return cached;
}
console.log(`Cache miss for emails ${userId}:${folder}:${page}:${perPage}${accountId ? ` for account ${accountId}` : ''}, fetching from IMAP`);
// If accountId is provided, connect to that specific account
let client: ImapFlow;
if (accountId) {
try {
// Get account from database
const account = await prisma.mailCredentials.findFirst({
where: {
AND: [
{ userId },
{ email: accountId }
]
}
});
if (!account) {
throw new Error(`Account with ID ${accountId} not found`);
}
// Connect to IMAP server for this specific account
client = new ImapFlow({
host: account.host,
port: account.port,
secure: true, // Default to secure connection
auth: {
user: account.email,
pass: account.password,
},
logger: false,
tls: {
rejectUnauthorized: false
}
});
await client.connect();
} catch (error) {
console.error(`Error connecting to account ${accountId}:`, error);
// Fallback to default connection
client = await getImapConnection(userId);
}
} else {
// Use the default connection logic
client = await getImapConnection(userId);
// Get IMAP connection for the specific account
const client = await getImapConnection(userId, accountId);
if (!client) {
throw new Error('Failed to get IMAP connection');
}
let mailboxes: string[] = [];
try {
console.log(`[DEBUG] Fetching mailboxes for user ${userId}`);
// Get list of mailboxes first
try {
mailboxes = await getMailboxes(client);
console.log(`[DEBUG] Found ${mailboxes.length} mailboxes:`, mailboxes);
// Save mailboxes in session data
const cachedSession = await getCachedImapSession(userId);
await cacheImapSession(userId, {
...(cachedSession || { lastActive: Date.now() }),
mailboxes
});
console.log(`[DEBUG] Updated cached session with mailboxes for user ${userId}`);
} catch (mailboxError) {
console.error(`[ERROR] Failed to fetch mailboxes:`, mailboxError);
}
// Select the mailbox
await client.mailboxOpen(folder);
// Open mailbox
const mailboxData = await client.mailboxOpen(folder);
const totalMessages = mailboxData.exists;
// Get total count
const totalEmails = await client.mailbox.messages.total;
const totalPages = Math.ceil(totalEmails / perPage);
// Calculate range based on total messages
const endIdx = page * perPage;
const startIdx = (page - 1) * perPage + 1;
const from = Math.max(totalMessages - endIdx + 1, 1);
const to = Math.max(totalMessages - startIdx + 1, 1);
// Empty result if no messages
if (totalMessages === 0 || from > to) {
const result = {
emails: [],
totalEmails: 0,
page,
perPage,
totalPages: 0,
folder,
mailboxes
};
// Cache even empty results
if (!searchQuery) {
await cacheEmailList(userId, accountId || 'default', folder, page, perPage, result);
}
return result;
}
// Search if needed
let messageIds: any[] = [];
if (searchQuery) {
messageIds = await client.search({ body: searchQuery });
messageIds = messageIds.filter(id => id >= from && id <= to);
} else {
messageIds = Array.from({ length: to - from + 1 }, (_, i) => from + i);
}
// Calculate range for this page
const start = (page - 1) * perPage + 1;
const end = Math.min(start + perPage - 1, totalEmails);
// Fetch messages
const messages = await client.fetch(`${start}:${end}`, {
envelope: true,
flags: true,
bodyStructure: true,
internalDate: true,
size: true,
bodyParts: [
{ part: 'TEXT', query: { headers: true } }
]
});
// Process messages
const emails: EmailMessage[] = [];
for (const id of messageIds) {
try {
// Define fetch options with proper typing
const fetchOptions: any = {
envelope: true,
flags: true,
bodyStructure: true,
internalDate: true,
size: true,
bodyParts: [{
part: '1',
query: { type: "text" },
limit: 5000
}]
};
const message = await client.fetchOne(id, fetchOptions);
if (!message) continue;
const { envelope, flags, bodyStructure, internalDate, size, bodyParts } = message;
// Extract preview content
let preview = '';
if (bodyParts && typeof bodyParts === 'object') {
// Convert to array if it's a Map
const partsArray = Array.isArray(bodyParts)
? bodyParts
: Array.from(bodyParts.values());
const textPart = partsArray.find((part: any) => part.type === 'text/plain');
const htmlPart = partsArray.find((part: any) => part.type === 'text/html');
const content = textPart?.content || htmlPart?.content || '';
if (typeof content === 'string') {
preview = content.substring(0, 150) + '...';
} else if (Buffer.isBuffer(content)) {
preview = content.toString('utf-8', 0, 150) + '...';
}
}
// Process attachments
const attachments: EmailAttachment[] = [];
const processAttachments = (node: any, path: Array<string | number> = []) => {
if (!node) return;
if (node.type === 'attachment') {
attachments.push({
contentId: node.contentId,
filename: node.filename || 'attachment',
contentType: node.contentType,
size: node.size,
path: [...path, node.part].join('.')
});
}
if (node.childNodes) {
node.childNodes.forEach((child: any, index: number) => {
processAttachments(child, [...path, node.part || index + 1]);
});
}
};
if (bodyStructure) {
processAttachments(bodyStructure);
}
// Convert flags from Set to boolean checks
const flagsArray = Array.from(flags as Set<string>);
emails.push({
id: id.toString(),
messageId: envelope.messageId,
subject: envelope.subject || "(No Subject)",
from: envelope.from.map((f: any) => ({
name: f.name || f.address,
address: f.address,
})),
to: envelope.to.map((t: any) => ({
name: t.name || t.address,
address: t.address,
})),
cc: (envelope.cc || []).map((c: any) => ({
name: c.name || c.address,
address: c.address,
})),
bcc: (envelope.bcc || []).map((b: any) => ({
name: b.name || b.address,
address: b.address,
})),
date: internalDate || new Date(),
flags: {
seen: flagsArray.includes("\\Seen"),
flagged: flagsArray.includes("\\Flagged"),
answered: flagsArray.includes("\\Answered"),
deleted: flagsArray.includes("\\Deleted"),
draft: flagsArray.includes("\\Draft"),
},
hasAttachments: attachments.length > 0,
attachments,
size,
preview,
folder,
contentFetched: false
});
} catch (error) {
console.error(`Error fetching message ${id}:`, error);
}
for await (const message of messages) {
const email: EmailMessage = {
id: message.uid.toString(),
from: message.envelope.from[0]?.address || '',
to: message.envelope.to.map(addr => addr.address).join(', '),
subject: message.envelope.subject || '',
date: message.internalDate,
flags: {
seen: message.flags.has('\\Seen'),
answered: message.flags.has('\\Answered'),
flagged: message.flags.has('\\Flagged'),
draft: message.flags.has('\\Draft'),
deleted: message.flags.has('\\Deleted')
},
size: message.size,
hasAttachments: message.bodyStructure?.childNodes?.some(node => node.disposition === 'attachment') || false
};
emails.push(email);
}
// Sort by date, newest first
emails.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
const result = {
const result: EmailListResult = {
emails,
totalEmails: totalMessages,
totalEmails,
page,
perPage,
totalPages: Math.ceil(totalMessages / perPage),
totalPages,
folder,
mailboxes
mailboxes: await getMailboxes(client)
};
// Always cache the result if it's not a search query, even for pagination
if (!searchQuery) {
console.log(`Caching email list for ${userId}:${folder}:${page}:${perPage}`);
await cacheEmailList(userId, accountId || 'default', folder, page, perPage, result);
}
// Cache the result
await cacheEmailList(cacheKey, result);
return result;
} catch (error) {
console.error(`Error fetching emails for ${userId}${accountId ? ` account ${accountId}` : ''}:`, error);
throw error;
} finally {
// Don't logout, keep connection in pool
if (folder !== 'INBOX') {
try {
await client.mailboxClose();
} catch (error) {
console.error('Error closing mailbox:', error);
}
}
// Don't close the connection, it's managed by the connection pool
}
}