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.
- package/LICENSE +22 -0
- package/README.md +461 -91
- package/index.js +112 -156
- package/package.json +18 -13
- 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
|
|
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
|
-
##
|
|
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
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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.
|
|
50
|
+
app.listen(3000);
|
|
45
51
|
```
|
|
46
52
|
|
|
47
|
-
|
|
53
|
+
**That's it!** Just configure your Mailgun webhook URL to point to `https://yourdomain.com/webhook/inbound`
|
|
48
54
|
|
|
49
|
-
|
|
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
|
-
|
|
57
|
+
## ✨ Features
|
|
54
58
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
69
|
+
## 📦 Installation
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
npm install mailgun-inbound-email
|
|
64
73
|
```
|
|
65
74
|
|
|
66
|
-
|
|
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('/
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
172
|
+
### Async Processing
|
|
90
173
|
|
|
91
|
-
|
|
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",
|
|
96
|
-
from: "sender@example.com",
|
|
97
|
-
to: ["recipient@example.com"],
|
|
98
|
-
cc: ["cc@example.com"],
|
|
99
|
-
subject: "Email Subject",
|
|
100
|
-
text: "Plain text body",
|
|
101
|
-
html: "<html>...</html>",
|
|
102
|
-
headers: {
|
|
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
|
-
|
|
208
|
+
"To": "...",
|
|
209
|
+
"Subject": "...",
|
|
210
|
+
// ... all other email headers
|
|
106
211
|
},
|
|
107
|
-
attachments: [
|
|
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
|
-
##
|
|
230
|
+
## 🛠️ API Reference
|
|
125
231
|
|
|
126
|
-
### `
|
|
232
|
+
### `processEmailData(req)`
|
|
127
233
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
- `
|
|
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
|
-
|
|
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
|
-
|
|
258
|
+
**Returns:**
|
|
259
|
+
- `boolean`: `true` if signature is valid
|
|
140
260
|
|
|
141
|
-
|
|
261
|
+
**Example:**
|
|
262
|
+
```javascript
|
|
263
|
+
const { verifyRequestSignature } = require('mailgun-inbound-email');
|
|
142
264
|
|
|
143
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
279
|
+
Verify Mailgun webhook signature manually (advanced usage). Use `verifyRequestSignature()` instead for simpler usage.
|
|
155
280
|
|
|
156
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
19
|
-
if (
|
|
20
|
-
console.error("
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
.
|
|
27
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
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 ||
|
|
179
|
+
mimetype: file.mimetype || "application/octet-stream",
|
|
96
180
|
size: file.size || 0,
|
|
97
|
-
extension: file.originalname
|
|
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
|
|
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
|
-
|
|
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": "
|
|
4
|
-
"description": "
|
|
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
|
-
|