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.
- package/README.md +366 -8
- package/index.js +244 -0
- 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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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": {
|