courrier multi account restore compose
This commit is contained in:
parent
d4b28b7974
commit
1506cc7390
@ -159,7 +159,7 @@ export async function getUserEmailCredentials(userId: string, accountId?: string
|
|||||||
where: {
|
where: {
|
||||||
AND: [
|
AND: [
|
||||||
{ userId },
|
{ userId },
|
||||||
{ email: accountId }
|
accountId ? { id: accountId } : {}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -272,265 +272,90 @@ export async function getEmails(
|
|||||||
searchQuery: string = '',
|
searchQuery: string = '',
|
||||||
accountId?: string
|
accountId?: string
|
||||||
): Promise<EmailListResult> {
|
): Promise<EmailListResult> {
|
||||||
|
console.log(`Fetching emails for user ${userId}${accountId ? ` account ${accountId}` : ''} in folder ${folder}`);
|
||||||
|
|
||||||
// Try to get from cache first
|
// Try to get from cache first
|
||||||
if (!searchQuery) {
|
const cacheKey = accountId
|
||||||
const cacheKey = accountId ? `${userId}:${accountId}:${folder}` : `${userId}:${folder}`;
|
? `email:list:${userId}:${accountId}:${folder}:${page}:${perPage}:${searchQuery}`
|
||||||
const cachedResult = await getCachedEmailList(userId, accountId || 'default', folder, page, perPage);
|
: `email:list:${userId}:${folder}:${page}:${perPage}:${searchQuery}`;
|
||||||
if (cachedResult) {
|
|
||||||
console.log(`Using cached email list for ${cacheKey}:${page}:${perPage}`);
|
const cached = await getCachedEmailList(cacheKey);
|
||||||
return cachedResult;
|
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`);
|
// Get IMAP connection for the specific account
|
||||||
|
const client = await getImapConnection(userId, accountId);
|
||||||
// If accountId is provided, connect to that specific account
|
if (!client) {
|
||||||
let client: ImapFlow;
|
throw new Error('Failed to get IMAP connection');
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mailboxes: string[] = [];
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log(`[DEBUG] Fetching mailboxes for user ${userId}`);
|
// Select the mailbox
|
||||||
// Get list of mailboxes first
|
await client.mailboxOpen(folder);
|
||||||
try {
|
|
||||||
mailboxes = await getMailboxes(client);
|
|
||||||
console.log(`[DEBUG] Found ${mailboxes.length} mailboxes:`, mailboxes);
|
|
||||||
|
|
||||||
// Save mailboxes in session data
|
// Get total count
|
||||||
const cachedSession = await getCachedImapSession(userId);
|
const totalEmails = await client.mailbox.messages.total;
|
||||||
await cacheImapSession(userId, {
|
const totalPages = Math.ceil(totalEmails / perPage);
|
||||||
...(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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Open mailbox
|
// Calculate range for this page
|
||||||
const mailboxData = await client.mailboxOpen(folder);
|
const start = (page - 1) * perPage + 1;
|
||||||
const totalMessages = mailboxData.exists;
|
const end = Math.min(start + perPage - 1, totalEmails);
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch messages
|
// Fetch messages
|
||||||
const emails: EmailMessage[] = [];
|
const messages = await client.fetch(`${start}:${end}`, {
|
||||||
|
|
||||||
for (const id of messageIds) {
|
|
||||||
try {
|
|
||||||
// Define fetch options with proper typing
|
|
||||||
const fetchOptions: any = {
|
|
||||||
envelope: true,
|
envelope: true,
|
||||||
flags: true,
|
flags: true,
|
||||||
bodyStructure: true,
|
bodyStructure: true,
|
||||||
internalDate: true,
|
internalDate: true,
|
||||||
size: true,
|
size: true,
|
||||||
bodyParts: [{
|
bodyParts: [
|
||||||
part: '1',
|
{ part: 'TEXT', query: { headers: true } }
|
||||||
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) {
|
// Process messages
|
||||||
node.childNodes.forEach((child: any, index: number) => {
|
const emails: EmailMessage[] = [];
|
||||||
processAttachments(child, [...path, node.part || index + 1]);
|
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(', '),
|
||||||
if (bodyStructure) {
|
subject: message.envelope.subject || '',
|
||||||
processAttachments(bodyStructure);
|
date: message.internalDate,
|
||||||
}
|
|
||||||
|
|
||||||
// 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: {
|
flags: {
|
||||||
seen: flagsArray.includes("\\Seen"),
|
seen: message.flags.has('\\Seen'),
|
||||||
flagged: flagsArray.includes("\\Flagged"),
|
answered: message.flags.has('\\Answered'),
|
||||||
answered: flagsArray.includes("\\Answered"),
|
flagged: message.flags.has('\\Flagged'),
|
||||||
deleted: flagsArray.includes("\\Deleted"),
|
draft: message.flags.has('\\Draft'),
|
||||||
draft: flagsArray.includes("\\Draft"),
|
deleted: message.flags.has('\\Deleted')
|
||||||
},
|
},
|
||||||
hasAttachments: attachments.length > 0,
|
size: message.size,
|
||||||
attachments,
|
hasAttachments: message.bodyStructure?.childNodes?.some(node => node.disposition === 'attachment') || false
|
||||||
size,
|
};
|
||||||
preview,
|
emails.push(email);
|
||||||
folder,
|
|
||||||
contentFetched: false
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Error fetching message ${id}:`, error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort by date, newest first
|
const result: EmailListResult = {
|
||||||
emails.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
|
|
||||||
|
|
||||||
const result = {
|
|
||||||
emails,
|
emails,
|
||||||
totalEmails: totalMessages,
|
totalEmails,
|
||||||
page,
|
page,
|
||||||
perPage,
|
perPage,
|
||||||
totalPages: Math.ceil(totalMessages / perPage),
|
totalPages,
|
||||||
folder,
|
folder,
|
||||||
mailboxes
|
mailboxes: await getMailboxes(client)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Always cache the result if it's not a search query, even for pagination
|
// Cache the result
|
||||||
if (!searchQuery) {
|
await cacheEmailList(cacheKey, result);
|
||||||
console.log(`Caching email list for ${userId}:${folder}:${page}:${perPage}`);
|
|
||||||
await cacheEmailList(userId, accountId || 'default', folder, page, perPage, result);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
} finally {
|
|
||||||
// Don't logout, keep connection in pool
|
|
||||||
if (folder !== 'INBOX') {
|
|
||||||
try {
|
|
||||||
await client.mailboxClose();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error closing mailbox:', error);
|
console.error(`Error fetching emails for ${userId}${accountId ? ` account ${accountId}` : ''}:`, error);
|
||||||
}
|
throw error;
|
||||||
}
|
} finally {
|
||||||
|
// Don't close the connection, it's managed by the connection pool
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user