267 lines
7.9 KiB
TypeScript
267 lines
7.9 KiB
TypeScript
import Imap from 'imap';
|
|
import { simpleParser } from 'mailparser';
|
|
import { getCredentialsFromSession, decryptCredentials } from '~/server/utils/encryption';
|
|
import { listFiles, getFileStats } from '~/server/utils/minio';
|
|
|
|
interface EmailMessage {
|
|
id: string;
|
|
from: string;
|
|
to: string | string[];
|
|
subject: string;
|
|
body: string;
|
|
html?: string;
|
|
timestamp: string;
|
|
direction: 'sent' | 'received';
|
|
threadId?: string;
|
|
}
|
|
|
|
export default defineEventHandler(async (event) => {
|
|
const xTagHeader = getRequestHeader(event, "x-tag");
|
|
|
|
if (!xTagHeader || (xTagHeader !== "094ut234" && xTagHeader !== "pjnvü1230")) {
|
|
throw createError({ statusCode: 401, statusMessage: "unauthenticated" });
|
|
}
|
|
|
|
try {
|
|
const body = await readBody(event);
|
|
const { clientEmail, interestId, sessionId, limit = 50 } = body;
|
|
|
|
if (!clientEmail || !sessionId) {
|
|
throw createError({
|
|
statusCode: 400,
|
|
statusMessage: "Client email and sessionId are required"
|
|
});
|
|
}
|
|
|
|
// Get encrypted credentials from session
|
|
const encryptedCredentials = getCredentialsFromSession(sessionId);
|
|
if (!encryptedCredentials) {
|
|
throw createError({
|
|
statusCode: 401,
|
|
statusMessage: "Email credentials not found. Please reconnect."
|
|
});
|
|
}
|
|
|
|
// Decrypt credentials
|
|
const { email: userEmail, password } = decryptCredentials(encryptedCredentials);
|
|
|
|
// First, get emails from MinIO cache if available
|
|
const cachedEmails: EmailMessage[] = [];
|
|
if (interestId) {
|
|
try {
|
|
const files = await listFiles(`client-emails/interest-${interestId}/`, true) as any[];
|
|
for (const file of files) {
|
|
if (file.name.endsWith('.json') && !file.isFolder) {
|
|
try {
|
|
const response = await fetch(`${process.env.NUXT_MINIO_ENDPOINT || 'http://localhost:9000'}/${useRuntimeConfig().minio.bucketName}/${file.name}`);
|
|
const emailData = await response.json();
|
|
cachedEmails.push(emailData);
|
|
} catch (err) {
|
|
console.error('Failed to read cached email:', err);
|
|
}
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to list cached emails:', err);
|
|
}
|
|
}
|
|
|
|
// Configure IMAP
|
|
const imapConfig = {
|
|
user: userEmail,
|
|
password: password,
|
|
host: process.env.NUXT_EMAIL_IMAP_HOST || 'mail.portnimara.com',
|
|
port: parseInt(process.env.NUXT_EMAIL_IMAP_PORT || '993'),
|
|
tls: true,
|
|
tlsOptions: {
|
|
rejectUnauthorized: false
|
|
}
|
|
};
|
|
|
|
// Fetch emails from IMAP
|
|
const imapEmails: EmailMessage[] = await new Promise((resolve, reject) => {
|
|
const emails: EmailMessage[] = [];
|
|
const imap = new Imap(imapConfig);
|
|
|
|
imap.once('ready', () => {
|
|
// Search for emails to/from the client
|
|
imap.openBox('INBOX', true, (err, box) => {
|
|
if (err) {
|
|
reject(err);
|
|
return;
|
|
}
|
|
|
|
const searchCriteria = [
|
|
'OR',
|
|
['FROM', clientEmail],
|
|
['TO', clientEmail]
|
|
];
|
|
|
|
imap.search(searchCriteria, (err, results) => {
|
|
if (err) {
|
|
reject(err);
|
|
return;
|
|
}
|
|
|
|
if (!results || results.length === 0) {
|
|
imap.end();
|
|
resolve(emails);
|
|
return;
|
|
}
|
|
|
|
// Limit results
|
|
const messagesToFetch = results.slice(-limit);
|
|
|
|
const fetch = imap.fetch(messagesToFetch, {
|
|
bodies: '',
|
|
struct: true,
|
|
envelope: true
|
|
});
|
|
|
|
fetch.on('message', (msg, seqno) => {
|
|
msg.on('body', (stream, info) => {
|
|
simpleParser(stream as any, async (err: any, parsed: any) => {
|
|
if (err) {
|
|
console.error('Parse error:', err);
|
|
return;
|
|
}
|
|
|
|
const email: EmailMessage = {
|
|
id: parsed.messageId || `${Date.now()}-${seqno}`,
|
|
from: parsed.from?.text || '',
|
|
to: Array.isArray(parsed.to)
|
|
? parsed.to.map((addr: any) => addr.text).join(', ')
|
|
: parsed.to?.text || '',
|
|
subject: parsed.subject || '',
|
|
body: parsed.text || '',
|
|
html: parsed.html || undefined,
|
|
timestamp: parsed.date?.toISOString() || new Date().toISOString(),
|
|
direction: parsed.from?.text.includes(userEmail) ? 'sent' : 'received'
|
|
};
|
|
|
|
// Extract thread ID from headers if available
|
|
if (parsed.headers.has('in-reply-to')) {
|
|
email.threadId = parsed.headers.get('in-reply-to') as string;
|
|
}
|
|
|
|
emails.push(email);
|
|
});
|
|
});
|
|
});
|
|
|
|
fetch.once('error', (err) => {
|
|
console.error('Fetch error:', err);
|
|
reject(err);
|
|
});
|
|
|
|
fetch.once('end', () => {
|
|
imap.end();
|
|
});
|
|
});
|
|
});
|
|
|
|
// Also check Sent folder
|
|
imap.openBox('[Gmail]/Sent Mail', true, (err, box) => {
|
|
if (err) {
|
|
// Try common sent folder names
|
|
['Sent', 'Sent Items', 'Sent Messages'].forEach(folderName => {
|
|
imap.openBox(folderName, true, (err, box) => {
|
|
if (!err) {
|
|
// Search in sent folder
|
|
imap.search([['TO', clientEmail]], (err, results) => {
|
|
if (!err && results && results.length > 0) {
|
|
// Process sent emails similarly
|
|
}
|
|
});
|
|
}
|
|
});
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
imap.once('error', (err: any) => {
|
|
reject(err);
|
|
});
|
|
|
|
imap.once('end', () => {
|
|
resolve(emails);
|
|
});
|
|
|
|
imap.connect();
|
|
});
|
|
|
|
// Combine cached and IMAP emails, remove duplicates
|
|
const allEmails = [...cachedEmails, ...imapEmails];
|
|
const uniqueEmails = Array.from(
|
|
new Map(allEmails.map(email => [email.id, email])).values()
|
|
);
|
|
|
|
// Sort by timestamp
|
|
uniqueEmails.sort((a, b) =>
|
|
new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
|
|
);
|
|
|
|
// Group into threads
|
|
const threads = groupIntoThreads(uniqueEmails);
|
|
|
|
return {
|
|
success: true,
|
|
emails: uniqueEmails,
|
|
threads: threads
|
|
};
|
|
} catch (error) {
|
|
console.error('Failed to fetch email thread:', error);
|
|
if (error instanceof Error) {
|
|
throw createError({
|
|
statusCode: 500,
|
|
statusMessage: `Failed to fetch emails: ${error.message}`
|
|
});
|
|
} else {
|
|
throw createError({
|
|
statusCode: 500,
|
|
statusMessage: "An unexpected error occurred",
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
// Group emails into threads based on subject and references
|
|
function groupIntoThreads(emails: EmailMessage[]): any[] {
|
|
const threads = new Map<string, EmailMessage[]>();
|
|
|
|
emails.forEach(email => {
|
|
// Normalize subject by removing Re:, Fwd:, etc.
|
|
const normalizedSubject = email.subject
|
|
.replace(/^(Re:|Fwd:|Fw:)\s*/gi, '')
|
|
.trim();
|
|
|
|
// Find existing thread or create new one
|
|
let threadFound = false;
|
|
for (const [threadId, threadEmails] of threads.entries()) {
|
|
const threadSubject = threadEmails[0].subject
|
|
.replace(/^(Re:|Fwd:|Fw:)\s*/gi, '')
|
|
.trim();
|
|
|
|
if (threadSubject === normalizedSubject) {
|
|
threadEmails.push(email);
|
|
threadFound = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!threadFound) {
|
|
threads.set(email.id, [email]);
|
|
}
|
|
});
|
|
|
|
// Convert to array format
|
|
return Array.from(threads.entries()).map(([threadId, emails]) => ({
|
|
id: threadId,
|
|
subject: emails[0].subject,
|
|
emailCount: emails.length,
|
|
latestTimestamp: emails[emails.length - 1].timestamp,
|
|
emails: emails
|
|
}));
|
|
}
|