778 lines
23 KiB
TypeScript
778 lines
23 KiB
TypeScript
'use server';
|
|
|
|
import 'server-only';
|
|
import { ImapFlow } from 'imapflow';
|
|
import nodemailer from 'nodemailer';
|
|
import { prisma } from '@/lib/prisma';
|
|
import { simpleParser } from 'mailparser';
|
|
import {
|
|
cacheEmailCredentials,
|
|
getCachedEmailCredentials,
|
|
cacheEmailList,
|
|
getCachedEmailList,
|
|
cacheEmailContent,
|
|
getCachedEmailContent,
|
|
cacheImapSession,
|
|
getCachedImapSession,
|
|
invalidateFolderCache,
|
|
invalidateEmailContentCache
|
|
} from '@/lib/redis';
|
|
import { EmailCredentials, EmailMessage, EmailAddress, EmailAttachment } from '@/lib/types';
|
|
|
|
// Types specific to this service
|
|
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}`);
|
|
try {
|
|
if (client.usable) {
|
|
client.logout().catch(err => {
|
|
console.error(`Error closing connection for ${key}:`, err);
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error(`Error checking connection status for ${key}:`, error);
|
|
} finally {
|
|
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,
|
|
accountId?: string
|
|
): Promise<ImapFlow> {
|
|
console.log(`Getting IMAP connection for user ${userId}${accountId ? ` account ${accountId}` : ''}`);
|
|
|
|
// First try to get credentials from Redis cache
|
|
let credentials = accountId
|
|
? await getCachedEmailCredentials(userId, accountId)
|
|
: await getCachedEmailCredentials(userId, 'default');
|
|
|
|
// If not in cache, get from database and cache them
|
|
if (!credentials) {
|
|
console.log(`Credentials not found in cache for ${userId}${accountId ? ` account ${accountId}` : ''}, attempting database lookup`);
|
|
credentials = await getUserEmailCredentials(userId, accountId);
|
|
if (!credentials) {
|
|
throw new Error('No email credentials found');
|
|
}
|
|
|
|
// Cache credentials for future use
|
|
await cacheEmailCredentials(userId, accountId || 'default', credentials);
|
|
}
|
|
|
|
// Validate credentials
|
|
if (!credentials.password) {
|
|
console.error(`Missing password in credentials for user ${userId}${accountId ? ` account ${accountId}` : ''}`);
|
|
throw new Error('No password configured');
|
|
}
|
|
|
|
if (!credentials.email || !credentials.host) {
|
|
console.error(`Incomplete credentials for user ${userId}${accountId ? ` account ${accountId}` : ''}`);
|
|
throw new Error('Invalid email credentials configuration');
|
|
}
|
|
|
|
// Use accountId in connection key to ensure different accounts get different connections
|
|
const connectionKey = `${userId}:${accountId || 'default'}`;
|
|
const existingConnection = connectionPool[connectionKey];
|
|
|
|
// Try to get session data from Redis
|
|
const sessionData = await getCachedImapSession(userId);
|
|
|
|
// Return existing connection if available and connected
|
|
if (existingConnection) {
|
|
try {
|
|
if (existingConnection.client.usable) {
|
|
existingConnection.lastUsed = Date.now();
|
|
console.log(`Reusing existing IMAP connection for ${connectionKey}`);
|
|
|
|
// Update session data in Redis
|
|
if (sessionData) {
|
|
await cacheImapSession(userId, {
|
|
...sessionData,
|
|
lastActive: 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
|
|
}
|
|
}
|
|
|
|
console.log(`Creating new IMAP connection for ${connectionKey}`);
|
|
|
|
// 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();
|
|
console.log(`Successfully connected to IMAP server for ${connectionKey}`);
|
|
|
|
// Store in connection pool
|
|
connectionPool[connectionKey] = {
|
|
client,
|
|
lastUsed: Date.now()
|
|
};
|
|
|
|
return client;
|
|
} catch (error: unknown) {
|
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
console.error(`IMAP connection error for ${connectionKey}:`, errorMessage);
|
|
throw new Error(`Failed to connect to IMAP server: ${errorMessage}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get user's email credentials from database
|
|
*/
|
|
export async function getUserEmailCredentials(userId: string, accountId?: string): Promise<EmailCredentials | null> {
|
|
const credentials = await prisma.mailCredentials.findFirst({
|
|
where: {
|
|
AND: [
|
|
{ userId },
|
|
accountId ? { id: accountId } : {}
|
|
]
|
|
}
|
|
});
|
|
|
|
if (!credentials) return null;
|
|
|
|
const mailCredentials = credentials as unknown as {
|
|
email: string;
|
|
password: string;
|
|
host: string;
|
|
port: number;
|
|
secure: boolean;
|
|
smtp_host: string | null;
|
|
smtp_port: number | null;
|
|
smtp_secure: boolean | null;
|
|
display_name: string | null;
|
|
color: string | null;
|
|
};
|
|
|
|
return {
|
|
email: mailCredentials.email,
|
|
password: mailCredentials.password,
|
|
host: mailCredentials.host,
|
|
port: mailCredentials.port,
|
|
secure: mailCredentials.secure,
|
|
smtp_host: mailCredentials.smtp_host || undefined,
|
|
smtp_port: mailCredentials.smtp_port || undefined,
|
|
smtp_secure: mailCredentials.smtp_secure || false,
|
|
display_name: mailCredentials.display_name || undefined,
|
|
color: mailCredentials.color || undefined
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Save or update user's email credentials
|
|
*/
|
|
export async function saveUserEmailCredentials(
|
|
userId: string,
|
|
accountId: string,
|
|
credentials: EmailCredentials
|
|
): Promise<void> {
|
|
console.log('Saving credentials for user:', userId, 'account:', accountId);
|
|
|
|
if (!credentials) {
|
|
throw new Error('No credentials provided');
|
|
}
|
|
|
|
// Extract only the fields that exist in the database schema
|
|
const dbCredentials = {
|
|
email: credentials.email,
|
|
password: credentials.password ?? '',
|
|
host: credentials.host,
|
|
port: credentials.port,
|
|
secure: credentials.secure ?? true,
|
|
smtp_host: credentials.smtp_host || null,
|
|
smtp_port: credentials.smtp_port || null,
|
|
smtp_secure: credentials.smtp_secure ?? false,
|
|
display_name: credentials.display_name || null,
|
|
color: credentials.color || null
|
|
};
|
|
|
|
try {
|
|
// Save to database using the unique constraint on [userId, email]
|
|
await prisma.mailCredentials.upsert({
|
|
where: {
|
|
id: await prisma.mailCredentials.findFirst({
|
|
where: {
|
|
AND: [
|
|
{ userId },
|
|
{ email: accountId }
|
|
]
|
|
},
|
|
select: { id: true }
|
|
}).then(result => result?.id ?? '')
|
|
},
|
|
update: dbCredentials,
|
|
create: {
|
|
userId,
|
|
...dbCredentials
|
|
}
|
|
});
|
|
|
|
// Cache the full credentials object in Redis
|
|
await cacheEmailCredentials(userId, accountId, credentials);
|
|
console.log('Successfully saved and cached credentials for user:', userId);
|
|
} catch (error) {
|
|
console.error('Error saving credentials:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Helper type for IMAP fetch options
|
|
interface FetchOptions {
|
|
envelope: boolean;
|
|
flags: boolean;
|
|
bodyStructure: boolean;
|
|
internalDate: boolean;
|
|
size: boolean;
|
|
bodyParts: { part: string; query: any; limit?: number }[];
|
|
}
|
|
|
|
/**
|
|
* Get list of emails for a user
|
|
*/
|
|
export async function getEmails(
|
|
userId: string,
|
|
folder: string,
|
|
page: number = 1,
|
|
perPage: number = 20,
|
|
accountId?: string
|
|
): Promise<EmailListResult> {
|
|
let client: ImapFlow | undefined;
|
|
|
|
try {
|
|
// Extract the actual folder name (remove account prefix if present)
|
|
const actualFolder = folder.includes(':') ? folder.split(':')[1] : folder;
|
|
console.log(`Fetching emails for folder: ${folder} (actual: ${actualFolder})`);
|
|
|
|
// Get IMAP connection
|
|
client = await getImapConnection(userId, accountId);
|
|
if (!client) {
|
|
throw new Error('Failed to establish IMAP connection');
|
|
}
|
|
|
|
// Open mailbox with the actual folder name
|
|
await client.mailboxOpen(actualFolder);
|
|
const mailbox = client.mailbox;
|
|
if (!mailbox || typeof mailbox === 'boolean') {
|
|
throw new Error(`Failed to open mailbox: ${actualFolder}`);
|
|
}
|
|
|
|
// Get total messages
|
|
const total = mailbox.exists || 0;
|
|
console.log(`Total messages in ${actualFolder}: ${total}`);
|
|
|
|
// If no messages, return empty result
|
|
if (total === 0) {
|
|
return {
|
|
emails: [],
|
|
totalEmails: 0,
|
|
page,
|
|
perPage,
|
|
totalPages: 0,
|
|
folder: actualFolder,
|
|
mailboxes: []
|
|
};
|
|
}
|
|
|
|
// Calculate message range for pagination
|
|
const start = Math.max(1, total - (page * perPage) + 1);
|
|
const end = Math.max(1, total - ((page - 1) * perPage));
|
|
console.log(`Fetching messages ${start}:${end} from ${actualFolder}`);
|
|
|
|
// Fetch messages
|
|
const messages = await client.fetch(`${start}:${end}`, {
|
|
envelope: true,
|
|
flags: true,
|
|
bodyStructure: true
|
|
});
|
|
|
|
const emails: EmailMessage[] = [];
|
|
for await (const message of messages) {
|
|
const email: EmailMessage = {
|
|
id: message.uid.toString(),
|
|
from: message.envelope.from?.map(addr => ({
|
|
name: addr.name || '',
|
|
address: addr.address || ''
|
|
})) || [],
|
|
to: message.envelope.to?.map(addr => ({
|
|
name: addr.name || '',
|
|
address: addr.address || ''
|
|
})) || [],
|
|
subject: message.envelope.subject || '',
|
|
date: message.envelope.date || new Date(),
|
|
flags: {
|
|
seen: message.flags.has('\\Seen'),
|
|
flagged: message.flags.has('\\Flagged'),
|
|
answered: message.flags.has('\\Answered'),
|
|
draft: message.flags.has('\\Draft'),
|
|
deleted: message.flags.has('\\Deleted')
|
|
},
|
|
size: message.size || 0,
|
|
hasAttachments: message.bodyStructure?.childNodes?.some(node => node.disposition === 'attachment') || false,
|
|
folder: actualFolder,
|
|
contentFetched: false,
|
|
accountId: accountId || 'default',
|
|
content: {
|
|
text: '',
|
|
html: ''
|
|
}
|
|
};
|
|
emails.push(email);
|
|
}
|
|
|
|
// Cache the result if we have an accountId
|
|
if (accountId) {
|
|
await cacheEmailList(userId, accountId, actualFolder, page, perPage, {
|
|
emails,
|
|
totalEmails: total,
|
|
page,
|
|
perPage,
|
|
totalPages: Math.ceil(total / perPage),
|
|
folder: actualFolder,
|
|
mailboxes: []
|
|
});
|
|
}
|
|
|
|
return {
|
|
emails,
|
|
totalEmails: total,
|
|
page,
|
|
perPage,
|
|
totalPages: Math.ceil(total / perPage),
|
|
folder: actualFolder,
|
|
mailboxes: []
|
|
};
|
|
} catch (error) {
|
|
console.error('Error fetching emails:', error);
|
|
throw error;
|
|
} finally {
|
|
if (client) {
|
|
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',
|
|
accountId?: string
|
|
): Promise<EmailMessage> {
|
|
// Validate parameters
|
|
if (!userId || !emailId || !folder) {
|
|
throw new Error('Missing required parameters');
|
|
}
|
|
|
|
// Validate UID format
|
|
if (!/^\d+$/.test(emailId)) {
|
|
throw new Error('Invalid email ID format: must be a numeric UID');
|
|
}
|
|
|
|
// Convert to number for IMAP
|
|
const numericId = parseInt(emailId, 10);
|
|
if (isNaN(numericId)) {
|
|
throw new Error('Email ID must be a number');
|
|
}
|
|
|
|
// Try to get from cache first, using account-specific cache key
|
|
const cacheKey = accountId ? `${accountId}:${folder}` : folder;
|
|
const cachedEmail = await getCachedEmailContent(userId, cacheKey, emailId);
|
|
if (cachedEmail) {
|
|
console.log(`Using cached email content for ${userId}:${accountId}:${emailId}`);
|
|
return cachedEmail;
|
|
}
|
|
|
|
console.log(`Cache miss for email content ${userId}:${accountId}:${emailId}, fetching from IMAP`);
|
|
|
|
const client = await getImapConnection(userId, accountId);
|
|
|
|
try {
|
|
// Remove accountId prefix if present in folder name
|
|
const actualFolder = folder.includes(':') ? folder.split(':')[1] : folder;
|
|
|
|
// Log connection details with account context
|
|
console.log(`[DEBUG] Fetching email ${emailId} from folder ${actualFolder} for account ${accountId || 'default'}`);
|
|
|
|
// Open mailbox with error handling
|
|
const mailbox = await client.mailboxOpen(actualFolder);
|
|
if (!mailbox || typeof mailbox === 'boolean') {
|
|
throw new Error(`Failed to open mailbox: ${actualFolder} for account ${accountId || 'default'}`);
|
|
}
|
|
|
|
// Log mailbox status with account context
|
|
console.log(`[DEBUG] Mailbox ${actualFolder} opened for account ${accountId || 'default'}, total messages: ${mailbox.exists}`);
|
|
|
|
// Get the UIDVALIDITY and UIDNEXT values
|
|
const uidValidity = mailbox.uidValidity;
|
|
const uidNext = mailbox.uidNext;
|
|
|
|
console.log(`[DEBUG] Mailbox UIDVALIDITY: ${uidValidity}, UIDNEXT: ${uidNext} for account ${accountId || 'default'}`);
|
|
|
|
// Validate UID exists in mailbox
|
|
if (numericId >= uidNext) {
|
|
throw new Error(`Email ID ${numericId} is greater than or equal to the highest UID in mailbox (${uidNext}) for account ${accountId || 'default'}`);
|
|
}
|
|
|
|
// First, try to get the sequence number for this UID
|
|
const searchResult = await client.search({ uid: numericId.toString() });
|
|
if (!searchResult || searchResult.length === 0) {
|
|
throw new Error(`Email with UID ${numericId} not found in folder ${actualFolder} for account ${accountId || 'default'}`);
|
|
}
|
|
|
|
const sequenceNumber = searchResult[0];
|
|
console.log(`[DEBUG] Found sequence number ${sequenceNumber} for UID ${numericId} in account ${accountId || 'default'}`);
|
|
|
|
// Now fetch using the sequence number
|
|
const message = await client.fetchOne(sequenceNumber.toString(), {
|
|
source: true,
|
|
envelope: true,
|
|
flags: true,
|
|
size: true
|
|
});
|
|
|
|
if (!message) {
|
|
throw new Error(`Email not found with sequence number ${sequenceNumber} in folder ${actualFolder} for account ${accountId || 'default'}`);
|
|
}
|
|
|
|
const { source, envelope, flags, size } = message;
|
|
|
|
// Parse the email content, ensuring all styles and structure are preserved
|
|
const parsedEmail = await simpleParser(source.toString(), {
|
|
skipHtmlToText: true,
|
|
keepCidLinks: true
|
|
});
|
|
|
|
// Convert flags from Set to boolean checks
|
|
const flagsArray = Array.from(flags as Set<string>);
|
|
|
|
// Preserve the raw HTML exactly as it was in the original email
|
|
const rawHtml = parsedEmail.html || '';
|
|
|
|
const email: EmailMessage = {
|
|
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
|
|
})),
|
|
content: {
|
|
text: parsedEmail.text || '',
|
|
html: rawHtml || ''
|
|
},
|
|
folder: actualFolder,
|
|
contentFetched: true,
|
|
size: size || 0,
|
|
accountId: accountId || 'default'
|
|
};
|
|
|
|
// Cache the email content with account-specific key
|
|
await cacheEmailContent(userId, cacheKey, emailId, email);
|
|
|
|
return email;
|
|
} catch (error) {
|
|
console.error('[ERROR] Email fetch failed:', {
|
|
userId,
|
|
emailId,
|
|
folder,
|
|
accountId,
|
|
error: error instanceof Error ? error.message : 'Unknown error',
|
|
details: error instanceof Error ? error.stack : undefined
|
|
});
|
|
throw error;
|
|
} 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']);
|
|
}
|
|
|
|
// Invalidate content cache since the flags changed
|
|
await invalidateEmailContentCache(userId, folder, emailId);
|
|
|
|
// Also invalidate folder cache because unread counts may have changed
|
|
await invalidateFolderCache(userId, folder, folder);
|
|
|
|
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 with user's SMTP settings if available
|
|
const transporter = nodemailer.createTransport({
|
|
host: credentials.smtp_host || 'smtp.infomaniak.com',
|
|
port: credentials.smtp_port || 587,
|
|
secure: credentials.smtp_secure || false,
|
|
auth: {
|
|
user: credentials.email,
|
|
pass: credentials.password,
|
|
},
|
|
tls: {
|
|
rejectUnauthorized: false
|
|
}
|
|
});
|
|
|
|
try {
|
|
const info = await transporter.sendMail({
|
|
from: credentials.email,
|
|
to: emailData.to,
|
|
cc: emailData.cc,
|
|
bcc: emailData.bcc,
|
|
subject: emailData.subject,
|
|
text: emailData.body,
|
|
html: emailData.body,
|
|
attachments: emailData.attachments?.map(att => ({
|
|
filename: att.name,
|
|
content: att.content,
|
|
contentType: att.type
|
|
})),
|
|
});
|
|
|
|
return {
|
|
success: true,
|
|
messageId: info.messageId
|
|
};
|
|
} catch (error) {
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Unknown error'
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get list of mailboxes from an IMAP connection
|
|
*/
|
|
export async function getMailboxes(client: ImapFlow, accountId?: string): Promise<string[]> {
|
|
try {
|
|
const mailboxes = await client.list();
|
|
|
|
// If we have an accountId, prefix the folder names to prevent namespace collisions
|
|
if (accountId) {
|
|
return mailboxes.map(mailbox => `${accountId}:${mailbox.path}`);
|
|
}
|
|
|
|
// For backward compatibility, return unprefixed names when no accountId
|
|
return mailboxes.map(mailbox => mailbox.path);
|
|
} catch (error) {
|
|
console.error('Error fetching mailboxes:', error);
|
|
// Return empty array on error to avoid showing incorrect folders
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Test IMAP and SMTP connections for an email account
|
|
*/
|
|
export async function testEmailConnection(credentials: EmailCredentials): Promise<{
|
|
imap: boolean;
|
|
smtp?: boolean;
|
|
error?: string;
|
|
folders?: string[];
|
|
}> {
|
|
console.log('Testing connection with:', {
|
|
...credentials,
|
|
password: '***'
|
|
});
|
|
|
|
// Test IMAP connection
|
|
try {
|
|
console.log(`Testing IMAP connection to ${credentials.host}:${credentials.port} for ${credentials.email}`);
|
|
const client = new ImapFlow({
|
|
host: credentials.host,
|
|
port: credentials.port,
|
|
secure: credentials.secure ?? true,
|
|
auth: {
|
|
user: credentials.email,
|
|
pass: credentials.password,
|
|
},
|
|
logger: false,
|
|
tls: {
|
|
rejectUnauthorized: false
|
|
}
|
|
});
|
|
|
|
await client.connect();
|
|
const folders = await getMailboxes(client);
|
|
await client.logout();
|
|
|
|
console.log(`IMAP connection successful for ${credentials.email}`);
|
|
console.log(`Found ${folders.length} folders:`, folders);
|
|
|
|
// Test SMTP connection if SMTP settings are provided
|
|
let smtpSuccess = false;
|
|
if (credentials.smtp_host && credentials.smtp_port) {
|
|
try {
|
|
console.log(`Testing SMTP connection to ${credentials.smtp_host}:${credentials.smtp_port}`);
|
|
const transporter = nodemailer.createTransport({
|
|
host: credentials.smtp_host,
|
|
port: credentials.smtp_port,
|
|
secure: credentials.smtp_secure ?? false,
|
|
auth: {
|
|
user: credentials.email,
|
|
pass: credentials.password,
|
|
},
|
|
tls: {
|
|
rejectUnauthorized: false
|
|
}
|
|
});
|
|
|
|
await transporter.verify();
|
|
console.log(`SMTP connection successful for ${credentials.email}`);
|
|
smtpSuccess = true;
|
|
} catch (smtpError) {
|
|
console.error(`SMTP connection failed for ${credentials.email}:`, smtpError);
|
|
return {
|
|
imap: true,
|
|
smtp: false,
|
|
error: `SMTP connection failed: ${smtpError instanceof Error ? smtpError.message : 'Unknown error'}`,
|
|
folders
|
|
};
|
|
}
|
|
}
|
|
|
|
return {
|
|
imap: true,
|
|
smtp: smtpSuccess,
|
|
folders
|
|
};
|
|
} catch (error) {
|
|
console.error(`IMAP connection failed for ${credentials.email}:`, error);
|
|
return {
|
|
imap: false,
|
|
error: `IMAP connection failed: ${error instanceof Error ? error.message : 'Unknown error'}`
|
|
};
|
|
}
|
|
} |