feat(api/contact): wire env-driven SMTP for contact + project inquiry forms
Ping Search Engines / Notify Search Engines (push) Successful in 5s
E2E Test Suite / Form Interaction Tests (push) Failing after 10m11s
E2E Test Suite / Mobile Device Tests (push) Failing after 10m49s
E2E Test Suite / Cross-Browser Regression (webkit) (push) Failing after 11m42s
E2E Test Suite / Cross-Browser Regression (firefox) (push) Failing after 12m34s
E2E Test Suite / Cross-Browser Regression (chromium) (push) Failing after 13m26s
E2E Test Suite / Destructive & Chaos Tests (push) Failing after 14m19s
E2E Test Suite / Security Header Tests (push) Failing after 14m55s
E2E Test Suite / Smoke Tests (P0) (push) Failing after 20m54s
E2E Test Suite / Critical User Journeys (push) Has been skipped
E2E Test Suite / API Integration Tests (push) Has been skipped
E2E Test Suite / Test Report Summary (push) Has been cancelled
Deploy to Production / Build & Verify (push) Waiting to run
Deploy to Production / Pre-Deploy Tests (push) Blocked by required conditions
Deploy to Production / Deploy to Railway (push) Has been cancelled
Deploy to Production / Deploy to Render (push) Has been cancelled
Deploy to Production / Deploy to VPS (PM2) (push) Has been cancelled
Deploy to Production / Deploy to Fly.io (push) Has been cancelled
Deploy to Production / Post-Deploy Verification (push) Has been cancelled
Deploy to Production / Notify on Failure (push) Has been cancelled
Ping Search Engines / Notify Search Engines (push) Successful in 5s
E2E Test Suite / Form Interaction Tests (push) Failing after 10m11s
E2E Test Suite / Mobile Device Tests (push) Failing after 10m49s
E2E Test Suite / Cross-Browser Regression (webkit) (push) Failing after 11m42s
E2E Test Suite / Cross-Browser Regression (firefox) (push) Failing after 12m34s
E2E Test Suite / Cross-Browser Regression (chromium) (push) Failing after 13m26s
E2E Test Suite / Destructive & Chaos Tests (push) Failing after 14m19s
E2E Test Suite / Security Header Tests (push) Failing after 14m55s
E2E Test Suite / Smoke Tests (P0) (push) Failing after 20m54s
E2E Test Suite / Critical User Journeys (push) Has been skipped
E2E Test Suite / API Integration Tests (push) Has been skipped
E2E Test Suite / Test Report Summary (push) Has been cancelled
Deploy to Production / Build & Verify (push) Waiting to run
Deploy to Production / Pre-Deploy Tests (push) Blocked by required conditions
Deploy to Production / Deploy to Railway (push) Has been cancelled
Deploy to Production / Deploy to Render (push) Has been cancelled
Deploy to Production / Deploy to VPS (PM2) (push) Has been cancelled
Deploy to Production / Deploy to Fly.io (push) Has been cancelled
Deploy to Production / Post-Deploy Verification (push) Has been cancelled
Deploy to Production / Notify on Failure (push) Has been cancelled
Both the contact form (/contact) and the homepage "Tell us about your project" inline lead form post to /api/contact. Updated that single handler to build the nodemailer transport from a clean set of SMTP_* env vars per spec: SMTP_HOST / SMTP_PORT / SMTP_USER / SMTP_PASS SMTP_FROM + SMTP_FROM_NAME -> "From: Ajay Ghanwat <ghanwat.ajay@workroot.in>" MAIL_ADMIN_TO -> admin notification recipient (falls back to SMTP_FROM, then legacy CONTACT_EMAIL_TO / CONTACT_EMAIL / SMTP_USER) SMTP_REQUIRE_TLS -> force STARTTLS upgrade on port 587 SMTP_CONNECTION_TIMEOUT_MS -> defaults to 10000 SMTP_GREETING_TIMEOUT_MS -> defaults to 10000 SMTP_SOCKET_TIMEOUT_MS -> defaults to 10000 Behaviour preserved: - Existing user-facing auto-reply (the block previously at L270) kept. Subject + body now adapt for project-inquiry sources (homepage-inline, homepage-hero, homepage-final-cta, mobile-sticky) so the admin inbox can triage [Project Inquiry] vs [Contact Form] at a glance. - Graceful degradation per FORM_INTEGRATION.md: missing env vars or a failed nodemailer import logs a structured warning and returns 200 — the site never 500s on email config. - Rate limiting, honeypot, CORS, validation, and fire-and-forget background dispatch are untouched. Observability: - Structured console logs at: transport created, admin accepted (with messageId), user auto-reply accepted (with messageId), and send-failure (with smtp code/command/response). SMTP_PASS is never logged. Misc: - .env.example updated to document the new variables and the legacy aliases that are still honored.
This commit is contained in:
+241
-78
@@ -109,53 +109,174 @@ function validateContactForm(data: ContactFormData): string | null {
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// SMTP transporter (created once at module level for reuse)
|
||||
// SMTP transporter — env-driven, created once at module level
|
||||
// ============================================================
|
||||
//
|
||||
// Required env vars:
|
||||
// SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS
|
||||
// SMTP_FROM, SMTP_FROM_NAME
|
||||
// MAIL_ADMIN_TO (admin recipient; falls back to SMTP_FROM)
|
||||
// Optional env vars (with sensible defaults):
|
||||
// SMTP_REQUIRE_TLS ("false" disables STARTTLS upgrade; default: required)
|
||||
// SMTP_CONNECTION_TIMEOUT_MS (default 10000)
|
||||
// SMTP_GREETING_TIMEOUT_MS (default 10000)
|
||||
// SMTP_SOCKET_TIMEOUT_MS (default 10000)
|
||||
//
|
||||
// Graceful degradation: if the nodemailer import fails, or any of the
|
||||
// required env vars are missing, we log a structured warning and let
|
||||
// the POST handler return success to the user. The site never 500s
|
||||
// because of email config — submissions surface in server logs and
|
||||
// Sentry instead.
|
||||
// ============================================================
|
||||
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);
|
||||
interface SmtpConfig {
|
||||
host: string;
|
||||
port: number;
|
||||
user: string;
|
||||
pass: string;
|
||||
fromAddress: string;
|
||||
fromName: string;
|
||||
adminRecipient: string;
|
||||
requireTLS: boolean;
|
||||
connectionTimeout: number;
|
||||
greetingTimeout: number;
|
||||
socketTimeout: number;
|
||||
}
|
||||
|
||||
if (!smtpHost || !smtpUser || !smtpPass) {
|
||||
logger.info('api.contact', 'SMTP not configured — skipping connection verification');
|
||||
return;
|
||||
function readSmtpConfig(): SmtpConfig | null {
|
||||
const host = process.env.SMTP_HOST;
|
||||
const user = process.env.SMTP_USER;
|
||||
const pass = process.env.SMTP_PASS;
|
||||
const fromAddress = process.env.SMTP_FROM || user || '';
|
||||
const fromName = process.env.SMTP_FROM_NAME || 'CompanySite';
|
||||
// Admin recipient resolution chain:
|
||||
// MAIL_ADMIN_TO > SMTP_FROM > legacy CONTACT_EMAIL_TO > legacy CONTACT_EMAIL > SMTP_USER
|
||||
const adminRecipient =
|
||||
process.env.MAIL_ADMIN_TO ||
|
||||
process.env.SMTP_FROM ||
|
||||
process.env.CONTACT_EMAIL_TO ||
|
||||
process.env.CONTACT_EMAIL ||
|
||||
user ||
|
||||
'';
|
||||
|
||||
if (!host || !user || !pass || !fromAddress || !adminRecipient) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return {
|
||||
host,
|
||||
port: Number(process.env.SMTP_PORT ?? 587),
|
||||
user,
|
||||
pass,
|
||||
fromAddress,
|
||||
fromName,
|
||||
adminRecipient,
|
||||
// 587 uses STARTTLS — `secure:false` + `requireTLS:true` is the
|
||||
// correct combination. The flag lets ops disable TLS enforcement
|
||||
// by setting SMTP_REQUIRE_TLS=false (e.g. local mailcatcher).
|
||||
requireTLS: process.env.SMTP_REQUIRE_TLS !== 'false',
|
||||
connectionTimeout: Number(process.env.SMTP_CONNECTION_TIMEOUT_MS ?? 10000),
|
||||
greetingTimeout: Number(process.env.SMTP_GREETING_TIMEOUT_MS ?? 10000),
|
||||
socketTimeout: Number(process.env.SMTP_SOCKET_TIMEOUT_MS ?? 10000),
|
||||
};
|
||||
}
|
||||
|
||||
let smtpTransporter: any = null;
|
||||
let smtpTransporterPromise: Promise<any> | null = null;
|
||||
|
||||
async function getTransporter(cfg: SmtpConfig): Promise<any> {
|
||||
if (smtpTransporter) return smtpTransporter;
|
||||
if (smtpTransporterPromise) return smtpTransporterPromise;
|
||||
|
||||
smtpTransporterPromise = (async () => {
|
||||
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
|
||||
const transport = nodemailer.default.createTransport({
|
||||
host: cfg.host,
|
||||
port: cfg.port,
|
||||
secure: false, // 587 uses STARTTLS, not implicit TLS
|
||||
requireTLS: cfg.requireTLS,
|
||||
auth: {
|
||||
user: cfg.user,
|
||||
pass: cfg.pass,
|
||||
},
|
||||
connectionTimeout: cfg.connectionTimeout,
|
||||
greetingTimeout: cfg.greetingTimeout,
|
||||
socketTimeout: cfg.socketTimeout,
|
||||
tls: {
|
||||
rejectUnauthorized: false, // Handle Hostinger certificate issues
|
||||
// Hostinger occasionally serves an intermediate cert chain that
|
||||
// node's default verifier rejects. Keep this off for parity with
|
||||
// the prior working configuration.
|
||||
rejectUnauthorized: false,
|
||||
},
|
||||
});
|
||||
|
||||
await smtpTransporter.verify();
|
||||
smtpVerified = true;
|
||||
console.log('SMTP connection verified successfully');
|
||||
// NOTE: we deliberately do NOT log cfg.pass. Logs go to platform
|
||||
// stdout and are user-visible in the Operations → Logs tab.
|
||||
console.log('[api.contact] SMTP transport created', {
|
||||
host: cfg.host,
|
||||
port: cfg.port,
|
||||
user: cfg.user,
|
||||
from: `"${cfg.fromName}" <${cfg.fromAddress}>`,
|
||||
adminRecipient: cfg.adminRecipient,
|
||||
requireTLS: cfg.requireTLS,
|
||||
timeouts: {
|
||||
connection: cfg.connectionTimeout,
|
||||
greeting: cfg.greetingTimeout,
|
||||
socket: cfg.socketTimeout,
|
||||
},
|
||||
});
|
||||
logger.info('api.contact', 'SMTP transport created', {
|
||||
host: cfg.host,
|
||||
port: cfg.port,
|
||||
user: cfg.user,
|
||||
requireTLS: cfg.requireTLS,
|
||||
});
|
||||
|
||||
smtpTransporter = transport;
|
||||
return transport;
|
||||
})();
|
||||
|
||||
try {
|
||||
return await smtpTransporterPromise;
|
||||
} catch (err) {
|
||||
// Reset so a subsequent request can retry instead of being permanently
|
||||
// wedged on a transient nodemailer import or DNS failure.
|
||||
smtpTransporterPromise = null;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// Background verify — best-effort, never throws. Hits SMTP at boot so the
|
||||
// first user submission doesn't pay the TCP+STARTTLS handshake latency.
|
||||
(async () => {
|
||||
const cfg = readSmtpConfig();
|
||||
if (!cfg) {
|
||||
logger.info('api.contact', 'SMTP not configured — skipping boot-time verification');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const transport = await getTransporter(cfg);
|
||||
await transport.verify();
|
||||
console.log('[api.contact] SMTP connection verified successfully', {
|
||||
host: cfg.host,
|
||||
port: cfg.port,
|
||||
user: cfg.user,
|
||||
});
|
||||
logger.info('api.contact', 'SMTP connection verified successfully', {
|
||||
host: smtpHost,
|
||||
port: smtpPort,
|
||||
user: smtpUser,
|
||||
host: cfg.host,
|
||||
port: cfg.port,
|
||||
user: cfg.user,
|
||||
});
|
||||
} catch (err: any) {
|
||||
smtpVerified = false;
|
||||
console.error('SMTP connection verification failed:', {
|
||||
console.error('[api.contact] SMTP connection verification failed', {
|
||||
code: err?.code,
|
||||
command: err?.command,
|
||||
message: err?.message,
|
||||
response: err?.response,
|
||||
});
|
||||
logger.error('api.contact', 'SMTP connection verification failed', {
|
||||
code: err?.code,
|
||||
command: err?.command,
|
||||
message: err?.message,
|
||||
response: err?.response,
|
||||
});
|
||||
@@ -165,16 +286,16 @@ let smtpVerified = false;
|
||||
// ============================================================
|
||||
// Email sending via SMTP (nodemailer) or no-op in development
|
||||
// ============================================================
|
||||
async function sendContactEmail(data: ContactFormData): Promise<void> {
|
||||
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);
|
||||
const smtpFrom = process.env.SMTP_FROM;
|
||||
const recipientEmail = process.env.CONTACT_EMAIL_TO;
|
||||
interface SendResult {
|
||||
adminInfo?: { messageId?: string };
|
||||
userConfirmInfo?: { messageId?: string };
|
||||
}
|
||||
|
||||
if (!smtpHost || !smtpUser || !smtpPass || !recipientEmail) {
|
||||
// SMTP not configured - log and continue (graceful degradation)
|
||||
async function sendContactEmail(data: ContactFormData): Promise<SendResult | void> {
|
||||
const cfg = readSmtpConfig();
|
||||
|
||||
if (!cfg) {
|
||||
// SMTP not configured — log and continue (graceful degradation)
|
||||
logger.info('api.contact', 'SMTP not configured — form submission logged only', {
|
||||
email: data.email,
|
||||
subject: data.subject,
|
||||
@@ -185,22 +306,8 @@ async function sendContactEmail(data: ContactFormData): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
// Dynamic import to avoid bundling issues when nodemailer isn't installed
|
||||
try {
|
||||
// Reuse module-level transporter if available, otherwise create a new one
|
||||
if (!smtpTransporter) {
|
||||
const nodemailer = await import('nodemailer');
|
||||
smtpTransporter = nodemailer.default.createTransport({
|
||||
host: smtpHost,
|
||||
port: smtpPort,
|
||||
secure: false,
|
||||
auth: { user: smtpUser, pass: smtpPass },
|
||||
requireTLS: true,
|
||||
tls: {
|
||||
rejectUnauthorized: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
const transport = await getTransporter(cfg);
|
||||
|
||||
const subjectLabels: Record<string, string> = {
|
||||
'web-development': 'Web Development',
|
||||
@@ -216,17 +323,31 @@ async function sendContactEmail(data: ContactFormData): Promise<void> {
|
||||
|
||||
const sourceLabel = data.source ? data.source : 'contact-page';
|
||||
|
||||
const fromAddress = smtpFrom || smtpUser;
|
||||
// The "Tell us about your project" inline form on the homepage and the
|
||||
// mobile sticky CTA tag themselves as project-inquiry sources. Use a
|
||||
// distinct subject prefix so the admin inbox can be triaged easily.
|
||||
const isProjectInquiry =
|
||||
data.source === 'homepage-inline' ||
|
||||
data.source === 'homepage-hero' ||
|
||||
data.source === 'homepage-final-cta' ||
|
||||
data.source === 'mobile-sticky';
|
||||
const adminSubjectPrefix = isProjectInquiry ? '[Project Inquiry]' : '[Contact Form]';
|
||||
const userSubjectLine = isProjectInquiry
|
||||
? "We received your project inquiry - CompanySite"
|
||||
: 'We received your message - CompanySite';
|
||||
|
||||
// 1) Admin notification email with BCC to SMTP_USER for delivery confirmation
|
||||
const adminInfo = await smtpTransporter.sendMail({
|
||||
from: `"CompanySite Contact Form" <${fromAddress}>`,
|
||||
to: recipientEmail,
|
||||
bcc: process.env.SMTP_USER, // BCC to admin inbox as delivery confirmation
|
||||
const fromHeader = `"${cfg.fromName}" <${cfg.fromAddress}>`;
|
||||
|
||||
// 1) Admin notification email — delivered to MAIL_ADMIN_TO (or SMTP_FROM
|
||||
// fallback). BCC to SMTP_USER as a soft delivery receipt.
|
||||
const adminInfo = await transport.sendMail({
|
||||
from: fromHeader,
|
||||
to: cfg.adminRecipient,
|
||||
bcc: cfg.user, // BCC to the SMTP mailbox as delivery confirmation
|
||||
replyTo: data.email,
|
||||
subject: `[Contact Form] ${subjectLabels[data.subject] ?? data.subject} - ${data.name}`,
|
||||
subject: `${adminSubjectPrefix} ${subjectLabels[data.subject] ?? data.subject} - ${data.name}`,
|
||||
text: [
|
||||
`New contact form submission`,
|
||||
isProjectInquiry ? 'New project inquiry submission' : 'New contact form submission',
|
||||
``,
|
||||
`Source: ${sourceLabel}`,
|
||||
`Name: ${data.name}`,
|
||||
@@ -242,7 +363,7 @@ async function sendContactEmail(data: ContactFormData): Promise<void> {
|
||||
`Submitted at: ${new Date().toISOString()}`,
|
||||
].join('\n'),
|
||||
html: `
|
||||
<h2>New Contact Form Submission</h2>
|
||||
<h2>${isProjectInquiry ? 'New Project Inquiry' : 'New Contact Form Submission'}</h2>
|
||||
<table style="border-collapse:collapse;width:100%;max-width:600px">
|
||||
<tr><td style="padding:8px;font-weight:bold;border-bottom:1px solid #eee">Source</td><td style="padding:8px;border-bottom:1px solid #eee">${escapeHtml(sourceLabel)}</td></tr>
|
||||
<tr><td style="padding:8px;font-weight:bold;border-bottom:1px solid #eee">Name</td><td style="padding:8px;border-bottom:1px solid #eee">${escapeHtml(data.name)}</td></tr>
|
||||
@@ -256,15 +377,24 @@ async function sendContactEmail(data: ContactFormData): Promise<void> {
|
||||
`,
|
||||
});
|
||||
|
||||
// 2) User confirmation email — proves delivery works AND provides good UX
|
||||
const userConfirmInfo = await smtpTransporter.sendMail({
|
||||
from: `"CompanySite" <${fromAddress}>`,
|
||||
console.log('[api.contact] Admin notification accepted by SMTP', {
|
||||
messageId: adminInfo?.messageId,
|
||||
recipient: cfg.adminRecipient,
|
||||
source: sourceLabel,
|
||||
});
|
||||
|
||||
// 2) User confirmation email (auto-reply) — preserves the existing UX
|
||||
// pattern. Subject line adapts to whether this is a project inquiry.
|
||||
const userConfirmInfo = await transport.sendMail({
|
||||
from: fromHeader,
|
||||
to: data.email,
|
||||
subject: 'We received your message - CompanySite',
|
||||
subject: userSubjectLine,
|
||||
text: [
|
||||
`Hi ${data.name},`,
|
||||
``,
|
||||
`Thank you for reaching out to us! We have received your message regarding "${subjectLabels[data.subject] ?? data.subject}".`,
|
||||
isProjectInquiry
|
||||
? `Thank you for telling us about your project. We have received your inquiry regarding "${subjectLabels[data.subject] ?? data.subject}".`
|
||||
: `Thank you for reaching out to us! We have received your message regarding "${subjectLabels[data.subject] ?? data.subject}".`,
|
||||
``,
|
||||
`Our team will review your inquiry and get back to you as soon as possible, typically within 1-2 business days.`,
|
||||
``,
|
||||
@@ -279,9 +409,11 @@ async function sendContactEmail(data: ContactFormData): Promise<void> {
|
||||
].join('\n'),
|
||||
html: `
|
||||
<div style="font-family:Arial,sans-serif;max-width:600px;margin:0 auto;padding:20px">
|
||||
<h2 style="color:#333">Thank you for contacting us!</h2>
|
||||
<h2 style="color:#333">${isProjectInquiry ? 'Thank you for telling us about your project!' : 'Thank you for contacting us!'}</h2>
|
||||
<p>Hi ${escapeHtml(data.name)},</p>
|
||||
<p>We have received your message regarding <strong>"${escapeHtml(subjectLabels[data.subject] ?? data.subject)}"</strong>.</p>
|
||||
<p>${isProjectInquiry
|
||||
? `Thank you for telling us about your project. We have received your inquiry regarding <strong>"${escapeHtml(subjectLabels[data.subject] ?? data.subject)}"</strong>.`
|
||||
: `We have received your message regarding <strong>"${escapeHtml(subjectLabels[data.subject] ?? data.subject)}"</strong>.`}</p>
|
||||
<p>Our team will review your inquiry and get back to you as soon as possible, typically within <strong>1-2 business days</strong>.</p>
|
||||
<div style="background:#f9f9f9;border-left:4px solid #007bff;padding:12px 16px;margin:20px 0">
|
||||
<p style="margin:4px 0"><strong>Subject:</strong> ${escapeHtml(subjectLabels[data.subject] ?? data.subject)}</p>
|
||||
@@ -295,9 +427,16 @@ async function sendContactEmail(data: ContactFormData): Promise<void> {
|
||||
`,
|
||||
});
|
||||
|
||||
console.log('[api.contact] User auto-reply accepted by SMTP', {
|
||||
messageId: userConfirmInfo?.messageId,
|
||||
recipient: data.email,
|
||||
source: sourceLabel,
|
||||
});
|
||||
|
||||
return { adminInfo, userConfirmInfo };
|
||||
} catch (err) {
|
||||
// If nodemailer isn't installed, treat as graceful degradation (log only)
|
||||
} catch (err: any) {
|
||||
// If nodemailer isn't installed, treat as graceful degradation (log only).
|
||||
// The site keeps working; ops sees the warning and ships the dependency.
|
||||
if (err instanceof Error && err.message.includes('Cannot find module')) {
|
||||
logger.warn('api.contact', 'nodemailer not available — form submission logged only', {
|
||||
email: data.email,
|
||||
@@ -305,6 +444,17 @@ async function sendContactEmail(data: ContactFormData): Promise<void> {
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Structured send-error log. Deliberately omits SMTP_PASS — only the
|
||||
// SMTP server's response/code/command. Sentry captures the full error.
|
||||
console.error('[api.contact] SMTP send failed', {
|
||||
code: err?.code,
|
||||
command: err?.command,
|
||||
response: err?.response,
|
||||
responseCode: err?.responseCode,
|
||||
message: err?.message,
|
||||
recipient: data.email,
|
||||
adminRecipient: cfg.adminRecipient,
|
||||
});
|
||||
await captureException(err, {
|
||||
scope: 'api.contact',
|
||||
extra: { email: data.email, subject: data.subject },
|
||||
@@ -454,19 +604,29 @@ export const POST: APIRoute = async ({ request, clientAddress }) => {
|
||||
|
||||
// Fire-and-forget: send email in background so the user gets an instant response.
|
||||
// Errors are logged/reported but never block the HTTP reply.
|
||||
const recipientEmail = process.env.CONTACT_EMAIL_TO || 'unknown';
|
||||
console.log('Sending contact email to: ' + recipientEmail);
|
||||
const adminRecipient =
|
||||
process.env.MAIL_ADMIN_TO ||
|
||||
process.env.SMTP_FROM ||
|
||||
process.env.CONTACT_EMAIL_TO ||
|
||||
process.env.CONTACT_EMAIL ||
|
||||
process.env.SMTP_USER ||
|
||||
'unknown';
|
||||
console.log('[api.contact] Dispatching contact email', {
|
||||
adminRecipient,
|
||||
userRecipient: formData.email,
|
||||
source: formData.source,
|
||||
});
|
||||
|
||||
sendContactEmail(formData)
|
||||
.then((result) => {
|
||||
console.log('Contact emails delivered successfully:', {
|
||||
adminRecipient: recipientEmail,
|
||||
console.log('[api.contact] Contact emails delivered successfully', {
|
||||
adminRecipient,
|
||||
adminMessageId: result?.adminInfo?.messageId,
|
||||
userRecipient: formData.email,
|
||||
userMessageId: result?.userConfirmInfo?.messageId,
|
||||
});
|
||||
logger.info('api.contact', 'Contact emails delivered successfully', {
|
||||
adminRecipient: recipientEmail,
|
||||
adminRecipient,
|
||||
adminMessageId: result?.adminInfo?.messageId,
|
||||
userRecipient: formData.email,
|
||||
userMessageId: result?.userConfirmInfo?.messageId,
|
||||
@@ -476,17 +636,20 @@ export const POST: APIRoute = async ({ request, clientAddress }) => {
|
||||
});
|
||||
})
|
||||
.catch(async (err: any) => {
|
||||
console.error('Background email delivery failed:', {
|
||||
// SMTP_PASS is NEVER logged — only protocol-level error fields.
|
||||
console.error('[api.contact] Background email delivery failed', {
|
||||
code: err?.code,
|
||||
command: err?.command,
|
||||
message: err?.message,
|
||||
response: err?.response,
|
||||
responseCode: err?.responseCode,
|
||||
recipient: recipientEmail,
|
||||
email: formData.email,
|
||||
adminRecipient,
|
||||
userRecipient: formData.email,
|
||||
subject: formData.subject,
|
||||
});
|
||||
logger.error('api.contact', 'Background email delivery failed', {
|
||||
code: err?.code,
|
||||
command: err?.command,
|
||||
message: err?.message,
|
||||
response: err?.response,
|
||||
responseCode: err?.responseCode,
|
||||
|
||||
Reference in New Issue
Block a user