253 lines
6.8 KiB
TypeScript
253 lines
6.8 KiB
TypeScript
import { requireAuth } from '~/server/utils/auth';
|
|
import { parseEmail, getIMAPConnection } from '~/server/utils/email-utils';
|
|
import { uploadFile } from '~/server/utils/minio';
|
|
import { getInterestByFieldAsync, updateInterest } from '~/server/utils/nocodb';
|
|
import type { ParsedMail } from 'mailparser';
|
|
|
|
interface ProcessedEOI {
|
|
clientName: string;
|
|
interestId?: string;
|
|
fileName: string;
|
|
processed: boolean;
|
|
error?: string;
|
|
}
|
|
|
|
export default defineEventHandler(async (event) => {
|
|
// Check authentication (x-tag header OR Keycloak session)
|
|
await requireAuth(event);
|
|
|
|
try {
|
|
console.log('[Process Sales EOIs] Starting email processing...');
|
|
|
|
// Sales email credentials
|
|
const credentials = {
|
|
user: 'sales@portnimara.com',
|
|
password: 'MDze7cSClQok8qWOf23X8Mb6lArdk0i42YnwJ1FskdtO2NCc9',
|
|
host: 'mail.portnimara.com',
|
|
port: 993,
|
|
tls: true
|
|
};
|
|
|
|
const connection = await getIMAPConnection(credentials);
|
|
const results: ProcessedEOI[] = [];
|
|
|
|
try {
|
|
// Open inbox
|
|
await new Promise((resolve, reject) => {
|
|
connection.openBox('INBOX', false, (err: any, box: any) => {
|
|
if (err) reject(err);
|
|
else resolve(box);
|
|
});
|
|
});
|
|
|
|
// Search for unread emails with attachments
|
|
const searchCriteria = ['UNSEEN'];
|
|
|
|
const messages = await new Promise<number[]>((resolve, reject) => {
|
|
connection.search(searchCriteria, (err: any, results: any) => {
|
|
if (err) reject(err);
|
|
else resolve(results || []);
|
|
});
|
|
});
|
|
|
|
console.log(`[Process Sales EOIs] Found ${messages.length} unread messages`);
|
|
|
|
for (const msgNum of messages) {
|
|
try {
|
|
const parsedEmail = await fetchAndParseEmail(connection, msgNum);
|
|
|
|
if (parsedEmail.attachments && parsedEmail.attachments.length > 0) {
|
|
// Process PDF attachments
|
|
for (const attachment of parsedEmail.attachments) {
|
|
if (attachment.contentType === 'application/pdf') {
|
|
const result = await processEOIAttachment(
|
|
attachment,
|
|
parsedEmail.subject || '',
|
|
parsedEmail.from?.text || ''
|
|
);
|
|
results.push(result);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Mark as read
|
|
connection.addFlags(msgNum, '\\Seen', (err: any) => {
|
|
if (err) console.error('Failed to mark message as read:', err);
|
|
});
|
|
} catch (error) {
|
|
console.error(`[Process Sales EOIs] Error processing message ${msgNum}:`, error);
|
|
}
|
|
}
|
|
|
|
connection.end();
|
|
} catch (error) {
|
|
connection.end();
|
|
throw error;
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
processed: results.length,
|
|
results
|
|
};
|
|
} catch (error: any) {
|
|
console.error('[Process Sales EOIs] Failed to process emails:', error);
|
|
throw createError({
|
|
statusCode: 500,
|
|
statusMessage: error.message || 'Failed to process sales emails',
|
|
});
|
|
}
|
|
});
|
|
|
|
async function fetchAndParseEmail(connection: any, msgNum: number): Promise<ParsedMail> {
|
|
return new Promise((resolve, reject) => {
|
|
const fetch = connection.fetch(msgNum, {
|
|
bodies: '',
|
|
struct: true
|
|
});
|
|
|
|
fetch.on('message', (msg: any) => {
|
|
let buffer = '';
|
|
|
|
msg.on('body', (stream: any) => {
|
|
stream.on('data', (chunk: any) => {
|
|
buffer += chunk.toString('utf8');
|
|
});
|
|
|
|
stream.once('end', async () => {
|
|
try {
|
|
const parsed = await parseEmail(buffer);
|
|
resolve(parsed);
|
|
} catch (err) {
|
|
reject(err);
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
fetch.once('error', reject);
|
|
});
|
|
}
|
|
|
|
async function processEOIAttachment(
|
|
attachment: any,
|
|
subject: string,
|
|
from: string
|
|
): Promise<ProcessedEOI> {
|
|
const fileName = attachment.filename || 'unknown.pdf';
|
|
|
|
try {
|
|
console.log(`[Process Sales EOIs] Processing attachment: ${fileName}`);
|
|
|
|
// Try to extract client name from filename or subject
|
|
const clientName = extractClientName(fileName, subject);
|
|
|
|
if (!clientName) {
|
|
return {
|
|
clientName: 'Unknown',
|
|
fileName,
|
|
processed: false,
|
|
error: 'Could not identify client from filename or subject'
|
|
};
|
|
}
|
|
|
|
// Find interest by client name
|
|
const interest = await getInterestByFieldAsync('Full Name', clientName);
|
|
|
|
if (!interest) {
|
|
return {
|
|
clientName,
|
|
fileName,
|
|
processed: false,
|
|
error: `No interest found for client: ${clientName}`
|
|
};
|
|
}
|
|
|
|
// Generate unique filename
|
|
const timestamp = Date.now();
|
|
const uploadFileName = `EOIs/${interest.Id}-${timestamp}-${fileName}`;
|
|
|
|
// Upload to MinIO
|
|
await uploadFile(uploadFileName, attachment.content, 'application/pdf');
|
|
|
|
// Update interest with EOI document
|
|
const documentData = {
|
|
title: fileName,
|
|
filename: uploadFileName,
|
|
url: `/api/files/proxy-download?fileName=${encodeURIComponent(uploadFileName)}`,
|
|
size: attachment.size,
|
|
mimetype: 'application/pdf',
|
|
icon: 'mdi-file-pdf-box',
|
|
uploadedAt: new Date().toISOString(),
|
|
source: 'email',
|
|
from: from
|
|
};
|
|
|
|
// Get existing documents and add new one
|
|
const existingDocs = interest['EOI Document'] || [];
|
|
const updatedDocs = [...existingDocs, documentData];
|
|
|
|
// Update interest
|
|
await updateInterest(interest.Id.toString(), {
|
|
'EOI Document': updatedDocs,
|
|
'EOI Status': 'Signed',
|
|
'Sales Process Level': 'Signed LOI and NDA'
|
|
});
|
|
|
|
console.log(`[Process Sales EOIs] Successfully processed EOI for ${clientName}`);
|
|
|
|
return {
|
|
clientName,
|
|
interestId: interest.Id.toString(),
|
|
fileName,
|
|
processed: true
|
|
};
|
|
} catch (error: any) {
|
|
console.error(`[Process Sales EOIs] Error processing attachment:`, error);
|
|
return {
|
|
clientName: 'Unknown',
|
|
fileName,
|
|
processed: false,
|
|
error: error.message
|
|
};
|
|
}
|
|
}
|
|
|
|
function extractClientName(fileName: string, subject: string): string | null {
|
|
// Try to extract from filename patterns like:
|
|
// "John_Doe_EOI_signed.pdf"
|
|
// "EOI_John_Doe.pdf"
|
|
// "John Doe - EOI.pdf"
|
|
|
|
// First try filename
|
|
const filePatterns = [
|
|
/^(.+?)[-_]EOI/i,
|
|
/EOI[-_](.+?)\.pdf/i,
|
|
/^(.+?)_signed/i,
|
|
/^(.+?)\s*-\s*EOI/i
|
|
];
|
|
|
|
for (const pattern of filePatterns) {
|
|
const match = fileName.match(pattern);
|
|
if (match && match[1]) {
|
|
return match[1].replace(/[_-]/g, ' ').trim();
|
|
}
|
|
}
|
|
|
|
// Then try subject
|
|
const subjectPatterns = [
|
|
/EOI\s+(?:for\s+)?(.+?)(?:\s+signed)?$/i,
|
|
/Signed\s+EOI\s*[-:]?\s*(.+)$/i,
|
|
/(.+?)\s*EOI\s*(?:signed|completed)/i
|
|
];
|
|
|
|
for (const pattern of subjectPatterns) {
|
|
const match = subject.match(pattern);
|
|
if (match && match[1]) {
|
|
return match[1].trim();
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|