235 lines
8.5 KiB
TypeScript
235 lines
8.5 KiB
TypeScript
import nodemailer from 'nodemailer';
|
|
import { getCredentialsFromSession, decryptCredentials } from '~/server/utils/encryption';
|
|
import { uploadFile, getMinioClient } from '~/server/utils/minio';
|
|
import { updateInterest } from '~/server/utils/nocodb';
|
|
|
|
export default defineEventHandler(async (event) => {
|
|
const xTagHeader = getRequestHeader(event, "x-tag");
|
|
|
|
console.log('[Email Send] Request received with x-tag:', xTagHeader);
|
|
|
|
if (!xTagHeader || (xTagHeader !== "094ut234" && xTagHeader !== "pjnvü1230")) {
|
|
console.error('[Email Send] Authentication failed - invalid x-tag');
|
|
throw createError({ statusCode: 401, statusMessage: "unauthenticated" });
|
|
}
|
|
|
|
try {
|
|
const body = await readBody(event);
|
|
const {
|
|
to,
|
|
subject,
|
|
body: emailBody,
|
|
interestId,
|
|
sessionId,
|
|
includeSignature = true,
|
|
signatureConfig,
|
|
attachments = []
|
|
} = body;
|
|
|
|
console.log('[Email Send] Request body:', {
|
|
to,
|
|
subject,
|
|
hasBody: !!emailBody,
|
|
interestId,
|
|
hasSessionId: !!sessionId,
|
|
attachmentCount: attachments.length
|
|
});
|
|
|
|
if (!to || !subject || !emailBody || !sessionId) {
|
|
console.error('[Email Send] Missing required fields:', {
|
|
hasTo: !!to,
|
|
hasSubject: !!subject,
|
|
hasBody: !!emailBody,
|
|
hasSessionId: !!sessionId
|
|
});
|
|
throw createError({
|
|
statusCode: 400,
|
|
statusMessage: "To, subject, body, and sessionId are required"
|
|
});
|
|
}
|
|
|
|
// Get encrypted credentials from session
|
|
console.log('[Email Send] Getting credentials for sessionId:', sessionId);
|
|
const encryptedCredentials = getCredentialsFromSession(sessionId);
|
|
if (!encryptedCredentials) {
|
|
console.error('[Email Send] No credentials found for sessionId:', sessionId);
|
|
throw createError({
|
|
statusCode: 401,
|
|
statusMessage: "Email credentials not found. Please reconnect."
|
|
});
|
|
}
|
|
|
|
// Decrypt credentials
|
|
console.log('[Email Send] Decrypting credentials');
|
|
let email: string;
|
|
let password: string;
|
|
|
|
try {
|
|
const decrypted = decryptCredentials(encryptedCredentials);
|
|
email = decrypted.email;
|
|
password = decrypted.password;
|
|
console.log('[Email Send] Successfully decrypted credentials for:', email);
|
|
} catch (decryptError) {
|
|
console.error('[Email Send] Failed to decrypt credentials:', decryptError);
|
|
throw createError({
|
|
statusCode: 401,
|
|
statusMessage: "Failed to decrypt email credentials. Please reconnect."
|
|
});
|
|
}
|
|
|
|
// Get user info for signature
|
|
const defaultName = email.split('@')[0].replace('.', ' ').replace(/\b\w/g, l => l.toUpperCase());
|
|
|
|
// Build email signature with customizable fields
|
|
const sig = signatureConfig || {};
|
|
const contactLines = sig.contactInfo ? sig.contactInfo.split('\n').filter((line: string) => line.trim()).join('<br>') : '';
|
|
const signature = includeSignature ? `
|
|
<br><br>
|
|
<div style="margin-top: 20px; font-family: Arial, sans-serif;">
|
|
<img src="${process.env.NUXT_EMAIL_LOGO_URL || 'https://portnimara.com/logo.png'}" alt="Port Nimara" style="width: 150px; height: auto; max-height: 40px; margin-bottom: 10px; display: block;">
|
|
<div style="font-weight: bold;">${sig.name || defaultName}</div>
|
|
<div style="color: #666; margin-bottom: 8px;">${sig.title || 'Sales & Marketing Director'}</div>
|
|
<div style="font-weight: bold; margin-bottom: 12px;">${sig.company || 'Port Nimara'}</div>
|
|
${contactLines ? contactLines + '<br>' : ''}
|
|
<a href="mailto:${sig.email || email}" style="color: #0066cc;">${sig.email || email}</a>
|
|
<br><br>
|
|
<div style="color: #666; font-size: 12px; margin-top: 10px;">
|
|
The information in this message is confidential and may be privileged.<br>
|
|
It is intended for the addressee alone.<br>
|
|
If you are not the intended recipient it is prohibited to disclose, use or copy this information.<br>
|
|
Please contact the Sender immediately should this message have been transmitted incorrectly.
|
|
</div>
|
|
</div>` : '';
|
|
|
|
// Convert plain text body to HTML with line breaks
|
|
const htmlBody = emailBody.replace(/\n/g, '<br>') + signature;
|
|
|
|
// Configure SMTP transport
|
|
const transporter = nodemailer.createTransport({
|
|
host: process.env.NUXT_EMAIL_SMTP_HOST || 'mail.portnimara.com',
|
|
port: parseInt(process.env.NUXT_EMAIL_SMTP_PORT || '587'),
|
|
secure: false, // false for STARTTLS
|
|
auth: {
|
|
user: email,
|
|
pass: password
|
|
},
|
|
tls: {
|
|
rejectUnauthorized: false // Allow self-signed certificates
|
|
}
|
|
});
|
|
|
|
// Prepare email attachments
|
|
console.log('[Email Send] Processing', attachments.length, 'attachments');
|
|
const emailAttachments = [];
|
|
if (attachments && attachments.length > 0) {
|
|
const client = getMinioClient();
|
|
|
|
for (const attachment of attachments) {
|
|
try {
|
|
// Determine which bucket to use
|
|
const bucket = attachment.bucket || 'client-portal'; // Default to client-portal
|
|
console.log('[Email Send] Processing attachment:', attachment.name, 'from bucket:', bucket, 'path:', attachment.path);
|
|
|
|
// Download file from MinIO
|
|
const stream = await client.getObject(bucket, attachment.path);
|
|
const chunks: Buffer[] = [];
|
|
|
|
await new Promise((resolve, reject) => {
|
|
stream.on('data', (chunk) => chunks.push(chunk));
|
|
stream.on('end', resolve);
|
|
stream.on('error', reject);
|
|
});
|
|
|
|
const content = Buffer.concat(chunks);
|
|
console.log('[Email Send] Successfully downloaded attachment:', attachment.name, 'size:', content.length);
|
|
|
|
emailAttachments.push({
|
|
filename: attachment.name,
|
|
content: content
|
|
});
|
|
} catch (error) {
|
|
console.error(`[Email Send] Failed to attach file ${attachment.name} from bucket ${attachment.bucket}:`, error);
|
|
// Continue with other attachments
|
|
}
|
|
}
|
|
}
|
|
|
|
// Send email
|
|
const fromName = sig.name || defaultName;
|
|
console.log('[Email Send] Sending email from:', email, 'to:', to);
|
|
|
|
const info = await transporter.sendMail({
|
|
from: `"${fromName}" <${email}>`,
|
|
to: to,
|
|
subject: subject,
|
|
text: emailBody, // Plain text version
|
|
html: htmlBody, // HTML version with signature
|
|
attachments: emailAttachments
|
|
});
|
|
|
|
console.log('[Email Send] Email sent successfully, messageId:', info.messageId);
|
|
|
|
// Store email in MinIO for thread history and update EOI Time Sent
|
|
if (interestId) {
|
|
try {
|
|
const emailData = {
|
|
id: info.messageId,
|
|
from: email,
|
|
to: to,
|
|
subject: subject,
|
|
body: emailBody,
|
|
html: htmlBody,
|
|
timestamp: new Date().toISOString(),
|
|
direction: 'sent',
|
|
interestId: interestId
|
|
};
|
|
|
|
const objectName = `interest-${interestId}/${Date.now()}-sent.json`;
|
|
const buffer = Buffer.from(JSON.stringify(emailData, null, 2));
|
|
|
|
// Upload to the client-emails bucket
|
|
const { getMinioClient } = await import('~/server/utils/minio');
|
|
const client = getMinioClient();
|
|
await client.putObject('client-emails', objectName, buffer, buffer.length, {
|
|
'Content-Type': 'application/json',
|
|
});
|
|
|
|
// Update EOI Time Sent if the email contains an EOI link
|
|
if (emailBody.includes('signatures.portnimara.dev/sign/')) {
|
|
try {
|
|
await updateInterest(interestId, {
|
|
'EOI Time Sent': new Date().toISOString()
|
|
});
|
|
} catch (updateError) {
|
|
console.error('Failed to update EOI Time Sent:', updateError);
|
|
// Continue even if update fails
|
|
}
|
|
}
|
|
} catch (storageError) {
|
|
console.error('Failed to store email in MinIO:', storageError);
|
|
// Continue even if storage fails
|
|
}
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
message: "Email sent successfully",
|
|
messageId: info.messageId
|
|
};
|
|
} catch (error) {
|
|
console.error('[Email Send] Failed to send email:', error);
|
|
console.error('[Email Send] Error stack:', error instanceof Error ? error.stack : 'No stack trace');
|
|
if (error instanceof Error) {
|
|
throw createError({
|
|
statusCode: 500,
|
|
statusMessage: `Failed to send email: ${error.message}`
|
|
});
|
|
} else {
|
|
throw createError({
|
|
statusCode: 500,
|
|
statusMessage: "An unexpected error occurred",
|
|
});
|
|
}
|
|
}
|
|
});
|