courrier multi account
This commit is contained in:
parent
9797e08533
commit
3d69698964
167
app/api/courrier/debug-account/route.ts
Normal file
167
app/api/courrier/debug-account/route.ts
Normal file
@ -0,0 +1,167 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getServerSession } from 'next-auth';
|
||||
import { authOptions } from '@/app/api/auth/[...nextauth]/route';
|
||||
import { getCachedEmailCredentials, getCachedImapSession } from '@/lib/redis';
|
||||
import { prisma } from '@/lib/prisma';
|
||||
import { getMailboxes } from '@/lib/services/email-service';
|
||||
import { ImapFlow } from 'imapflow';
|
||||
|
||||
export async function GET() {
|
||||
// Verify auth
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Unauthorized' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
const userId = session.user.id;
|
||||
const debugData: any = {
|
||||
userId,
|
||||
timestamp: new Date().toISOString(),
|
||||
redis: {
|
||||
emailCredentials: null,
|
||||
session: null
|
||||
},
|
||||
database: {
|
||||
accounts: []
|
||||
},
|
||||
imap: {
|
||||
connectionAttempt: false,
|
||||
connected: false,
|
||||
folders: []
|
||||
}
|
||||
};
|
||||
|
||||
// Check Redis cache for credentials
|
||||
try {
|
||||
const credentials = await getCachedEmailCredentials(userId);
|
||||
if (credentials) {
|
||||
debugData.redis.emailCredentials = {
|
||||
found: true,
|
||||
email: credentials.email,
|
||||
host: credentials.host,
|
||||
port: credentials.port,
|
||||
hasPassword: !!credentials.password,
|
||||
hasSmtp: !!credentials.smtp_host
|
||||
};
|
||||
} else {
|
||||
debugData.redis.emailCredentials = { found: false };
|
||||
}
|
||||
} catch (e) {
|
||||
debugData.redis.emailCredentials = {
|
||||
error: e instanceof Error ? e.message : 'Unknown error'
|
||||
};
|
||||
}
|
||||
|
||||
// Check Redis for session data (which contains folders)
|
||||
try {
|
||||
const sessionData = await getCachedImapSession(userId);
|
||||
if (sessionData) {
|
||||
debugData.redis.session = {
|
||||
found: true,
|
||||
lastActive: new Date(sessionData.lastActive).toISOString(),
|
||||
hasFolders: !!sessionData.mailboxes,
|
||||
folderCount: sessionData.mailboxes?.length || 0,
|
||||
folders: sessionData.mailboxes || []
|
||||
};
|
||||
} else {
|
||||
debugData.redis.session = { found: false };
|
||||
}
|
||||
} catch (e) {
|
||||
debugData.redis.session = {
|
||||
error: e instanceof Error ? e.message : 'Unknown error'
|
||||
};
|
||||
}
|
||||
|
||||
// Check database for accounts
|
||||
try {
|
||||
const accounts = await prisma.mailCredentials.findMany({
|
||||
where: { userId },
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
host: true,
|
||||
port: true
|
||||
}
|
||||
});
|
||||
|
||||
// Also try to get additional fields from raw query
|
||||
const accountsWithMetadata = await Promise.all(accounts.map(async (account) => {
|
||||
try {
|
||||
const rawAccount = await prisma.$queryRaw`
|
||||
SELECT display_name, color, smtp_host, smtp_port, smtp_secure, secure
|
||||
FROM "MailCredentials"
|
||||
WHERE id = ${account.id}
|
||||
`;
|
||||
|
||||
const metadata = Array.isArray(rawAccount) && rawAccount.length > 0
|
||||
? rawAccount[0]
|
||||
: {};
|
||||
|
||||
return {
|
||||
...account,
|
||||
display_name: metadata.display_name,
|
||||
color: metadata.color,
|
||||
smtp_host: metadata.smtp_host,
|
||||
smtp_port: metadata.smtp_port,
|
||||
smtp_secure: metadata.smtp_secure,
|
||||
secure: metadata.secure
|
||||
};
|
||||
} catch (e) {
|
||||
return {
|
||||
...account,
|
||||
_queryError: e instanceof Error ? e.message : 'Unknown error'
|
||||
};
|
||||
}
|
||||
}));
|
||||
|
||||
debugData.database.accounts = accountsWithMetadata;
|
||||
debugData.database.accountCount = accounts.length;
|
||||
} catch (e) {
|
||||
debugData.database.error = e instanceof Error ? e.message : 'Unknown error';
|
||||
}
|
||||
|
||||
// Try to get IMAP folders for the main account
|
||||
if (debugData.redis.emailCredentials?.found || debugData.database.accountCount > 0) {
|
||||
try {
|
||||
debugData.imap.connectionAttempt = true;
|
||||
|
||||
// Use cached credentials
|
||||
const credentials = await getCachedEmailCredentials(userId);
|
||||
|
||||
if (credentials && credentials.email && credentials.password) {
|
||||
const client = new ImapFlow({
|
||||
host: credentials.host,
|
||||
port: credentials.port,
|
||||
secure: true,
|
||||
auth: {
|
||||
user: credentials.email,
|
||||
pass: credentials.password,
|
||||
},
|
||||
logger: false,
|
||||
tls: {
|
||||
rejectUnauthorized: false
|
||||
}
|
||||
});
|
||||
|
||||
await client.connect();
|
||||
debugData.imap.connected = true;
|
||||
|
||||
// Get folders
|
||||
const folders = await getMailboxes(client);
|
||||
debugData.imap.folders = folders;
|
||||
|
||||
// Close connection
|
||||
await client.logout();
|
||||
} else {
|
||||
debugData.imap.error = "No valid credentials found";
|
||||
}
|
||||
} catch (e) {
|
||||
debugData.imap.error = e instanceof Error ? e.message : 'Unknown error';
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json(debugData);
|
||||
}
|
||||
@ -4,6 +4,7 @@ import { authOptions } from '@/app/api/auth/[...nextauth]/route';
|
||||
import { getUserEmailCredentials } 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';
|
||||
|
||||
// Keep track of last prefetch time for each user
|
||||
const lastPrefetchMap = new Map<string, number>();
|
||||
@ -42,66 +43,131 @@ export async function GET() {
|
||||
|
||||
// Check if we have a cached session
|
||||
const cachedSession = await getCachedImapSession(userId);
|
||||
console.log(`[DEBUG] Cached session for user ${userId}:`,
|
||||
cachedSession ? {
|
||||
hasMailboxes: Array.isArray(cachedSession.mailboxes),
|
||||
mailboxCount: cachedSession.mailboxes?.length || 0
|
||||
} : 'null');
|
||||
|
||||
// First, check Redis cache for credentials
|
||||
// First, check Redis cache for credentials - this is for backward compatibility
|
||||
let credentials = await getCachedEmailCredentials(userId);
|
||||
console.log(`[DEBUG] Cached credentials for user ${userId}:`,
|
||||
credentials ? { email: credentials.email, hasPassword: !!credentials.password } : 'null');
|
||||
let credentialsSource = 'cache';
|
||||
|
||||
// If not in cache, check database
|
||||
if (!credentials) {
|
||||
credentials = await getUserEmailCredentials(userId);
|
||||
credentialsSource = 'database';
|
||||
}
|
||||
// Now fetch all email accounts for this user
|
||||
// Query the database directly using Prisma
|
||||
try {
|
||||
const allAccounts = await prisma.mailCredentials.findMany({
|
||||
where: { userId },
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
host: true,
|
||||
port: true
|
||||
}
|
||||
});
|
||||
console.log(`[DEBUG] Found ${allAccounts.length} accounts in database for user ${userId}:`);
|
||||
allAccounts.forEach((account, idx) => {
|
||||
console.log(`[DEBUG] Account ${idx + 1}: ID=${account.id}, Email=${account.email}`);
|
||||
});
|
||||
|
||||
// If no credentials found
|
||||
if (!credentials) {
|
||||
// Get additional fields that might be in the database but not in the type
|
||||
const accountsWithMetadata = await Promise.all(allAccounts.map(async (account) => {
|
||||
// Get the raw account data to access fields not in the type
|
||||
const rawAccount = await prisma.$queryRaw`
|
||||
SELECT display_name, color
|
||||
FROM "MailCredentials"
|
||||
WHERE id = ${account.id}
|
||||
`;
|
||||
|
||||
// Cast the raw result to an array and get the first item
|
||||
const metadata = Array.isArray(rawAccount) ? rawAccount[0] : rawAccount;
|
||||
|
||||
return {
|
||||
...account,
|
||||
display_name: metadata?.display_name || account.email,
|
||||
color: metadata?.color || "#0082c9"
|
||||
};
|
||||
}));
|
||||
|
||||
console.log(`Found ${allAccounts.length} email accounts for user ${userId}`);
|
||||
|
||||
// If not in cache and no accounts found, check database for single account (backward compatibility)
|
||||
if (!credentials && allAccounts.length === 0) {
|
||||
credentials = await getUserEmailCredentials(userId);
|
||||
credentialsSource = 'database';
|
||||
}
|
||||
|
||||
// If no credentials found
|
||||
if (!credentials && allAccounts.length === 0) {
|
||||
return NextResponse.json({
|
||||
authenticated: true,
|
||||
hasEmailCredentials: false,
|
||||
redisStatus,
|
||||
message: "No email credentials found"
|
||||
});
|
||||
}
|
||||
|
||||
let prefetchStarted = false;
|
||||
|
||||
// Only prefetch if the cooldown period has elapsed
|
||||
if (shouldPrefetch) {
|
||||
// Update the last prefetch time
|
||||
lastPrefetchMap.set(userId, now);
|
||||
|
||||
// Start prefetching email data in the background
|
||||
// We don't await this to avoid blocking the response
|
||||
prefetchUserEmailData(userId).catch(err => {
|
||||
console.error('Background prefetch error:', err);
|
||||
});
|
||||
|
||||
prefetchStarted = true;
|
||||
} else {
|
||||
console.log(`Skipping prefetch for ${userId}, last prefetch was ${Math.round((now - lastPrefetchTime)/1000)}s ago`);
|
||||
}
|
||||
|
||||
// Store last visit time in session data
|
||||
if (cachedSession) {
|
||||
await cacheImapSession(userId, {
|
||||
...cachedSession,
|
||||
lastActive: cachedSession.lastActive || Date.now(), // Ensure lastActive is set
|
||||
lastVisit: now
|
||||
});
|
||||
} else {
|
||||
await cacheImapSession(userId, {
|
||||
lastActive: Date.now(),
|
||||
lastVisit: now
|
||||
});
|
||||
}
|
||||
|
||||
// Return all accounts information
|
||||
return NextResponse.json({
|
||||
authenticated: true,
|
||||
hasEmailCredentials: true,
|
||||
email: credentials?.email || (allAccounts.length > 0 ? allAccounts[0].email : ''),
|
||||
redisStatus,
|
||||
prefetchStarted,
|
||||
credentialsSource,
|
||||
lastVisit: cachedSession?.lastVisit,
|
||||
mailboxes: cachedSession?.mailboxes || [],
|
||||
allAccounts: accountsWithMetadata.map(account => ({
|
||||
id: account.id,
|
||||
email: account.email,
|
||||
display_name: account.display_name || account.email,
|
||||
color: account.color || "#0082c9"
|
||||
}))
|
||||
});
|
||||
} catch (dbError) {
|
||||
console.error(`[ERROR] Database query failed:`, dbError);
|
||||
return NextResponse.json({
|
||||
authenticated: true,
|
||||
hasEmailCredentials: false,
|
||||
redisStatus,
|
||||
message: "No email credentials found"
|
||||
});
|
||||
error: "Database query failed",
|
||||
details: dbError instanceof Error ? dbError.message : "Unknown database error",
|
||||
redisStatus
|
||||
}, { status: 500 });
|
||||
}
|
||||
|
||||
let prefetchStarted = false;
|
||||
|
||||
// Only prefetch if the cooldown period has elapsed
|
||||
if (shouldPrefetch) {
|
||||
// Update the last prefetch time
|
||||
lastPrefetchMap.set(userId, now);
|
||||
|
||||
// Start prefetching email data in the background
|
||||
// We don't await this to avoid blocking the response
|
||||
prefetchUserEmailData(userId).catch(err => {
|
||||
console.error('Background prefetch error:', err);
|
||||
});
|
||||
|
||||
prefetchStarted = true;
|
||||
} else {
|
||||
console.log(`Skipping prefetch for ${userId}, last prefetch was ${Math.round((now - lastPrefetchTime)/1000)}s ago`);
|
||||
}
|
||||
|
||||
// Store last visit time in session data
|
||||
if (cachedSession) {
|
||||
await cacheImapSession(userId, {
|
||||
...cachedSession,
|
||||
lastVisit: now
|
||||
});
|
||||
} else {
|
||||
await cacheImapSession(userId, { lastVisit: now });
|
||||
}
|
||||
|
||||
// Return session info without sensitive data
|
||||
return NextResponse.json({
|
||||
authenticated: true,
|
||||
hasEmailCredentials: true,
|
||||
email: credentials.email,
|
||||
redisStatus,
|
||||
prefetchStarted,
|
||||
credentialsSource,
|
||||
lastVisit: cachedSession?.lastVisit,
|
||||
mailboxes: cachedSession?.mailboxes || []
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error checking session:", error);
|
||||
return NextResponse.json({
|
||||
|
||||
@ -127,22 +127,46 @@ export default function CourrierPage() {
|
||||
|
||||
// Update account folders when mailboxes change
|
||||
useEffect(() => {
|
||||
console.log('Mailboxes updated:', mailboxes);
|
||||
setAccounts(prev => {
|
||||
const updated = [...prev];
|
||||
if (updated[1]) {
|
||||
updated[1].folders = mailboxes;
|
||||
}
|
||||
console.log('Updated accounts with new mailboxes:', updated);
|
||||
return updated;
|
||||
});
|
||||
}, [mailboxes]);
|
||||
|
||||
// Debug accounts state
|
||||
useEffect(() => {
|
||||
console.log('Current accounts state:', accounts);
|
||||
}, [accounts]);
|
||||
|
||||
// Calculate unread count (this would be replaced with actual data in production)
|
||||
useEffect(() => {
|
||||
// Example: counting unread emails in the inbox
|
||||
const unreadInInbox = (emails || []).filter(email => !email.read && currentFolder === 'INBOX').length;
|
||||
const unreadInInbox = (emails || []).filter(email => {
|
||||
// Access the 'read' property safely, handling both old and new email formats
|
||||
return (!email.read && email.read !== undefined) ||
|
||||
(email.flags && !email.flags.seen) ||
|
||||
false;
|
||||
}).filter(email => currentFolder === 'INBOX').length;
|
||||
setUnreadCount(unreadInInbox);
|
||||
}, [emails, currentFolder]);
|
||||
|
||||
// Ensure accounts section is never empty
|
||||
useEffect(() => {
|
||||
// If accounts array becomes empty (bug), restore default accounts
|
||||
if (!accounts || accounts.length === 0) {
|
||||
console.warn('Accounts array is empty, restoring defaults');
|
||||
setAccounts([
|
||||
{ id: 0, name: 'All', email: '', color: 'bg-gray-500' },
|
||||
{ id: 1, name: 'Loading...', email: '', color: 'bg-blue-500', folders: mailboxes }
|
||||
]);
|
||||
}
|
||||
}, [accounts, mailboxes]);
|
||||
|
||||
// Initialize session and start prefetching
|
||||
useEffect(() => {
|
||||
// Flag to prevent multiple initialization attempts
|
||||
@ -167,6 +191,18 @@ export default function CourrierPage() {
|
||||
const response = await fetch('/api/courrier/session');
|
||||
const data = await response.json();
|
||||
|
||||
console.log('[DEBUG] Session API response:', {
|
||||
authenticated: data.authenticated,
|
||||
hasEmailCredentials: data.hasEmailCredentials,
|
||||
email: data.email,
|
||||
allAccountsExists: !!data.allAccounts,
|
||||
allAccountsIsArray: Array.isArray(data.allAccounts),
|
||||
allAccountsLength: data.allAccounts?.length || 0,
|
||||
mailboxesExists: !!data.mailboxes,
|
||||
mailboxesIsArray: Array.isArray(data.mailboxes),
|
||||
mailboxesLength: data.mailboxes?.length || 0
|
||||
});
|
||||
|
||||
if (!isMounted) return;
|
||||
|
||||
if (data.authenticated) {
|
||||
@ -174,20 +210,45 @@ export default function CourrierPage() {
|
||||
console.log('Session initialized, prefetch status:', data.prefetchStarted ? 'running' : 'not started');
|
||||
setPrefetchStarted(Boolean(data.prefetchStarted));
|
||||
|
||||
// Update the accounts with the actual email address
|
||||
if (data.email) {
|
||||
setAccounts(prev => {
|
||||
const updated = [...prev];
|
||||
updated[1] = {
|
||||
...updated[1],
|
||||
name: data.email,
|
||||
email: data.email,
|
||||
folders: data.mailboxes || mailboxes
|
||||
// Update accounts with the default email as fallback
|
||||
const updatedAccounts = [
|
||||
{ id: 0, name: 'All', email: '', color: 'bg-gray-500' }
|
||||
];
|
||||
|
||||
// Check if we have multiple accounts returned
|
||||
if (data.allAccounts && Array.isArray(data.allAccounts) && data.allAccounts.length > 0) {
|
||||
console.log('Multiple accounts found:', data.allAccounts.length);
|
||||
|
||||
// Add each account from the server
|
||||
data.allAccounts.forEach((account, index) => {
|
||||
console.log(`[DEBUG] Processing account: ${account.email}, display_name: ${account.display_name}, has folders: ${!!data.mailboxes}`);
|
||||
|
||||
const accountWithFolders = {
|
||||
id: account.id || index + 1,
|
||||
name: account.display_name || account.email,
|
||||
email: account.email,
|
||||
color: account.color || 'bg-blue-500',
|
||||
folders: data.mailboxes || []
|
||||
};
|
||||
return updated;
|
||||
console.log(`[DEBUG] Adding account with ${accountWithFolders.folders.length} folders:`, accountWithFolders.folders);
|
||||
updatedAccounts.push(accountWithFolders);
|
||||
});
|
||||
} else if (data.email) {
|
||||
// Fallback to single account if allAccounts is not available
|
||||
console.log(`[DEBUG] Fallback to single account: ${data.email}`);
|
||||
|
||||
updatedAccounts.push({
|
||||
id: 1,
|
||||
name: data.email,
|
||||
email: data.email,
|
||||
color: 'bg-blue-500',
|
||||
folders: data.mailboxes || []
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Setting accounts:', updatedAccounts);
|
||||
setAccounts(updatedAccounts);
|
||||
|
||||
// Preload first page of emails for faster initial rendering
|
||||
if (session?.user?.id) {
|
||||
await loadEmails();
|
||||
|
||||
1
db_query.sql
Normal file
1
db_query.sql
Normal file
@ -0,0 +1 @@
|
||||
SELECT * FROM "MailCredentials" LIMIT 5;
|
||||
@ -248,8 +248,26 @@ export async function getEmails(
|
||||
console.log(`Cache miss for emails ${userId}:${folder}:${page}:${perPage}, fetching from IMAP`);
|
||||
|
||||
const client = await getImapConnection(userId);
|
||||
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);
|
||||
}
|
||||
|
||||
// Open mailbox
|
||||
const mailboxData = await client.mailboxOpen(folder);
|
||||
const totalMessages = mailboxData.exists;
|
||||
@ -262,14 +280,6 @@ export async function getEmails(
|
||||
|
||||
// Empty result if no messages
|
||||
if (totalMessages === 0 || from > to) {
|
||||
const mailboxes = await getMailboxes(client);
|
||||
|
||||
// Cache mailbox list in session data
|
||||
await cacheImapSession(userId, {
|
||||
lastActive: Date.now(),
|
||||
mailboxes
|
||||
});
|
||||
|
||||
const result = {
|
||||
emails: [],
|
||||
totalEmails: 0,
|
||||
@ -414,14 +424,6 @@ export async function getEmails(
|
||||
// Sort by date, newest first
|
||||
emails.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
|
||||
|
||||
const mailboxes = await getMailboxes(client);
|
||||
|
||||
// Cache mailbox list in session data
|
||||
await cacheImapSession(userId, {
|
||||
lastActive: Date.now(),
|
||||
mailboxes
|
||||
});
|
||||
|
||||
const result = {
|
||||
emails,
|
||||
totalEmails: totalMessages,
|
||||
|
||||
@ -19,7 +19,7 @@ model User {
|
||||
updatedAt DateTime @updatedAt
|
||||
calendars Calendar[]
|
||||
events Event[]
|
||||
mailCredentials MailCredentials?
|
||||
mailCredentials MailCredentials[]
|
||||
webdavCredentials WebDAVCredentials?
|
||||
}
|
||||
|
||||
@ -58,7 +58,7 @@ model Event {
|
||||
|
||||
model MailCredentials {
|
||||
id String @id @default(uuid())
|
||||
userId String @unique
|
||||
userId String
|
||||
email String
|
||||
password String
|
||||
host String
|
||||
|
||||
Loading…
Reference in New Issue
Block a user