feat: add SMTP connection verification and detailed delivery logging
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
48058c544e
commit
40ed89793b
+111
-29
@@ -90,6 +90,60 @@ function validateContactForm(data: ContactFormData): string | null {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// SMTP transporter (created once at module level for reuse)
|
||||||
|
// ============================================================
|
||||||
|
let smtpTransporter: any = null;
|
||||||
|
let smtpVerified = false;
|
||||||
|
|
||||||
|
// Verify SMTP connection once at module initialization
|
||||||
|
(async () => {
|
||||||
|
const smtpHost = process.env.SMTP_HOST;
|
||||||
|
const smtpUser = process.env.SMTP_USER;
|
||||||
|
const smtpPass = process.env.SMTP_PASS;
|
||||||
|
const smtpPort = parseInt(process.env.SMTP_PORT ?? '587', 10);
|
||||||
|
|
||||||
|
if (!smtpHost || !smtpUser || !smtpPass) {
|
||||||
|
logger.info('api.contact', 'SMTP not configured — skipping connection verification');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const nodemailer = await import('nodemailer');
|
||||||
|
smtpTransporter = nodemailer.default.createTransport({
|
||||||
|
host: smtpHost,
|
||||||
|
port: smtpPort,
|
||||||
|
secure: false, // Port 587 uses STARTTLS, not SSL
|
||||||
|
auth: { user: smtpUser, pass: smtpPass },
|
||||||
|
requireTLS: true, // Force STARTTLS upgrade
|
||||||
|
tls: {
|
||||||
|
rejectUnauthorized: false, // Handle Hostinger certificate issues
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await smtpTransporter.verify();
|
||||||
|
smtpVerified = true;
|
||||||
|
console.log('SMTP connection verified successfully');
|
||||||
|
logger.info('api.contact', 'SMTP connection verified successfully', {
|
||||||
|
host: smtpHost,
|
||||||
|
port: smtpPort,
|
||||||
|
user: smtpUser,
|
||||||
|
});
|
||||||
|
} catch (err: any) {
|
||||||
|
smtpVerified = false;
|
||||||
|
console.error('SMTP connection verification failed:', {
|
||||||
|
code: err?.code,
|
||||||
|
message: err?.message,
|
||||||
|
response: err?.response,
|
||||||
|
});
|
||||||
|
logger.error('api.contact', 'SMTP connection verification failed', {
|
||||||
|
code: err?.code,
|
||||||
|
message: err?.message,
|
||||||
|
response: err?.response,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Email sending via SMTP (nodemailer) or no-op in development
|
// Email sending via SMTP (nodemailer) or no-op in development
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -113,17 +167,20 @@ async function sendContactEmail(data: ContactFormData): Promise<void> {
|
|||||||
|
|
||||||
// Dynamic import to avoid bundling issues when nodemailer isn't installed
|
// Dynamic import to avoid bundling issues when nodemailer isn't installed
|
||||||
try {
|
try {
|
||||||
const nodemailer = await import('nodemailer');
|
// Reuse module-level transporter if available, otherwise create a new one
|
||||||
const transporter = nodemailer.default.createTransport({
|
if (!smtpTransporter) {
|
||||||
host: smtpHost,
|
const nodemailer = await import('nodemailer');
|
||||||
port: smtpPort,
|
smtpTransporter = nodemailer.default.createTransport({
|
||||||
secure: false, // Port 587 uses STARTTLS, not SSL
|
host: smtpHost,
|
||||||
auth: { user: smtpUser, pass: smtpPass },
|
port: smtpPort,
|
||||||
requireTLS: true, // Force STARTTLS upgrade
|
secure: false,
|
||||||
tls: {
|
auth: { user: smtpUser, pass: smtpPass },
|
||||||
rejectUnauthorized: false, // Handle Hostinger certificate issues
|
requireTLS: true,
|
||||||
},
|
tls: {
|
||||||
});
|
rejectUnauthorized: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const subjectLabels: Record<string, string> = {
|
const subjectLabels: Record<string, string> = {
|
||||||
'web-development': 'Web Development',
|
'web-development': 'Web Development',
|
||||||
@@ -137,7 +194,7 @@ async function sendContactEmail(data: ContactFormData): Promise<void> {
|
|||||||
|
|
||||||
const fromAddress = smtpFrom || smtpUser;
|
const fromAddress = smtpFrom || smtpUser;
|
||||||
|
|
||||||
await transporter.sendMail({
|
const info = await smtpTransporter.sendMail({
|
||||||
from: `"CompanySite Contact Form" <${fromAddress}>`,
|
from: `"CompanySite Contact Form" <${fromAddress}>`,
|
||||||
to: recipientEmail,
|
to: recipientEmail,
|
||||||
replyTo: data.email,
|
replyTo: data.email,
|
||||||
@@ -168,6 +225,8 @@ async function sendContactEmail(data: ContactFormData): Promise<void> {
|
|||||||
<p style="color:#888;font-size:12px;margin-top:16px">Submitted at: ${new Date().toISOString()}</p>
|
<p style="color:#888;font-size:12px;margin-top:16px">Submitted at: ${new Date().toISOString()}</p>
|
||||||
`,
|
`,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return info;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// If nodemailer isn't installed, treat as graceful degradation (log only)
|
// If nodemailer isn't installed, treat as graceful degradation (log only)
|
||||||
if (err instanceof Error && err.message.includes('Cannot find module')) {
|
if (err instanceof Error && err.message.includes('Cannot find module')) {
|
||||||
@@ -314,24 +373,47 @@ export const POST: APIRoute = async ({ request, clientAddress }) => {
|
|||||||
const recipientEmail = process.env.CONTACT_EMAIL_TO || 'unknown';
|
const recipientEmail = process.env.CONTACT_EMAIL_TO || 'unknown';
|
||||||
console.log('Sending contact email to: ' + recipientEmail);
|
console.log('Sending contact email to: ' + recipientEmail);
|
||||||
|
|
||||||
sendContactEmail(formData).catch(async (err) => {
|
sendContactEmail(formData)
|
||||||
console.error('Background email delivery failed:', {
|
.then((info) => {
|
||||||
recipient: recipientEmail,
|
console.log('Contact email delivered successfully to:', recipientEmail, {
|
||||||
email: formData.email,
|
messageId: info?.messageId,
|
||||||
subject: formData.subject,
|
});
|
||||||
error: err instanceof Error ? err.message : String(err),
|
logger.info('api.contact', 'Contact email delivered successfully', {
|
||||||
stack: err instanceof Error ? err.stack : undefined,
|
recipient: recipientEmail,
|
||||||
|
messageId: info?.messageId,
|
||||||
|
from: formData.email,
|
||||||
|
subject: formData.subject,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(async (err: any) => {
|
||||||
|
console.error('Background email delivery failed:', {
|
||||||
|
code: err?.code,
|
||||||
|
message: err?.message,
|
||||||
|
response: err?.response,
|
||||||
|
responseCode: err?.responseCode,
|
||||||
|
recipient: recipientEmail,
|
||||||
|
email: formData.email,
|
||||||
|
subject: formData.subject,
|
||||||
|
});
|
||||||
|
logger.error('api.contact', 'Background email delivery failed', {
|
||||||
|
code: err?.code,
|
||||||
|
message: err?.message,
|
||||||
|
response: err?.response,
|
||||||
|
responseCode: err?.responseCode,
|
||||||
|
email: formData.email,
|
||||||
|
subject: formData.subject,
|
||||||
|
});
|
||||||
|
await captureException(err, {
|
||||||
|
scope: 'api.contact.background',
|
||||||
|
extra: {
|
||||||
|
email: formData.email,
|
||||||
|
subject: formData.subject,
|
||||||
|
smtpCode: err?.code,
|
||||||
|
smtpResponse: err?.response,
|
||||||
|
smtpResponseCode: err?.responseCode,
|
||||||
|
},
|
||||||
|
});
|
||||||
});
|
});
|
||||||
logger.error('api.contact', 'Background email delivery failed', {
|
|
||||||
email: formData.email,
|
|
||||||
subject: formData.subject,
|
|
||||||
error: err instanceof Error ? err.message : String(err),
|
|
||||||
});
|
|
||||||
await captureException(err, {
|
|
||||||
scope: 'api.contact.background',
|
|
||||||
extra: { email: formData.email, subject: formData.subject },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
logApiRequest({
|
logApiRequest({
|
||||||
scope: 'api.contact',
|
scope: 'api.contact',
|
||||||
|
|||||||
Reference in New Issue
Block a user