courrier multi account restore compose

This commit is contained in:
alma 2025-04-28 13:52:42 +02:00
parent 0fbc339447
commit bd8b39ad9b
2 changed files with 141 additions and 86 deletions

View File

@ -1,11 +1,9 @@
import { NextResponse } from 'next/server'; import { NextResponse } from 'next/server';
import { getServerSession } from 'next-auth'; import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth'; import { authOptions } from '@/lib/auth';
import { getMailboxes } from '@/lib/services/email-service'; import { EmailService } from '@/lib/services/email-service';
import { getRedisClient } from '@/lib/redis';
import { getImapConnection } from '@/lib/services/email-service';
import { MailCredentials } from '@prisma/client';
import { prisma } from '@/lib/prisma'; import { prisma } from '@/lib/prisma';
import { MailCredentials } from '@prisma/client';
// Keep track of last prefetch time for each user // Keep track of last prefetch time for each user
const lastPrefetchMap = new Map<string, number>(); const lastPrefetchMap = new Map<string, number>();
@ -23,54 +21,37 @@ const FOLDERS_CACHE_KEY = (userId: string, accountId: string) => `email:folders:
*/ */
export async function GET() { export async function GET() {
try { try {
// Get session with detailed logging // Authentication check
console.log('Attempting to get server session...');
const session = await getServerSession(authOptions); const session = await getServerSession(authOptions);
if (!session?.user?.id) {
if (!session) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
console.error('No session found');
return NextResponse.json({ error: 'No session found' }, { status: 401 });
}
if (!session.user) {
console.error('No user in session');
return NextResponse.json({ error: 'No user in session' }, { status: 401 });
}
if (!session.user.id) {
console.error('No user ID in session');
return NextResponse.json({ error: 'No user ID in session' }, { status: 401 });
}
console.log('Session validated successfully:', {
userId: session.user.id,
email: session.user.email,
name: session.user.name
});
// Get Redis connection
const redis = getRedisClient();
if (!redis) {
console.error('Redis connection failed');
return NextResponse.json({ error: 'Redis connection failed' }, { status: 500 });
} }
// Get user with their accounts // Get user with their accounts
console.log('Fetching user with ID:', session.user.id);
const user = await prisma.user.findUnique({ const user = await prisma.user.findUnique({
where: { id: session.user.id }, where: { id: session.user.id },
include: { mailCredentials: true } include: {
mailCredentials: {
select: {
id: true,
email: true,
password: true,
host: true,
port: true,
userId: true,
createdAt: true,
updatedAt: true
}
}
}
}); });
if (!user) { if (!user) {
console.error('User not found in database');
return NextResponse.json({ error: 'User not found' }, { status: 404 }); return NextResponse.json({ error: 'User not found' }, { status: 404 });
} }
// Get all accounts for the user const accounts = Array.isArray(user.mailCredentials) ? user.mailCredentials : [];
const accounts = (user.mailCredentials || []) as MailCredentials[];
if (accounts.length === 0) { if (accounts.length === 0) {
console.log('No email accounts found for user:', session.user.id);
return NextResponse.json({ return NextResponse.json({
authenticated: true, authenticated: true,
accounts: [], accounts: [],
@ -78,54 +59,20 @@ export async function GET() {
}); });
} }
console.log(`Found ${accounts.length} accounts for user:`, accounts.map(a => a.email)); // Get folders for each account using EmailService
const emailService = EmailService.getInstance();
// Fetch folders for each account
const accountsWithFolders = await Promise.all( const accountsWithFolders = await Promise.all(
accounts.map(async (account: MailCredentials) => { accounts.map(async (account: MailCredentials) => {
const cacheKey = FOLDERS_CACHE_KEY(user.id, account.id); const folders = await emailService.getFolders(user.id, {
// Try to get folders from Redis cache first ...account,
const cachedFolders = await redis.get(cacheKey); imapHost: account.host,
if (cachedFolders) { imapPort: account.port,
console.log(`Using cached folders for account ${account.email}`); imapSecure: true
return { });
...account, return {
folders: JSON.parse(cachedFolders) ...account,
}; folders
} };
// If not in cache, fetch from IMAP
console.log(`Fetching folders from IMAP for account ${account.email}`);
const client = await getImapConnection(user.id, account.id);
if (!client) {
console.warn(`Failed to get IMAP connection for account ${account.email}`);
return {
...account,
folders: ['INBOX', 'Sent', 'Drafts', 'Trash', 'Junk']
};
}
try {
const folders = await getMailboxes(client);
console.log(`Fetched ${folders.length} folders for account ${account.email}`);
// Cache the folders in Redis
await redis.set(
cacheKey,
JSON.stringify(folders),
'EX',
FOLDERS_CACHE_TTL
);
return {
...account,
folders
};
} catch (error) {
console.error(`Error fetching folders for account ${account.id}:`, error);
return {
...account,
folders: ['INBOX', 'Sent', 'Drafts', 'Trash', 'Junk']
};
}
}) })
); );
@ -136,7 +83,7 @@ export async function GET() {
} catch (error) { } catch (error) {
console.error('Error in session route:', error); console.error('Error in session route:', error);
return NextResponse.json( return NextResponse.json(
{ error: 'Internal server error', details: error instanceof Error ? error.message : 'Unknown error' }, { error: 'Internal server error' },
{ status: 500 } { status: 500 }
); );
} }

View File

@ -18,6 +18,8 @@ import {
invalidateEmailContentCache invalidateEmailContentCache
} from '@/lib/redis'; } from '@/lib/redis';
import { EmailCredentials, EmailMessage, EmailAddress, EmailAttachment } from '@/lib/types'; import { EmailCredentials, EmailMessage, EmailAddress, EmailAttachment } from '@/lib/types';
import { MailCredentials } from '@prisma/client';
import { getRedisClient } from '@/lib/redis';
// Types specific to this service // Types specific to this service
export interface EmailListResult { export interface EmailListResult {
@ -30,6 +32,112 @@ export interface EmailListResult {
mailboxes: string[]; mailboxes: string[];
} }
const FOLDERS_CACHE_TTL = 3600; // 1 hour
const FOLDERS_CACHE_KEY = (userId: string, accountId: string) => `email:folders:${userId}:${accountId}`;
interface ExtendedMailCredentials extends MailCredentials {
imapHost: string;
imapPort: number;
imapSecure: boolean;
}
export class EmailService {
private static instance: EmailService;
private connections: Map<string, ImapFlow> = new Map();
private constructor() {}
public static getInstance(): EmailService {
if (!EmailService.instance) {
EmailService.instance = new EmailService();
}
return EmailService.instance;
}
private getConnectionKey(userId: string, accountId: string): string {
return `${userId}:${accountId}`;
}
public async getConnection(userId: string, account: ExtendedMailCredentials): Promise<ImapFlow | null> {
const key = this.getConnectionKey(userId, account.id);
if (this.connections.has(key)) {
return this.connections.get(key)!;
}
try {
const client = new ImapFlow({
host: account.imapHost,
port: account.imapPort,
secure: account.imapSecure,
auth: {
user: account.email,
pass: account.password
},
logger: false
});
await client.connect();
this.connections.set(key, client);
return client;
} catch (error) {
console.error(`Failed to create IMAP connection for ${account.email}:`, error);
return null;
}
}
public async getFolders(userId: string, account: ExtendedMailCredentials): Promise<string[]> {
const redis = getRedisClient();
const cacheKey = FOLDERS_CACHE_KEY(userId, account.id);
// Try cache first
if (redis) {
const cachedFolders = await redis.get(cacheKey);
if (cachedFolders) {
return JSON.parse(cachedFolders);
}
}
// Fetch from IMAP
const client = await this.getConnection(userId, account);
if (!client) {
return ['INBOX', 'Sent', 'Drafts', 'Trash', 'Junk'];
}
try {
const folders = await this.fetchMailboxes(client);
// Cache the result
if (redis) {
await redis.set(cacheKey, JSON.stringify(folders), 'EX', FOLDERS_CACHE_TTL);
}
return folders;
} catch (error) {
console.error(`Error fetching folders for ${account.email}:`, error);
return ['INBOX', 'Sent', 'Drafts', 'Trash', 'Junk'];
}
}
private async fetchMailboxes(client: ImapFlow): Promise<string[]> {
const folders: string[] = [];
const mailboxes = await client.list();
for (const mailbox of mailboxes) {
folders.push(mailbox.path);
}
return folders;
}
public async closeConnection(userId: string, accountId: string): Promise<void> {
const key = this.getConnectionKey(userId, accountId);
const client = this.connections.get(key);
if (client) {
await client.logout();
this.connections.delete(key);
}
}
}
// Connection pool to reuse IMAP clients // Connection pool to reuse IMAP clients
const connectionPool: Record<string, { client: ImapFlow; lastUsed: number }> = {}; const connectionPool: Record<string, { client: ImapFlow; lastUsed: number }> = {};
const CONNECTION_TIMEOUT = 5 * 60 * 1000; // 5 minutes const CONNECTION_TIMEOUT = 5 * 60 * 1000; // 5 minutes