courrier multi account

This commit is contained in:
alma 2025-04-27 16:52:21 +02:00
parent 9797e08533
commit 3d69698964
6 changed files with 377 additions and 80 deletions

View 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);
}

View File

@ -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({

View File

@ -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
View File

@ -0,0 +1 @@
SELECT * FROM "MailCredentials" LIMIT 5;

View File

@ -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,

View File

@ -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