mailgun-inbound-email 1.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 (5) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +811 -83
  3. package/index.js +339 -139
  4. package/package.json +18 -13
  5. package/SETUP.md +0 -62
package/README.md CHANGED
@@ -1,110 +1,339 @@
1
1
  # mailgun-inbound-email
2
2
 
3
- A reusable npm package for handling Mailgun inbound email webhooks with Express.js. This package provides a clean, tested solution for receiving and processing inbound emails from Mailgun without rewriting the same code in every project.
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
- ## Features
5
+ ## 🚀 Quick Start
6
6
 
7
- - ✅ Mailgun webhook signature verification
8
- - Replay attack prevention (15-minute window)
9
- - ✅ Automatic email parsing (from, to, cc, subject, body, attachments)
10
- - ✅ Header extraction and normalization
11
- - Attachment metadata processing
12
- - ✅ Express.js router and middleware support
13
- - ✅ Customizable callback functions
14
- - Error handling that prevents Mailgun retries
7
+ ```bash
8
+ npm install mailgun-inbound-email
9
+ ```
10
+
11
+ > ⚠️ **REQUIRED**: Set `MAILGUN_WEBHOOK_SIGNING_KEY` environment variable before using webhooks. See [Security](#-security) section for details.
12
+
13
+ ```javascript
14
+ const express = require('express');
15
+ const multer = require('multer');
16
+ const { processEmailData, verifyRequestSignature } = require('mailgun-inbound-email');
17
+
18
+ const app = express();
19
+ const upload = multer({ storage: multer.memoryStorage() });
15
20
 
16
- ## Installation
21
+ app.post('/webhook/inbound',
22
+ express.urlencoded({ extended: true }),
23
+ upload.any(),
24
+ (req, res) => {
25
+ try {
26
+ // Verify signature automatically (only need signing key)
27
+ if (!verifyRequestSignature(req, process.env.MAILGUN_WEBHOOK_SIGNING_KEY)) {
28
+ return res.status(401).json({ error: 'Invalid signature' });
29
+ }
30
+
31
+ // Process email data
32
+ const { emailData } = processEmailData(req);
33
+
34
+ // Manual processing - you have full control
35
+ console.log('Email from:', emailData.from);
36
+ console.log('Subject:', emailData.subject);
37
+
38
+ // Your custom logic here
39
+ // - Save to database
40
+ // - Process attachments
41
+ // - Send notifications
42
+ // - etc.
43
+
44
+ res.status(200).json({ received: true });
45
+ } catch (error) {
46
+ console.error('Error:', error);
47
+ res.status(200).json({ received: true }); // Always return 200 to Mailgun
48
+ }
49
+ }
50
+ );
51
+
52
+ app.listen(3000);
53
+ ```
54
+
55
+ **That's it!** Just configure your Mailgun webhook URL to point to `https://yourdomain.com/webhook/inbound`
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
+
59
+ > 📖 **Need help setting up the webhook?** See the detailed guide: [Setting Up Mailgun Inbound Webhook](#-setting-up-mailgun-inbound-webhook)
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
+
87
+ ## ✨ Features
88
+
89
+ - ✅ **Full Manual Control** - You handle everything, no magic
90
+ - ✅ **Automatic Signature Verification** - Just provide signing key, package handles the rest
91
+ - ✅ **Production-ready utilities** - Battle-tested functions
92
+ - ✅ **Mailgun signature verification** - Secure by default
93
+ - ✅ **Replay attack prevention** - 15-minute timestamp window
94
+ - ✅ **Automatic email parsing** - Clean, structured email data
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
99
+ - ✅ **Zero dependencies** - Only Node.js built-ins
100
+ - ✅ **Simple & lightweight** - Just utility functions
101
+
102
+ ## 📦 Installation
17
103
 
18
104
  ```bash
19
105
  npm install mailgun-inbound-email
20
106
  ```
21
107
 
22
- ## Quick Start
108
+ > ⚠️ **REQUIRED**: After installation, you must set the `MAILGUN_WEBHOOK_SIGNING_KEY` environment variable. See [Security](#-security) section for instructions.
109
+
110
+ ## 🎯 Usage
23
111
 
24
- ### Option 1: Using Router (Recommended)
112
+ ### Inbound Email Webhooks
113
+
114
+ For receiving and processing inbound emails sent to your domain.
115
+
116
+ ### Basic Example
25
117
 
26
118
  ```javascript
27
119
  const express = require('express');
28
- const { createMailgunInboundRouter } = require('mailgun-inbound-email');
120
+ const multer = require('multer');
121
+ const { processEmailData, verifyRequestSignature } = require('mailgun-inbound-email');
29
122
 
30
123
  const app = express();
124
+ const upload = multer({ storage: multer.memoryStorage() });
31
125
 
32
- // Create router with callback
33
- const mailgunRouter = createMailgunInboundRouter({
34
- signingKey: process.env.MAILGUN_WEBHOOK_SIGNING_KEY, // or use env var
35
- onEmailReceived: (emailData) => {
36
- // Handle the email data
37
- console.log('Received email:', emailData);
38
- // Save to database, send notifications, etc.
39
- },
40
- path: '/inbound', // optional, defaults to '/inbound'
41
- requireSignature: true, // optional, defaults to true
126
+ app.post('/webhook/inbound',
127
+ express.urlencoded({ extended: true }),
128
+ upload.any(),
129
+ (req, res) => {
130
+ try {
131
+ // Verify signature automatically - only need signing key!
132
+ const signingKey = process.env.MAILGUN_WEBHOOK_SIGNING_KEY;
133
+ if (!verifyRequestSignature(req, signingKey)) {
134
+ return res.status(401).json({ error: 'Invalid Mailgun signature' });
135
+ }
136
+
137
+ // Process the email data
138
+ const { emailData } = processEmailData(req);
139
+
140
+ // Validate required fields
141
+ if (!emailData.from || !emailData.to || emailData.to.length === 0) {
142
+ return res.status(200).json({
143
+ received: true,
144
+ error: 'Missing required fields'
145
+ });
146
+ }
147
+
148
+ // Manual processing - you control everything
149
+ console.log('Processing email:', emailData.messageId);
150
+ console.log('From:', emailData.from);
151
+ console.log('To:', emailData.to);
152
+ console.log('Subject:', emailData.subject);
153
+ console.log('Attachments:', emailData.attachmentCount);
154
+
155
+ // Your custom processing logic here
156
+ // Example: Save to database
157
+ // await db.emails.create(emailData);
158
+
159
+ // Example: Process attachments
160
+ // emailData.attachments.forEach(attachment => {
161
+ // if (attachment.buffer) {
162
+ // fs.writeFileSync(`./uploads/${attachment.filename}`, attachment.buffer);
163
+ // }
164
+ // });
165
+
166
+ // Always return 200 to Mailgun
167
+ res.status(200).json({
168
+ received: true,
169
+ messageId: emailData.messageId
170
+ });
171
+
172
+ } catch (error) {
173
+ console.error('Error processing email:', error);
174
+ // Always return 200 to prevent Mailgun retries
175
+ res.status(200).json({ received: true });
176
+ }
177
+ }
178
+ );
179
+
180
+ app.listen(3000);
181
+ ```
182
+
183
+ ### Processing Attachments
184
+
185
+ ```javascript
186
+ const { processEmailData } = require('mailgun-inbound-email');
187
+ const fs = require('fs');
188
+
189
+ app.post('/webhook/inbound', express.urlencoded({ extended: true }), upload.any(), (req, res) => {
190
+ const { emailData } = processEmailData(req);
191
+
192
+ // Process attachments manually
193
+ emailData.attachments.forEach(attachment => {
194
+ if (attachment.buffer) {
195
+ // Save to file system
196
+ fs.writeFileSync(`./uploads/${attachment.filename}`, attachment.buffer);
197
+
198
+ // Or upload to S3, process image, etc.
199
+ // await s3.upload({
200
+ // Key: attachment.filename,
201
+ // Body: attachment.buffer,
202
+ // ContentType: attachment.mimetype,
203
+ // }).promise();
204
+ }
205
+ });
206
+
207
+ res.status(200).json({ received: true });
42
208
  });
209
+ ```
210
+
211
+ ### Async Processing
43
212
 
44
- app.use('/email', mailgunRouter);
213
+ ```javascript
214
+ app.post('/webhook/inbound', express.urlencoded({ extended: true }), upload.any(), async (req, res) => {
215
+ try {
216
+ const { emailData } = processEmailData(req);
217
+
218
+ // Async operations
219
+ await db.emails.create(emailData);
220
+ await notifyTeam(emailData);
221
+ await processAttachments(emailData);
222
+
223
+ res.status(200).json({ received: true });
224
+ } catch (error) {
225
+ console.error('Error:', error);
226
+ res.status(200).json({ received: true });
227
+ }
228
+ });
45
229
  ```
46
230
 
47
- ### Option 2: Using Middleware
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
48
236
 
49
237
  ```javascript
50
238
  const express = require('express');
51
- const { createMailgunInboundMiddleware } = require('mailgun-inbound-email');
239
+ const { mailgunWebhook } = require('mailgun-inbound-email');
52
240
 
53
241
  const app = express();
54
242
 
55
- const mailgunMiddleware = createMailgunInboundMiddleware({
56
- signingKey: process.env.MAILGUN_WEBHOOK_SIGNING_KEY,
57
- onEmailReceived: (emailData) => {
58
- // Handle the email data
59
- console.log('Received email:', emailData);
60
- },
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
+ }
61
266
  });
62
267
 
63
- app.post('/email/inbound', ...mailgunMiddleware);
268
+ app.listen(3000);
64
269
  ```
65
270
 
66
- ### Option 3: Manual Processing
271
+ #### Advanced Example with Event Handling
67
272
 
68
273
  ```javascript
69
274
  const express = require('express');
70
- const { processEmailData, verifyMailgunSignature } = require('mailgun-inbound-email');
71
- const multer = require('multer');
275
+ const { mailgunWebhook } = require('mailgun-inbound-email');
72
276
 
73
- const upload = multer({ storage: multer.memoryStorage() });
74
277
  const app = express();
75
278
 
76
- app.post('/email/inbound', express.urlencoded({ extended: true }), upload.any(), (req, res) => {
77
- const { emailData, token, timestamp, signature } = processEmailData(req);
279
+ app.post('/webhook/mailgun-events', express.json(), async (req, res) => {
280
+ const eventData = await mailgunWebhook(req, res);
78
281
 
79
- if (!verifyMailgunSignature(token, timestamp, signature, process.env.MAILGUN_WEBHOOK_SIGNING_KEY)) {
80
- return res.status(401).json({ error: 'Invalid signature' });
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);
81
310
  }
82
-
83
- // Your custom logic here
84
- console.log(emailData);
85
- res.status(200).json({ received: true });
86
311
  });
312
+
313
+ app.listen(3000);
87
314
  ```
88
315
 
89
- ## Email Data Structure
316
+ ## 📧 Email Data Structure
90
317
 
91
- The `emailData` object contains:
318
+ The `emailData` object contains all parsed email information:
92
319
 
93
320
  ```javascript
94
321
  {
95
- messageId: "string", // Cleaned message ID
96
- from: "sender@example.com", // Sender email address
97
- to: ["recipient@example.com"], // Array of recipient emails
98
- cc: ["cc@example.com"], // Array of CC emails
99
- subject: "Email Subject", // Email subject
100
- text: "Plain text body", // Plain text body
101
- html: "<html>...</html>", // HTML body
102
- headers: { // Parsed headers object
322
+ messageId: "string", // Cleaned message ID (without angle brackets)
323
+ from: "sender@example.com", // Sender email address (extracted from "Name <email>")
324
+ to: ["recipient@example.com"], // Array of recipient emails
325
+ cc: ["cc@example.com"], // Array of CC emails
326
+ subject: "Email Subject", // Email subject line
327
+ text: "Plain text body", // Plain text body content
328
+ html: "<html>...</html>", // HTML body content
329
+ headers: { // Parsed headers object
103
330
  "Message-ID": "...",
104
331
  "From": "...",
105
- // ... other headers
332
+ "To": "...",
333
+ "Subject": "...",
334
+ // ... all other email headers
106
335
  },
107
- attachments: [ // Attachment metadata
336
+ attachments: [ // Attachment metadata + buffers
108
337
  {
109
338
  filename: "document.pdf",
110
339
  originalname: "document.pdf",
@@ -112,50 +341,549 @@ The `emailData` object contains:
112
341
  size: 12345,
113
342
  extension: "pdf",
114
343
  encoding: "base64",
115
- fieldname: "attachment-1"
344
+ fieldname: "attachment-1",
345
+ buffer: Buffer, // File buffer for manual processing
116
346
  }
117
347
  ],
118
- attachmentCount: 1,
119
- receivedAt: "2024-01-01T00:00:00.000Z",
120
- timestamp: "2024-01-01T00:00:00.000Z"
348
+ attachmentCount: 1, // Number of attachments
349
+ receivedAt: "2024-01-01T00:00:00.000Z", // ISO timestamp when received
350
+ timestamp: "2024-01-01T00:00:00.000Z" // ISO timestamp (same as receivedAt)
121
351
  }
122
352
  ```
123
353
 
124
- ## Configuration Options
354
+ ## 🛠️ API Reference
355
+
356
+ ### `processEmailData(req)`
357
+
358
+ Process raw Express request and return structured email data.
359
+
360
+ **Parameters:**
361
+ - `req` (Object): Express request object with `body` and `files` properties
362
+
363
+ **Returns:**
364
+ - `Object`: `{ emailData, token, timestamp, signature }`
365
+
366
+ **Throws:**
367
+ - `Error`: If request body is invalid
368
+
369
+ **Example:**
370
+ ```javascript
371
+ const { emailData, token, timestamp, signature } = processEmailData(req);
372
+ ```
373
+
374
+ ### `verifyRequestSignature(req, signingKey)`
125
375
 
126
- ### `createMailgunInboundRouter(options)`
376
+ Verify Mailgun webhook signature automatically from request. This is the **recommended** method as it automatically extracts token, timestamp, and signature from the request.
127
377
 
378
+ **Parameters:**
379
+ - `req` (Object): Express request object with body
128
380
  - `signingKey` (string, optional): Mailgun webhook signing key. Defaults to `process.env.MAILGUN_WEBHOOK_SIGNING_KEY`
129
- - `onEmailReceived` (function, optional): Callback function called when email is received. Receives `emailData` as parameter
130
- - `path` (string, optional): Route path. Defaults to `'/inbound'`
131
- - `requireSignature` (boolean, optional): Whether to require signature verification. Defaults to `true`
132
381
 
133
- ### `createMailgunInboundMiddleware(options)`
382
+ **Returns:**
383
+ - `boolean`: `true` if signature is valid
384
+
385
+ **Example:**
386
+ ```javascript
387
+ const { verifyRequestSignature } = require('mailgun-inbound-email');
388
+
389
+ // Simple usage - automatically extracts token, timestamp, signature from req.body
390
+ // Uses MAILGUN_WEBHOOK_SIGNING_KEY from environment automatically
391
+ if (!verifyRequestSignature(req)) {
392
+ return res.status(401).json({ error: 'Invalid signature' });
393
+ }
394
+
395
+ // Or explicitly pass signing key
396
+ if (!verifyRequestSignature(req, process.env.MAILGUN_WEBHOOK_SIGNING_KEY)) {
397
+ return res.status(401).json({ error: 'Invalid signature' });
398
+ }
399
+ ```
400
+
401
+ ### `verifyMailgunSignature(token, timestamp, signature, signingKey)`
402
+
403
+ Verify Mailgun webhook signature manually (advanced usage). Use `verifyRequestSignature()` instead for simpler usage.
404
+
405
+ **Parameters:**
406
+ - `token` (string): Mailgun token from request
407
+ - `timestamp` (string): Request timestamp
408
+ - `signature` (string): Mailgun signature
409
+ - `signingKey` (string): Your Mailgun webhook signing key
410
+
411
+ **Returns:**
412
+ - `boolean`: `true` if signature is valid
413
+
414
+ **Example:**
415
+ ```javascript
416
+ // Advanced usage - manually extract and verify
417
+ const { token, timestamp, signature } = req.body;
418
+ const isValid = verifyMailgunSignature(token, timestamp, signature, signingKey);
419
+ if (!isValid) {
420
+ return res.status(401).json({ error: 'Invalid signature' });
421
+ }
422
+ ```
423
+
424
+ ### `mailgunWebhook(req, res, signingKey)`
134
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
135
431
  - `signingKey` (string, optional): Mailgun webhook signing key. Defaults to `process.env.MAILGUN_WEBHOOK_SIGNING_KEY`
136
- - `onEmailReceived` (function, optional): Callback function called when email is received. Receives `emailData` as parameter
137
- - `requireSignature` (boolean, optional): Whether to require signature verification. Defaults to `true`
138
432
 
139
- ## Environment Variables
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
+
500
+ ### Utility Functions
501
+
502
+ | Function | Description |
503
+ |----------|-------------|
504
+ | `extractEmail(value)` | Extract email from "Name <email@domain.com>" format |
505
+ | `extractEmails(value)` | Extract multiple emails from comma-separated string |
506
+ | `cleanMessageId(value)` | Remove angle brackets from message ID |
507
+ | `parseHeaders(headers)` | Safely parse email headers array to object |
508
+
509
+ ## 🔐 Security
510
+
511
+ ### Required Environment Variable
512
+
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
519
+
520
+ ### Security Features
521
+
522
+ - ✅ **Signature Verification**: Validates all webhook requests using HMAC SHA-256
523
+ - ✅ **Replay Attack Prevention**: Rejects requests older than 15 minutes
524
+ - ✅ **Timing-Safe Comparison**: Uses `crypto.timingSafeEqual` to prevent timing attacks
525
+ - ✅ **Input Validation**: Validates all required fields before processing
526
+
527
+ ### Getting Your Signing Key
528
+
529
+ 1. Log in to [Mailgun Dashboard](https://app.mailgun.com)
530
+ 2. Go to **Settings** → **Webhooks**
531
+ 3. Copy your **Webhook Signing Key**
532
+ 4. Set it as environment variable: `export MAILGUN_WEBHOOK_SIGNING_KEY=your-key-here`
533
+
534
+ ## 📝 Setting Up Mailgun Inbound Webhook
535
+
536
+ ### Step 1: Install Package and Dependencies
537
+
538
+ ```bash
539
+ # Install the package
540
+ npm install mailgun-inbound-email
541
+
542
+ # Install required dependencies
543
+ npm install express multer
544
+ ```
545
+
546
+ ### Step 2: Set Up Environment Variable (REQUIRED)
547
+
548
+ > ⚠️ **REQUIRED**: You must set `MAILGUN_WEBHOOK_SIGNING_KEY` before setting up your server.
140
549
 
141
- - `MAILGUN_WEBHOOK_SIGNING_KEY`: Your Mailgun webhook signing key (if not provided in options)
550
+ 1. Get your webhook signing key from Mailgun Dashboard Settings → Webhooks
551
+ 2. Set it as an environment variable:
142
552
 
143
- ## Utilities
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
563
+
564
+ Set up your Express server with the webhook endpoint (see examples above).
565
+
566
+ ### Step 4: Configure Mailgun Inbound Route (Dashboard Method)
567
+
568
+ Follow these steps to configure the inbound webhook URL in Mailgun Dashboard:
569
+
570
+ #### Option A: Using Mailgun Dashboard (Recommended for beginners)
571
+
572
+ 1. **Log in to Mailgun Dashboard**
573
+ - Go to [https://app.mailgun.com](https://app.mailgun.com)
574
+ - Log in with your Mailgun account
575
+
576
+ 2. **Navigate to Your Domain**
577
+ - Click on **Sending** in the left sidebar
578
+ - Click on **Domains**
579
+ - Select your verified domain (or add a new domain if needed)
144
580
 
145
- The package also exports utility functions:
581
+ 3. **Go to Receiving Settings**
582
+ - In your domain settings, click on the **Receiving** tab
583
+ - You'll see options for handling inbound emails
146
584
 
147
- - `processEmailData(req)`: Process raw request and return email data
148
- - `verifyMailgunSignature(token, timestamp, signature, signingKey)`: Verify webhook signature
149
- - `extractEmail(value)`: Extract email from "Name <email@domain.com>" format
150
- - `extractEmails(value)`: Extract multiple emails from comma-separated string
151
- - `cleanMessageId(value)`: Remove angle brackets from message ID
152
- - `parseHeaders(headers)`: Safely parse email headers
585
+ 4. **Create Inbound Route**
586
+ - Click on **Routes** (or **Add Route**)
587
+ - Click **Create Route** button
153
588
 
154
- ## Error Handling
589
+ 5. **Configure Route Settings**
590
+ - **Route Description**: Give it a name like "Inbound Email Webhook"
591
+ - **Filter Expression**:
592
+ - For all emails: Select `catch_all()` or leave default
593
+ - For specific emails: Use `match_recipient("your-email@yourdomain.com")`
594
+ - **Actions**:
595
+ - Select **Forward** or **Store and notify**
596
+ - Enter your webhook URL: `https://yourdomain.com/webhook/inbound`
597
+ - **Important**: Must use HTTPS (Mailgun requires it)
155
598
 
156
- The package automatically handles errors and always returns `200` status to Mailgun to prevent retries. Errors are logged to the console.
599
+ 6. **Save the Route**
600
+ - Click **Create Route** or **Save**
601
+ - Your route is now active
157
602
 
158
- ## License
603
+ #### Option B: Using Mailgun API (Recommended for automation)
604
+
605
+ You can also create routes programmatically using the Mailgun API:
606
+
607
+ ```bash
608
+ curl -X POST "https://api.mailgun.net/v3/routes" \
609
+ -u "api:YOUR_API_KEY" \
610
+ -F "priority=0" \
611
+ -F "description=Inbound Email Webhook" \
612
+ -F "expression=catch_all()" \
613
+ -F "action=forward('https://yourdomain.com/webhook/inbound')"
614
+ ```
615
+
616
+ Or using Node.js:
617
+
618
+ ```javascript
619
+ const formData = require('form-data');
620
+ const Mailgun = require('mailgun.js');
621
+ const mailgun = new Mailgun(formData);
622
+
623
+ const mg = mailgun.client({
624
+ username: 'api',
625
+ key: process.env.MAILGUN_API_KEY
626
+ });
627
+
628
+ // Create inbound route
629
+ mg.routes.create({
630
+ priority: 0,
631
+ description: 'Inbound Email Webhook',
632
+ expression: 'catch_all()',
633
+ action: ['forward("https://yourdomain.com/webhook/inbound")']
634
+ })
635
+ .then(msg => console.log('Route created:', msg))
636
+ .catch(err => console.error('Error:', err));
637
+ ```
638
+
639
+ > **Note**: If you already set `MAILGUN_WEBHOOK_SIGNING_KEY` in Step 2, you can skip this step.
640
+
641
+ 1. **Navigate to Webhooks Settings**
642
+ - In Mailgun Dashboard, go to **Settings** → **Webhooks**
643
+ - Or go to: [https://app.mailgun.com/app/webhooks](https://app.mailgun.com/app/webhooks)
644
+
645
+ 2. **Copy Your Signing Key**
646
+ - Find **Webhook Signing Key** section
647
+ - Click **Show** or **Reveal** to see your key
648
+ - Copy the signing key (it looks like: `key-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx`)
649
+
650
+ 3. **Set Environment Variable** (if not already set in Step 2)
651
+ ```bash
652
+ export MAILGUN_WEBHOOK_SIGNING_KEY=your-signing-key-here
653
+ ```
654
+
655
+ Or add to your `.env` file:
656
+ ```
657
+ MAILGUN_WEBHOOK_SIGNING_KEY=your-signing-key-here
658
+ ```
659
+
660
+ ### Step 5: Test Your Webhook
661
+
662
+ 1. **Deploy Your Server**
663
+ - Make sure your Express server is running and accessible via HTTPS
664
+ - Your webhook URL should be publicly accessible
665
+
666
+ 2. **Send a Test Email**
667
+ - Send an email to any address at your domain (e.g., `test@yourdomain.com`)
668
+ - Mailgun will forward it to your webhook URL
669
+
670
+ 3. **Check Your Logs**
671
+ - Check your server logs to see if the webhook was received
672
+ - Verify the email data is being processed correctly
673
+
674
+ 4. **Verify in Mailgun Dashboard**
675
+ - Go to **Logs** → **Webhooks** in Mailgun Dashboard
676
+ - You should see webhook delivery attempts and their status
677
+
678
+ ### Step 6: Verify Domain DNS Settings (If Needed)
679
+
680
+ If you haven't set up your domain yet, make sure to:
681
+
682
+ 1. **Add MX Records** (for receiving emails)
683
+ - Go to **Sending** → **Domains** → Your Domain → **DNS Records**
684
+ - Add MX record pointing to Mailgun:
685
+ - Priority: `10`
686
+ - Value: `mxa.mailgun.org`
687
+ - Add second MX record:
688
+ - Priority: `10`
689
+ - Value: `mxb.mailgun.org`
690
+
691
+ 2. **Verify Domain**
692
+ - Mailgun will provide DNS records to verify domain ownership
693
+ - Add the TXT record to your domain's DNS settings
694
+ - Wait for DNS propagation (can take up to 48 hours)
695
+
696
+ ### Troubleshooting
697
+
698
+ **Webhook not receiving emails?**
699
+ - ✅ Verify your webhook URL is accessible (test with curl or browser)
700
+ - ✅ Ensure you're using HTTPS (Mailgun requires it)
701
+ - ✅ Check Mailgun logs for delivery errors
702
+ - ✅ Verify your route is active in Mailgun Dashboard
703
+ - ✅ Check your server logs for incoming requests
704
+
705
+ **Signature verification failing?**
706
+ - ✅ **REQUIRED**: Verify `MAILGUN_WEBHOOK_SIGNING_KEY` environment variable is set (this is required!)
707
+ - ✅ Check that you copied the full signing key
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)`)
710
+
711
+ **Emails not being forwarded?**
712
+ - ✅ Verify MX records are set correctly
713
+ - ✅ Check domain verification status
714
+ - ✅ Ensure route filter expression matches your test email
715
+ - ✅ Check Mailgun logs for any errors
716
+
717
+ ### Example Webhook URL Formats
718
+
719
+ - Production: `https://api.yourdomain.com/webhook/inbound`
720
+ - Staging: `https://staging-api.yourdomain.com/webhook/inbound`
721
+ - Local testing (using ngrok): `https://abc123.ngrok.io/webhook/inbound`
722
+
723
+ **Note**: For local development, use a tool like [ngrok](https://ngrok.com/) to expose your local server:
724
+ ```bash
725
+ ngrok http 3000
726
+ # Use the HTTPS URL provided by ngrok
727
+ ```
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
+
852
+ ## 🎯 Production Checklist
853
+
854
+ ### Inbound Email Webhooks
855
+ - ✅ **REQUIRED**: Set `MAILGUN_WEBHOOK_SIGNING_KEY` environment variable
856
+ - ✅ Use HTTPS for webhook URL (Mailgun requires it)
857
+ - ✅ Implement your email processing logic
858
+ - ✅ Handle attachments if needed (buffers are included)
859
+ - ✅ Set up error monitoring/logging
860
+ - ✅ Test webhook signature verification
861
+ - ✅ Always return 200 status to Mailgun (prevents retries)
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
+
873
+ ## ⚠️ Important Notes
874
+
875
+ - **Always return 200** to Mailgun (even on errors) to prevent retries
876
+ - **Use HTTPS** for webhook URLs (Mailgun requirement)
877
+ - **Full manual control** - this package only provides utilities, you handle everything
878
+ - **Attachments include buffers** - handle large files appropriately
879
+ - **Event webhooks return data** - `mailgunWebhook()` returns event data for manual saving
880
+ - **Zero dependencies** - only uses Node.js built-in modules
881
+ - **Two webhook types** - Inbound email webhooks (form-data) vs Event webhooks (JSON)
882
+
883
+ ## 📄 License
159
884
 
160
885
  MIT
161
886
 
887
+ ## 🤝 Contributing
888
+
889
+ Contributions welcome! Please open an issue or submit a pull request.