/** * MailGun Webhook Router * * Handles incoming webhook events from MailGun and logs them to the console. * This router processes POST requests containing email event data such as * delivered, failed, opened, clicked, bounced, complained, and unsubscribed events. */ import { Router, Request, Response, NextFunction } from 'express'; import createError from 'http-errors'; import { AppLocals } from '../types/AppLocals'; import { isValidMailgunEvent, MailgunWebhookEvent } from '../types/MailgunWebhookEvent'; import { successfulRequestCounter } from '../lib/metricsCounters'; import { logInfo, logError, logWarn } from '../lib/logger'; /** * Formats a Unix timestamp into a human-readable date string * @param timestamp - Unix timestamp as string * @returns Formatted date string in ISO format */ const formatTimestamp = (timestamp: string): string => { try { const timestampNum = parseInt(timestamp, 10); if (isNaN(timestampNum)) { return 'Invalid timestamp'; } return new Date(timestampNum * 1000).toISOString(); } catch (error) { return 'Invalid timestamp'; } }; /** * Logs MailGun webhook event data to console with structured formatting * @param eventData - The MailGun webhook event data */ const logWebhookEvent = (eventData: MailgunWebhookEvent): void => { const separator = '========================================'; const minorSeparator = '----------------------------------------'; console.log('\n' + separator); console.log('MailGun Webhook Event Received'); console.log(separator); console.log(`Event Type: ${eventData.event}`); console.log(`Timestamp: ${eventData.timestamp} (${formatTimestamp(eventData.timestamp)})`); console.log(`Recipient: ${eventData.recipient}`); console.log(`Domain: ${eventData.domain}`); if (eventData['message-id']) { console.log(`MailGun Message ID: ${eventData['message-id']}`); } if (eventData['Message-Id']) { console.log(`SMTP Message ID: ${eventData['Message-Id']}`); } // Log event-specific data console.log(minorSeparator); console.log('Event-Specific Data:'); switch (eventData.event) { case 'delivered': if (eventData['message-headers']) { console.log(`Message Headers: ${eventData['message-headers'].substring(0, 200)}...`); } break; case 'failed': if (eventData.severity) console.log(`Severity: ${eventData.severity}`); if (eventData.reason) console.log(`Reason: ${eventData.reason}`); if (eventData.notification) console.log(`Notification: ${eventData.notification}`); break; case 'opened': if (eventData.city) console.log(`City: ${eventData.city}`); if (eventData.country) console.log(`Country: ${eventData.country}`); if (eventData['device-type']) console.log(`Device Type: ${eventData['device-type']}`); if (eventData['client-os']) console.log(`Client OS: ${eventData['client-os']}`); if (eventData['client-name']) console.log(`Client Name: ${eventData['client-name']}`); if (eventData.ip) console.log(`IP Address: ${eventData.ip}`); break; case 'clicked': console.log(`URL Clicked: ${eventData.url}`); if (eventData.city) console.log(`City: ${eventData.city}`); if (eventData.country) console.log(`Country: ${eventData.country}`); if (eventData['device-type']) console.log(`Device Type: ${eventData['device-type']}`); if (eventData['client-os']) console.log(`Client OS: ${eventData['client-os']}`); if (eventData['client-name']) console.log(`Client Name: ${eventData['client-name']}`); if (eventData.ip) console.log(`IP Address: ${eventData.ip}`); break; case 'bounced': if (eventData.code) console.log(`SMTP Code: ${eventData.code}`); if (eventData.error) console.log(`Error: ${eventData.error}`); if (eventData.notification) console.log(`Notification: ${eventData.notification}`); break; case 'complained': console.log('User marked email as spam'); break; case 'unsubscribed': console.log('User unsubscribed from mailing list'); break; } // Log full event data for debugging console.log(minorSeparator); console.log('Full Event Data (JSON):'); console.log(JSON.stringify(eventData, null, 2)); console.log(separator + '\n'); }; /** * Main webhook request handler * Processes incoming MailGun webhook events and logs them to console */ export const webhookRequestHandler = async (req: Request, res: Response, next: NextFunction) => { try { const eventData = req.body as any; // Validate that we have the minimum required fields if (!isValidMailgunEvent(eventData)) { logWarn('Invalid webhook event received', JSON.stringify(eventData)); next(createError(400, 'Invalid request format: missing required fields (event, recipient)')); return; } // Log the webhook event to console logWebhookEvent(eventData as MailgunWebhookEvent); // Log using the structured logger as well logInfo('MailGun webhook event processed', `Event: ${eventData.event}, Recipient: ${eventData.recipient}`); // Return success response to MailGun res.status(200).json({ status: 'received', message: 'Webhook event logged successfully' }); // Increment successful requests counter for metrics successfulRequestCounter.inc(); } catch (ex: any) { logError('Error processing webhook', ex.message); next(createError(500, 'Internal server error')); } // Stop Prometheus timer (res.locals as AppLocals).stopPrometheusTimer({ path: req.path }); }; /** * Express router for MailGun webhook endpoint */ const router = Router(); export const webhookRouter = router.post('/', webhookRequestHandler);