courrier multi account
This commit is contained in:
parent
4d01953b7a
commit
c787d6a1a5
@ -83,70 +83,80 @@ export async function GET(request: Request) {
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Get folders for all accounts
|
||||
// Get all accounts for this user
|
||||
const accounts = await prisma.mailCredentials.findMany({
|
||||
where: { userId },
|
||||
select: {
|
||||
id: true,
|
||||
email: true
|
||||
email: true,
|
||||
password: true,
|
||||
host: true,
|
||||
port: true
|
||||
}
|
||||
});
|
||||
|
||||
// For demo purposes, rather than connecting to all accounts,
|
||||
// get the default set of folders from the first cached account
|
||||
const credentials = await getCachedEmailCredentials(userId);
|
||||
|
||||
if (!credentials) {
|
||||
if (accounts.length === 0) {
|
||||
return NextResponse.json({
|
||||
accounts: accounts.map(account => ({
|
||||
id: account.id,
|
||||
email: account.email,
|
||||
folders: ['INBOX', 'Sent', 'Drafts', 'Trash'] // Fallback folders
|
||||
}))
|
||||
accounts: []
|
||||
});
|
||||
}
|
||||
|
||||
// Connect to IMAP server
|
||||
const client = new ImapFlow({
|
||||
host: credentials.host,
|
||||
port: credentials.port,
|
||||
secure: true,
|
||||
auth: {
|
||||
user: credentials.email,
|
||||
pass: credentials.password,
|
||||
},
|
||||
logger: false,
|
||||
tls: {
|
||||
rejectUnauthorized: false
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
await client.connect();
|
||||
|
||||
// Get folders
|
||||
const folders = await getMailboxes(client);
|
||||
|
||||
// Close connection
|
||||
await client.logout();
|
||||
|
||||
return NextResponse.json({
|
||||
accounts: accounts.map(account => ({
|
||||
// Fetch folders for each account individually
|
||||
const accountsWithFolders = await Promise.all(accounts.map(async (account) => {
|
||||
try {
|
||||
// Connect to IMAP server for this specific account
|
||||
const client = new ImapFlow({
|
||||
host: account.host,
|
||||
port: account.port,
|
||||
secure: true,
|
||||
auth: {
|
||||
user: account.email,
|
||||
pass: account.password,
|
||||
},
|
||||
logger: false,
|
||||
tls: {
|
||||
rejectUnauthorized: false
|
||||
}
|
||||
});
|
||||
|
||||
await client.connect();
|
||||
|
||||
// Get folders for this account
|
||||
const folders = await getMailboxes(client);
|
||||
|
||||
// Close connection
|
||||
await client.logout();
|
||||
|
||||
// Add display_name and color from database
|
||||
const metadata = await prisma.$queryRaw`
|
||||
SELECT display_name, color
|
||||
FROM "MailCredentials"
|
||||
WHERE id = ${account.id}
|
||||
`;
|
||||
|
||||
const displayMetadata = Array.isArray(metadata) && metadata.length > 0 ? metadata[0] : {};
|
||||
|
||||
return {
|
||||
id: account.id,
|
||||
email: account.email,
|
||||
display_name: displayMetadata.display_name || account.email,
|
||||
color: displayMetadata.color || "#0082c9",
|
||||
folders
|
||||
}))
|
||||
});
|
||||
} catch (error) {
|
||||
return NextResponse.json({
|
||||
accounts: accounts.map(account => ({
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`Error fetching folders for account ${account.email}:`, error);
|
||||
// Return fallback folders on error
|
||||
return {
|
||||
id: account.id,
|
||||
email: account.email,
|
||||
folders: ['INBOX', 'Sent', 'Drafts', 'Trash'] // Fallback folders on error
|
||||
})),
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
}));
|
||||
|
||||
return NextResponse.json({
|
||||
accounts: accountsWithFolders
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
|
||||
@ -35,25 +35,28 @@ export async function GET(request: Request) {
|
||||
const perPage = parseInt(searchParams.get("perPage") || "20");
|
||||
const folder = searchParams.get("folder") || "INBOX";
|
||||
const searchQuery = searchParams.get("search") || "";
|
||||
const accountId = searchParams.get("accountId") || "";
|
||||
|
||||
// Try to get from Redis cache first, but only if it's not a search query
|
||||
if (!searchQuery) {
|
||||
const cacheKey = accountId ? `${session.user.id}:${accountId}:${folder}` : `${session.user.id}:${folder}`;
|
||||
const cachedEmails = await getCachedEmailList(session.user.id, folder, page, perPage);
|
||||
if (cachedEmails) {
|
||||
console.log(`Using Redis cached emails for ${session.user.id}:${folder}:${page}:${perPage}`);
|
||||
console.log(`Using Redis cached emails for ${cacheKey}:${page}:${perPage}`);
|
||||
return NextResponse.json(cachedEmails);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Redis cache miss for ${session.user.id}:${folder}:${page}:${perPage}, fetching emails from IMAP`);
|
||||
|
||||
// Use the email service to fetch emails
|
||||
// Use the email service to fetch emails, passing the accountId if provided
|
||||
const emailsResult = await getEmails(
|
||||
session.user.id,
|
||||
folder,
|
||||
page,
|
||||
perPage,
|
||||
searchQuery
|
||||
searchQuery,
|
||||
accountId
|
||||
);
|
||||
|
||||
// The result is already cached in the getEmails function
|
||||
|
||||
@ -1,15 +1,71 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getServerSession } from 'next-auth';
|
||||
import { authOptions } from '@/app/api/auth/[...nextauth]/route';
|
||||
import { getUserEmailCredentials } from '@/lib/services/email-service';
|
||||
import { getUserEmailCredentials, getMailboxes } from '@/lib/services/email-service';
|
||||
import { prefetchUserEmailData } from '@/lib/services/prefetch-service';
|
||||
import { getCachedEmailCredentials, getRedisStatus, warmupRedisCache, getCachedImapSession, cacheImapSession } from '@/lib/redis';
|
||||
import { prisma } from '@/lib/prisma';
|
||||
import { ImapFlow } from 'imapflow';
|
||||
|
||||
// Keep track of last prefetch time for each user
|
||||
const lastPrefetchMap = new Map<string, number>();
|
||||
const PREFETCH_COOLDOWN_MS = 30000; // 30 seconds cooldown between prefetches
|
||||
|
||||
// Cache to store account folders to avoid repeated calls to the IMAP server
|
||||
const accountFoldersCache = new Map<string, { folders: string[], timestamp: number }>();
|
||||
const FOLDERS_CACHE_TTL = 5 * 60 * 1000; // 5 minute cache
|
||||
|
||||
/**
|
||||
* Get folders for a specific account
|
||||
*/
|
||||
async function getAccountFolders(accountId: string, account: any): Promise<string[]> {
|
||||
// Check cache first
|
||||
const cacheKey = `folders:${accountId}`;
|
||||
const cachedData = accountFoldersCache.get(cacheKey);
|
||||
const now = Date.now();
|
||||
|
||||
if (cachedData && (now - cachedData.timestamp < FOLDERS_CACHE_TTL)) {
|
||||
return cachedData.folders;
|
||||
}
|
||||
|
||||
try {
|
||||
// Connect to IMAP server for this account
|
||||
const client = new ImapFlow({
|
||||
host: account.host,
|
||||
port: account.port,
|
||||
secure: true,
|
||||
auth: {
|
||||
user: account.email,
|
||||
pass: account.password,
|
||||
},
|
||||
logger: false,
|
||||
tls: {
|
||||
rejectUnauthorized: false
|
||||
}
|
||||
});
|
||||
|
||||
await client.connect();
|
||||
|
||||
// Get folders for this account
|
||||
const folders = await getMailboxes(client);
|
||||
|
||||
// Close connection
|
||||
await client.logout();
|
||||
|
||||
// Cache the result
|
||||
accountFoldersCache.set(cacheKey, {
|
||||
folders,
|
||||
timestamp: now
|
||||
});
|
||||
|
||||
return folders;
|
||||
} catch (error) {
|
||||
console.error(`Error fetching folders for account ${account.email}:`, error);
|
||||
// Return fallback folders on error
|
||||
return ['INBOX', 'Sent', 'Drafts', 'Trash'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This endpoint is called when the app initializes to check if the user has email credentials
|
||||
* and to start prefetching email data in the background if they do
|
||||
@ -63,6 +119,7 @@ export async function GET() {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
password: true,
|
||||
host: true,
|
||||
port: true
|
||||
}
|
||||
@ -84,10 +141,17 @@ export async function GET() {
|
||||
// Cast the raw result to an array and get the first item
|
||||
const metadata = Array.isArray(rawAccount) ? rawAccount[0] : rawAccount;
|
||||
|
||||
// Get folders for this specific account
|
||||
const accountFolders = await getAccountFolders(account.id, {
|
||||
...account,
|
||||
...metadata
|
||||
});
|
||||
|
||||
return {
|
||||
...account,
|
||||
display_name: metadata?.display_name || account.email,
|
||||
color: metadata?.color || "#0082c9"
|
||||
color: metadata?.color || "#0082c9",
|
||||
folders: accountFolders
|
||||
};
|
||||
}));
|
||||
|
||||
@ -141,7 +205,7 @@ export async function GET() {
|
||||
});
|
||||
}
|
||||
|
||||
// Return all accounts information
|
||||
// Return all accounts information with their specific folders
|
||||
return NextResponse.json({
|
||||
authenticated: true,
|
||||
hasEmailCredentials: true,
|
||||
@ -150,13 +214,13 @@ export async function GET() {
|
||||
prefetchStarted,
|
||||
credentialsSource,
|
||||
lastVisit: cachedSession?.lastVisit,
|
||||
mailboxes: cachedSession?.mailboxes || [],
|
||||
mailboxes: cachedSession?.mailboxes || [], // For backward compatibility
|
||||
allAccounts: accountsWithMetadata.map(account => ({
|
||||
id: account.id,
|
||||
email: account.email,
|
||||
display_name: account.display_name || account.email,
|
||||
color: account.color || "#0082c9",
|
||||
folders: cachedSession?.mailboxes || [] // Add folders directly to each account
|
||||
folders: account.folders || [] // Use account-specific folders
|
||||
}))
|
||||
});
|
||||
} catch (dbError) {
|
||||
|
||||
@ -402,7 +402,13 @@ export default function CourrierPage() {
|
||||
// Also prefetch additional pages to make scrolling smoother
|
||||
if (session?.user?.id) {
|
||||
// Prefetch next 2 pages beyond the current next page
|
||||
prefetchFolderEmails(session.user.id, currentFolder, 2, nextPage + 1).catch(err => {
|
||||
prefetchFolderEmails(
|
||||
session.user.id,
|
||||
currentFolder,
|
||||
2,
|
||||
nextPage + 1,
|
||||
selectedAccount?.id
|
||||
).catch(err => {
|
||||
console.error(`Error prefetching additional pages for ${currentFolder}:`, err);
|
||||
});
|
||||
}
|
||||
@ -465,10 +471,16 @@ export default function CourrierPage() {
|
||||
};
|
||||
|
||||
// Handle mailbox change with prefetching
|
||||
const handleMailboxChange = (folder: string) => {
|
||||
const handleMailboxChange = (folder: string, accountId?: string) => {
|
||||
// Reset to page 1 when changing folders
|
||||
setPage(1);
|
||||
|
||||
// If we have a specific accountId, store it with the folder
|
||||
if (accountId) {
|
||||
// Store the current account ID with the folder change
|
||||
console.log(`Changing folder to ${folder} for account ${accountId}`);
|
||||
}
|
||||
|
||||
// Change folder in the state
|
||||
changeFolder(folder);
|
||||
setCurrentView(folder);
|
||||
@ -476,7 +488,7 @@ export default function CourrierPage() {
|
||||
// Start prefetching additional pages for this folder
|
||||
if (session?.user?.id && folder) {
|
||||
// First two pages are most important - prefetch immediately
|
||||
prefetchFolderEmails(session.user.id, folder, 3).catch(err => {
|
||||
prefetchFolderEmails(session.user.id, folder, 3, 1, accountId).catch(err => {
|
||||
console.error(`Error prefetching ${folder}:`, err);
|
||||
});
|
||||
}
|
||||
@ -795,23 +807,51 @@ export default function CourrierPage() {
|
||||
if (selectedAccount?.id === account.id) {
|
||||
setShowFolders(!showFolders);
|
||||
} else {
|
||||
// When selecting a new account, make sure we show its folders
|
||||
setShowFolders(true);
|
||||
|
||||
// Reset to the inbox folder of the new account by default
|
||||
if (account.folders && account.folders.length > 0) {
|
||||
// Find INBOX or default to first folder
|
||||
const inboxFolder = account.folders.find(f =>
|
||||
f.toLowerCase() === 'inbox') || account.folders[0];
|
||||
|
||||
// Change to this account's inbox folder
|
||||
handleMailboxChange(inboxFolder, account.id);
|
||||
}
|
||||
}
|
||||
|
||||
setSelectedAccount(account);
|
||||
|
||||
// Force the account to have folders if it doesn't already
|
||||
if (account.id !== 'all-accounts' && (!account.folders || account.folders.length === 0)) {
|
||||
const accountWithFolders = {
|
||||
...account,
|
||||
folders: ['INBOX', 'Sent', 'Drafts', 'Trash', 'Junk']
|
||||
};
|
||||
setSelectedAccount(accountWithFolders);
|
||||
|
||||
// Also update the account in the accounts array
|
||||
const newAccounts = accounts.map(a =>
|
||||
a.id === account.id ? accountWithFolders : a
|
||||
);
|
||||
setAccounts(newAccounts);
|
||||
// Fetch folders for this account if not already available
|
||||
fetch(`/api/courrier/account-folders?accountId=${account.id}`)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.folders && Array.isArray(data.folders)) {
|
||||
const accountWithFolders = {
|
||||
...account,
|
||||
folders: data.folders
|
||||
};
|
||||
setSelectedAccount(accountWithFolders);
|
||||
|
||||
// Also update the account in the accounts array
|
||||
const newAccounts = accounts.map(a =>
|
||||
a.id === account.id ? accountWithFolders : a
|
||||
);
|
||||
setAccounts(newAccounts);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(`Error fetching folders for account ${account.id}:`, err);
|
||||
// Use default folders if API fails
|
||||
const accountWithFolders = {
|
||||
...account,
|
||||
folders: ['INBOX', 'Sent', 'Drafts', 'Trash', 'Junk']
|
||||
};
|
||||
setSelectedAccount(accountWithFolders);
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
@ -842,7 +882,8 @@ export default function CourrierPage() {
|
||||
}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleMailboxChange(folder);
|
||||
// Pass the account ID along with the folder name
|
||||
handleMailboxChange(folder, selectedAccount?.id);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-between w-full gap-1.5">
|
||||
|
||||
@ -77,7 +77,7 @@ export const useCourrier = () => {
|
||||
const { toast } = useToast();
|
||||
|
||||
// Load emails from the server
|
||||
const loadEmails = useCallback(async (isLoadMore = false) => {
|
||||
const loadEmails = useCallback(async (isLoadMore = false, accountId?: string) => {
|
||||
if (!session?.user?.id) return;
|
||||
|
||||
setIsLoading(true);
|
||||
@ -87,6 +87,22 @@ export const useCourrier = () => {
|
||||
const currentRequestPage = page;
|
||||
|
||||
try {
|
||||
// Build query params
|
||||
const queryParams = new URLSearchParams({
|
||||
folder: currentFolder,
|
||||
page: currentRequestPage.toString(),
|
||||
perPage: perPage.toString()
|
||||
});
|
||||
|
||||
if (searchQuery) {
|
||||
queryParams.set('search', searchQuery);
|
||||
}
|
||||
|
||||
// Add accountId if provided
|
||||
if (accountId) {
|
||||
queryParams.set('accountId', accountId);
|
||||
}
|
||||
|
||||
// First try Redis cache with low timeout
|
||||
const cachedEmails = await getCachedEmailsWithTimeout(session.user.id, currentFolder, currentRequestPage, perPage, 100);
|
||||
if (cachedEmails) {
|
||||
@ -157,17 +173,6 @@ export const useCourrier = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Build query params
|
||||
const queryParams = new URLSearchParams({
|
||||
folder: currentFolder,
|
||||
page: currentRequestPage.toString(),
|
||||
perPage: perPage.toString()
|
||||
});
|
||||
|
||||
if (searchQuery) {
|
||||
queryParams.set('search', searchQuery);
|
||||
}
|
||||
|
||||
// Fetch emails from API
|
||||
const response = await fetch(`/api/courrier?${queryParams.toString()}`);
|
||||
|
||||
@ -513,12 +518,16 @@ export const useCourrier = () => {
|
||||
}
|
||||
}, [emails, selectedEmailIds]);
|
||||
|
||||
// Change the current folder
|
||||
const changeFolder = useCallback((folder: MailFolder) => {
|
||||
// Change folder and load emails
|
||||
const changeFolder = useCallback((folder: string, accountId?: string) => {
|
||||
setCurrentFolder(folder);
|
||||
setPage(1);
|
||||
setPage(1); // Reset to first page when changing folders
|
||||
setSelectedEmail(null);
|
||||
setSelectedEmailIds([]);
|
||||
|
||||
// Load the emails for this folder
|
||||
// The loadEmails function will be called by the useEffect above
|
||||
// due to the dependency on currentFolder
|
||||
}, []);
|
||||
|
||||
// Search emails
|
||||
|
||||
@ -234,20 +234,64 @@ export async function getEmails(
|
||||
folder: string = 'INBOX',
|
||||
page: number = 1,
|
||||
perPage: number = 20,
|
||||
searchQuery: string = ''
|
||||
searchQuery: string = '',
|
||||
accountId?: string
|
||||
): Promise<EmailListResult> {
|
||||
// Try to get from cache first
|
||||
if (!searchQuery) {
|
||||
const cacheKey = accountId ? `${userId}:${accountId}:${folder}` : `${userId}:${folder}`;
|
||||
const cachedResult = await getCachedEmailList(userId, folder, page, perPage);
|
||||
if (cachedResult) {
|
||||
console.log(`Using cached email list for ${userId}:${folder}:${page}:${perPage}`);
|
||||
console.log(`Using cached email list for ${cacheKey}:${page}:${perPage}`);
|
||||
return cachedResult;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Cache miss for emails ${userId}:${folder}:${page}:${perPage}, fetching from IMAP`);
|
||||
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.findUnique({
|
||||
where: {
|
||||
id: accountId,
|
||||
userId
|
||||
}
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
const client = await getImapConnection(userId);
|
||||
let mailboxes: string[] = [];
|
||||
|
||||
try {
|
||||
|
||||
@ -226,9 +226,10 @@ export async function prefetchFolderEmails(
|
||||
userId: string,
|
||||
folder: string,
|
||||
pages: number = 3,
|
||||
startPage: number = 1
|
||||
startPage: number = 1,
|
||||
accountId?: string
|
||||
): Promise<void> {
|
||||
const prefetchKey = `folder:${folder}:${startPage}`;
|
||||
const prefetchKey = `folder:${folder}:${startPage}${accountId ? `:${accountId}` : ''}`;
|
||||
|
||||
// Skip if already in progress or in cooldown
|
||||
if (!shouldPrefetch(userId, prefetchKey)) {
|
||||
@ -236,7 +237,7 @@ export async function prefetchFolderEmails(
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`Prefetching ${pages} pages of emails for folder ${folder} starting from page ${startPage}`);
|
||||
console.log(`Prefetching ${pages} pages of emails for folder ${folder} starting from page ${startPage}${accountId ? ` for account ${accountId}` : ''}`);
|
||||
|
||||
// Calculate the range of pages to prefetch
|
||||
const pagesToFetch = Array.from(
|
||||
|
||||
Loading…
Reference in New Issue
Block a user