mailgun-inbound-email 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +366 -8
  2. package/index.js +244 -0
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # mailgun-inbound-email
2
2
 
3
- A **production-ready** utility package for manual processing of Mailgun inbound email webhooks. Full manual control - you handle everything from webhook setup to email processing.
3
+ A **production-ready** utility package for manual processing of Mailgun webhooks. Supports both **inbound email webhooks** and **event webhooks** (delivered, opened, clicked, bounced, etc.). Full manual control - you handle everything from webhook setup to data processing.
4
4
 
5
5
  ## 🚀 Quick Start
6
6
 
@@ -8,6 +8,8 @@ A **production-ready** utility package for manual processing of Mailgun inbound
8
8
  npm install mailgun-inbound-email
9
9
  ```
10
10
 
11
+ > âš ī¸ **REQUIRED**: Set `MAILGUN_WEBHOOK_SIGNING_KEY` environment variable before using webhooks. See [Security](#-security) section for details.
12
+
11
13
  ```javascript
12
14
  const express = require('express');
13
15
  const multer = require('multer');
@@ -52,8 +54,36 @@ app.listen(3000);
52
54
 
53
55
  **That's it!** Just configure your Mailgun webhook URL to point to `https://yourdomain.com/webhook/inbound`
54
56
 
57
+ > âš ī¸ **IMPORTANT**: Make sure to set `MAILGUN_WEBHOOK_SIGNING_KEY` environment variable before running the server. Without it, webhook signature verification will fail.
58
+
55
59
  > 📖 **Need help setting up the webhook?** See the detailed guide: [Setting Up Mailgun Inbound Webhook](#-setting-up-mailgun-inbound-webhook)
56
60
 
61
+ ### Event Webhooks Quick Start
62
+
63
+ For handling Mailgun event webhooks (delivered, opened, clicked, bounced, etc.):
64
+
65
+ ```javascript
66
+ const express = require('express');
67
+ const { mailgunWebhook } = require('mailgun-inbound-email');
68
+
69
+ const app = express();
70
+
71
+ app.post('/webhook/mailgun-events', express.json(), async (req, res) => {
72
+ const eventData = await mailgunWebhook(req, res);
73
+
74
+ // Save event data manually
75
+ if (eventData && eventData.received && eventData.event) {
76
+ await db.events.create(eventData);
77
+ }
78
+ });
79
+
80
+ app.listen(3000);
81
+ ```
82
+
83
+ **That's it!** Configure your Mailgun event webhook URL in Mailgun Dashboard → Settings → Webhooks → Add webhook → Select events → Enter URL: `https://yourdomain.com/webhook/mailgun-events`
84
+
85
+ > âš ī¸ **IMPORTANT**: Make sure to set `MAILGUN_WEBHOOK_SIGNING_KEY` environment variable before running the server. Without it, webhook signature verification will fail.
86
+
57
87
  ## ✨ Features
58
88
 
59
89
  - ✅ **Full Manual Control** - You handle everything, no magic
@@ -63,6 +93,9 @@ app.listen(3000);
63
93
  - ✅ **Replay attack prevention** - 15-minute timestamp window
64
94
  - ✅ **Automatic email parsing** - Clean, structured email data
65
95
  - ✅ **Attachment support** - Metadata + buffers for manual handling
96
+ - ✅ **Event webhook handler** - Production-ready handler for Mailgun event webhooks (delivered, opened, clicked, bounced, etc.)
97
+ - ✅ **Returns event data** - Get processed event data for manual saving to database
98
+ - ✅ **Structured logging** - Built-in logging with correlation IDs for tracking
66
99
  - ✅ **Zero dependencies** - Only Node.js built-ins
67
100
  - ✅ **Simple & lightweight** - Just utility functions
68
101
 
@@ -72,8 +105,14 @@ app.listen(3000);
72
105
  npm install mailgun-inbound-email
73
106
  ```
74
107
 
108
+ > âš ī¸ **REQUIRED**: After installation, you must set the `MAILGUN_WEBHOOK_SIGNING_KEY` environment variable. See [Security](#-security) section for instructions.
109
+
75
110
  ## đŸŽ¯ Usage
76
111
 
112
+ ### Inbound Email Webhooks
113
+
114
+ For receiving and processing inbound emails sent to your domain.
115
+
77
116
  ### Basic Example
78
117
 
79
118
  ```javascript
@@ -189,6 +228,91 @@ app.post('/webhook/inbound', express.urlencoded({ extended: true }), upload.any(
189
228
  });
190
229
  ```
191
230
 
231
+ ### Event Webhooks (delivered, opened, clicked, bounced, etc.)
232
+
233
+ For handling Mailgun event webhooks that track email delivery, opens, clicks, and other events.
234
+
235
+ #### Simple Example
236
+
237
+ ```javascript
238
+ const express = require('express');
239
+ const { mailgunWebhook } = require('mailgun-inbound-email');
240
+
241
+ const app = express();
242
+
243
+ // Example database
244
+ const db = {
245
+ events: {
246
+ async create(eventData) {
247
+ // Save event to your database
248
+ console.log('Saving event:', eventData.event);
249
+ }
250
+ }
251
+ };
252
+
253
+ app.post('/webhook/mailgun-events', express.json(), async (req, res) => {
254
+ // Call mailgunWebhook - it handles signature verification and returns event data
255
+ const eventData = await mailgunWebhook(req, res);
256
+
257
+ // Save event data manually if event was successfully processed
258
+ if (eventData && eventData.received && eventData.event) {
259
+ try {
260
+ await db.events.create(eventData);
261
+ console.log('✅ Event saved successfully');
262
+ } catch (error) {
263
+ console.error('❌ Error saving event:', error);
264
+ }
265
+ }
266
+ });
267
+
268
+ app.listen(3000);
269
+ ```
270
+
271
+ #### Advanced Example with Event Handling
272
+
273
+ ```javascript
274
+ const express = require('express');
275
+ const { mailgunWebhook } = require('mailgun-inbound-email');
276
+
277
+ const app = express();
278
+
279
+ app.post('/webhook/mailgun-events', express.json(), async (req, res) => {
280
+ const eventData = await mailgunWebhook(req, res);
281
+
282
+ if (eventData && eventData.received && eventData.event) {
283
+ // Handle different event types
284
+ switch (eventData.event) {
285
+ case 'delivered':
286
+ await updateEmailStatus(eventData.messageId, 'delivered');
287
+ break;
288
+ case 'opened':
289
+ await trackEmailOpen(eventData.messageId, eventData.recipient);
290
+ break;
291
+ case 'clicked':
292
+ await trackLinkClick(eventData.messageId, eventData.url);
293
+ break;
294
+ case 'bounced':
295
+ await markRecipientAsBounced(eventData.recipient, eventData.reason);
296
+ break;
297
+ case 'complained':
298
+ await markRecipientAsComplained(eventData.recipient);
299
+ break;
300
+ case 'failed':
301
+ await handleEmailFailure(eventData);
302
+ break;
303
+ case 'unsubscribed':
304
+ await unsubscribeRecipient(eventData.recipient);
305
+ break;
306
+ }
307
+
308
+ // Save event to database
309
+ await db.events.create(eventData);
310
+ }
311
+ });
312
+
313
+ app.listen(3000);
314
+ ```
315
+
192
316
  ## 📧 Email Data Structure
193
317
 
194
318
  The `emailData` object contains all parsed email information:
@@ -297,6 +421,82 @@ if (!isValid) {
297
421
  }
298
422
  ```
299
423
 
424
+ ### `mailgunWebhook(req, res, signingKey)`
425
+
426
+ Production-ready handler for Mailgun event webhooks (delivered, opened, clicked, bounced, complained, failed, unsubscribed, stored, etc.). Handles signature verification, event parsing, and returns processed event data for manual saving.
427
+
428
+ **Parameters:**
429
+ - `req` (Object): Express request object
430
+ - `res` (Object): Express response object
431
+ - `signingKey` (string, optional): Mailgun webhook signing key. Defaults to `process.env.MAILGUN_WEBHOOK_SIGNING_KEY`
432
+
433
+ **Returns:**
434
+ - `Promise<Object|null>`: Returns processed event data if successful, `null` if error or invalid request
435
+
436
+ **Example:**
437
+ ```javascript
438
+ const { mailgunWebhook } = require('mailgun-inbound-email');
439
+
440
+ app.post('/webhook/mailgun-events', express.json(), async (req, res) => {
441
+ const eventData = await mailgunWebhook(req, res);
442
+
443
+ // Save event data manually if webhook was successful
444
+ if (eventData && eventData.received && eventData.event) {
445
+ await db.events.create(eventData);
446
+ }
447
+ });
448
+ ```
449
+
450
+ **Event Data Structure:**
451
+ ```javascript
452
+ {
453
+ received: true,
454
+ event: "delivered" | "opened" | "clicked" | "bounced" | "complained" | "failed" | "unsubscribed" | "stored" | "unknown",
455
+ eventId: "string", // Unique event ID (for idempotency)
456
+ recipient: "user@example.com", // Email recipient
457
+ messageId: "string", // Email message ID
458
+ timestamp: "2024-01-01T00:00:00.000Z", // ISO timestamp
459
+ domain: "example.com", // Mailgun domain
460
+ correlationId: "string", // Request correlation ID for tracking
461
+ processedAt: "2024-01-01T00:00:00.000Z", // When webhook was processed
462
+ status: "delivered" | "opened" | "clicked" | "bounced" | "complained" | "failed" | "unsubscribed" | "stored" | "unknown",
463
+
464
+ // Event-specific fields:
465
+ url: "string", // For 'clicked' events
466
+ reason: "string", // For 'bounced'/'failed' events
467
+ deliveryStatus: { // For 'delivered'/'bounced'/'failed' events
468
+ code: number,
469
+ message: string,
470
+ description: string,
471
+ tls: boolean,
472
+ certificateVerified: boolean,
473
+ attemptNo: number,
474
+ sessionSeconds: number,
475
+ },
476
+ clientInfo: { // For 'opened'/'clicked' events
477
+ clientName: string,
478
+ clientType: string,
479
+ deviceType: string,
480
+ userAgent: string,
481
+ },
482
+ geolocation: { // For 'opened'/'clicked' events
483
+ country: string,
484
+ region: string,
485
+ city: string,
486
+ },
487
+ severity: "permanent" | "temporary", // For 'bounced' events
488
+ deliveredAt: "ISO string", // For 'delivered' events
489
+ openedAt: "ISO string", // For 'opened' events
490
+ clickedAt: "ISO string", // For 'clicked' events
491
+ bouncedAt: "ISO string", // For 'bounced' events
492
+ complainedAt: "ISO string", // For 'complained' events
493
+ failedAt: "ISO string", // For 'failed' events
494
+ unsubscribedAt: "ISO string", // For 'unsubscribed' events
495
+ storedAt: "ISO string", // For 'stored' events
496
+ fullEventData: {}, // For 'unknown' events - contains raw event data
497
+ }
498
+ ```
499
+
300
500
  ### Utility Functions
301
501
 
302
502
  | Function | Description |
@@ -310,7 +510,12 @@ if (!isValid) {
310
510
 
311
511
  ### Required Environment Variable
312
512
 
313
- - `MAILGUN_WEBHOOK_SIGNING_KEY`: Your Mailgun webhook signing key (found in Mailgun dashboard → Settings → Webhooks)
513
+ > âš ī¸ **REQUIRED**: `MAILGUN_WEBHOOK_SIGNING_KEY` must be set for webhook signature verification to work.
514
+
515
+ - **`MAILGUN_WEBHOOK_SIGNING_KEY`** (REQUIRED): Your Mailgun webhook signing key (found in Mailgun dashboard → Settings → Webhooks)
516
+ - This is **required** for both inbound email webhooks and event webhooks
517
+ - Without this key, all webhook requests will be rejected with 401 Unauthorized
518
+ - Get your key from: Mailgun Dashboard → Settings → Webhooks → Webhook Signing Key
314
519
 
315
520
  ### Security Features
316
521
 
@@ -338,11 +543,27 @@ npm install mailgun-inbound-email
338
543
  npm install express multer
339
544
  ```
340
545
 
341
- ### Step 2: Set Up Your Express Server
546
+ ### Step 2: Set Up Environment Variable (REQUIRED)
547
+
548
+ > âš ī¸ **REQUIRED**: You must set `MAILGUN_WEBHOOK_SIGNING_KEY` before setting up your server.
549
+
550
+ 1. Get your webhook signing key from Mailgun Dashboard → Settings → Webhooks
551
+ 2. Set it as an environment variable:
552
+
553
+ ```bash
554
+ export MAILGUN_WEBHOOK_SIGNING_KEY=your-signing-key-here
555
+ ```
556
+
557
+ Or add to your `.env` file:
558
+ ```
559
+ MAILGUN_WEBHOOK_SIGNING_KEY=your-signing-key-here
560
+ ```
561
+
562
+ ### Step 3: Set Up Your Express Server
342
563
 
343
564
  Set up your Express server with the webhook endpoint (see examples above).
344
565
 
345
- ### Step 3: Configure Mailgun Inbound Route (Dashboard Method)
566
+ ### Step 4: Configure Mailgun Inbound Route (Dashboard Method)
346
567
 
347
568
  Follow these steps to configure the inbound webhook URL in Mailgun Dashboard:
348
569
 
@@ -415,7 +636,7 @@ mg.routes.create({
415
636
  .catch(err => console.error('Error:', err));
416
637
  ```
417
638
 
418
- ### Step 4: Get Your Webhook Signing Key
639
+ > **Note**: If you already set `MAILGUN_WEBHOOK_SIGNING_KEY` in Step 2, you can skip this step.
419
640
 
420
641
  1. **Navigate to Webhooks Settings**
421
642
  - In Mailgun Dashboard, go to **Settings** → **Webhooks**
@@ -426,7 +647,7 @@ mg.routes.create({
426
647
  - Click **Show** or **Reveal** to see your key
427
648
  - Copy the signing key (it looks like: `key-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx`)
428
649
 
429
- 3. **Set Environment Variable**
650
+ 3. **Set Environment Variable** (if not already set in Step 2)
430
651
  ```bash
431
652
  export MAILGUN_WEBHOOK_SIGNING_KEY=your-signing-key-here
432
653
  ```
@@ -482,9 +703,10 @@ If you haven't set up your domain yet, make sure to:
482
703
  - ✅ Check your server logs for incoming requests
483
704
 
484
705
  **Signature verification failing?**
485
- - ✅ Verify `MAILGUN_WEBHOOK_SIGNING_KEY` is set correctly
706
+ - ✅ **REQUIRED**: Verify `MAILGUN_WEBHOOK_SIGNING_KEY` environment variable is set (this is required!)
486
707
  - ✅ Check that you copied the full signing key
487
708
  - ✅ Ensure the key matches the one in Mailgun Dashboard
709
+ - ✅ Verify the environment variable is loaded in your application (check with `console.log(process.env.MAILGUN_WEBHOOK_SIGNING_KEY)`)
488
710
 
489
711
  **Emails not being forwarded?**
490
712
  - ✅ Verify MX records are set correctly
@@ -504,9 +726,133 @@ ngrok http 3000
504
726
  # Use the HTTPS URL provided by ngrok
505
727
  ```
506
728
 
729
+ ## 📝 Setting Up Mailgun Event Webhooks
730
+
731
+ Event webhooks track email events like delivered, opened, clicked, bounced, etc. These are different from inbound email webhooks.
732
+
733
+ ### Step 1: Install Package and Dependencies
734
+
735
+ ```bash
736
+ # Install the package
737
+ npm install mailgun-inbound-email
738
+
739
+ # Install required dependencies (only express needed for event webhooks)
740
+ npm install express
741
+ ```
742
+
743
+ ### Step 2: Set Up Environment Variable (REQUIRED)
744
+
745
+ > âš ī¸ **REQUIRED**: You must set `MAILGUN_WEBHOOK_SIGNING_KEY` before setting up your server.
746
+
747
+ 1. Get your webhook signing key from Mailgun Dashboard → Settings → Webhooks
748
+ 2. Set it as an environment variable:
749
+
750
+ ```bash
751
+ export MAILGUN_WEBHOOK_SIGNING_KEY=your-signing-key-here
752
+ ```
753
+
754
+ Or add to your `.env` file:
755
+ ```
756
+ MAILGUN_WEBHOOK_SIGNING_KEY=your-signing-key-here
757
+ ```
758
+
759
+ ### Step 3: Set Up Your Express Server
760
+
761
+ Set up your Express server with the event webhook endpoint (see examples above in the Event Webhooks section).
762
+
763
+ ### Step 4: Configure Event Webhook in Mailgun Dashboard
764
+
765
+ 1. **Log in to Mailgun Dashboard**
766
+ - Go to [https://app.mailgun.com](https://app.mailgun.com)
767
+ - Log in with your Mailgun account
768
+
769
+ 2. **Navigate to Webhooks Settings**
770
+ - Click on **Settings** in the left sidebar
771
+ - Click on **Webhooks**
772
+ - Or go directly to: [https://app.mailgun.com/app/webhooks](https://app.mailgun.com/app/webhooks)
773
+
774
+ 3. **Add New Webhook**
775
+ - Click **Add webhook** button
776
+ - Select the events you want to track:
777
+ - ✅ **Delivered** - Email successfully delivered
778
+ - ✅ **Opened** - Email was opened by recipient
779
+ - ✅ **Clicked** - Link in email was clicked
780
+ - ✅ **Bounced** - Email bounced (permanent or temporary)
781
+ - ✅ **Complained** - Recipient marked email as spam
782
+ - ✅ **Failed** - Email delivery failed
783
+ - ✅ **Unsubscribed** - Recipient unsubscribed
784
+ - ✅ **Stored** - Email was stored
785
+
786
+ 4. **Enter Webhook URL**
787
+ - Enter your webhook URL: `https://yourdomain.com/webhook/mailgun-events`
788
+ - **Important**: Must use HTTPS (Mailgun requires it)
789
+ - The webhook will receive JSON payloads (not form-data like inbound emails)
790
+
791
+ 5. **Save the Webhook**
792
+ - Click **Save** or **Add webhook**
793
+ - Your webhook is now active and will receive events
794
+
795
+ > **Note**: If you already set `MAILGUN_WEBHOOK_SIGNING_KEY` in Step 2, you can skip this step. The same signing key is used for both inbound and event webhooks.
796
+
797
+ 1. **Navigate to Webhooks Settings**
798
+ - In Mailgun Dashboard, go to **Settings** → **Webhooks**
799
+ - Find **Webhook Signing Key** section
800
+
801
+ 2. **Copy Your Signing Key**
802
+ - Click **Show** or **Reveal** to see your key
803
+ - Copy the signing key (it looks like: `key-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx`)
804
+
805
+ 3. **Set Environment Variable** (if not already set in Step 2)
806
+ ```bash
807
+ export MAILGUN_WEBHOOK_SIGNING_KEY=your-signing-key-here
808
+ ```
809
+
810
+ Or add to your `.env` file:
811
+ ```
812
+ MAILGUN_WEBHOOK_SIGNING_KEY=your-signing-key-here
813
+ ```
814
+
815
+ ### Step 5: Test Your Event Webhook
816
+
817
+ 1. **Send a Test Email**
818
+ - Send an email using Mailgun API or dashboard
819
+ - The email should trigger events (delivered, opened, clicked, etc.)
820
+
821
+ 2. **Check Your Logs**
822
+ - Check your server logs to see if events are being received
823
+ - Verify the event data is being processed correctly
824
+
825
+ 3. **Verify in Mailgun Dashboard**
826
+ - Go to **Logs** → **Webhooks** in Mailgun Dashboard
827
+ - You should see webhook delivery attempts and their status
828
+ - Check that events are being sent to your webhook URL
829
+
830
+ ### Troubleshooting Event Webhooks
831
+
832
+ **Events not being received?**
833
+ - ✅ Verify your webhook URL is accessible (test with curl or browser)
834
+ - ✅ Ensure you're using HTTPS (Mailgun requires it)
835
+ - ✅ Check that you selected the correct events in Mailgun Dashboard
836
+ - ✅ Verify your webhook is active in Mailgun Dashboard
837
+ - ✅ Check Mailgun logs for delivery errors
838
+ - ✅ Check your server logs for incoming requests
839
+
840
+ **Event data not saving?**
841
+ - ✅ Verify `mailgunWebhook()` is returning event data
842
+ - ✅ Check that you're checking `eventData.received && eventData.event` before saving
843
+ - ✅ Ensure your database connection is working
844
+ - ✅ Check for errors in your event saving logic
845
+
846
+ **Signature verification failing?**
847
+ - ✅ **REQUIRED**: Verify `MAILGUN_WEBHOOK_SIGNING_KEY` environment variable is set (this is required!)
848
+ - ✅ Check that you copied the full signing key
849
+ - ✅ Ensure the key matches the one in Mailgun Dashboard
850
+ - ✅ Verify the environment variable is loaded in your application (check with `console.log(process.env.MAILGUN_WEBHOOK_SIGNING_KEY)`)
851
+
507
852
  ## đŸŽ¯ Production Checklist
508
853
 
509
- - ✅ Set `MAILGUN_WEBHOOK_SIGNING_KEY` environment variable
854
+ ### Inbound Email Webhooks
855
+ - ✅ **REQUIRED**: Set `MAILGUN_WEBHOOK_SIGNING_KEY` environment variable
510
856
  - ✅ Use HTTPS for webhook URL (Mailgun requires it)
511
857
  - ✅ Implement your email processing logic
512
858
  - ✅ Handle attachments if needed (buffers are included)
@@ -514,13 +860,25 @@ ngrok http 3000
514
860
  - ✅ Test webhook signature verification
515
861
  - ✅ Always return 200 status to Mailgun (prevents retries)
516
862
 
863
+ ### Event Webhooks (delivered, opened, clicked, etc.)
864
+ - ✅ **REQUIRED**: Set `MAILGUN_WEBHOOK_SIGNING_KEY` environment variable
865
+ - ✅ Use HTTPS for webhook URL (Mailgun requires it)
866
+ - ✅ Implement event data saving logic (use returned event data)
867
+ - ✅ Handle different event types appropriately
868
+ - ✅ Set up error monitoring/logging
869
+ - ✅ Test webhook signature verification
870
+ - ✅ Always return 200 status to Mailgun (prevents retries)
871
+ - ✅ Consider implementing idempotency checks using `eventId`
872
+
517
873
  ## âš ī¸ Important Notes
518
874
 
519
875
  - **Always return 200** to Mailgun (even on errors) to prevent retries
520
876
  - **Use HTTPS** for webhook URLs (Mailgun requirement)
521
877
  - **Full manual control** - this package only provides utilities, you handle everything
522
878
  - **Attachments include buffers** - handle large files appropriately
879
+ - **Event webhooks return data** - `mailgunWebhook()` returns event data for manual saving
523
880
  - **Zero dependencies** - only uses Node.js built-in modules
881
+ - **Two webhook types** - Inbound email webhooks (form-data) vs Event webhooks (JSON)
524
882
 
525
883
  ## 📄 License
526
884
 
package/index.js CHANGED
@@ -226,11 +226,255 @@ function processEmailData(req) {
226
226
  };
227
227
  }
228
228
 
229
+ /**
230
+ * Production-ready Mailgun event webhook handler
231
+ *
232
+ * Handles Mailgun event webhooks (delivered, opened, clicked, bounced, etc.)
233
+ * with proper error handling, validation, and logging. Returns event data
234
+ * for manual processing and saving to database.
235
+ *
236
+ * @param {Object} req - Express request object
237
+ * @param {Object} res - Express response object
238
+ * @param {string} signingKey - Mailgun webhook signing key (optional, defaults to MAILGUN_WEBHOOK_SIGNING_KEY env var)
239
+ * @returns {Promise<Object|null>} Returns event data if successfully processed, null otherwise
240
+ *
241
+ * @example
242
+ * const { mailgunWebhook } = require('mailgun-inbound-email');
243
+ *
244
+ * app.post('/webhook/mailgun-events', express.json(), async (req, res) => {
245
+ * const eventData = await mailgunWebhook(req, res);
246
+ * // eventData contains the processed event data for manual saving
247
+ * if (eventData && eventData.received && eventData.event) {
248
+ * await db.events.create(eventData);
249
+ * }
250
+ * });
251
+ */
252
+ async function mailgunWebhook(req, res, signingKey = process.env.MAILGUN_WEBHOOK_SIGNING_KEY) {
253
+ const startTime = Date.now();
254
+ const correlationId = req.headers['x-request-id'] ||
255
+ req.headers['x-correlation-id'] ||
256
+ `mg-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
257
+
258
+ try {
259
+ // Validate request body
260
+ if (!req || !req.body) {
261
+ console.error(`[MailgunWebhook:${correlationId}] Invalid request: missing body`, {
262
+ ip: req.ip || req.connection?.remoteAddress,
263
+ userAgent: req.headers['user-agent'],
264
+ });
265
+ res.status(400).json({
266
+ received: false,
267
+ error: 'Invalid request',
268
+ correlationId
269
+ });
270
+ return null;
271
+ }
272
+
273
+ // 🔐 Verify Mailgun request signature
274
+ const isValid = verifyRequestSignature(req, signingKey);
275
+ if (!isValid) {
276
+ console.warn(`[MailgunWebhook:${correlationId}] Invalid Mailgun webhook signature`, {
277
+ ip: req.ip || req.connection?.remoteAddress,
278
+ userAgent: req.headers['user-agent'],
279
+ hasBody: !!req.body,
280
+ hasToken: !!req.body.token,
281
+ hasTimestamp: !!req.body.timestamp,
282
+ hasSignature: !!req.body.signature,
283
+ });
284
+ res.status(401).json({
285
+ received: false,
286
+ error: 'Invalid signature',
287
+ correlationId
288
+ });
289
+ return null;
290
+ }
291
+
292
+ // Extract event data from request body
293
+ const eventData = req.body['event-data'] || {};
294
+ const event = eventData.event;
295
+ const eventId = eventData.id || eventData['event-id'] || null;
296
+ const recipient = eventData.recipient;
297
+ const messageId = eventData.message?.headers?.['message-id'] ||
298
+ eventData['message-id'] ||
299
+ eventData.messageId ||
300
+ null;
301
+ const url = eventData.url;
302
+ const timestamp = eventData.timestamp || Date.now() / 1000;
303
+ const domain = eventData.domain?.name || eventData.domain;
304
+ const reason = eventData['delivery-status']?.description ||
305
+ eventData.reason ||
306
+ eventData['failure-reason'] ||
307
+ null;
308
+
309
+ // Validate required fields
310
+ if (!event) {
311
+ console.warn(`[MailgunWebhook:${correlationId}] Missing event type`, {
312
+ body: req.body
313
+ });
314
+ const errorResponse = {
315
+ received: true,
316
+ error: 'Missing event type',
317
+ correlationId
318
+ };
319
+ res.status(200).json(errorResponse);
320
+ return null;
321
+ }
322
+
323
+ // Prepare response data with correlation ID for tracking
324
+ let responseData = {
325
+ received: true,
326
+ event,
327
+ eventId,
328
+ recipient,
329
+ messageId,
330
+ timestamp: typeof timestamp === 'number' ? new Date(timestamp * 1000).toISOString() : timestamp,
331
+ domain,
332
+ correlationId,
333
+ processedAt: new Date().toISOString(),
334
+ };
335
+
336
+ // Handle different event types and add event-specific data
337
+ switch (event) {
338
+ case "delivered":
339
+ console.log(`[MailgunWebhook:${correlationId}] ✅ Email delivered to:`, recipient);
340
+ console.log(`[MailgunWebhook:${correlationId}] Message ID:`, messageId);
341
+ responseData.status = "delivered";
342
+ responseData.deliveredAt = typeof timestamp === 'number' ? new Date(timestamp * 1000).toISOString() : timestamp;
343
+ if (eventData['delivery-status']) {
344
+ responseData.deliveryStatus = {
345
+ code: eventData['delivery-status'].code,
346
+ message: eventData['delivery-status'].message,
347
+ description: eventData['delivery-status'].description,
348
+ tls: eventData['delivery-status'].tls,
349
+ certificateVerified: eventData['delivery-status']['certificate-verified'],
350
+ };
351
+ }
352
+ break;
353
+
354
+ case "opened":
355
+ console.log(`[MailgunWebhook:${correlationId}] 👀 Email opened by:`, recipient);
356
+ console.log(`[MailgunWebhook:${correlationId}] Message ID:`, messageId);
357
+ responseData.status = "opened";
358
+ responseData.openedAt = typeof timestamp === 'number' ? new Date(timestamp * 1000).toISOString() : timestamp;
359
+ responseData.clientInfo = eventData['client-info'] || null;
360
+ responseData.geolocation = eventData.geolocation || null;
361
+ responseData.userAgent = eventData['client-info']?.clientName || null;
362
+ break;
363
+
364
+ case "clicked":
365
+ console.log(`[MailgunWebhook:${correlationId}] 🔗 Link clicked:`, url);
366
+ console.log(`[MailgunWebhook:${correlationId}] Recipient:`, recipient);
367
+ console.log(`[MailgunWebhook:${correlationId}] Message ID:`, messageId);
368
+ responseData.status = "clicked";
369
+ responseData.url = url;
370
+ responseData.clickedAt = typeof timestamp === 'number' ? new Date(timestamp * 1000).toISOString() : timestamp;
371
+ responseData.clientInfo = eventData['client-info'] || null;
372
+ responseData.geolocation = eventData.geolocation || null;
373
+ break;
374
+
375
+ case "bounced":
376
+ console.log(`[MailgunWebhook:${correlationId}] ❌ Email bounced:`, recipient);
377
+ console.log(`[MailgunWebhook:${correlationId}] Message ID:`, messageId);
378
+ responseData.status = "bounced";
379
+ responseData.reason = reason;
380
+ responseData.bouncedAt = typeof timestamp === 'number' ? new Date(timestamp * 1000).toISOString() : timestamp;
381
+ if (eventData['delivery-status']) {
382
+ responseData.deliveryStatus = {
383
+ code: eventData['delivery-status'].code,
384
+ message: eventData['delivery-status'].message,
385
+ description: eventData['delivery-status'].description,
386
+ attemptNo: eventData['delivery-status']['attempt-no'],
387
+ sessionSeconds: eventData['delivery-status']['session-seconds'],
388
+ };
389
+ }
390
+ responseData.severity = eventData.severity || 'permanent';
391
+ break;
392
+
393
+ case "complained":
394
+ console.log(`[MailgunWebhook:${correlationId}] 🚨 Spam complaint:`, recipient);
395
+ console.log(`[MailgunWebhook:${correlationId}] Message ID:`, messageId);
396
+ responseData.status = "complained";
397
+ responseData.complainedAt = typeof timestamp === 'number' ? new Date(timestamp * 1000).toISOString() : timestamp;
398
+ break;
399
+
400
+ case "failed":
401
+ console.log(`[MailgunWebhook:${correlationId}] âš ī¸ Email failed:`, recipient);
402
+ console.log(`[MailgunWebhook:${correlationId}] Message ID:`, messageId);
403
+ responseData.status = "failed";
404
+ responseData.reason = reason;
405
+ responseData.failedAt = typeof timestamp === 'number' ? new Date(timestamp * 1000).toISOString() : timestamp;
406
+ if (eventData['delivery-status']) {
407
+ responseData.deliveryStatus = {
408
+ code: eventData['delivery-status'].code,
409
+ message: eventData['delivery-status'].message,
410
+ description: eventData['delivery-status'].description,
411
+ attemptNo: eventData['delivery-status']['attempt-no'],
412
+ sessionSeconds: eventData['delivery-status']['session-seconds'],
413
+ };
414
+ }
415
+ break;
416
+
417
+ case "unsubscribed":
418
+ console.log(`[MailgunWebhook:${correlationId}] 📤 User unsubscribed:`, recipient);
419
+ console.log(`[MailgunWebhook:${correlationId}] Message ID:`, messageId);
420
+ responseData.status = "unsubscribed";
421
+ responseData.unsubscribedAt = typeof timestamp === 'number' ? new Date(timestamp * 1000).toISOString() : timestamp;
422
+ break;
423
+
424
+ case "stored":
425
+ console.log(`[MailgunWebhook:${correlationId}] 💾 Email stored:`, recipient);
426
+ console.log(`[MailgunWebhook:${correlationId}] Message ID:`, messageId);
427
+ responseData.status = "stored";
428
+ responseData.storedAt = typeof timestamp === 'number' ? new Date(timestamp * 1000).toISOString() : timestamp;
429
+ break;
430
+
431
+ default:
432
+ console.log(`[MailgunWebhook:${correlationId}] â„šī¸ Other event:`, event);
433
+ console.log(`[MailgunWebhook:${correlationId}] Message ID:`, messageId);
434
+ responseData.status = "unknown";
435
+ responseData.fullEventData = eventData;
436
+ }
437
+
438
+ // Log successful processing
439
+ const duration = Date.now() - startTime;
440
+ console.log(`[MailgunWebhook:${correlationId}] ✅ Webhook processed successfully`, {
441
+ event,
442
+ eventId,
443
+ duration: `${duration}ms`,
444
+ });
445
+
446
+ // ✅ MUST return 200 OK with event data for manual saving
447
+ res.status(200).json(responseData);
448
+
449
+ // Return event data so caller can save it manually
450
+ return responseData;
451
+
452
+ } catch (error) {
453
+ const duration = Date.now() - startTime;
454
+ console.error(`[MailgunWebhook:${correlationId}] ❌ Webhook Error:`, {
455
+ error: error.message,
456
+ stack: error.stack,
457
+ duration: `${duration}ms`,
458
+ });
459
+
460
+ // âš ī¸ Still return 200 so Mailgun doesn't retry forever
461
+ const errorResponse = {
462
+ received: true,
463
+ error: 'Processing failed but webhook acknowledged',
464
+ correlationId,
465
+ timestamp: new Date().toISOString(),
466
+ };
467
+ res.status(200).json(errorResponse);
468
+ return null;
469
+ }
470
+ }
471
+
229
472
  // Export utility functions for manual processing
230
473
  module.exports = {
231
474
  processEmailData,
232
475
  verifyRequestSignature, // Automatic signature verification (recommended)
233
476
  verifyMailgunSignature, // Manual signature verification (advanced)
477
+ mailgunWebhook, // Production-ready event webhook handler
234
478
  extractEmail,
235
479
  extractEmails,
236
480
  cleanMessageId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mailgun-inbound-email",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "Production-ready utility functions for manual processing of Mailgun inbound email webhooks. Full manual control - you handle everything.",
5
5
  "main": "index.js",
6
6
  "scripts": {