import nodemailer from 'nodemailer'; import { RateLimiterMemory } from 'rate-limiter-flexible'; import { createHash } from 'crypto'; import 'dotenv/config'; // Environment variables const { SMTP_HOST = '', SMTP_PORT = '587', SMTP_USER = '', SMTP_PASS = '', ADMIN_EMAIL = '', WEBSITE_NAME = '365DevNet Support', } = process.env; // Email configuration // Force production mode for testing const isProduction = true; // NODE_ENV === 'production'; // Create a transporter for sending emails let transporter: nodemailer.Transporter; // Initialize the transporter based on environment function initializeTransporter() { if (isProduction && SMTP_HOST) { // Use local Postfix mail relay (no authentication) transporter = nodemailer.createTransport({ host: SMTP_HOST, port: parseInt(SMTP_PORT, 10) || 25, // default to port 25 if not set secure: false, // No SSL for local relay tls: { rejectUnauthorized: false, // Accept self-signed certificates if present }, }); transporter.verify((error, success) => { if (error) { console.error('❌ SMTP connection error:', error); } else { console.log('✅ SMTP server is ready to take messages.'); } }); } else { // Fallback for development: log email output to console transporter = nodemailer.createTransport({ streamTransport: true, newline: 'unix', buffer: true, }); console.log('⚠️ Email transporter using streamTransport (development mode)'); } } // Rate limiter configuration const rateLimiter = new RateLimiterMemory({ points: 5, // 5 attempts duration: 3600, // per hour }); // CSRF protection const csrfTokens = new Map(); // Generate a CSRF token export function generateCsrfToken(): string { const token = createHash('sha256').update(Math.random().toString()).digest('hex'); // Token expires after 1 hour const expires = new Date(); expires.setHours(expires.getHours() + 1); csrfTokens.set(token, { token, expires }); // Clean up expired tokens for (const [key, value] of csrfTokens.entries()) { if (value.expires < new Date()) { csrfTokens.delete(key); } } return token; } // Validate a CSRF token export function validateCsrfToken(token: string): boolean { const storedToken = csrfTokens.get(token); if (!storedToken) { return false; } if (storedToken.expires < new Date()) { csrfTokens.delete(token); return false; } return true; } // Check rate limit for an IP address export async function checkRateLimit(ipAddress: string): Promise<{ limited: boolean; message?: string }> { try { await rateLimiter.consume(ipAddress); return { limited: false }; } catch (error) { if (error instanceof Error) { return { limited: true, message: 'Too many requests. Please try again later.', }; } // RateLimiterRes with msBeforeNext property interface RateLimiterResponse { msBeforeNext: number; } const resetTime = Math.ceil((error as RateLimiterResponse).msBeforeNext / 1000 / 60); return { limited: true, message: `Too many requests. Please try again in ${resetTime} minutes.`, }; } } // Log email sending attempts export function logEmailAttempt(success: boolean, recipient: string, subject: string, error?: Error): void { const timestamp = new Date().toISOString(); const status = success ? 'SUCCESS' : 'FAILURE'; const errorMessage = error ? `: ${error.message}` : ''; const logMessage = `[${timestamp}] [EMAIL ${status}] To: ${recipient}, Subject: ${subject}${errorMessage}`; if (isProduction) { // In production, you might want to log to a file or a logging service console.log(logMessage); } else { // In development, log to console console.log(logMessage); } } // Send an email export async function sendEmail(to: string, subject: string, html: string, text: string): Promise { // Initialize transporter if not already done if (!transporter) { initializeTransporter(); } try { const fromAddress = isProduction ? `"${WEBSITE_NAME}" <${SMTP_USER || 'noreply@' + WEBSITE_NAME}>` : `"${WEBSITE_NAME}" <${ADMIN_EMAIL}>`; const mailOptions = { from: fromAddress, to, subject, html, text, }; // Log the connection target console.log(`[MAILER DEBUG] Sending via: ${SMTP_HOST}:${SMTP_PORT}`); console.log(`[MAILER DEBUG] From: ${fromAddress} → To: ${to}`); await transporter.sendMail(mailOptions); logEmailAttempt(true, to, subject); return true; } catch (error) { logEmailAttempt(false, to, subject, error as Error); console.error('Full SMTP error stack:', error); return false; } } // Send admin notification email export async function sendAdminNotification( name: string, email: string, message: string, ipAddress?: string, userAgent?: string ): Promise { // Validate inputs if (!name || name.trim() === '') { console.error('Cannot send admin notification: name is empty'); return false; } if (!email || email.trim() === '') { console.error('Cannot send admin notification: email is empty'); return false; } if (!message || message.trim() === '') { console.error('Cannot send admin notification: message is empty'); return false; } const subject = `New Contact Form Submission from ${name}`; const html = ` New Contact Form Submission

New Contact Form Submission

Name
${name}
Email
${email}
Message
${message.replace(/\n/g, '
')}
${ipAddress ? `
IP Address: ${ipAddress}
` : ''} ${userAgent ? `
User Agent: ${userAgent}
` : ''}
Time: ${new Date().toLocaleString()}
`; const text = ` New Contact Form Submission Name: ${name} Email: ${email} Message: ${message} ${ipAddress ? `IP Address: ${ipAddress}` : ''} ${userAgent ? `User Agent: ${userAgent}` : ''} Time: ${new Date().toLocaleString()} This message was sent from the contact form on ${WEBSITE_NAME} `; return sendEmail(ADMIN_EMAIL, subject, html, text); } // Send user confirmation email export async function sendUserConfirmation(name: string, email: string, message: string): Promise { // Validate inputs if (!name || name.trim() === '') { console.error('Cannot send user confirmation: name is empty'); return false; } if (!email || email.trim() === '') { console.error('Cannot send user confirmation: email is empty'); return false; } const subject = `Thank you for contacting ${WEBSITE_NAME}`; const html = ` Thank you for your message

Thank you for your message

Dear ${name},

Thank you for contacting ${WEBSITE_NAME}. We have received your message and will get back to you as soon as possible.

Your Message:

${message.replace(/\n/g, '
')}

If you have any additional information to share, please don't hesitate to reply to this email.

Visit Our Website
`; const text = ` Thank you for your message Dear ${name}, Thank you for contacting ${WEBSITE_NAME}. We have received your message and will get back to you as soon as possible. Here's a copy of your message: ${message} If you have any additional information to share, please don't hesitate to reply to this email. Best regards, ${WEBSITE_NAME} Team This is an automated message, please do not reply directly to this email. `; return sendEmail(email, subject, html, text); } // Initialize the email system export function initializeEmailSystem(): void { initializeTransporter(); } // Test email configuration export async function testEmailConfiguration(): Promise { if (!transporter) { initializeTransporter(); } try { await transporter.verify(); return true; } catch (error) { console.error('Email configuration test failed:', error); return false; } }