panel 2 courier api restore
This commit is contained in:
parent
079d0a484b
commit
d73bf3b773
@ -1,11 +1,9 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import { ImapFlow } from 'imapflow';
|
|
||||||
import { getServerSession } from 'next-auth';
|
import { getServerSession } from 'next-auth';
|
||||||
import { authOptions } from '@/app/api/auth/[...nextauth]/route';
|
import { authOptions } from '@/app/api/auth/[...nextauth]/route';
|
||||||
import { prisma } from '@/lib/prisma';
|
import { markEmailReadStatus } from '@/lib/services/email-service';
|
||||||
|
|
||||||
// Get the email list cache from main API route
|
// Global cache reference (will be moved to a proper cache solution in the future)
|
||||||
// This is a hack - ideally we'd use a shared module or Redis for caching
|
|
||||||
declare global {
|
declare global {
|
||||||
var emailListCache: { [key: string]: { data: any, timestamp: number } };
|
var emailListCache: { [key: string]: { data: any, timestamp: number } };
|
||||||
}
|
}
|
||||||
@ -47,79 +45,42 @@ export async function POST(
|
|||||||
return NextResponse.json({ error: 'Email ID is required' }, { status: 400 });
|
return NextResponse.json({ error: 'Email ID is required' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get credentials from database
|
|
||||||
const credentials = await prisma.mailCredentials.findUnique({
|
|
||||||
where: { userId: session.user.id },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!credentials) {
|
|
||||||
return NextResponse.json({ error: 'No mail credentials found' }, { status: 401 });
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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,
|
|
||||||
emitLogs: false,
|
|
||||||
tls: {
|
|
||||||
rejectUnauthorized: false
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await client.connect();
|
// Use the email service to mark the email as read
|
||||||
|
// First try with INBOX folder
|
||||||
|
let success = await markEmailReadStatus(session.user.id, emailId, true, 'INBOX');
|
||||||
|
|
||||||
// Find which folder contains this email
|
// If not found in INBOX, try to find it in other common folders
|
||||||
const mailboxes = await client.list();
|
if (!success) {
|
||||||
let emailFolder = 'INBOX'; // Default to INBOX
|
const commonFolders = ['Sent', 'Drafts', 'Trash', 'Spam', 'Junk'];
|
||||||
let foundEmail = false;
|
|
||||||
|
for (const folder of commonFolders) {
|
||||||
// Search through folders to find the email
|
success = await markEmailReadStatus(session.user.id, emailId, true, folder);
|
||||||
for (const box of mailboxes) {
|
if (success) {
|
||||||
try {
|
// If found in a different folder, invalidate that folder's cache
|
||||||
await client.mailboxOpen(box.path);
|
invalidateCache(session.user.id, folder);
|
||||||
|
|
||||||
// Search for the email by UID
|
|
||||||
const message = await client.fetchOne(emailId, { flags: true });
|
|
||||||
if (message) {
|
|
||||||
emailFolder = box.path;
|
|
||||||
foundEmail = true;
|
|
||||||
|
|
||||||
// Mark as read if not already
|
|
||||||
if (!message.flags.has('\\Seen')) {
|
|
||||||
await client.messageFlagsAdd(emailId, ['\\Seen']);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.log(`Error searching in folder ${box.path}:`, error);
|
|
||||||
// Continue with next folder
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Email found in INBOX, invalidate INBOX cache
|
||||||
|
invalidateCache(session.user.id, 'INBOX');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!foundEmail) {
|
if (!success) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: 'Email not found' },
|
{ error: 'Email not found in any folder' },
|
||||||
{ status: 404 }
|
{ status: 404 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Invalidate the cache for this folder
|
|
||||||
invalidateCache(session.user.id, emailFolder);
|
|
||||||
|
|
||||||
return NextResponse.json({ success: true });
|
return NextResponse.json({ success: true });
|
||||||
} finally {
|
} catch (error) {
|
||||||
try {
|
console.error('Error marking email as read:', error);
|
||||||
await client.logout();
|
return NextResponse.json(
|
||||||
} catch (e) {
|
{ error: 'Failed to mark email as read' },
|
||||||
console.error('Error during logout:', e);
|
{ status: 500 }
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error marking email as read:', error);
|
console.error('Error marking email as read:', error);
|
||||||
|
|||||||
@ -7,14 +7,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import { ImapFlow } from 'imapflow';
|
|
||||||
import { getServerSession } from 'next-auth';
|
import { getServerSession } from 'next-auth';
|
||||||
import { authOptions } from '@/app/api/auth/[...nextauth]/route';
|
import { authOptions } from '@/app/api/auth/[...nextauth]/route';
|
||||||
import { prisma } from '@/lib/prisma';
|
import { getEmailContent, markEmailReadStatus } from '@/lib/services/email-service';
|
||||||
import { simpleParser } from 'mailparser';
|
|
||||||
|
|
||||||
// Simple in-memory cache for email content
|
|
||||||
const emailContentCache = new Map<string, any>();
|
|
||||||
|
|
||||||
export async function GET(
|
export async function GET(
|
||||||
request: Request,
|
request: Request,
|
||||||
@ -37,67 +32,18 @@ export async function GET(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get mail credentials
|
|
||||||
const credentials = await prisma.mailCredentials.findUnique({
|
|
||||||
where: {
|
|
||||||
userId: session.user.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!credentials) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "No mail credentials found" },
|
|
||||||
{ status: 404 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { searchParams } = new URL(request.url);
|
const { searchParams } = new URL(request.url);
|
||||||
const folder = searchParams.get("folder") || "INBOX";
|
const folder = searchParams.get("folder") || "INBOX";
|
||||||
|
|
||||||
// Create IMAP client
|
|
||||||
let imapClient: any = null;
|
|
||||||
try {
|
try {
|
||||||
imapClient = new ImapFlow({
|
// Use the email service to fetch the email content
|
||||||
host: credentials.host,
|
const email = await getEmailContent(session.user.id, id, folder);
|
||||||
port: credentials.port,
|
|
||||||
secure: true,
|
|
||||||
auth: {
|
|
||||||
user: credentials.email,
|
|
||||||
pass: credentials.password,
|
|
||||||
},
|
|
||||||
logger: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
await imapClient.connect();
|
|
||||||
console.log(`Connected to IMAP server to fetch full email ${id}`);
|
|
||||||
|
|
||||||
// Select mailbox
|
|
||||||
const mailboxData = await imapClient.mailboxOpen(folder);
|
|
||||||
console.log(`Opened mailbox ${folder} to fetch email ${id}`);
|
|
||||||
|
|
||||||
// Fetch the complete email with its source
|
|
||||||
const message = await imapClient.fetchOne(Number(id), {
|
|
||||||
source: true,
|
|
||||||
envelope: true
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!message) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "Email not found" },
|
|
||||||
{ status: 404 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { source, envelope } = message;
|
|
||||||
|
|
||||||
// Parse the full email content
|
// Return only what's needed for displaying the email
|
||||||
const parsedEmail = await simpleParser(source.toString());
|
|
||||||
|
|
||||||
// Return only the content
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
id,
|
id,
|
||||||
subject: envelope.subject,
|
subject: email.subject,
|
||||||
content: parsedEmail.html || parsedEmail.textAsHtml || parsedEmail.text || '',
|
content: email.content,
|
||||||
contentFetched: true
|
contentFetched: true
|
||||||
});
|
});
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@ -106,16 +52,6 @@ export async function GET(
|
|||||||
{ error: "Failed to fetch email content", message: error.message },
|
{ error: "Failed to fetch email content", message: error.message },
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
);
|
);
|
||||||
} finally {
|
|
||||||
// Close the mailbox and connection
|
|
||||||
if (imapClient) {
|
|
||||||
try {
|
|
||||||
await imapClient.mailboxClose();
|
|
||||||
await imapClient.logout();
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Error closing IMAP connection:", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("Error in GET:", error);
|
console.error("Error in GET:", error);
|
||||||
@ -157,67 +93,25 @@ export async function POST(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get mail credentials
|
|
||||||
const credentials = await prisma.mailCredentials.findUnique({
|
|
||||||
where: {
|
|
||||||
userId: session.user.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!credentials) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "No mail credentials found" },
|
|
||||||
{ status: 404 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { searchParams } = new URL(request.url);
|
const { searchParams } = new URL(request.url);
|
||||||
const folder = searchParams.get("folder") || "INBOX";
|
const folder = searchParams.get("folder") || "INBOX";
|
||||||
|
|
||||||
// Create IMAP client
|
// Use the email service to mark the email
|
||||||
let imapClient: any = null;
|
const success = await markEmailReadStatus(
|
||||||
try {
|
session.user.id,
|
||||||
imapClient = new ImapFlow({
|
id,
|
||||||
host: credentials.host,
|
action === 'mark-read',
|
||||||
port: credentials.port,
|
folder
|
||||||
secure: true,
|
);
|
||||||
auth: {
|
|
||||||
user: credentials.email,
|
|
||||||
pass: credentials.password,
|
|
||||||
},
|
|
||||||
logger: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
await imapClient.connect();
|
if (!success) {
|
||||||
|
|
||||||
// Select mailbox
|
|
||||||
await imapClient.mailboxOpen(folder);
|
|
||||||
|
|
||||||
// Set flag based on action
|
|
||||||
if (action === 'mark-read') {
|
|
||||||
await imapClient.messageFlagsAdd(Number(id), ['\\Seen']);
|
|
||||||
} else {
|
|
||||||
await imapClient.messageFlagsRemove(Number(id), ['\\Seen']);
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json({ success: true });
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error(`Error ${action === 'mark-read' ? 'marking email as read' : 'marking email as unread'}:`, error);
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: `Failed to ${action === 'mark-read' ? 'mark email as read' : 'mark email as unread'}`, message: error.message },
|
{ error: `Failed to ${action === 'mark-read' ? 'mark email as read' : 'mark email as unread'}` },
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
);
|
);
|
||||||
} finally {
|
|
||||||
// Close the mailbox and connection
|
|
||||||
if (imapClient) {
|
|
||||||
try {
|
|
||||||
await imapClient.mailboxClose();
|
|
||||||
await imapClient.logout();
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Error closing IMAP connection:", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true });
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("Error in POST:", error);
|
console.error("Error in POST:", error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import { getServerSession } from 'next-auth';
|
import { getServerSession } from 'next-auth';
|
||||||
import { authOptions } from '@/app/api/auth/[...nextauth]/route';
|
import { authOptions } from '@/app/api/auth/[...nextauth]/route';
|
||||||
import { prisma } from '@/lib/prisma';
|
import { getUserEmailCredentials } from '@/lib/services/email-service';
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
try {
|
try {
|
||||||
@ -24,17 +24,8 @@ export async function GET() {
|
|||||||
}, { status: 401 });
|
}, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch mail credentials for this user
|
// Fetch mail credentials for this user using our service
|
||||||
const mailCredentials = await prisma.mailCredentials.findUnique({
|
const mailCredentials = await getUserEmailCredentials(userId);
|
||||||
where: {
|
|
||||||
userId: userId
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
email: true,
|
|
||||||
host: true,
|
|
||||||
port: true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// If no credentials found
|
// If no credentials found
|
||||||
if (!mailCredentials) {
|
if (!mailCredentials) {
|
||||||
@ -44,10 +35,14 @@ export async function GET() {
|
|||||||
}, { status: 404 });
|
}, { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the credentials
|
// Return the credentials (excluding password)
|
||||||
console.log(`Successfully retrieved mail credentials for user ID: ${userId}`);
|
console.log(`Successfully retrieved mail credentials for user ID: ${userId}`);
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
credentials: mailCredentials
|
credentials: {
|
||||||
|
email: mailCredentials.email,
|
||||||
|
host: mailCredentials.host,
|
||||||
|
port: mailCredentials.port
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@ -1,107 +1,65 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import { ImapFlow } from 'imapflow';
|
|
||||||
import { getServerSession } from 'next-auth';
|
import { getServerSession } from 'next-auth';
|
||||||
import { authOptions } from '@/app/api/auth/[...nextauth]/route';
|
import { authOptions } from '@/app/api/auth/[...nextauth]/route';
|
||||||
import { prisma } from '@/lib/prisma';
|
import {
|
||||||
|
saveUserEmailCredentials,
|
||||||
|
getUserEmailCredentials,
|
||||||
|
testEmailConnection
|
||||||
|
} from '@/lib/services/email-service';
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
try {
|
try {
|
||||||
console.log('Processing login POST request');
|
// Authenticate user
|
||||||
const session = await getServerSession(authOptions);
|
const session = await getServerSession(authOptions);
|
||||||
if (!session?.user?.id) {
|
if (!session?.user?.id) {
|
||||||
console.log('No authenticated session found');
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: 'Unauthorized' },
|
{ error: 'Unauthorized' },
|
||||||
{ status: 401 }
|
{ status: 401 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get credentials from request
|
||||||
const { email, password, host, port } = await request.json();
|
const { email, password, host, port } = await request.json();
|
||||||
console.log('Login attempt for:', email, 'to server:', host);
|
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
if (!email || !password || !host || !port) {
|
if (!email || !password || !host || !port) {
|
||||||
console.log('Missing required login fields');
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: 'Missing required fields' },
|
{ error: 'Missing required fields' },
|
||||||
{ status: 400 }
|
{ status: 400 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test IMAP connection
|
// Test connection before saving
|
||||||
console.log('Testing IMAP connection to:', host, port);
|
const connectionSuccess = await testEmailConnection({
|
||||||
const client = new ImapFlow({
|
email,
|
||||||
host: host,
|
password,
|
||||||
port: parseInt(port),
|
host,
|
||||||
secure: true,
|
port: parseInt(port)
|
||||||
auth: {
|
|
||||||
user: email,
|
|
||||||
pass: password,
|
|
||||||
},
|
|
||||||
logger: false,
|
|
||||||
emitLogs: false,
|
|
||||||
tls: {
|
|
||||||
rejectUnauthorized: false
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
if (!connectionSuccess) {
|
||||||
await client.connect();
|
|
||||||
console.log('IMAP connection successful');
|
|
||||||
await client.mailboxOpen('INBOX');
|
|
||||||
console.log('INBOX opened successfully');
|
|
||||||
|
|
||||||
// Store or update credentials in database
|
|
||||||
await prisma.mailCredentials.upsert({
|
|
||||||
where: {
|
|
||||||
userId: session.user.id
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
email,
|
|
||||||
password,
|
|
||||||
host,
|
|
||||||
port: parseInt(port)
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
userId: session.user.id,
|
|
||||||
email,
|
|
||||||
password,
|
|
||||||
host,
|
|
||||||
port: parseInt(port)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
console.log('Credentials stored in database');
|
|
||||||
|
|
||||||
return NextResponse.json({ success: true });
|
|
||||||
} catch (error) {
|
|
||||||
console.error('IMAP connection error:', error);
|
|
||||||
if (error instanceof Error) {
|
|
||||||
if (error.message.includes('Invalid login')) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Invalid login or password' },
|
|
||||||
{ status: 401 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: `IMAP connection error: ${error.message}` },
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: 'Failed to connect to email server' },
|
{ error: 'Failed to connect to email server. Please check your credentials.' },
|
||||||
{ status: 500 }
|
{ status: 401 }
|
||||||
);
|
);
|
||||||
} finally {
|
|
||||||
try {
|
|
||||||
await client.logout();
|
|
||||||
console.log('IMAP client logged out');
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error during logout:', e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save credentials in the database
|
||||||
|
await saveUserEmailCredentials(session.user.id, {
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
host,
|
||||||
|
port: parseInt(port)
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error in login handler:', error);
|
console.error('Error in login handler:', error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: 'An unexpected error occurred' },
|
{
|
||||||
|
error: 'An unexpected error occurred',
|
||||||
|
details: error instanceof Error ? error.message : 'Unknown error'
|
||||||
|
},
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -109,37 +67,31 @@ export async function POST(request: Request) {
|
|||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
try {
|
try {
|
||||||
console.log('Fetching mail credentials');
|
// Authenticate user
|
||||||
const session = await getServerSession(authOptions);
|
const session = await getServerSession(authOptions);
|
||||||
if (!session?.user?.id) {
|
if (!session?.user?.id) {
|
||||||
console.log('No authenticated session found');
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: 'Unauthorized' },
|
{ error: 'Unauthorized' },
|
||||||
{ status: 401 }
|
{ status: 401 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const credentials = await prisma.mailCredentials.findUnique({
|
// Get user credentials from database
|
||||||
where: {
|
const credentials = await getUserEmailCredentials(session.user.id);
|
||||||
userId: session.user.id
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
email: true,
|
|
||||||
host: true,
|
|
||||||
port: true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!credentials) {
|
if (!credentials) {
|
||||||
console.log('No mail credentials found for user');
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: 'No stored credentials found' },
|
{ error: 'No stored credentials found' },
|
||||||
{ status: 404 }
|
{ status: 404 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Credentials found for:', credentials.email);
|
// Return credentials without the password
|
||||||
return NextResponse.json(credentials);
|
return NextResponse.json({
|
||||||
|
email: credentials.email,
|
||||||
|
host: credentials.host,
|
||||||
|
port: credentials.port
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching credentials:', error);
|
console.error('Error fetching credentials:', error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
|
|||||||
@ -1,88 +1,21 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import { ImapFlow } from 'imapflow';
|
|
||||||
import { getServerSession } from 'next-auth';
|
import { getServerSession } from 'next-auth';
|
||||||
import { authOptions } from '@/app/api/auth/[...nextauth]/route';
|
import { authOptions } from '@/app/api/auth/[...nextauth]/route';
|
||||||
import { prisma } from '@/lib/prisma';
|
import { getEmails } from '@/lib/services/email-service';
|
||||||
import { simpleParser } from 'mailparser';
|
|
||||||
|
|
||||||
// Type definitions
|
// Simple in-memory cache (will be removed in a future update)
|
||||||
interface EmailCacheEntry {
|
interface EmailCacheEntry {
|
||||||
data: any;
|
data: any;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CredentialsCacheEntry {
|
// Cache for 1 minute only
|
||||||
client: any; // Use any for ImapFlow to avoid type issues
|
const CACHE_TTL = 60 * 1000;
|
||||||
timestamp: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Email cache structure
|
|
||||||
const emailListCache: Record<string, EmailCacheEntry> = {};
|
const emailListCache: Record<string, EmailCacheEntry> = {};
|
||||||
const credentialsCache: Record<string, CredentialsCacheEntry> = {};
|
|
||||||
|
|
||||||
// Cache TTL in milliseconds
|
|
||||||
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
|
||||||
|
|
||||||
// Helper function to get credentials with caching
|
|
||||||
async function getCredentialsWithCache(userId: string) {
|
|
||||||
// Check if we have fresh cached credentials
|
|
||||||
const cachedCreds = credentialsCache[userId];
|
|
||||||
const now = Date.now();
|
|
||||||
|
|
||||||
if (cachedCreds && now - cachedCreds.timestamp < CACHE_TTL) {
|
|
||||||
return cachedCreds.client;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise fetch from database
|
|
||||||
const credentials = await prisma.mailCredentials.findUnique({
|
|
||||||
where: { userId }
|
|
||||||
});
|
|
||||||
|
|
||||||
// Cache the result
|
|
||||||
if (credentials) {
|
|
||||||
credentialsCache[userId] = {
|
|
||||||
client: credentialsCache[userId]?.client || new ImapFlow({
|
|
||||||
host: credentials.host,
|
|
||||||
port: credentials.port,
|
|
||||||
secure: true,
|
|
||||||
auth: {
|
|
||||||
user: credentials.email,
|
|
||||||
pass: credentials.password,
|
|
||||||
},
|
|
||||||
logger: false,
|
|
||||||
}),
|
|
||||||
timestamp: now
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return credentialsCache[userId]?.client || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retry logic for IMAP operations
|
|
||||||
async function retryOperation<T>(operation: () => Promise<T>, maxAttempts = 3, delay = 1000): Promise<T> {
|
|
||||||
let lastError: Error;
|
|
||||||
|
|
||||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
||||||
try {
|
|
||||||
return await operation();
|
|
||||||
} catch (error) {
|
|
||||||
lastError = error as Error;
|
|
||||||
console.warn(`Operation failed (attempt ${attempt}/${maxAttempts}):`, error);
|
|
||||||
|
|
||||||
if (attempt < maxAttempts) {
|
|
||||||
// Exponential backoff
|
|
||||||
const backoffDelay = delay * Math.pow(2, attempt - 1);
|
|
||||||
console.log(`Retrying in ${backoffDelay}ms...`);
|
|
||||||
await new Promise(resolve => setTimeout(resolve, backoffDelay));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw lastError!;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function GET(request: Request) {
|
export async function GET(request: Request) {
|
||||||
try {
|
try {
|
||||||
|
// Authenticate user
|
||||||
const session = await getServerSession(authOptions);
|
const session = await getServerSession(authOptions);
|
||||||
if (!session || !session.user?.id) {
|
if (!session || !session.user?.id) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
@ -91,310 +24,45 @@ export async function GET(request: Request) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get mail credentials
|
// Extract query parameters
|
||||||
const credentials = await prisma.mailCredentials.findUnique({
|
|
||||||
where: {
|
|
||||||
userId: session.user.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!credentials) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "No mail credentials found" },
|
|
||||||
{ status: 404 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { searchParams } = new URL(request.url);
|
const { searchParams } = new URL(request.url);
|
||||||
const page = parseInt(searchParams.get("page") || "1");
|
const page = parseInt(searchParams.get("page") || "1");
|
||||||
const perPage = parseInt(searchParams.get("perPage") || "20");
|
const perPage = parseInt(searchParams.get("perPage") || "20");
|
||||||
const folder = searchParams.get("folder") || "INBOX";
|
const folder = searchParams.get("folder") || "INBOX";
|
||||||
const searchQuery = searchParams.get("search") || "";
|
const searchQuery = searchParams.get("search") || "";
|
||||||
|
|
||||||
// Check for entry in emailCache
|
// Check cache - temporary until we implement a proper server-side cache
|
||||||
const cacheKey = `${session.user.id}:${folder}:${page}:${perPage}:${searchQuery}`;
|
const cacheKey = `${session.user.id}:${folder}:${page}:${perPage}:${searchQuery}`;
|
||||||
|
const now = Date.now();
|
||||||
const cachedEmails = emailListCache[cacheKey];
|
const cachedEmails = emailListCache[cacheKey];
|
||||||
|
|
||||||
if (cachedEmails) {
|
if (cachedEmails && now - cachedEmails.timestamp < CACHE_TTL) {
|
||||||
console.log(`Using cached emails for ${cacheKey}`);
|
console.log(`Using cached emails for ${cacheKey}`);
|
||||||
return NextResponse.json(cachedEmails.data);
|
return NextResponse.json(cachedEmails.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Cache miss for ${cacheKey}, fetching from IMAP`);
|
console.log(`Cache miss for ${cacheKey}, fetching emails`);
|
||||||
|
|
||||||
// Fetch from IMAP
|
// Use the email service to fetch emails
|
||||||
const cacheCredKey = `credentials:${session.user.id}`;
|
const emailsResult = await getEmails(
|
||||||
let imapClient: any = credentialsCache[cacheCredKey]?.client || null;
|
session.user.id,
|
||||||
|
folder,
|
||||||
|
page,
|
||||||
|
perPage,
|
||||||
|
searchQuery
|
||||||
|
);
|
||||||
|
|
||||||
if (!imapClient) {
|
// Cache the results
|
||||||
// Create IMAP client
|
emailListCache[cacheKey] = {
|
||||||
const connectWithRetry = async (retries = 3, delay = 1000): Promise<any> => {
|
data: emailsResult,
|
||||||
try {
|
timestamp: now
|
||||||
console.log(`Attempting to connect to IMAP server (${credentials.host}:${credentials.port})...`);
|
|
||||||
const client = new ImapFlow({
|
|
||||||
host: credentials.host,
|
|
||||||
port: credentials.port,
|
|
||||||
secure: true,
|
|
||||||
auth: {
|
|
||||||
user: credentials.email,
|
|
||||||
pass: credentials.password,
|
|
||||||
},
|
|
||||||
logger: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
await client.connect();
|
|
||||||
console.log("Successfully connected to IMAP server");
|
|
||||||
return client;
|
|
||||||
} catch (error) {
|
|
||||||
if (retries > 0) {
|
|
||||||
console.log(`Connection failed, retrying... (${retries} attempts left)`);
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
||||||
return connectWithRetry(retries - 1, delay * 1.5);
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
imapClient = await connectWithRetry();
|
|
||||||
// Cache for future use
|
|
||||||
credentialsCache[cacheCredKey] = {
|
|
||||||
client: imapClient,
|
|
||||||
timestamp: Date.now()
|
|
||||||
};
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error("Failed to connect to IMAP server after retries:", error);
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "Failed to connect to IMAP server", message: error.message },
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log("Using cached IMAP client connection");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to get mailboxes
|
|
||||||
const getMailboxes = async () => {
|
|
||||||
try {
|
|
||||||
console.log("Getting list of mailboxes...");
|
|
||||||
const mailboxes = [];
|
|
||||||
const list = await imapClient.list();
|
|
||||||
console.log(`Found ${list.length} mailboxes from IMAP server`);
|
|
||||||
|
|
||||||
for (const mailbox of list) {
|
|
||||||
mailboxes.push(mailbox.path);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Available mailboxes:", mailboxes);
|
|
||||||
return mailboxes;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error listing mailboxes:", error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Setup paging
|
return NextResponse.json(emailsResult);
|
||||||
const startIdx = (page - 1) * perPage + 1;
|
|
||||||
const endIdx = page * perPage;
|
|
||||||
|
|
||||||
let emails: any[] = [];
|
|
||||||
let mailboxData = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Select and lock mailbox
|
|
||||||
mailboxData = await imapClient.mailboxOpen(folder);
|
|
||||||
console.log(`Opened mailbox ${folder}, ${mailboxData.exists} messages total`);
|
|
||||||
|
|
||||||
// Calculate range based on total messages
|
|
||||||
const totalMessages = mailboxData.exists;
|
|
||||||
const from = Math.max(totalMessages - endIdx + 1, 1);
|
|
||||||
const to = Math.max(totalMessages - startIdx + 1, 1);
|
|
||||||
|
|
||||||
// Skip if no messages or invalid range
|
|
||||||
if (totalMessages === 0 || from > to) {
|
|
||||||
console.log("No messages in range, returning empty array");
|
|
||||||
const result = {
|
|
||||||
emails: [],
|
|
||||||
totalEmails: 0,
|
|
||||||
page,
|
|
||||||
perPage,
|
|
||||||
totalPages: 0,
|
|
||||||
folder,
|
|
||||||
mailboxes: await getMailboxes(),
|
|
||||||
};
|
|
||||||
emailListCache[cacheKey] = {
|
|
||||||
data: result,
|
|
||||||
timestamp: Date.now()
|
|
||||||
};
|
|
||||||
return NextResponse.json(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Fetching messages ${from}:${to} (page ${page}, ${perPage} per page)`);
|
|
||||||
|
|
||||||
// Search if needed
|
|
||||||
let messageIds: any[] = [];
|
|
||||||
if (searchQuery) {
|
|
||||||
console.log(`Searching for: "${searchQuery}"`);
|
|
||||||
messageIds = await imapClient.search({
|
|
||||||
body: searchQuery
|
|
||||||
});
|
|
||||||
|
|
||||||
// Filter to our page range
|
|
||||||
messageIds = messageIds.filter(id => id >= from && id <= to);
|
|
||||||
console.log(`Found ${messageIds.length} messages matching search`);
|
|
||||||
} else {
|
|
||||||
messageIds = Array.from(
|
|
||||||
{ length: to - from + 1 },
|
|
||||||
(_, i) => from + i
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch messages with their content
|
|
||||||
for (const id of messageIds) {
|
|
||||||
try {
|
|
||||||
const message = await imapClient.fetchOne(id, {
|
|
||||||
envelope: true,
|
|
||||||
flags: true,
|
|
||||||
bodyStructure: true,
|
|
||||||
internalDate: true,
|
|
||||||
size: true,
|
|
||||||
// Only fetch a preview of the body initially for faster loading
|
|
||||||
bodyParts: [
|
|
||||||
{
|
|
||||||
query: {
|
|
||||||
type: "text",
|
|
||||||
},
|
|
||||||
limit: 5000, // Limit to first 5KB of text
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!message) continue;
|
|
||||||
|
|
||||||
const { envelope, flags, bodyStructure, internalDate, size, bodyParts } = message;
|
|
||||||
|
|
||||||
// Extract content from the body parts for a preview
|
|
||||||
let preview = '';
|
|
||||||
if (bodyParts && bodyParts.length > 0) {
|
|
||||||
const textPart = bodyParts.find((part: any) => part.type === 'text/plain');
|
|
||||||
const htmlPart = bodyParts.find((part: any) => part.type === 'text/html');
|
|
||||||
// Prefer text for preview as it's smaller and faster to process
|
|
||||||
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) + '...';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert attachments to our format
|
|
||||||
const attachments: Array<{
|
|
||||||
contentId?: string;
|
|
||||||
filename: string;
|
|
||||||
contentType: string;
|
|
||||||
size: number;
|
|
||||||
path: string;
|
|
||||||
}> = [];
|
|
||||||
|
|
||||||
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) {
|
|
||||||
node.childNodes.forEach((child: any, index: number) => {
|
|
||||||
processAttachments(child, [...path, node.part || index + 1]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (bodyStructure) {
|
|
||||||
processAttachments(bodyStructure);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert flags from Set to boolean checks
|
|
||||||
const flagsArray = Array.from(flags as Set<string>);
|
|
||||||
|
|
||||||
emails.push({
|
|
||||||
id,
|
|
||||||
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: {
|
|
||||||
seen: flagsArray.includes("\\Seen"),
|
|
||||||
flagged: flagsArray.includes("\\Flagged"),
|
|
||||||
answered: flagsArray.includes("\\Answered"),
|
|
||||||
deleted: flagsArray.includes("\\Deleted"),
|
|
||||||
draft: flagsArray.includes("\\Draft"),
|
|
||||||
},
|
|
||||||
hasAttachments: attachments.length > 0,
|
|
||||||
attachments,
|
|
||||||
size,
|
|
||||||
// Just include the preview instead of the full content initially
|
|
||||||
preview,
|
|
||||||
// Store the fetched state to know we only have preview
|
|
||||||
contentFetched: false
|
|
||||||
});
|
|
||||||
} catch (messageError) {
|
|
||||||
console.error(`Error fetching message ${id}:`, messageError);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort by date, newest first
|
|
||||||
emails.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
|
|
||||||
|
|
||||||
const result = {
|
|
||||||
emails,
|
|
||||||
totalEmails: totalMessages,
|
|
||||||
page,
|
|
||||||
perPage,
|
|
||||||
totalPages: Math.ceil(totalMessages / perPage),
|
|
||||||
folder,
|
|
||||||
mailboxes: await getMailboxes(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Cache the result
|
|
||||||
emailListCache[cacheKey] = {
|
|
||||||
data: result,
|
|
||||||
timestamp: Date.now()
|
|
||||||
};
|
|
||||||
|
|
||||||
return NextResponse.json(result);
|
|
||||||
} finally {
|
|
||||||
// If we opened a mailbox, close it
|
|
||||||
if (mailboxData) {
|
|
||||||
await imapClient.mailboxClose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("Error in GET:", error);
|
console.error("Error fetching emails:", error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "Internal server error", message: error.message },
|
{ error: "Failed to fetch emails", message: error.message },
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -407,30 +75,25 @@ export async function POST(request: Request) {
|
|||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { emailId, folderName, action } = await request.json();
|
const { emailId, folderName } = await request.json();
|
||||||
|
|
||||||
if (!emailId) {
|
if (!emailId) {
|
||||||
return NextResponse.json({ error: 'Missing emailId parameter' }, { status: 400 });
|
return NextResponse.json({ error: 'Missing emailId parameter' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Invalidate cache entries for this folder
|
// Invalidate cache entries for this folder or all folders if none specified
|
||||||
const userId = session.user.id;
|
const userId = session.user.id;
|
||||||
|
Object.keys(emailListCache).forEach(key => {
|
||||||
// If folder is specified, only invalidate that folder's cache
|
if (folderName) {
|
||||||
if (folderName) {
|
|
||||||
Object.keys(emailListCache).forEach(key => {
|
|
||||||
if (key.includes(`${userId}:${folderName}`)) {
|
if (key.includes(`${userId}:${folderName}`)) {
|
||||||
delete emailListCache[key];
|
delete emailListCache[key];
|
||||||
}
|
}
|
||||||
});
|
} else {
|
||||||
} else {
|
|
||||||
// Otherwise invalidate all cache entries for this user
|
|
||||||
Object.keys(emailListCache).forEach(key => {
|
|
||||||
if (key.startsWith(`${userId}:`)) {
|
if (key.startsWith(`${userId}:`)) {
|
||||||
delete emailListCache[key];
|
delete emailListCache[key];
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
return NextResponse.json({ success: true });
|
return NextResponse.json({ success: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@ -1,100 +1,53 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import { getServerSession } from 'next-auth';
|
import { getServerSession } from 'next-auth';
|
||||||
import { authOptions } from '@/app/api/auth/[...nextauth]/route';
|
import { authOptions } from '@/app/api/auth/[...nextauth]/route';
|
||||||
import { prisma } from '@/lib/prisma';
|
import { sendEmail } from '@/lib/services/email-service';
|
||||||
import nodemailer from 'nodemailer';
|
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
try {
|
try {
|
||||||
console.log('Starting email send process...');
|
// Authenticate user
|
||||||
|
|
||||||
const session = await getServerSession(authOptions);
|
const session = await getServerSession(authOptions);
|
||||||
if (!session?.user?.id) {
|
if (!session?.user?.id) {
|
||||||
console.log('No session found');
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: 'Unauthorized' },
|
{ error: 'Unauthorized' },
|
||||||
{ status: 401 }
|
{ status: 401 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get credentials from database
|
// Parse request body
|
||||||
console.log('Fetching credentials for user:', session.user.id);
|
|
||||||
const credentials = await prisma.mailCredentials.findUnique({
|
|
||||||
where: {
|
|
||||||
userId: session.user.id
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!credentials) {
|
|
||||||
console.log('No credentials found for user');
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'No mail credentials found. Please configure your email account.' },
|
|
||||||
{ status: 401 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the email data from the request
|
|
||||||
const { to, cc, bcc, subject, body, attachments } = await request.json();
|
const { to, cc, bcc, subject, body, attachments } = await request.json();
|
||||||
console.log('Email data received:', { to, cc, bcc, subject, attachments: attachments?.length || 0 });
|
|
||||||
|
// Validate required fields
|
||||||
if (!to) {
|
if (!to) {
|
||||||
console.log('No recipient specified');
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: 'Recipient is required' },
|
{ error: 'Recipient is required' },
|
||||||
{ status: 400 }
|
{ status: 400 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create SMTP transporter with Infomaniak SMTP settings
|
// Use email service to send the email
|
||||||
console.log('Creating SMTP transporter...');
|
const result = await sendEmail(session.user.id, {
|
||||||
const transporter = nodemailer.createTransport({
|
to,
|
||||||
host: 'smtp.infomaniak.com',
|
cc,
|
||||||
port: 587,
|
bcc,
|
||||||
secure: false,
|
subject,
|
||||||
auth: {
|
body,
|
||||||
user: credentials.email,
|
attachments
|
||||||
pass: credentials.password,
|
|
||||||
},
|
|
||||||
tls: {
|
|
||||||
rejectUnauthorized: false
|
|
||||||
},
|
|
||||||
debug: true // Enable debug logging
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Verify SMTP connection
|
if (!result.success) {
|
||||||
console.log('Verifying SMTP connection...');
|
return NextResponse.json(
|
||||||
try {
|
{
|
||||||
await transporter.verify();
|
error: 'Failed to send email',
|
||||||
console.log('SMTP connection verified successfully');
|
details: result.error
|
||||||
} catch (error) {
|
},
|
||||||
console.error('SMTP connection verification failed:', error);
|
{ status: 500 }
|
||||||
throw error;
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare email options
|
|
||||||
console.log('Preparing email options...');
|
|
||||||
const mailOptions = {
|
|
||||||
from: credentials.email,
|
|
||||||
to: to,
|
|
||||||
cc: cc || undefined,
|
|
||||||
bcc: bcc || undefined,
|
|
||||||
subject: subject || '(No subject)',
|
|
||||||
html: body,
|
|
||||||
attachments: attachments?.map((file: any) => ({
|
|
||||||
filename: file.name,
|
|
||||||
content: file.content,
|
|
||||||
contentType: file.type
|
|
||||||
})) || []
|
|
||||||
};
|
|
||||||
|
|
||||||
// Send the email
|
|
||||||
console.log('Sending email...');
|
|
||||||
const info = await transporter.sendMail(mailOptions);
|
|
||||||
console.log('Email sent successfully:', info.messageId);
|
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
messageId: info.messageId
|
messageId: result.messageId
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error sending email:', error);
|
console.error('Error sending email:', error);
|
||||||
|
|||||||
@ -1,88 +1,22 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import { ImapFlow } from 'imapflow';
|
|
||||||
import { getServerSession } from 'next-auth';
|
|
||||||
import { authOptions } from '@/app/api/auth/[...nextauth]/route';
|
|
||||||
import { prisma } from '@/lib/prisma';
|
|
||||||
|
|
||||||
export async function GET() {
|
/**
|
||||||
try {
|
* This route is deprecated. It redirects to the new courrier API endpoint.
|
||||||
const session = await getServerSession(authOptions);
|
* @deprecated Use the /api/courrier endpoint instead
|
||||||
if (!session?.user?.id) {
|
*/
|
||||||
return NextResponse.json(
|
export async function GET(request: Request) {
|
||||||
{ error: 'Unauthorized' },
|
console.warn('Deprecated: /api/mail route is being used. Update your code to use /api/courrier instead.');
|
||||||
{ status: 401 }
|
|
||||||
);
|
// Extract query parameters
|
||||||
}
|
const url = new URL(request.url);
|
||||||
|
|
||||||
// Get credentials from database
|
// Redirect to the new API endpoint
|
||||||
const credentials = await prisma.mailCredentials.findUnique({
|
const redirectUrl = new URL('/api/courrier', url.origin);
|
||||||
where: {
|
|
||||||
userId: session.user.id
|
// Copy all search parameters
|
||||||
}
|
url.searchParams.forEach((value, key) => {
|
||||||
});
|
redirectUrl.searchParams.set(key, value);
|
||||||
|
});
|
||||||
if (!credentials) {
|
|
||||||
return NextResponse.json(
|
return NextResponse.redirect(redirectUrl.toString());
|
||||||
{ error: 'No mail credentials found. Please configure your email account.' },
|
|
||||||
{ status: 401 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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,
|
|
||||||
emitLogs: false,
|
|
||||||
tls: {
|
|
||||||
rejectUnauthorized: false
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
await client.connect();
|
|
||||||
const mailbox = await client.mailboxOpen('INBOX');
|
|
||||||
|
|
||||||
// Fetch only essential message data
|
|
||||||
const messages = await client.fetch('1:20', {
|
|
||||||
envelope: true,
|
|
||||||
flags: true
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = [];
|
|
||||||
for await (const message of messages) {
|
|
||||||
result.push({
|
|
||||||
id: message.uid.toString(),
|
|
||||||
from: message.envelope.from[0].address,
|
|
||||||
subject: message.envelope.subject || '(No subject)',
|
|
||||||
date: message.envelope.date.toISOString(),
|
|
||||||
read: message.flags.has('\\Seen'),
|
|
||||||
starred: message.flags.has('\\Flagged'),
|
|
||||||
folder: mailbox.path
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
emails: result,
|
|
||||||
folders: ['INBOX', 'Sent', 'Drafts', 'Trash', 'Spam']
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
try {
|
|
||||||
await client.logout();
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error during logout:', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error in mail route:', error);
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'An unexpected error occurred' },
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -1,103 +1,32 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import { createTransport } from 'nodemailer';
|
|
||||||
import { cookies } from 'next/headers';
|
|
||||||
|
|
||||||
interface StoredCredentials {
|
|
||||||
email: string;
|
|
||||||
password: string;
|
|
||||||
host: string;
|
|
||||||
port: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Maximum attachment size in bytes (10MB)
|
|
||||||
const MAX_ATTACHMENT_SIZE = 10 * 1024 * 1024;
|
|
||||||
|
|
||||||
function getStoredCredentials(): StoredCredentials | null {
|
|
||||||
const cookieStore = cookies();
|
|
||||||
const credentialsCookie = cookieStore.get('imap_credentials');
|
|
||||||
|
|
||||||
if (!credentialsCookie?.value) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const credentials = JSON.parse(credentialsCookie.value);
|
|
||||||
if (!credentials.email || !credentials.password || !credentials.host || !credentials.port) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return credentials;
|
|
||||||
} catch (error) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This route is deprecated. It redirects to the new courrier API endpoint.
|
||||||
|
* @deprecated Use the /api/courrier/send endpoint instead
|
||||||
|
*/
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
|
console.warn('Deprecated: /api/mail/send route is being used. Update your code to use /api/courrier/send instead.');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const credentials = getStoredCredentials();
|
// Clone the request body
|
||||||
if (!credentials) {
|
const body = await request.json();
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'No stored credentials found' },
|
// Make a new request to the courrier API
|
||||||
{ status: 401 }
|
const newRequest = new Request(new URL('/api/courrier/send', request.url).toString(), {
|
||||||
);
|
method: 'POST',
|
||||||
}
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
const { to, cc, bcc, subject, body, attachments } = await request.json();
|
|
||||||
|
|
||||||
// Check attachment sizes
|
|
||||||
if (attachments?.length) {
|
|
||||||
const oversizedAttachments = attachments.filter((attachment: any) => {
|
|
||||||
// Calculate size from base64 content
|
|
||||||
const size = Math.ceil((attachment.content.length * 3) / 4);
|
|
||||||
return size > MAX_ATTACHMENT_SIZE;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (oversizedAttachments.length > 0) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
error: 'Attachment size limit exceeded',
|
|
||||||
details: {
|
|
||||||
maxSize: MAX_ATTACHMENT_SIZE,
|
|
||||||
oversizedFiles: oversizedAttachments.map((a: any) => a.name)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a transporter using SMTP with the same credentials
|
|
||||||
// Use port 465 for SMTP (Infomaniak's SMTP port)
|
|
||||||
const transporter = createTransport({
|
|
||||||
host: credentials.host,
|
|
||||||
port: 465, // SMTP port for Infomaniak
|
|
||||||
secure: true, // Use TLS
|
|
||||||
auth: {
|
|
||||||
user: credentials.email,
|
|
||||||
pass: credentials.password,
|
|
||||||
},
|
},
|
||||||
|
body: JSON.stringify(body)
|
||||||
});
|
});
|
||||||
|
|
||||||
// Prepare email options
|
// Forward the request
|
||||||
const mailOptions = {
|
const response = await fetch(newRequest);
|
||||||
from: credentials.email,
|
const data = await response.json();
|
||||||
to,
|
|
||||||
cc,
|
return NextResponse.json(data, { status: response.status });
|
||||||
bcc,
|
|
||||||
subject,
|
|
||||||
text: body,
|
|
||||||
attachments: attachments?.map((attachment: any) => ({
|
|
||||||
filename: attachment.name,
|
|
||||||
content: attachment.content,
|
|
||||||
encoding: attachment.encoding,
|
|
||||||
})),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Send the email
|
|
||||||
await transporter.sendMail(mailOptions);
|
|
||||||
|
|
||||||
return NextResponse.json({ success: true });
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error sending email:', error);
|
console.error('Error forwarding to courrier/send:', error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: 'Failed to send email' },
|
{ error: 'Failed to send email' },
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
|
|||||||
@ -164,19 +164,22 @@ export default function ComposeEmail({
|
|||||||
let formattedContent = '';
|
let formattedContent = '';
|
||||||
|
|
||||||
if (forwardFrom) {
|
if (forwardFrom) {
|
||||||
formattedContent = `
|
// Create a clean header for the forwarded email
|
||||||
<div class="forwarded-message">
|
const headerHtml = `
|
||||||
<p>---------- Forwarded message ---------</p>
|
<div style="border-bottom: 1px solid #e2e2e2; margin-bottom: 15px; padding-bottom: 15px; font-family: Arial, sans-serif;">
|
||||||
<p>From: ${decoded.from || ''}</p>
|
<p style="margin: 4px 0;">---------- Forwarded message ---------</p>
|
||||||
<p>Date: ${formatDate(decoded.date)}</p>
|
<p style="margin: 4px 0;"><b>From:</b> ${decoded.from || ''}</p>
|
||||||
<p>Subject: ${decoded.subject || ''}</p>
|
<p style="margin: 4px 0;"><b>Date:</b> ${formatDate(decoded.date)}</p>
|
||||||
<p>To: ${decoded.to || ''}</p>
|
<p style="margin: 4px 0;"><b>Subject:</b> ${decoded.subject || ''}</p>
|
||||||
<br>
|
<p style="margin: 4px 0;"><b>To:</b> ${decoded.to || ''}</p>
|
||||||
<div class="email-content prose prose-sm max-w-none dark:prose-invert">
|
|
||||||
${decoded.html || `<pre>${decoded.text || ''}</pre>`}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// Use the original HTML as-is without DOMPurify or any modification
|
||||||
|
formattedContent = `
|
||||||
|
${headerHtml}
|
||||||
|
${decoded.html || decoded.text || 'No content available'}
|
||||||
|
`;
|
||||||
} else {
|
} else {
|
||||||
formattedContent = `
|
formattedContent = `
|
||||||
<div class="quoted-message">
|
<div class="quoted-message">
|
||||||
|
|||||||
@ -3,11 +3,8 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { RefreshCw, Mail } from "lucide-react";
|
import { RefreshCw, MessageSquare, Mail, MailOpen, Loader2 } from "lucide-react";
|
||||||
import { useSession } from "next-auth/react";
|
import Link from 'next/link';
|
||||||
import { formatDistance } from 'date-fns/formatDistance';
|
|
||||||
import { fr } from 'date-fns/locale/fr';
|
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
|
|
||||||
interface Email {
|
interface Email {
|
||||||
id: string;
|
id: string;
|
||||||
@ -28,161 +25,125 @@ interface EmailResponse {
|
|||||||
|
|
||||||
export function Email() {
|
export function Email() {
|
||||||
const [emails, setEmails] = useState<Email[]>([]);
|
const [emails, setEmails] = useState<Email[]>([]);
|
||||||
const [mailUrl, setMailUrl] = useState<string | null>(null);
|
const [loading, setLoading] = useState(false);
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [refreshing, setRefreshing] = useState(false);
|
const [mailUrl, setMailUrl] = useState<string | null>(null);
|
||||||
const { data: session, status } = useSession();
|
|
||||||
const router = useRouter();
|
useEffect(() => {
|
||||||
|
fetchEmails();
|
||||||
|
}, []);
|
||||||
|
|
||||||
const fetchEmails = async (isRefresh = false) => {
|
const fetchEmails = async (isRefresh = false) => {
|
||||||
if (status !== 'authenticated') {
|
setLoading(true);
|
||||||
setError('Please sign in to view emails');
|
setError(null);
|
||||||
setLoading(false);
|
|
||||||
setRefreshing(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isRefresh) setRefreshing(true);
|
|
||||||
if (!isRefresh) setLoading(true);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/mail');
|
const response = await fetch('/api/courrier?folder=INBOX&page=1&perPage=5');
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorData = await response.json();
|
throw new Error('Failed to fetch emails');
|
||||||
throw new Error(errorData.error || 'Failed to fetch emails');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
throw new Error(data.error);
|
setError(data.error);
|
||||||
|
setEmails([]);
|
||||||
|
} else {
|
||||||
|
// Transform data format if needed
|
||||||
|
const transformedEmails = data.emails.map((email: any) => ({
|
||||||
|
id: email.id,
|
||||||
|
subject: email.subject,
|
||||||
|
from: email.from[0]?.address || '',
|
||||||
|
fromName: email.from[0]?.name || '',
|
||||||
|
date: email.date,
|
||||||
|
read: email.flags.seen,
|
||||||
|
starred: email.flags.flagged,
|
||||||
|
folder: email.folder
|
||||||
|
})).slice(0, 5); // Only show the first 5 emails
|
||||||
|
|
||||||
|
setEmails(transformedEmails);
|
||||||
|
setMailUrl('/courrier');
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
const validatedEmails = data.emails.map((email: any) => ({
|
console.error('Error fetching emails:', error);
|
||||||
id: email.id || Date.now().toString(),
|
setError('Failed to load emails');
|
||||||
subject: email.subject || '(No subject)',
|
|
||||||
from: email.from || '',
|
|
||||||
fromName: email.fromName || email.from?.split('@')[0] || 'Unknown',
|
|
||||||
date: email.date || new Date().toISOString(),
|
|
||||||
read: !!email.read,
|
|
||||||
starred: !!email.starred,
|
|
||||||
folder: email.folder || 'INBOX'
|
|
||||||
}));
|
|
||||||
|
|
||||||
setEmails(validatedEmails);
|
|
||||||
setMailUrl(data.mailUrl || 'https://espace.slm-lab.net/apps/courrier/');
|
|
||||||
setError(null);
|
|
||||||
} catch (err) {
|
|
||||||
setError(err instanceof Error ? err.message : 'Error fetching emails');
|
|
||||||
setEmails([]);
|
setEmails([]);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setRefreshing(false);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initial fetch
|
|
||||||
useEffect(() => {
|
|
||||||
if (status === 'authenticated') {
|
|
||||||
fetchEmails();
|
|
||||||
} else if (status === 'unauthenticated') {
|
|
||||||
setError('Please sign in to view emails');
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}, [status]);
|
|
||||||
|
|
||||||
// Auto-refresh every 5 minutes
|
|
||||||
useEffect(() => {
|
|
||||||
if (status !== 'authenticated') return;
|
|
||||||
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
fetchEmails(true);
|
|
||||||
}, 5 * 60 * 1000);
|
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
|
||||||
}, [status]);
|
|
||||||
|
|
||||||
const formatDate = (dateString: string) => {
|
const formatDate = (dateString: string) => {
|
||||||
try {
|
try {
|
||||||
const date = new Date(dateString);
|
const date = new Date(dateString);
|
||||||
return formatDistance(date, new Date(), {
|
return new Intl.DateTimeFormat('fr-FR', {
|
||||||
addSuffix: true,
|
month: 'short',
|
||||||
locale: fr
|
day: 'numeric'
|
||||||
});
|
}).format(date);
|
||||||
} catch (err) {
|
} catch (e) {
|
||||||
return dateString;
|
return '';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (status === 'loading' || loading) {
|
|
||||||
return (
|
|
||||||
<Card className="transition-transform duration-500 ease-in-out transform hover:scale-105 bg-white/95 backdrop-blur-sm border-0 shadow-lg h-full">
|
|
||||||
<CardHeader className="flex flex-row items-center justify-between pb-2 border-b border-gray-100">
|
|
||||||
<CardTitle className="text-lg font-semibold text-gray-800">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Mail className="h-5 w-5 text-gray-600" />
|
|
||||||
<span>Courrier</span>
|
|
||||||
</div>
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="p-6">
|
|
||||||
<div className="flex items-center justify-center">
|
|
||||||
<RefreshCw className="h-5 w-5 animate-spin text-gray-400" />
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="transition-transform duration-500 ease-in-out transform hover:scale-105 bg-white/95 backdrop-blur-sm border-0 shadow-lg h-full">
|
<Card className="h-full">
|
||||||
<CardHeader className="flex flex-row items-center justify-between pb-2 space-x-4 border-b border-gray-100">
|
<CardHeader className="flex flex-row items-center justify-between pb-2 border-b border-gray-100">
|
||||||
<CardTitle className="text-lg font-semibold text-gray-800">
|
<CardTitle className="text-lg font-semibold text-gray-800 flex items-center gap-2">
|
||||||
<div className="flex items-center gap-2">
|
<MessageSquare className="h-5 w-5 text-gray-600" />
|
||||||
<Mail className="h-5 w-5 text-gray-600" />
|
Emails non lus
|
||||||
<span>Courrier</span>
|
|
||||||
</div>
|
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={() => fetchEmails(true)}
|
onClick={() => fetchEmails(true)}
|
||||||
disabled={refreshing}
|
disabled={loading}
|
||||||
className={`${refreshing ? 'animate-spin' : ''} text-gray-600 hover:text-gray-900`}
|
|
||||||
>
|
>
|
||||||
<RefreshCw className="h-4 w-4" />
|
{loading ?
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin" /> :
|
||||||
|
<RefreshCw className="h-4 w-4" />
|
||||||
|
}
|
||||||
</Button>
|
</Button>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="p-3">
|
<CardContent className="p-4">
|
||||||
{error ? (
|
{error ? (
|
||||||
<p className="text-center text-red-500">{error}</p>
|
<div className="text-center py-4 text-gray-500">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
) : loading && emails.length === 0 ? (
|
||||||
|
<div className="text-center py-6 flex flex-col items-center">
|
||||||
|
<Loader2 className="h-6 w-6 animate-spin text-gray-400 mb-2" />
|
||||||
|
<p className="text-gray-500">Chargement des emails...</p>
|
||||||
|
</div>
|
||||||
|
) : emails.length === 0 ? (
|
||||||
|
<div className="text-center py-6">
|
||||||
|
<p className="text-gray-500">Aucun email non lu</p>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-2 max-h-[220px] overflow-y-auto">
|
<div className="space-y-3">
|
||||||
{emails.length === 0 ? (
|
{emails.map((email) => (
|
||||||
<p className="text-center text-gray-500">
|
<div key={email.id} className="flex items-start gap-3 py-1 border-b border-gray-100 last:border-0">
|
||||||
{loading ? 'Loading emails...' : 'No unread emails'}
|
<div className="pt-1">
|
||||||
</p>
|
{email.read ?
|
||||||
) : (
|
<MailOpen className="h-4 w-4 text-gray-400" /> :
|
||||||
emails.map((email) => (
|
<Mail className="h-4 w-4 text-blue-500" />
|
||||||
<div
|
}
|
||||||
key={email.id}
|
|
||||||
className="p-2 hover:bg-gray-50/50 rounded-lg transition-colors cursor-pointer"
|
|
||||||
onClick={() => router.push('/mail')}
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between mb-1">
|
|
||||||
<span className="text-sm text-gray-600 truncate max-w-[60%]" title={email.fromName || email.from}>
|
|
||||||
{email.fromName || email.from}
|
|
||||||
</span>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
{!email.read && <span className="w-1.5 h-1.5 bg-blue-600 rounded-full"></span>}
|
|
||||||
<span className="text-xs text-gray-500">{formatDate(email.date)}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-gray-800 truncate">{email.subject}</p>
|
|
||||||
</div>
|
</div>
|
||||||
))
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<p className="font-medium truncate" style={{maxWidth: '180px'}}>{email.fromName || email.from.split('@')[0]}</p>
|
||||||
|
<p className="text-xs text-gray-500">{formatDate(email.date)}</p>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-gray-700 truncate">{email.subject}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{mailUrl && (
|
||||||
|
<div className="pt-2">
|
||||||
|
<Link href={mailUrl} className="text-sm text-blue-600 hover:text-blue-800">
|
||||||
|
Voir tous les emails →
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
600
lib/services/email-service.ts
Normal file
600
lib/services/email-service.ts
Normal file
@ -0,0 +1,600 @@
|
|||||||
|
import { ImapFlow } from 'imapflow';
|
||||||
|
import nodemailer from 'nodemailer';
|
||||||
|
import { prisma } from '@/lib/prisma';
|
||||||
|
import { simpleParser } from 'mailparser';
|
||||||
|
|
||||||
|
// Types for the email service
|
||||||
|
export interface EmailCredentials {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EmailMessage {
|
||||||
|
id: string;
|
||||||
|
messageId?: string;
|
||||||
|
subject: string;
|
||||||
|
from: EmailAddress[];
|
||||||
|
to: EmailAddress[];
|
||||||
|
cc?: EmailAddress[];
|
||||||
|
bcc?: EmailAddress[];
|
||||||
|
date: Date;
|
||||||
|
flags: {
|
||||||
|
seen: boolean;
|
||||||
|
flagged: boolean;
|
||||||
|
answered: boolean;
|
||||||
|
deleted: boolean;
|
||||||
|
draft: boolean;
|
||||||
|
};
|
||||||
|
preview?: string;
|
||||||
|
content?: string;
|
||||||
|
html?: string;
|
||||||
|
text?: string;
|
||||||
|
hasAttachments: boolean;
|
||||||
|
attachments?: EmailAttachment[];
|
||||||
|
folder: string;
|
||||||
|
size?: number;
|
||||||
|
contentFetched: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EmailAddress {
|
||||||
|
name: string;
|
||||||
|
address: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EmailAttachment {
|
||||||
|
contentId?: string;
|
||||||
|
filename: string;
|
||||||
|
contentType: string;
|
||||||
|
size: number;
|
||||||
|
path?: string;
|
||||||
|
content?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EmailListResult {
|
||||||
|
emails: EmailMessage[];
|
||||||
|
totalEmails: number;
|
||||||
|
page: number;
|
||||||
|
perPage: number;
|
||||||
|
totalPages: number;
|
||||||
|
folder: string;
|
||||||
|
mailboxes: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connection pool to reuse IMAP clients
|
||||||
|
const connectionPool: Record<string, { client: ImapFlow; lastUsed: number }> = {};
|
||||||
|
const CONNECTION_TIMEOUT = 5 * 60 * 1000; // 5 minutes
|
||||||
|
|
||||||
|
// Clean up idle connections periodically
|
||||||
|
setInterval(() => {
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
Object.entries(connectionPool).forEach(([key, { client, lastUsed }]) => {
|
||||||
|
if (now - lastUsed > CONNECTION_TIMEOUT) {
|
||||||
|
console.log(`Closing idle IMAP connection for ${key}`);
|
||||||
|
client.logout().catch(err => {
|
||||||
|
console.error(`Error closing connection for ${key}:`, err);
|
||||||
|
});
|
||||||
|
delete connectionPool[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 60 * 1000); // Check every minute
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get IMAP connection for a user, reusing existing connections when possible
|
||||||
|
*/
|
||||||
|
export async function getImapConnection(userId: string): Promise<ImapFlow> {
|
||||||
|
// Get credentials from database
|
||||||
|
const credentials = await getUserEmailCredentials(userId);
|
||||||
|
if (!credentials) {
|
||||||
|
throw new Error('No email credentials found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const connectionKey = `${userId}:${credentials.email}`;
|
||||||
|
const existingConnection = connectionPool[connectionKey];
|
||||||
|
|
||||||
|
// Return existing connection if available and connected
|
||||||
|
if (existingConnection) {
|
||||||
|
try {
|
||||||
|
if (existingConnection.client.usable) {
|
||||||
|
existingConnection.lastUsed = Date.now();
|
||||||
|
return existingConnection.client;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Existing connection for ${connectionKey} is not usable, creating new connection`);
|
||||||
|
// Will create a new connection below
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new connection
|
||||||
|
const client = new ImapFlow({
|
||||||
|
host: credentials.host,
|
||||||
|
port: credentials.port,
|
||||||
|
secure: true,
|
||||||
|
auth: {
|
||||||
|
user: credentials.email,
|
||||||
|
pass: credentials.password,
|
||||||
|
},
|
||||||
|
logger: false,
|
||||||
|
emitLogs: false,
|
||||||
|
tls: {
|
||||||
|
rejectUnauthorized: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.connect();
|
||||||
|
|
||||||
|
// Store in connection pool
|
||||||
|
connectionPool[connectionKey] = {
|
||||||
|
client,
|
||||||
|
lastUsed: Date.now()
|
||||||
|
};
|
||||||
|
|
||||||
|
return client;
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to connect to IMAP server: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get user's email credentials from database
|
||||||
|
*/
|
||||||
|
export async function getUserEmailCredentials(userId: string): Promise<EmailCredentials | null> {
|
||||||
|
const credentials = await prisma.mailCredentials.findUnique({
|
||||||
|
where: { userId }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!credentials) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
email: credentials.email,
|
||||||
|
password: credentials.password,
|
||||||
|
host: credentials.host,
|
||||||
|
port: credentials.port
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save or update user's email credentials
|
||||||
|
*/
|
||||||
|
export async function saveUserEmailCredentials(
|
||||||
|
userId: string,
|
||||||
|
credentials: EmailCredentials
|
||||||
|
): Promise<void> {
|
||||||
|
await prisma.mailCredentials.upsert({
|
||||||
|
where: { userId },
|
||||||
|
update: {
|
||||||
|
email: credentials.email,
|
||||||
|
password: credentials.password,
|
||||||
|
host: credentials.host,
|
||||||
|
port: credentials.port
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
userId,
|
||||||
|
email: credentials.email,
|
||||||
|
password: credentials.password,
|
||||||
|
host: credentials.host,
|
||||||
|
port: credentials.port
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get list of emails for a user
|
||||||
|
*/
|
||||||
|
export async function getEmails(
|
||||||
|
userId: string,
|
||||||
|
folder: string = 'INBOX',
|
||||||
|
page: number = 1,
|
||||||
|
perPage: number = 20,
|
||||||
|
searchQuery: string = ''
|
||||||
|
): Promise<EmailListResult> {
|
||||||
|
const client = await getImapConnection(userId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Open mailbox
|
||||||
|
const mailboxData = await client.mailboxOpen(folder);
|
||||||
|
const totalMessages = mailboxData.exists;
|
||||||
|
|
||||||
|
// 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 mailboxes = await getMailboxes(client);
|
||||||
|
return {
|
||||||
|
emails: [],
|
||||||
|
totalEmails: 0,
|
||||||
|
page,
|
||||||
|
perPage,
|
||||||
|
totalPages: 0,
|
||||||
|
folder,
|
||||||
|
mailboxes
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
const emails: EmailMessage[] = [];
|
||||||
|
|
||||||
|
for (const id of messageIds) {
|
||||||
|
try {
|
||||||
|
const message = await client.fetchOne(id, {
|
||||||
|
envelope: true,
|
||||||
|
flags: true,
|
||||||
|
bodyStructure: true,
|
||||||
|
internalDate: true,
|
||||||
|
size: true,
|
||||||
|
bodyParts: [
|
||||||
|
{
|
||||||
|
query: { type: "text" },
|
||||||
|
limit: 5000
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!message) continue;
|
||||||
|
|
||||||
|
const { envelope, flags, bodyStructure, internalDate, size, bodyParts } = message;
|
||||||
|
|
||||||
|
// Extract preview content
|
||||||
|
let preview = '';
|
||||||
|
if (bodyParts && bodyParts.length > 0) {
|
||||||
|
const textPart = bodyParts.find((part: any) => part.type === 'text/plain');
|
||||||
|
const htmlPart = bodyParts.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) {
|
||||||
|
node.childNodes.forEach((child: any, index: number) => {
|
||||||
|
processAttachments(child, [...path, node.part || index + 1]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (bodyStructure) {
|
||||||
|
processAttachments(bodyStructure);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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: {
|
||||||
|
seen: flagsArray.includes("\\Seen"),
|
||||||
|
flagged: flagsArray.includes("\\Flagged"),
|
||||||
|
answered: flagsArray.includes("\\Answered"),
|
||||||
|
deleted: flagsArray.includes("\\Deleted"),
|
||||||
|
draft: flagsArray.includes("\\Draft"),
|
||||||
|
},
|
||||||
|
hasAttachments: attachments.length > 0,
|
||||||
|
attachments,
|
||||||
|
size,
|
||||||
|
preview,
|
||||||
|
folder,
|
||||||
|
contentFetched: false
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error fetching message ${id}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by date, newest first
|
||||||
|
emails.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
|
||||||
|
|
||||||
|
const mailboxes = await getMailboxes(client);
|
||||||
|
|
||||||
|
return {
|
||||||
|
emails,
|
||||||
|
totalEmails: totalMessages,
|
||||||
|
page,
|
||||||
|
perPage,
|
||||||
|
totalPages: Math.ceil(totalMessages / perPage),
|
||||||
|
folder,
|
||||||
|
mailboxes
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
// Don't logout, keep connection in pool
|
||||||
|
if (folder !== 'INBOX') {
|
||||||
|
try {
|
||||||
|
await client.mailboxClose();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error closing mailbox:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a single email with full content
|
||||||
|
*/
|
||||||
|
export async function getEmailContent(
|
||||||
|
userId: string,
|
||||||
|
emailId: string,
|
||||||
|
folder: string = 'INBOX'
|
||||||
|
): Promise<EmailMessage> {
|
||||||
|
const client = await getImapConnection(userId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.mailboxOpen(folder);
|
||||||
|
|
||||||
|
const message = await client.fetchOne(emailId, {
|
||||||
|
source: true,
|
||||||
|
envelope: true,
|
||||||
|
flags: true
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!message) {
|
||||||
|
throw new Error('Email not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { source, envelope, flags } = message;
|
||||||
|
|
||||||
|
// Parse the email content
|
||||||
|
const parsedEmail = await simpleParser(source.toString());
|
||||||
|
|
||||||
|
// Convert flags from Set to boolean checks
|
||||||
|
const flagsArray = Array.from(flags as Set<string>);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: emailId,
|
||||||
|
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: envelope.date || new Date(),
|
||||||
|
flags: {
|
||||||
|
seen: flagsArray.includes("\\Seen"),
|
||||||
|
flagged: flagsArray.includes("\\Flagged"),
|
||||||
|
answered: flagsArray.includes("\\Answered"),
|
||||||
|
deleted: flagsArray.includes("\\Deleted"),
|
||||||
|
draft: flagsArray.includes("\\Draft"),
|
||||||
|
},
|
||||||
|
hasAttachments: parsedEmail.attachments?.length > 0,
|
||||||
|
attachments: parsedEmail.attachments?.map(att => ({
|
||||||
|
filename: att.filename || 'attachment',
|
||||||
|
contentType: att.contentType,
|
||||||
|
size: att.size || 0
|
||||||
|
})),
|
||||||
|
html: parsedEmail.html || undefined,
|
||||||
|
text: parsedEmail.text || undefined,
|
||||||
|
content: parsedEmail.html || parsedEmail.textAsHtml || parsedEmail.text || '',
|
||||||
|
folder,
|
||||||
|
contentFetched: true
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
await client.mailboxClose();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error closing mailbox:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark an email as read or unread
|
||||||
|
*/
|
||||||
|
export async function markEmailReadStatus(
|
||||||
|
userId: string,
|
||||||
|
emailId: string,
|
||||||
|
isRead: boolean,
|
||||||
|
folder: string = 'INBOX'
|
||||||
|
): Promise<boolean> {
|
||||||
|
const client = await getImapConnection(userId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.mailboxOpen(folder);
|
||||||
|
|
||||||
|
if (isRead) {
|
||||||
|
await client.messageFlagsAdd(emailId, ['\\Seen']);
|
||||||
|
} else {
|
||||||
|
await client.messageFlagsRemove(emailId, ['\\Seen']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error marking email ${emailId} as ${isRead ? 'read' : 'unread'}:`, error);
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
await client.mailboxClose();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error closing mailbox:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send an email
|
||||||
|
*/
|
||||||
|
export async function sendEmail(
|
||||||
|
userId: string,
|
||||||
|
emailData: {
|
||||||
|
to: string;
|
||||||
|
cc?: string;
|
||||||
|
bcc?: string;
|
||||||
|
subject: string;
|
||||||
|
body: string;
|
||||||
|
attachments?: Array<{
|
||||||
|
name: string;
|
||||||
|
content: string;
|
||||||
|
type: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
): Promise<{ success: boolean; messageId?: string; error?: string }> {
|
||||||
|
const credentials = await getUserEmailCredentials(userId);
|
||||||
|
|
||||||
|
if (!credentials) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'No email credentials found'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create SMTP transporter
|
||||||
|
const transporter = nodemailer.createTransport({
|
||||||
|
host: 'smtp.infomaniak.com', // Using Infomaniak SMTP server
|
||||||
|
port: 587,
|
||||||
|
secure: false,
|
||||||
|
auth: {
|
||||||
|
user: credentials.email,
|
||||||
|
pass: credentials.password,
|
||||||
|
},
|
||||||
|
tls: {
|
||||||
|
rejectUnauthorized: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Verify connection
|
||||||
|
await transporter.verify();
|
||||||
|
|
||||||
|
// Prepare email options
|
||||||
|
const mailOptions = {
|
||||||
|
from: credentials.email,
|
||||||
|
to: emailData.to,
|
||||||
|
cc: emailData.cc || undefined,
|
||||||
|
bcc: emailData.bcc || undefined,
|
||||||
|
subject: emailData.subject || '(No subject)',
|
||||||
|
html: emailData.body,
|
||||||
|
attachments: emailData.attachments?.map(file => ({
|
||||||
|
filename: file.name,
|
||||||
|
content: file.content,
|
||||||
|
contentType: file.type
|
||||||
|
})) || []
|
||||||
|
};
|
||||||
|
|
||||||
|
// Send email
|
||||||
|
const info = await transporter.sendMail(mailOptions);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
messageId: info.messageId
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error sending email:', error);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get list of mailboxes (folders)
|
||||||
|
*/
|
||||||
|
export async function getMailboxes(client: ImapFlow): Promise<string[]> {
|
||||||
|
try {
|
||||||
|
const list = await client.list();
|
||||||
|
return list.map(mailbox => mailbox.path);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error listing mailboxes:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test email connection with given credentials
|
||||||
|
*/
|
||||||
|
export async function testEmailConnection(credentials: EmailCredentials): Promise<boolean> {
|
||||||
|
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();
|
||||||
|
await client.mailboxOpen('INBOX');
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Connection test failed:', error);
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
await client.logout();
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore logout errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user