mailgun-inbound-email 1.0.0 → 2.0.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 +461 -91
  3. package/index.js +112 -156
  4. package/package.json +18 -13
  5. package/SETUP.md +0 -62
package/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
package/README.md CHANGED
@@ -1,110 +1,215 @@
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 inbound email webhooks. Full manual control - you handle everything from webhook setup to email processing.
4
4
 
5
- ## Features
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
15
-
16
- ## Installation
5
+ ## 🚀 Quick Start
17
6
 
18
7
  ```bash
19
8
  npm install mailgun-inbound-email
20
9
  ```
21
10
 
22
- ## Quick Start
23
-
24
- ### Option 1: Using Router (Recommended)
25
-
26
11
  ```javascript
27
12
  const express = require('express');
28
- const { createMailgunInboundRouter } = require('mailgun-inbound-email');
13
+ const multer = require('multer');
14
+ const { processEmailData, verifyRequestSignature } = require('mailgun-inbound-email');
29
15
 
30
16
  const app = express();
17
+ const upload = multer({ storage: multer.memoryStorage() });
31
18
 
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
42
- });
19
+ app.post('/webhook/inbound',
20
+ express.urlencoded({ extended: true }),
21
+ upload.any(),
22
+ (req, res) => {
23
+ try {
24
+ // Verify signature automatically (only need signing key)
25
+ if (!verifyRequestSignature(req, process.env.MAILGUN_WEBHOOK_SIGNING_KEY)) {
26
+ return res.status(401).json({ error: 'Invalid signature' });
27
+ }
28
+
29
+ // Process email data
30
+ const { emailData } = processEmailData(req);
31
+
32
+ // Manual processing - you have full control
33
+ console.log('Email from:', emailData.from);
34
+ console.log('Subject:', emailData.subject);
35
+
36
+ // Your custom logic here
37
+ // - Save to database
38
+ // - Process attachments
39
+ // - Send notifications
40
+ // - etc.
41
+
42
+ res.status(200).json({ received: true });
43
+ } catch (error) {
44
+ console.error('Error:', error);
45
+ res.status(200).json({ received: true }); // Always return 200 to Mailgun
46
+ }
47
+ }
48
+ );
43
49
 
44
- app.use('/email', mailgunRouter);
50
+ app.listen(3000);
45
51
  ```
46
52
 
47
- ### Option 2: Using Middleware
53
+ **That's it!** Just configure your Mailgun webhook URL to point to `https://yourdomain.com/webhook/inbound`
48
54
 
49
- ```javascript
50
- const express = require('express');
51
- const { createMailgunInboundMiddleware } = require('mailgun-inbound-email');
55
+ > 📖 **Need help setting up the webhook?** See the detailed guide: [Setting Up Mailgun Inbound Webhook](#-setting-up-mailgun-inbound-webhook)
52
56
 
53
- const app = express();
57
+ ## Features
54
58
 
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
- },
61
- });
59
+ - **Full Manual Control** - You handle everything, no magic
60
+ - ✅ **Automatic Signature Verification** - Just provide signing key, package handles the rest
61
+ - **Production-ready utilities** - Battle-tested functions
62
+ - **Mailgun signature verification** - Secure by default
63
+ - **Replay attack prevention** - 15-minute timestamp window
64
+ - ✅ **Automatic email parsing** - Clean, structured email data
65
+ - ✅ **Attachment support** - Metadata + buffers for manual handling
66
+ - ✅ **Zero dependencies** - Only Node.js built-ins
67
+ - ✅ **Simple & lightweight** - Just utility functions
62
68
 
63
- app.post('/email/inbound', ...mailgunMiddleware);
69
+ ## 📦 Installation
70
+
71
+ ```bash
72
+ npm install mailgun-inbound-email
64
73
  ```
65
74
 
66
- ### Option 3: Manual Processing
75
+ ## 🎯 Usage
76
+
77
+ ### Basic Example
67
78
 
68
79
  ```javascript
69
80
  const express = require('express');
70
- const { processEmailData, verifyMailgunSignature } = require('mailgun-inbound-email');
71
81
  const multer = require('multer');
82
+ const { processEmailData, verifyRequestSignature } = require('mailgun-inbound-email');
72
83
 
73
- const upload = multer({ storage: multer.memoryStorage() });
74
84
  const app = express();
85
+ const upload = multer({ storage: multer.memoryStorage() });
75
86
 
76
- app.post('/email/inbound', express.urlencoded({ extended: true }), upload.any(), (req, res) => {
77
- const { emailData, token, timestamp, signature } = processEmailData(req);
78
-
79
- if (!verifyMailgunSignature(token, timestamp, signature, process.env.MAILGUN_WEBHOOK_SIGNING_KEY)) {
80
- return res.status(401).json({ error: 'Invalid signature' });
87
+ app.post('/webhook/inbound',
88
+ express.urlencoded({ extended: true }),
89
+ upload.any(),
90
+ (req, res) => {
91
+ try {
92
+ // Verify signature automatically - only need signing key!
93
+ const signingKey = process.env.MAILGUN_WEBHOOK_SIGNING_KEY;
94
+ if (!verifyRequestSignature(req, signingKey)) {
95
+ return res.status(401).json({ error: 'Invalid Mailgun signature' });
96
+ }
97
+
98
+ // Process the email data
99
+ const { emailData } = processEmailData(req);
100
+
101
+ // Validate required fields
102
+ if (!emailData.from || !emailData.to || emailData.to.length === 0) {
103
+ return res.status(200).json({
104
+ received: true,
105
+ error: 'Missing required fields'
106
+ });
107
+ }
108
+
109
+ // Manual processing - you control everything
110
+ console.log('Processing email:', emailData.messageId);
111
+ console.log('From:', emailData.from);
112
+ console.log('To:', emailData.to);
113
+ console.log('Subject:', emailData.subject);
114
+ console.log('Attachments:', emailData.attachmentCount);
115
+
116
+ // Your custom processing logic here
117
+ // Example: Save to database
118
+ // await db.emails.create(emailData);
119
+
120
+ // Example: Process attachments
121
+ // emailData.attachments.forEach(attachment => {
122
+ // if (attachment.buffer) {
123
+ // fs.writeFileSync(`./uploads/${attachment.filename}`, attachment.buffer);
124
+ // }
125
+ // });
126
+
127
+ // Always return 200 to Mailgun
128
+ res.status(200).json({
129
+ received: true,
130
+ messageId: emailData.messageId
131
+ });
132
+
133
+ } catch (error) {
134
+ console.error('Error processing email:', error);
135
+ // Always return 200 to prevent Mailgun retries
136
+ res.status(200).json({ received: true });
137
+ }
81
138
  }
139
+ );
140
+
141
+ app.listen(3000);
142
+ ```
143
+
144
+ ### Processing Attachments
145
+
146
+ ```javascript
147
+ const { processEmailData } = require('mailgun-inbound-email');
148
+ const fs = require('fs');
149
+
150
+ app.post('/webhook/inbound', express.urlencoded({ extended: true }), upload.any(), (req, res) => {
151
+ const { emailData } = processEmailData(req);
152
+
153
+ // Process attachments manually
154
+ emailData.attachments.forEach(attachment => {
155
+ if (attachment.buffer) {
156
+ // Save to file system
157
+ fs.writeFileSync(`./uploads/${attachment.filename}`, attachment.buffer);
158
+
159
+ // Or upload to S3, process image, etc.
160
+ // await s3.upload({
161
+ // Key: attachment.filename,
162
+ // Body: attachment.buffer,
163
+ // ContentType: attachment.mimetype,
164
+ // }).promise();
165
+ }
166
+ });
82
167
 
83
- // Your custom logic here
84
- console.log(emailData);
85
168
  res.status(200).json({ received: true });
86
169
  });
87
170
  ```
88
171
 
89
- ## Email Data Structure
172
+ ### Async Processing
90
173
 
91
- The `emailData` object contains:
174
+ ```javascript
175
+ app.post('/webhook/inbound', express.urlencoded({ extended: true }), upload.any(), async (req, res) => {
176
+ try {
177
+ const { emailData } = processEmailData(req);
178
+
179
+ // Async operations
180
+ await db.emails.create(emailData);
181
+ await notifyTeam(emailData);
182
+ await processAttachments(emailData);
183
+
184
+ res.status(200).json({ received: true });
185
+ } catch (error) {
186
+ console.error('Error:', error);
187
+ res.status(200).json({ received: true });
188
+ }
189
+ });
190
+ ```
191
+
192
+ ## 📧 Email Data Structure
193
+
194
+ The `emailData` object contains all parsed email information:
92
195
 
93
196
  ```javascript
94
197
  {
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
198
+ messageId: "string", // Cleaned message ID (without angle brackets)
199
+ from: "sender@example.com", // Sender email address (extracted from "Name <email>")
200
+ to: ["recipient@example.com"], // Array of recipient emails
201
+ cc: ["cc@example.com"], // Array of CC emails
202
+ subject: "Email Subject", // Email subject line
203
+ text: "Plain text body", // Plain text body content
204
+ html: "<html>...</html>", // HTML body content
205
+ headers: { // Parsed headers object
103
206
  "Message-ID": "...",
104
207
  "From": "...",
105
- // ... other headers
208
+ "To": "...",
209
+ "Subject": "...",
210
+ // ... all other email headers
106
211
  },
107
- attachments: [ // Attachment metadata
212
+ attachments: [ // Attachment metadata + buffers
108
213
  {
109
214
  filename: "document.pdf",
110
215
  originalname: "document.pdf",
@@ -112,50 +217,315 @@ The `emailData` object contains:
112
217
  size: 12345,
113
218
  extension: "pdf",
114
219
  encoding: "base64",
115
- fieldname: "attachment-1"
220
+ fieldname: "attachment-1",
221
+ buffer: Buffer, // File buffer for manual processing
116
222
  }
117
223
  ],
118
- attachmentCount: 1,
119
- receivedAt: "2024-01-01T00:00:00.000Z",
120
- timestamp: "2024-01-01T00:00:00.000Z"
224
+ attachmentCount: 1, // Number of attachments
225
+ receivedAt: "2024-01-01T00:00:00.000Z", // ISO timestamp when received
226
+ timestamp: "2024-01-01T00:00:00.000Z" // ISO timestamp (same as receivedAt)
121
227
  }
122
228
  ```
123
229
 
124
- ## Configuration Options
230
+ ## 🛠️ API Reference
125
231
 
126
- ### `createMailgunInboundRouter(options)`
232
+ ### `processEmailData(req)`
127
233
 
128
- - `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`
234
+ Process raw Express request and return structured email data.
235
+
236
+ **Parameters:**
237
+ - `req` (Object): Express request object with `body` and `files` properties
132
238
 
133
- ### `createMailgunInboundMiddleware(options)`
239
+ **Returns:**
240
+ - `Object`: `{ emailData, token, timestamp, signature }`
134
241
 
242
+ **Throws:**
243
+ - `Error`: If request body is invalid
244
+
245
+ **Example:**
246
+ ```javascript
247
+ const { emailData, token, timestamp, signature } = processEmailData(req);
248
+ ```
249
+
250
+ ### `verifyRequestSignature(req, signingKey)`
251
+
252
+ Verify Mailgun webhook signature automatically from request. This is the **recommended** method as it automatically extracts token, timestamp, and signature from the request.
253
+
254
+ **Parameters:**
255
+ - `req` (Object): Express request object with body
135
256
  - `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
257
 
139
- ## Environment Variables
258
+ **Returns:**
259
+ - `boolean`: `true` if signature is valid
140
260
 
141
- - `MAILGUN_WEBHOOK_SIGNING_KEY`: Your Mailgun webhook signing key (if not provided in options)
261
+ **Example:**
262
+ ```javascript
263
+ const { verifyRequestSignature } = require('mailgun-inbound-email');
142
264
 
143
- ## Utilities
265
+ // Simple usage - automatically extracts token, timestamp, signature from req.body
266
+ // Uses MAILGUN_WEBHOOK_SIGNING_KEY from environment automatically
267
+ if (!verifyRequestSignature(req)) {
268
+ return res.status(401).json({ error: 'Invalid signature' });
269
+ }
144
270
 
145
- The package also exports utility functions:
271
+ // Or explicitly pass signing key
272
+ if (!verifyRequestSignature(req, process.env.MAILGUN_WEBHOOK_SIGNING_KEY)) {
273
+ return res.status(401).json({ error: 'Invalid signature' });
274
+ }
275
+ ```
146
276
 
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
277
+ ### `verifyMailgunSignature(token, timestamp, signature, signingKey)`
153
278
 
154
- ## Error Handling
279
+ Verify Mailgun webhook signature manually (advanced usage). Use `verifyRequestSignature()` instead for simpler usage.
155
280
 
156
- The package automatically handles errors and always returns `200` status to Mailgun to prevent retries. Errors are logged to the console.
281
+ **Parameters:**
282
+ - `token` (string): Mailgun token from request
283
+ - `timestamp` (string): Request timestamp
284
+ - `signature` (string): Mailgun signature
285
+ - `signingKey` (string): Your Mailgun webhook signing key
157
286
 
158
- ## License
287
+ **Returns:**
288
+ - `boolean`: `true` if signature is valid
289
+
290
+ **Example:**
291
+ ```javascript
292
+ // Advanced usage - manually extract and verify
293
+ const { token, timestamp, signature } = req.body;
294
+ const isValid = verifyMailgunSignature(token, timestamp, signature, signingKey);
295
+ if (!isValid) {
296
+ return res.status(401).json({ error: 'Invalid signature' });
297
+ }
298
+ ```
299
+
300
+ ### Utility Functions
301
+
302
+ | Function | Description |
303
+ |----------|-------------|
304
+ | `extractEmail(value)` | Extract email from "Name <email@domain.com>" format |
305
+ | `extractEmails(value)` | Extract multiple emails from comma-separated string |
306
+ | `cleanMessageId(value)` | Remove angle brackets from message ID |
307
+ | `parseHeaders(headers)` | Safely parse email headers array to object |
308
+
309
+ ## 🔐 Security
310
+
311
+ ### Required Environment Variable
312
+
313
+ - `MAILGUN_WEBHOOK_SIGNING_KEY`: Your Mailgun webhook signing key (found in Mailgun dashboard → Settings → Webhooks)
314
+
315
+ ### Security Features
316
+
317
+ - ✅ **Signature Verification**: Validates all webhook requests using HMAC SHA-256
318
+ - ✅ **Replay Attack Prevention**: Rejects requests older than 15 minutes
319
+ - ✅ **Timing-Safe Comparison**: Uses `crypto.timingSafeEqual` to prevent timing attacks
320
+ - ✅ **Input Validation**: Validates all required fields before processing
321
+
322
+ ### Getting Your Signing Key
323
+
324
+ 1. Log in to [Mailgun Dashboard](https://app.mailgun.com)
325
+ 2. Go to **Settings** → **Webhooks**
326
+ 3. Copy your **Webhook Signing Key**
327
+ 4. Set it as environment variable: `export MAILGUN_WEBHOOK_SIGNING_KEY=your-key-here`
328
+
329
+ ## 📝 Setting Up Mailgun Inbound Webhook
330
+
331
+ ### Step 1: Install Package and Dependencies
332
+
333
+ ```bash
334
+ # Install the package
335
+ npm install mailgun-inbound-email
336
+
337
+ # Install required dependencies
338
+ npm install express multer
339
+ ```
340
+
341
+ ### Step 2: Set Up Your Express Server
342
+
343
+ Set up your Express server with the webhook endpoint (see examples above).
344
+
345
+ ### Step 3: Configure Mailgun Inbound Route (Dashboard Method)
346
+
347
+ Follow these steps to configure the inbound webhook URL in Mailgun Dashboard:
348
+
349
+ #### Option A: Using Mailgun Dashboard (Recommended for beginners)
350
+
351
+ 1. **Log in to Mailgun Dashboard**
352
+ - Go to [https://app.mailgun.com](https://app.mailgun.com)
353
+ - Log in with your Mailgun account
354
+
355
+ 2. **Navigate to Your Domain**
356
+ - Click on **Sending** in the left sidebar
357
+ - Click on **Domains**
358
+ - Select your verified domain (or add a new domain if needed)
359
+
360
+ 3. **Go to Receiving Settings**
361
+ - In your domain settings, click on the **Receiving** tab
362
+ - You'll see options for handling inbound emails
363
+
364
+ 4. **Create Inbound Route**
365
+ - Click on **Routes** (or **Add Route**)
366
+ - Click **Create Route** button
367
+
368
+ 5. **Configure Route Settings**
369
+ - **Route Description**: Give it a name like "Inbound Email Webhook"
370
+ - **Filter Expression**:
371
+ - For all emails: Select `catch_all()` or leave default
372
+ - For specific emails: Use `match_recipient("your-email@yourdomain.com")`
373
+ - **Actions**:
374
+ - Select **Forward** or **Store and notify**
375
+ - Enter your webhook URL: `https://yourdomain.com/webhook/inbound`
376
+ - **Important**: Must use HTTPS (Mailgun requires it)
377
+
378
+ 6. **Save the Route**
379
+ - Click **Create Route** or **Save**
380
+ - Your route is now active
381
+
382
+ #### Option B: Using Mailgun API (Recommended for automation)
383
+
384
+ You can also create routes programmatically using the Mailgun API:
385
+
386
+ ```bash
387
+ curl -X POST "https://api.mailgun.net/v3/routes" \
388
+ -u "api:YOUR_API_KEY" \
389
+ -F "priority=0" \
390
+ -F "description=Inbound Email Webhook" \
391
+ -F "expression=catch_all()" \
392
+ -F "action=forward('https://yourdomain.com/webhook/inbound')"
393
+ ```
394
+
395
+ Or using Node.js:
396
+
397
+ ```javascript
398
+ const formData = require('form-data');
399
+ const Mailgun = require('mailgun.js');
400
+ const mailgun = new Mailgun(formData);
401
+
402
+ const mg = mailgun.client({
403
+ username: 'api',
404
+ key: process.env.MAILGUN_API_KEY
405
+ });
406
+
407
+ // Create inbound route
408
+ mg.routes.create({
409
+ priority: 0,
410
+ description: 'Inbound Email Webhook',
411
+ expression: 'catch_all()',
412
+ action: ['forward("https://yourdomain.com/webhook/inbound")']
413
+ })
414
+ .then(msg => console.log('Route created:', msg))
415
+ .catch(err => console.error('Error:', err));
416
+ ```
417
+
418
+ ### Step 4: Get Your Webhook Signing Key
419
+
420
+ 1. **Navigate to Webhooks Settings**
421
+ - In Mailgun Dashboard, go to **Settings** → **Webhooks**
422
+ - Or go to: [https://app.mailgun.com/app/webhooks](https://app.mailgun.com/app/webhooks)
423
+
424
+ 2. **Copy Your Signing Key**
425
+ - Find **Webhook Signing Key** section
426
+ - Click **Show** or **Reveal** to see your key
427
+ - Copy the signing key (it looks like: `key-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx`)
428
+
429
+ 3. **Set Environment Variable**
430
+ ```bash
431
+ export MAILGUN_WEBHOOK_SIGNING_KEY=your-signing-key-here
432
+ ```
433
+
434
+ Or add to your `.env` file:
435
+ ```
436
+ MAILGUN_WEBHOOK_SIGNING_KEY=your-signing-key-here
437
+ ```
438
+
439
+ ### Step 5: Test Your Webhook
440
+
441
+ 1. **Deploy Your Server**
442
+ - Make sure your Express server is running and accessible via HTTPS
443
+ - Your webhook URL should be publicly accessible
444
+
445
+ 2. **Send a Test Email**
446
+ - Send an email to any address at your domain (e.g., `test@yourdomain.com`)
447
+ - Mailgun will forward it to your webhook URL
448
+
449
+ 3. **Check Your Logs**
450
+ - Check your server logs to see if the webhook was received
451
+ - Verify the email data is being processed correctly
452
+
453
+ 4. **Verify in Mailgun Dashboard**
454
+ - Go to **Logs** → **Webhooks** in Mailgun Dashboard
455
+ - You should see webhook delivery attempts and their status
456
+
457
+ ### Step 6: Verify Domain DNS Settings (If Needed)
458
+
459
+ If you haven't set up your domain yet, make sure to:
460
+
461
+ 1. **Add MX Records** (for receiving emails)
462
+ - Go to **Sending** → **Domains** → Your Domain → **DNS Records**
463
+ - Add MX record pointing to Mailgun:
464
+ - Priority: `10`
465
+ - Value: `mxa.mailgun.org`
466
+ - Add second MX record:
467
+ - Priority: `10`
468
+ - Value: `mxb.mailgun.org`
469
+
470
+ 2. **Verify Domain**
471
+ - Mailgun will provide DNS records to verify domain ownership
472
+ - Add the TXT record to your domain's DNS settings
473
+ - Wait for DNS propagation (can take up to 48 hours)
474
+
475
+ ### Troubleshooting
476
+
477
+ **Webhook not receiving emails?**
478
+ - ✅ Verify your webhook URL is accessible (test with curl or browser)
479
+ - ✅ Ensure you're using HTTPS (Mailgun requires it)
480
+ - ✅ Check Mailgun logs for delivery errors
481
+ - ✅ Verify your route is active in Mailgun Dashboard
482
+ - ✅ Check your server logs for incoming requests
483
+
484
+ **Signature verification failing?**
485
+ - ✅ Verify `MAILGUN_WEBHOOK_SIGNING_KEY` is set correctly
486
+ - ✅ Check that you copied the full signing key
487
+ - ✅ Ensure the key matches the one in Mailgun Dashboard
488
+
489
+ **Emails not being forwarded?**
490
+ - ✅ Verify MX records are set correctly
491
+ - ✅ Check domain verification status
492
+ - ✅ Ensure route filter expression matches your test email
493
+ - ✅ Check Mailgun logs for any errors
494
+
495
+ ### Example Webhook URL Formats
496
+
497
+ - Production: `https://api.yourdomain.com/webhook/inbound`
498
+ - Staging: `https://staging-api.yourdomain.com/webhook/inbound`
499
+ - Local testing (using ngrok): `https://abc123.ngrok.io/webhook/inbound`
500
+
501
+ **Note**: For local development, use a tool like [ngrok](https://ngrok.com/) to expose your local server:
502
+ ```bash
503
+ ngrok http 3000
504
+ # Use the HTTPS URL provided by ngrok
505
+ ```
506
+
507
+ ## 🎯 Production Checklist
508
+
509
+ - ✅ Set `MAILGUN_WEBHOOK_SIGNING_KEY` environment variable
510
+ - ✅ Use HTTPS for webhook URL (Mailgun requires it)
511
+ - ✅ Implement your email processing logic
512
+ - ✅ Handle attachments if needed (buffers are included)
513
+ - ✅ Set up error monitoring/logging
514
+ - ✅ Test webhook signature verification
515
+ - ✅ Always return 200 status to Mailgun (prevents retries)
516
+
517
+ ## ⚠️ Important Notes
518
+
519
+ - **Always return 200** to Mailgun (even on errors) to prevent retries
520
+ - **Use HTTPS** for webhook URLs (Mailgun requirement)
521
+ - **Full manual control** - this package only provides utilities, you handle everything
522
+ - **Attachments include buffers** - handle large files appropriately
523
+ - **Zero dependencies** - only uses Node.js built-in modules
524
+
525
+ ## 📄 License
159
526
 
160
527
  MIT
161
528
 
529
+ ## 🤝 Contributing
530
+
531
+ Contributions welcome! Please open an issue or submit a pull request.
package/index.js CHANGED
@@ -1,36 +1,63 @@
1
- const express = require("express");
2
1
  const crypto = require("crypto");
3
- const multer = require("multer");
4
-
5
- const upload = multer({ storage: multer.memoryStorage() });
6
2
 
7
3
  /**
8
4
  * Verify Mailgun webhook signature
5
+ * @param {string} token - Mailgun token
6
+ * @param {string} timestamp - Request timestamp
7
+ * @param {string} signature - Mailgun signature
8
+ * @param {string} signingKey - Mailgun webhook signing key
9
+ * @returns {boolean} True if signature is valid
9
10
  */
10
11
  function verifyMailgunSignature(token, timestamp, signature, signingKey) {
11
12
  if (!signingKey) {
12
- console.error("MAILGUN_WEBHOOK_SIGNING_KEY missing");
13
+ console.error("[MailgunInbound] MAILGUN_WEBHOOK_SIGNING_KEY missing");
14
+ return false;
15
+ }
16
+
17
+ if (!token || !timestamp || !signature) {
18
+ console.error("[MailgunInbound] Missing required signature parameters");
13
19
  return false;
14
20
  }
15
21
 
16
22
  const currentTime = Math.floor(Date.now() / 1000);
23
+ const requestTime = Number(timestamp);
17
24
 
18
- // Prevent replay attack (15 min)
19
- if (Math.abs(currentTime - Number(timestamp)) > 900) {
20
- console.error("Expired timestamp");
25
+ // Validate timestamp is a number
26
+ if (isNaN(requestTime)) {
27
+ console.error("[MailgunInbound] Invalid timestamp format");
21
28
  return false;
22
29
  }
23
30
 
24
- const hmac = crypto
25
- .createHmac("sha256", signingKey)
26
- .update(timestamp + token)
27
- .digest("hex");
31
+ // Prevent replay attack (15 min window)
32
+ if (Math.abs(currentTime - requestTime) > 900) {
33
+ console.error("[MailgunInbound] Expired timestamp", {
34
+ currentTime,
35
+ requestTime,
36
+ difference: Math.abs(currentTime - requestTime)
37
+ });
38
+ return false;
39
+ }
28
40
 
29
- return hmac === signature;
41
+ try {
42
+ const hmac = crypto
43
+ .createHmac("sha256", signingKey)
44
+ .update(timestamp + token)
45
+ .digest("hex");
46
+
47
+ return crypto.timingSafeEqual(
48
+ Buffer.from(hmac),
49
+ Buffer.from(signature)
50
+ );
51
+ } catch (error) {
52
+ console.error("[MailgunInbound] Signature verification error:", error.message);
53
+ return false;
54
+ }
30
55
  }
31
56
 
32
57
  /**
33
58
  * Parse headers safely
59
+ * @param {string|Array} headers - Email headers as string or array
60
+ * @returns {Array} Parsed headers array
34
61
  */
35
62
  function parseHeaders(headers) {
36
63
  if (Array.isArray(headers)) return headers;
@@ -44,6 +71,8 @@ function parseHeaders(headers) {
44
71
 
45
72
  /**
46
73
  * Extract email from "Name <email@domain.com>" or plain email
74
+ * @param {string} value - Email string
75
+ * @returns {string} Extracted email address
47
76
  */
48
77
  function extractEmail(value = "") {
49
78
  if (!value || typeof value !== 'string') return "";
@@ -53,6 +82,8 @@ function extractEmail(value = "") {
53
82
 
54
83
  /**
55
84
  * Extract multiple emails from comma-separated string
85
+ * @param {string} value - Comma-separated email string
86
+ * @returns {Array<string>} Array of email addresses
56
87
  */
57
88
  function extractEmails(value = "") {
58
89
  if (!value || typeof value !== 'string') return [];
@@ -61,6 +92,8 @@ function extractEmails(value = "") {
61
92
 
62
93
  /**
63
94
  * Remove angle brackets from message ID
95
+ * @param {string} value - Message ID string
96
+ * @returns {string|null} Cleaned message ID
64
97
  */
65
98
  function cleanMessageId(value) {
66
99
  if (!value || typeof value !== 'string') return null;
@@ -68,9 +101,60 @@ function cleanMessageId(value) {
68
101
  }
69
102
 
70
103
  /**
71
- * Process email data from Mailgun webhook
104
+ * Verify Mailgun webhook signature automatically from request
105
+ *
106
+ * This function automatically extracts token, timestamp, and signature
107
+ * from the request body and verifies the signature. You only need to
108
+ * provide the signing key.
109
+ *
110
+ * @param {Object} req - Express request object with body
111
+ * @param {Object} req.body - Request body containing token, timestamp, signature
112
+ * @param {string} signingKey - Mailgun webhook signing key (or use MAILGUN_WEBHOOK_SIGNING_KEY env var)
113
+ * @returns {boolean} True if signature is valid
114
+ *
115
+ * @example
116
+ * const { verifyRequestSignature } = require('mailgun-inbound-email');
117
+ * const signingKey = process.env.MAILGUN_WEBHOOK_SIGNING_KEY;
118
+ * if (!verifyRequestSignature(req, signingKey)) {
119
+ * return res.status(401).json({ error: 'Invalid signature' });
120
+ * }
121
+ */
122
+ function verifyRequestSignature(req, signingKey = process.env.MAILGUN_WEBHOOK_SIGNING_KEY) {
123
+ if (!req || !req.body) {
124
+ console.error("[MailgunInbound] Invalid request: missing body");
125
+ return false;
126
+ }
127
+
128
+ const { token, timestamp, signature } = req.body;
129
+ return verifyMailgunSignature(token, timestamp, signature, signingKey);
130
+ }
131
+
132
+ /**
133
+ * Process email data from Mailgun webhook request
134
+ *
135
+ * This function processes the raw Express request body and files
136
+ * to extract and structure email data for manual processing.
137
+ *
138
+ * @param {Object} req - Express request object with body and files
139
+ * @param {Object} req.body - Request body containing email fields
140
+ * @param {Array} req.files - Array of uploaded files (attachments)
141
+ * @returns {Object} Processed email data with token, timestamp, signature
142
+ * @returns {Object} return.emailData - Structured email data
143
+ * @returns {string} return.token - Mailgun token
144
+ * @returns {string} return.timestamp - Request timestamp
145
+ * @returns {string} return.signature - Mailgun signature
146
+ * @throws {Error} If request body is invalid
147
+ *
148
+ * @example
149
+ * const { processEmailData } = require('mailgun-inbound-email');
150
+ * const { emailData } = processEmailData(req);
151
+ * console.log(emailData.from, emailData.subject);
72
152
  */
73
153
  function processEmailData(req) {
154
+ if (!req || !req.body) {
155
+ throw new Error("Invalid request: missing body");
156
+ }
157
+
74
158
  const {
75
159
  token,
76
160
  timestamp,
@@ -88,18 +172,21 @@ function processEmailData(req) {
88
172
  "attachment-count": attachmentCount,
89
173
  } = req.body;
90
174
 
91
- // Process attachments metadata (without buffer for clean JSON)
92
- const processedAttachments = (req.files || []).map((file) => ({
93
- filename: file.originalname || `attachment-${Date.now()}`,
175
+ // Process attachments metadata with buffers for manual processing
176
+ const processedAttachments = (req.files || []).map((file, index) => ({
177
+ filename: file.originalname || `attachment-${Date.now()}-${index}`,
94
178
  originalname: file.originalname || null,
95
- mimetype: file.mimetype || null,
179
+ mimetype: file.mimetype || "application/octet-stream",
96
180
  size: file.size || 0,
97
- extension: file.originalname ? file.originalname.split('.').pop() : null,
181
+ extension: file.originalname
182
+ ? file.originalname.split('.').pop().toLowerCase()
183
+ : null,
98
184
  encoding: file.encoding || null,
99
185
  fieldname: file.fieldname || null,
186
+ buffer: file.buffer || null, // Include buffer for manual processing
100
187
  }));
101
188
 
102
- // Convert headers array to object for easier storage
189
+ // Convert headers array to object for easier access
103
190
  const headersObj = {};
104
191
  const parsedHeaders = parseHeaders(messageHeaders);
105
192
  if (parsedHeaders && parsedHeaders.length > 0) {
@@ -112,9 +199,9 @@ function processEmailData(req) {
112
199
  }
113
200
 
114
201
  // Extract CC from body or headers
115
- const ccValue = cc || headersObj['Cc'] || headersObj['CC'] || ""
202
+ const ccValue = cc || headersObj['Cc'] || headersObj['CC'] || "";
116
203
  // Extract TO from body or headers (can be multiple recipients)
117
- const toValue = recipient || headersObj['To'] || headersObj['TO'] || ""
204
+ const toValue = recipient || headersObj['To'] || headersObj['TO'] || "";
118
205
 
119
206
  const emailData = {
120
207
  messageId: cleanMessageId(headersObj['Message-ID'] || headersObj['Message-Id'] || null),
@@ -139,144 +226,13 @@ function processEmailData(req) {
139
226
  };
140
227
  }
141
228
 
142
- /**
143
- * Create Express router for Mailgun inbound email webhook
144
- *
145
- * @param {Object} options - Configuration options
146
- * @param {string} options.signingKey - Mailgun webhook signing key (or use MAILGUN_WEBHOOK_SIGNING_KEY env var)
147
- * @param {Function} options.onEmailReceived - Callback function called when email is received (emailData) => {}
148
- * @param {string} options.path - Route path (default: '/inbound')
149
- * @param {boolean} options.requireSignature - Whether to require signature verification (default: true)
150
- * @returns {express.Router} Express router
151
- */
152
- function createMailgunInboundRouter(options = {}) {
153
- const router = express.Router();
154
- const {
155
- signingKey = process.env.MAILGUN_WEBHOOK_SIGNING_KEY,
156
- onEmailReceived,
157
- path = '/inbound',
158
- requireSignature = true,
159
- } = options;
160
-
161
- router.post(path, express.urlencoded({ extended: true }), upload.any(), (req, res) => {
162
- try {
163
- const { emailData, token, timestamp, signature } = processEmailData(req);
164
-
165
- // 🔐 Verify Mailgun authenticity
166
- if (requireSignature && !verifyMailgunSignature(token, timestamp, signature, signingKey)) {
167
- return res.status(401).json({ error: "Invalid Mailgun signature" });
168
- }
169
-
170
- // Validate required fields
171
- if (!emailData.from || !emailData.to || emailData.to.length === 0) {
172
- console.error("Missing required email fields:", { from: emailData.from, to: emailData.to });
173
- // Still return 200 to prevent Mailgun retries
174
- return res.status(200).json({ received: true, error: "Missing required fields" });
175
- }
176
-
177
- // Call user-provided callback if provided
178
- if (onEmailReceived && typeof onEmailReceived === 'function') {
179
- try {
180
- onEmailReceived(emailData);
181
- } catch (callbackError) {
182
- console.error("Error in onEmailReceived callback:", callbackError);
183
- }
184
- } else {
185
- // Default: log clean JSON data ready to save
186
- console.log(JSON.stringify(emailData, null, 2));
187
- }
188
-
189
- res.status(200).json({ received: true });
190
-
191
- } catch (error) {
192
- console.error("Inbound email processing error:", {
193
- error: error.message,
194
- stack: error.stack,
195
- timestamp: new Date().toISOString(),
196
- });
197
-
198
- // Mailgun MUST receive 200 or it will retry
199
- res.status(200).json({ received: true });
200
- }
201
- });
202
-
203
- return router;
204
- }
205
-
206
- /**
207
- * Create Express middleware for Mailgun inbound email webhook
208
- *
209
- * @param {Object} options - Configuration options
210
- * @param {string} options.signingKey - Mailgun webhook signing key (or use MAILGUN_WEBHOOK_SIGNING_KEY env var)
211
- * @param {Function} options.onEmailReceived - Callback function called when email is received (emailData) => {}
212
- * @param {boolean} options.requireSignature - Whether to require signature verification (default: true)
213
- * @returns {Array} Express middleware array
214
- */
215
- function createMailgunInboundMiddleware(options = {}) {
216
- const {
217
- signingKey = process.env.MAILGUN_WEBHOOK_SIGNING_KEY,
218
- onEmailReceived,
219
- requireSignature = true,
220
- } = options;
221
-
222
- return [
223
- express.urlencoded({ extended: true }),
224
- upload.any(),
225
- (req, res, next) => {
226
- try {
227
- const { emailData, token, timestamp, signature } = processEmailData(req);
228
-
229
- // 🔐 Verify Mailgun authenticity
230
- if (requireSignature && !verifyMailgunSignature(token, timestamp, signature, signingKey)) {
231
- return res.status(401).json({ error: "Invalid Mailgun signature" });
232
- }
233
-
234
- // Validate required fields
235
- if (!emailData.from || !emailData.to || emailData.to.length === 0) {
236
- console.error("Missing required email fields:", { from: emailData.from, to: emailData.to });
237
- return res.status(200).json({ received: true, error: "Missing required fields" });
238
- }
239
-
240
- // Attach emailData to request object
241
- req.emailData = emailData;
242
-
243
- // Call user-provided callback if provided
244
- if (onEmailReceived && typeof onEmailReceived === 'function') {
245
- try {
246
- onEmailReceived(emailData);
247
- } catch (callbackError) {
248
- console.error("Error in onEmailReceived callback:", callbackError);
249
- }
250
- } else {
251
- // Default: log clean JSON data ready to save
252
- console.log(JSON.stringify(emailData, null, 2));
253
- }
254
-
255
- res.status(200).json({ received: true });
256
-
257
- } catch (error) {
258
- console.error("Inbound email processing error:", {
259
- error: error.message,
260
- stack: error.stack,
261
- timestamp: new Date().toISOString(),
262
- });
263
-
264
- // Mailgun MUST receive 200 or it will retry
265
- res.status(200).json({ received: true });
266
- }
267
- }
268
- ];
269
- }
270
-
271
- // Export utilities and main functions
229
+ // Export utility functions for manual processing
272
230
  module.exports = {
273
- createMailgunInboundRouter,
274
- createMailgunInboundMiddleware,
275
231
  processEmailData,
276
- verifyMailgunSignature,
232
+ verifyRequestSignature, // Automatic signature verification (recommended)
233
+ verifyMailgunSignature, // Manual signature verification (advanced)
277
234
  extractEmail,
278
235
  extractEmails,
279
236
  cleanMessageId,
280
237
  parseHeaders,
281
238
  };
282
-
package/package.json CHANGED
@@ -1,10 +1,12 @@
1
1
  {
2
2
  "name": "mailgun-inbound-email",
3
- "version": "1.0.0",
4
- "description": "A reusable npm package for handling Mailgun inbound email webhooks with Express.js",
3
+ "version": "2.0.0",
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": {
7
- "test": "echo \"Error: no test specified\" && exit 1"
7
+ "test": "echo \"Error: no test specified\" && exit 1",
8
+ "lint": "echo \"Linting not configured\"",
9
+ "prepublishOnly": "echo \"Ready to publish\""
8
10
  },
9
11
  "keywords": [
10
12
  "mailgun",
@@ -12,19 +14,17 @@
12
14
  "email",
13
15
  "webhook",
14
16
  "express",
15
- "middleware"
17
+ "middleware",
18
+ "email-processing",
19
+ "mailgun-webhook",
20
+ "inbound-email",
21
+ "email-handler"
16
22
  ],
17
23
  "author": "",
18
24
  "license": "MIT",
19
- "dependencies": {
20
- "express": "^4.16.1",
21
- "multer": "^2.0.2"
22
- },
23
- "peerDependencies": {
24
- "express": "^4.0.0"
25
- },
25
+ "dependencies": {},
26
26
  "engines": {
27
- "node": ">=14"
27
+ "node": ">=14.0.0"
28
28
  },
29
29
  "devDependencies": {},
30
30
  "repository": {
@@ -35,5 +35,10 @@
35
35
  "bugs": {
36
36
  "url": "https://github.com/RitikKumarSahoo/mailgun-inbound/issues"
37
37
  },
38
- "homepage": "https://github.com/RitikKumarSahoo/mailgun-inbound#readme"
38
+ "homepage": "https://github.com/RitikKumarSahoo/mailgun-inbound#readme",
39
+ "files": [
40
+ "index.js",
41
+ "README.md",
42
+ "LICENSE"
43
+ ]
39
44
  }
package/SETUP.md DELETED
@@ -1,62 +0,0 @@
1
- # Setup Instructions
2
-
3
- ## For Local Development
4
-
5
- To use this package in your project before publishing to npm:
6
-
7
- ### Option 1: npm link (Recommended for development)
8
-
9
- 1. In the `mailgun-inbound-email` directory:
10
- ```bash
11
- cd /home/ritik-ls/Desktop/mailgun-inbound-email
12
- npm link
13
- ```
14
-
15
- 2. In your project directory:
16
- ```bash
17
- cd /home/ritik-ls/Desktop/node_skeleton
18
- npm link mailgun-inbound-email
19
- ```
20
-
21
- ### Option 2: Install from local path
22
-
23
- In your project's `package.json`, add:
24
- ```json
25
- {
26
- "dependencies": {
27
- "mailgun-inbound-email": "file:../mailgun-inbound-email"
28
- }
29
- }
30
- ```
31
-
32
- Then run:
33
- ```bash
34
- npm install
35
- ```
36
-
37
- ### Option 3: Publish to npm (for production use)
38
-
39
- 1. Update `package.json` with your details (name, author, etc.)
40
- 2. Login to npm:
41
- ```bash
42
- npm login
43
- ```
44
-
45
- 3. Publish:
46
- ```bash
47
- npm publish
48
- ```
49
-
50
- 4. Then install in your project:
51
- ```bash
52
- npm install mailgun-inbound-email
53
- ```
54
-
55
- ## After Setup
56
-
57
- Make sure to install dependencies in the package directory:
58
- ```bash
59
- cd /home/ritik-ls/Desktop/mailgun-inbound-email
60
- npm install
61
- ```
62
-