panel 2 courier api restore
This commit is contained in:
parent
079d0a484b
commit
d73bf3b773
@ -1,11 +1,9 @@
|
||||
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';
|
||||
import { markEmailReadStatus } from '@/lib/services/email-service';
|
||||
|
||||
// Get the email list cache from main API route
|
||||
// This is a hack - ideally we'd use a shared module or Redis for caching
|
||||
// Global cache reference (will be moved to a proper cache solution in the future)
|
||||
declare global {
|
||||
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 });
|
||||
}
|
||||
|
||||
// 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 {
|
||||
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
|
||||
const mailboxes = await client.list();
|
||||
let emailFolder = 'INBOX'; // Default to INBOX
|
||||
let foundEmail = false;
|
||||
|
||||
// Search through folders to find the email
|
||||
for (const box of mailboxes) {
|
||||
try {
|
||||
await client.mailboxOpen(box.path);
|
||||
|
||||
// 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']);
|
||||
}
|
||||
// If not found in INBOX, try to find it in other common folders
|
||||
if (!success) {
|
||||
const commonFolders = ['Sent', 'Drafts', 'Trash', 'Spam', 'Junk'];
|
||||
|
||||
for (const folder of commonFolders) {
|
||||
success = await markEmailReadStatus(session.user.id, emailId, true, folder);
|
||||
if (success) {
|
||||
// If found in a different folder, invalidate that folder's cache
|
||||
invalidateCache(session.user.id, folder);
|
||||
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(
|
||||
{ error: 'Email not found' },
|
||||
{ error: 'Email not found in any folder' },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
// Invalidate the cache for this folder
|
||||
invalidateCache(session.user.id, emailFolder);
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} finally {
|
||||
try {
|
||||
await client.logout();
|
||||
} catch (e) {
|
||||
console.error('Error during logout:', e);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error marking email as read:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to mark email as read' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error marking email as read:', error);
|
||||
|
||||
@ -7,14 +7,9 @@
|
||||
*/
|
||||
|
||||
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';
|
||||
import { simpleParser } from 'mailparser';
|
||||
|
||||
// Simple in-memory cache for email content
|
||||
const emailContentCache = new Map<string, any>();
|
||||
import { getEmailContent, markEmailReadStatus } from '@/lib/services/email-service';
|
||||
|
||||
export async function GET(
|
||||
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 folder = searchParams.get("folder") || "INBOX";
|
||||
|
||||
// Create IMAP client
|
||||
let imapClient: any = null;
|
||||
try {
|
||||
imapClient = new ImapFlow({
|
||||
host: credentials.host,
|
||||
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;
|
||||
// Use the email service to fetch the email content
|
||||
const email = await getEmailContent(session.user.id, id, folder);
|
||||
|
||||
// Parse the full email content
|
||||
const parsedEmail = await simpleParser(source.toString());
|
||||
|
||||
// Return only the content
|
||||
// Return only what's needed for displaying the email
|
||||
return NextResponse.json({
|
||||
id,
|
||||
subject: envelope.subject,
|
||||
content: parsedEmail.html || parsedEmail.textAsHtml || parsedEmail.text || '',
|
||||
subject: email.subject,
|
||||
content: email.content,
|
||||
contentFetched: true
|
||||
});
|
||||
} catch (error: any) {
|
||||
@ -106,16 +52,6 @@ export async function GET(
|
||||
{ error: "Failed to fetch email content", message: error.message },
|
||||
{ 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) {
|
||||
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 folder = searchParams.get("folder") || "INBOX";
|
||||
|
||||
// Create IMAP client
|
||||
let imapClient: any = null;
|
||||
try {
|
||||
imapClient = new ImapFlow({
|
||||
host: credentials.host,
|
||||
port: credentials.port,
|
||||
secure: true,
|
||||
auth: {
|
||||
user: credentials.email,
|
||||
pass: credentials.password,
|
||||
},
|
||||
logger: false,
|
||||
});
|
||||
// Use the email service to mark the email
|
||||
const success = await markEmailReadStatus(
|
||||
session.user.id,
|
||||
id,
|
||||
action === 'mark-read',
|
||||
folder
|
||||
);
|
||||
|
||||
await imapClient.connect();
|
||||
|
||||
// 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);
|
||||
if (!success) {
|
||||
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 }
|
||||
);
|
||||
} 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) {
|
||||
console.error("Error in POST:", error);
|
||||
return NextResponse.json(
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getServerSession } from 'next-auth';
|
||||
import { authOptions } from '@/app/api/auth/[...nextauth]/route';
|
||||
import { prisma } from '@/lib/prisma';
|
||||
import { getUserEmailCredentials } from '@/lib/services/email-service';
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
@ -24,17 +24,8 @@ export async function GET() {
|
||||
}, { status: 401 });
|
||||
}
|
||||
|
||||
// Fetch mail credentials for this user
|
||||
const mailCredentials = await prisma.mailCredentials.findUnique({
|
||||
where: {
|
||||
userId: userId
|
||||
},
|
||||
select: {
|
||||
email: true,
|
||||
host: true,
|
||||
port: true
|
||||
}
|
||||
});
|
||||
// Fetch mail credentials for this user using our service
|
||||
const mailCredentials = await getUserEmailCredentials(userId);
|
||||
|
||||
// If no credentials found
|
||||
if (!mailCredentials) {
|
||||
@ -44,10 +35,14 @@ export async function GET() {
|
||||
}, { status: 404 });
|
||||
}
|
||||
|
||||
// Return the credentials
|
||||
// Return the credentials (excluding password)
|
||||
console.log(`Successfully retrieved mail credentials for user ID: ${userId}`);
|
||||
return NextResponse.json({
|
||||
credentials: mailCredentials
|
||||
credentials: {
|
||||
email: mailCredentials.email,
|
||||
host: mailCredentials.host,
|
||||
port: mailCredentials.port
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
|
||||
@ -1,107 +1,65 @@
|
||||
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';
|
||||
import {
|
||||
saveUserEmailCredentials,
|
||||
getUserEmailCredentials,
|
||||
testEmailConnection
|
||||
} from '@/lib/services/email-service';
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
console.log('Processing login POST request');
|
||||
// Authenticate user
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session?.user?.id) {
|
||||
console.log('No authenticated session found');
|
||||
return NextResponse.json(
|
||||
{ error: 'Unauthorized' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
// Get credentials from request
|
||||
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) {
|
||||
console.log('Missing required login fields');
|
||||
return NextResponse.json(
|
||||
{ error: 'Missing required fields' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Test IMAP connection
|
||||
console.log('Testing IMAP connection to:', host, port);
|
||||
const client = new ImapFlow({
|
||||
host: host,
|
||||
port: parseInt(port),
|
||||
secure: true,
|
||||
auth: {
|
||||
user: email,
|
||||
pass: password,
|
||||
},
|
||||
logger: false,
|
||||
emitLogs: false,
|
||||
tls: {
|
||||
rejectUnauthorized: false
|
||||
}
|
||||
// Test connection before saving
|
||||
const connectionSuccess = await testEmailConnection({
|
||||
email,
|
||||
password,
|
||||
host,
|
||||
port: parseInt(port)
|
||||
});
|
||||
|
||||
try {
|
||||
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 }
|
||||
);
|
||||
}
|
||||
if (!connectionSuccess) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to connect to email server' },
|
||||
{ status: 500 }
|
||||
{ error: 'Failed to connect to email server. Please check your credentials.' },
|
||||
{ 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) {
|
||||
console.error('Error in login handler:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'An unexpected error occurred' },
|
||||
{
|
||||
error: 'An unexpected error occurred',
|
||||
details: error instanceof Error ? error.message : 'Unknown error'
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
@ -109,37 +67,31 @@ export async function POST(request: Request) {
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
console.log('Fetching mail credentials');
|
||||
// Authenticate user
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session?.user?.id) {
|
||||
console.log('No authenticated session found');
|
||||
return NextResponse.json(
|
||||
{ error: 'Unauthorized' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
const credentials = await prisma.mailCredentials.findUnique({
|
||||
where: {
|
||||
userId: session.user.id
|
||||
},
|
||||
select: {
|
||||
email: true,
|
||||
host: true,
|
||||
port: true
|
||||
}
|
||||
});
|
||||
// Get user credentials from database
|
||||
const credentials = await getUserEmailCredentials(session.user.id);
|
||||
|
||||
if (!credentials) {
|
||||
console.log('No mail credentials found for user');
|
||||
return NextResponse.json(
|
||||
{ error: 'No stored credentials found' },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
console.log('Credentials found for:', credentials.email);
|
||||
return NextResponse.json(credentials);
|
||||
// Return credentials without the password
|
||||
return NextResponse.json({
|
||||
email: credentials.email,
|
||||
host: credentials.host,
|
||||
port: credentials.port
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching credentials:', error);
|
||||
return NextResponse.json(
|
||||
|
||||
@ -1,88 +1,21 @@
|
||||
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';
|
||||
import { simpleParser } from 'mailparser';
|
||||
import { getEmails } from '@/lib/services/email-service';
|
||||
|
||||
// Type definitions
|
||||
// Simple in-memory cache (will be removed in a future update)
|
||||
interface EmailCacheEntry {
|
||||
data: any;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
interface CredentialsCacheEntry {
|
||||
client: any; // Use any for ImapFlow to avoid type issues
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
// Email cache structure
|
||||
// Cache for 1 minute only
|
||||
const CACHE_TTL = 60 * 1000;
|
||||
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) {
|
||||
try {
|
||||
// Authenticate user
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session || !session.user?.id) {
|
||||
return NextResponse.json(
|
||||
@ -91,310 +24,45 @@ export async function GET(request: Request) {
|
||||
);
|
||||
}
|
||||
|
||||
// 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 }
|
||||
);
|
||||
}
|
||||
|
||||
// Extract query parameters
|
||||
const { searchParams } = new URL(request.url);
|
||||
const page = parseInt(searchParams.get("page") || "1");
|
||||
const perPage = parseInt(searchParams.get("perPage") || "20");
|
||||
const folder = searchParams.get("folder") || "INBOX";
|
||||
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 now = Date.now();
|
||||
const cachedEmails = emailListCache[cacheKey];
|
||||
|
||||
if (cachedEmails) {
|
||||
if (cachedEmails && now - cachedEmails.timestamp < CACHE_TTL) {
|
||||
console.log(`Using cached emails for ${cacheKey}`);
|
||||
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
|
||||
const cacheCredKey = `credentials:${session.user.id}`;
|
||||
let imapClient: any = credentialsCache[cacheCredKey]?.client || null;
|
||||
// Use the email service to fetch emails
|
||||
const emailsResult = await getEmails(
|
||||
session.user.id,
|
||||
folder,
|
||||
page,
|
||||
perPage,
|
||||
searchQuery
|
||||
);
|
||||
|
||||
if (!imapClient) {
|
||||
// Create IMAP client
|
||||
const connectWithRetry = async (retries = 3, delay = 1000): Promise<any> => {
|
||||
try {
|
||||
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 [];
|
||||
}
|
||||
// Cache the results
|
||||
emailListCache[cacheKey] = {
|
||||
data: emailsResult,
|
||||
timestamp: now
|
||||
};
|
||||
|
||||
// Setup paging
|
||||
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();
|
||||
}
|
||||
}
|
||||
return NextResponse.json(emailsResult);
|
||||
} catch (error: any) {
|
||||
console.error("Error in GET:", error);
|
||||
console.error("Error fetching emails:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Internal server error", message: error.message },
|
||||
{ error: "Failed to fetch emails", message: error.message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
@ -407,30 +75,25 @@ export async function POST(request: Request) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { emailId, folderName, action } = await request.json();
|
||||
const { emailId, folderName } = await request.json();
|
||||
|
||||
if (!emailId) {
|
||||
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;
|
||||
|
||||
// If folder is specified, only invalidate that folder's cache
|
||||
if (folderName) {
|
||||
Object.keys(emailListCache).forEach(key => {
|
||||
Object.keys(emailListCache).forEach(key => {
|
||||
if (folderName) {
|
||||
if (key.includes(`${userId}:${folderName}`)) {
|
||||
delete emailListCache[key];
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Otherwise invalidate all cache entries for this user
|
||||
Object.keys(emailListCache).forEach(key => {
|
||||
} else {
|
||||
if (key.startsWith(`${userId}:`)) {
|
||||
delete emailListCache[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
|
||||
@ -1,100 +1,53 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getServerSession } from 'next-auth';
|
||||
import { authOptions } from '@/app/api/auth/[...nextauth]/route';
|
||||
import { prisma } from '@/lib/prisma';
|
||||
import nodemailer from 'nodemailer';
|
||||
import { sendEmail } from '@/lib/services/email-service';
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
console.log('Starting email send process...');
|
||||
|
||||
// Authenticate user
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session?.user?.id) {
|
||||
console.log('No session found');
|
||||
return NextResponse.json(
|
||||
{ error: 'Unauthorized' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
// Get credentials from database
|
||||
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
|
||||
// Parse request body
|
||||
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) {
|
||||
console.log('No recipient specified');
|
||||
return NextResponse.json(
|
||||
{ error: 'Recipient is required' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Create SMTP transporter with Infomaniak SMTP settings
|
||||
console.log('Creating SMTP transporter...');
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: 'smtp.infomaniak.com',
|
||||
port: 587,
|
||||
secure: false,
|
||||
auth: {
|
||||
user: credentials.email,
|
||||
pass: credentials.password,
|
||||
},
|
||||
tls: {
|
||||
rejectUnauthorized: false
|
||||
},
|
||||
debug: true // Enable debug logging
|
||||
// Use email service to send the email
|
||||
const result = await sendEmail(session.user.id, {
|
||||
to,
|
||||
cc,
|
||||
bcc,
|
||||
subject,
|
||||
body,
|
||||
attachments
|
||||
});
|
||||
|
||||
// Verify SMTP connection
|
||||
console.log('Verifying SMTP connection...');
|
||||
try {
|
||||
await transporter.verify();
|
||||
console.log('SMTP connection verified successfully');
|
||||
} catch (error) {
|
||||
console.error('SMTP connection verification failed:', error);
|
||||
throw error;
|
||||
if (!result.success) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Failed to send email',
|
||||
details: result.error
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
// 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({
|
||||
success: true,
|
||||
messageId: info.messageId
|
||||
messageId: result.messageId
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error sending email:', error);
|
||||
|
||||
@ -1,88 +1,22 @@
|
||||
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 {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Unauthorized' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
// 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. 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 }
|
||||
);
|
||||
}
|
||||
/**
|
||||
* This route is deprecated. It redirects to the new courrier API endpoint.
|
||||
* @deprecated Use the /api/courrier endpoint instead
|
||||
*/
|
||||
export async function GET(request: Request) {
|
||||
console.warn('Deprecated: /api/mail route is being used. Update your code to use /api/courrier instead.');
|
||||
|
||||
// Extract query parameters
|
||||
const url = new URL(request.url);
|
||||
|
||||
// Redirect to the new API endpoint
|
||||
const redirectUrl = new URL('/api/courrier', url.origin);
|
||||
|
||||
// Copy all search parameters
|
||||
url.searchParams.forEach((value, key) => {
|
||||
redirectUrl.searchParams.set(key, value);
|
||||
});
|
||||
|
||||
return NextResponse.redirect(redirectUrl.toString());
|
||||
}
|
||||
@ -1,103 +1,32 @@
|
||||
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) {
|
||||
console.warn('Deprecated: /api/mail/send route is being used. Update your code to use /api/courrier/send instead.');
|
||||
|
||||
try {
|
||||
const credentials = getStoredCredentials();
|
||||
if (!credentials) {
|
||||
return NextResponse.json(
|
||||
{ error: 'No stored credentials found' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
// Clone the request body
|
||||
const body = await request.json();
|
||||
|
||||
// Make a new request to the courrier API
|
||||
const newRequest = new Request(new URL('/api/courrier/send', request.url).toString(), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
|
||||
// Prepare email options
|
||||
const mailOptions = {
|
||||
from: credentials.email,
|
||||
to,
|
||||
cc,
|
||||
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 });
|
||||
|
||||
// Forward the request
|
||||
const response = await fetch(newRequest);
|
||||
const data = await response.json();
|
||||
|
||||
return NextResponse.json(data, { status: response.status });
|
||||
} catch (error) {
|
||||
console.error('Error sending email:', error);
|
||||
console.error('Error forwarding to courrier/send:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to send email' },
|
||||
{ status: 500 }
|
||||
|
||||
@ -164,19 +164,22 @@ export default function ComposeEmail({
|
||||
let formattedContent = '';
|
||||
|
||||
if (forwardFrom) {
|
||||
formattedContent = `
|
||||
<div class="forwarded-message">
|
||||
<p>---------- Forwarded message ---------</p>
|
||||
<p>From: ${decoded.from || ''}</p>
|
||||
<p>Date: ${formatDate(decoded.date)}</p>
|
||||
<p>Subject: ${decoded.subject || ''}</p>
|
||||
<p>To: ${decoded.to || ''}</p>
|
||||
<br>
|
||||
<div class="email-content prose prose-sm max-w-none dark:prose-invert">
|
||||
${decoded.html || `<pre>${decoded.text || ''}</pre>`}
|
||||
</div>
|
||||
// Create a clean header for the forwarded email
|
||||
const headerHtml = `
|
||||
<div style="border-bottom: 1px solid #e2e2e2; margin-bottom: 15px; padding-bottom: 15px; font-family: Arial, sans-serif;">
|
||||
<p style="margin: 4px 0;">---------- Forwarded message ---------</p>
|
||||
<p style="margin: 4px 0;"><b>From:</b> ${decoded.from || ''}</p>
|
||||
<p style="margin: 4px 0;"><b>Date:</b> ${formatDate(decoded.date)}</p>
|
||||
<p style="margin: 4px 0;"><b>Subject:</b> ${decoded.subject || ''}</p>
|
||||
<p style="margin: 4px 0;"><b>To:</b> ${decoded.to || ''}</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Use the original HTML as-is without DOMPurify or any modification
|
||||
formattedContent = `
|
||||
${headerHtml}
|
||||
${decoded.html || decoded.text || 'No content available'}
|
||||
`;
|
||||
} else {
|
||||
formattedContent = `
|
||||
<div class="quoted-message">
|
||||
|
||||
@ -3,11 +3,8 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { RefreshCw, Mail } from "lucide-react";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { formatDistance } from 'date-fns/formatDistance';
|
||||
import { fr } from 'date-fns/locale/fr';
|
||||
import { useRouter } from "next/navigation";
|
||||
import { RefreshCw, MessageSquare, Mail, MailOpen, Loader2 } from "lucide-react";
|
||||
import Link from 'next/link';
|
||||
|
||||
interface Email {
|
||||
id: string;
|
||||
@ -28,161 +25,125 @@ interface EmailResponse {
|
||||
|
||||
export function Email() {
|
||||
const [emails, setEmails] = useState<Email[]>([]);
|
||||
const [mailUrl, setMailUrl] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const { data: session, status } = useSession();
|
||||
const router = useRouter();
|
||||
const [mailUrl, setMailUrl] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchEmails();
|
||||
}, []);
|
||||
|
||||
const fetchEmails = async (isRefresh = false) => {
|
||||
if (status !== 'authenticated') {
|
||||
setError('Please sign in to view emails');
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
if (isRefresh) setRefreshing(true);
|
||||
if (!isRefresh) setLoading(true);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/mail');
|
||||
|
||||
const response = await fetch('/api/courrier?folder=INBOX&page=1&perPage=5');
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.error || 'Failed to fetch emails');
|
||||
throw new Error('Failed to fetch emails');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
const validatedEmails = data.emails.map((email: any) => ({
|
||||
id: email.id || Date.now().toString(),
|
||||
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');
|
||||
} catch (error) {
|
||||
console.error('Error fetching emails:', error);
|
||||
setError('Failed to load emails');
|
||||
setEmails([]);
|
||||
} finally {
|
||||
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) => {
|
||||
try {
|
||||
const date = new Date(dateString);
|
||||
return formatDistance(date, new Date(), {
|
||||
addSuffix: true,
|
||||
locale: fr
|
||||
});
|
||||
} catch (err) {
|
||||
return dateString;
|
||||
return new Intl.DateTimeFormat('fr-FR', {
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
}).format(date);
|
||||
} catch (e) {
|
||||
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 (
|
||||
<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 space-x-4 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>
|
||||
<Card className="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 flex items-center gap-2">
|
||||
<MessageSquare className="h-5 w-5 text-gray-600" />
|
||||
Emails non lus
|
||||
</CardTitle>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => fetchEmails(true)}
|
||||
disabled={refreshing}
|
||||
className={`${refreshing ? 'animate-spin' : ''} text-gray-600 hover:text-gray-900`}
|
||||
disabled={loading}
|
||||
>
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
{loading ?
|
||||
<Loader2 className="h-4 w-4 animate-spin" /> :
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
}
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent className="p-3">
|
||||
<CardContent className="p-4">
|
||||
{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">
|
||||
{emails.length === 0 ? (
|
||||
<p className="text-center text-gray-500">
|
||||
{loading ? 'Loading emails...' : 'No unread emails'}
|
||||
</p>
|
||||
) : (
|
||||
emails.map((email) => (
|
||||
<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 className="space-y-3">
|
||||
{emails.map((email) => (
|
||||
<div key={email.id} className="flex items-start gap-3 py-1 border-b border-gray-100 last:border-0">
|
||||
<div className="pt-1">
|
||||
{email.read ?
|
||||
<MailOpen className="h-4 w-4 text-gray-400" /> :
|
||||
<Mail className="h-4 w-4 text-blue-500" />
|
||||
}
|
||||
</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>
|
||||
)}
|
||||
|
||||
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