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/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),
@@ -140,143 +227,256 @@ function processEmailData(req) {
140
227
  }
141
228
 
142
229
  /**
143
- * Create Express router for Mailgun inbound email webhook
230
+ * Production-ready Mailgun event webhook handler
231
+ *
232
+ * Handles Mailgun event webhooks (delivered, opened, clicked, bounced, etc.)
233
+ * with proper error handling, validation, and logging. Returns event data
234
+ * for manual processing and saving to database.
144
235
  *
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
236
+ * @param {Object} req - Express request object
237
+ * @param {Object} res - Express response object
238
+ * @param {string} signingKey - Mailgun webhook signing key (optional, defaults to MAILGUN_WEBHOOK_SIGNING_KEY env var)
239
+ * @returns {Promise<Object|null>} Returns event data if successfully processed, null otherwise
240
+ *
241
+ * @example
242
+ * const { mailgunWebhook } = require('mailgun-inbound-email');
243
+ *
244
+ * app.post('/webhook/mailgun-events', express.json(), async (req, res) => {
245
+ * const eventData = await mailgunWebhook(req, res);
246
+ * // eventData contains the processed event data for manual saving
247
+ * if (eventData && eventData.received && eventData.event) {
248
+ * await db.events.create(eventData);
249
+ * }
250
+ * });
151
251
  */
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 });
252
+ async function mailgunWebhook(req, res, signingKey = process.env.MAILGUN_WEBHOOK_SIGNING_KEY) {
253
+ const startTime = Date.now();
254
+ const correlationId = req.headers['x-request-id'] ||
255
+ req.headers['x-correlation-id'] ||
256
+ `mg-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
190
257
 
191
- } catch (error) {
192
- console.error("Inbound email processing error:", {
193
- error: error.message,
194
- stack: error.stack,
195
- timestamp: new Date().toISOString(),
258
+ try {
259
+ // Validate request body
260
+ if (!req || !req.body) {
261
+ console.error(`[MailgunWebhook:${correlationId}] Invalid request: missing body`, {
262
+ ip: req.ip || req.connection?.remoteAddress,
263
+ userAgent: req.headers['user-agent'],
264
+ });
265
+ res.status(400).json({
266
+ received: false,
267
+ error: 'Invalid request',
268
+ correlationId
196
269
  });
270
+ return null;
271
+ }
197
272
 
198
- // Mailgun MUST receive 200 or it will retry
199
- res.status(200).json({ received: true });
273
+ // 🔐 Verify Mailgun request signature
274
+ const isValid = verifyRequestSignature(req, signingKey);
275
+ if (!isValid) {
276
+ console.warn(`[MailgunWebhook:${correlationId}] Invalid Mailgun webhook signature`, {
277
+ ip: req.ip || req.connection?.remoteAddress,
278
+ userAgent: req.headers['user-agent'],
279
+ hasBody: !!req.body,
280
+ hasToken: !!req.body.token,
281
+ hasTimestamp: !!req.body.timestamp,
282
+ hasSignature: !!req.body.signature,
283
+ });
284
+ res.status(401).json({
285
+ received: false,
286
+ error: 'Invalid signature',
287
+ correlationId
288
+ });
289
+ return null;
200
290
  }
201
- });
202
291
 
203
- return router;
204
- }
292
+ // Extract event data from request body
293
+ const eventData = req.body['event-data'] || {};
294
+ const event = eventData.event;
295
+ const eventId = eventData.id || eventData['event-id'] || null;
296
+ const recipient = eventData.recipient;
297
+ const messageId = eventData.message?.headers?.['message-id'] ||
298
+ eventData['message-id'] ||
299
+ eventData.messageId ||
300
+ null;
301
+ const url = eventData.url;
302
+ const timestamp = eventData.timestamp || Date.now() / 1000;
303
+ const domain = eventData.domain?.name || eventData.domain;
304
+ const reason = eventData['delivery-status']?.description ||
305
+ eventData.reason ||
306
+ eventData['failure-reason'] ||
307
+ null;
308
+
309
+ // Validate required fields
310
+ if (!event) {
311
+ console.warn(`[MailgunWebhook:${correlationId}] Missing event type`, {
312
+ body: req.body
313
+ });
314
+ const errorResponse = {
315
+ received: true,
316
+ error: 'Missing event type',
317
+ correlationId
318
+ };
319
+ res.status(200).json(errorResponse);
320
+ return null;
321
+ }
205
322
 
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" });
323
+ // Prepare response data with correlation ID for tracking
324
+ let responseData = {
325
+ received: true,
326
+ event,
327
+ eventId,
328
+ recipient,
329
+ messageId,
330
+ timestamp: typeof timestamp === 'number' ? new Date(timestamp * 1000).toISOString() : timestamp,
331
+ domain,
332
+ correlationId,
333
+ processedAt: new Date().toISOString(),
334
+ };
335
+
336
+ // Handle different event types and add event-specific data
337
+ switch (event) {
338
+ case "delivered":
339
+ console.log(`[MailgunWebhook:${correlationId}] ✅ Email delivered to:`, recipient);
340
+ console.log(`[MailgunWebhook:${correlationId}] Message ID:`, messageId);
341
+ responseData.status = "delivered";
342
+ responseData.deliveredAt = typeof timestamp === 'number' ? new Date(timestamp * 1000).toISOString() : timestamp;
343
+ if (eventData['delivery-status']) {
344
+ responseData.deliveryStatus = {
345
+ code: eventData['delivery-status'].code,
346
+ message: eventData['delivery-status'].message,
347
+ description: eventData['delivery-status'].description,
348
+ tls: eventData['delivery-status'].tls,
349
+ certificateVerified: eventData['delivery-status']['certificate-verified'],
350
+ };
232
351
  }
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" });
352
+ break;
353
+
354
+ case "opened":
355
+ console.log(`[MailgunWebhook:${correlationId}] 👀 Email opened by:`, recipient);
356
+ console.log(`[MailgunWebhook:${correlationId}] Message ID:`, messageId);
357
+ responseData.status = "opened";
358
+ responseData.openedAt = typeof timestamp === 'number' ? new Date(timestamp * 1000).toISOString() : timestamp;
359
+ responseData.clientInfo = eventData['client-info'] || null;
360
+ responseData.geolocation = eventData.geolocation || null;
361
+ responseData.userAgent = eventData['client-info']?.clientName || null;
362
+ break;
363
+
364
+ case "clicked":
365
+ console.log(`[MailgunWebhook:${correlationId}] 🔗 Link clicked:`, url);
366
+ console.log(`[MailgunWebhook:${correlationId}] Recipient:`, recipient);
367
+ console.log(`[MailgunWebhook:${correlationId}] Message ID:`, messageId);
368
+ responseData.status = "clicked";
369
+ responseData.url = url;
370
+ responseData.clickedAt = typeof timestamp === 'number' ? new Date(timestamp * 1000).toISOString() : timestamp;
371
+ responseData.clientInfo = eventData['client-info'] || null;
372
+ responseData.geolocation = eventData.geolocation || null;
373
+ break;
374
+
375
+ case "bounced":
376
+ console.log(`[MailgunWebhook:${correlationId}] ❌ Email bounced:`, recipient);
377
+ console.log(`[MailgunWebhook:${correlationId}] Message ID:`, messageId);
378
+ responseData.status = "bounced";
379
+ responseData.reason = reason;
380
+ responseData.bouncedAt = typeof timestamp === 'number' ? new Date(timestamp * 1000).toISOString() : timestamp;
381
+ if (eventData['delivery-status']) {
382
+ responseData.deliveryStatus = {
383
+ code: eventData['delivery-status'].code,
384
+ message: eventData['delivery-status'].message,
385
+ description: eventData['delivery-status'].description,
386
+ attemptNo: eventData['delivery-status']['attempt-no'],
387
+ sessionSeconds: eventData['delivery-status']['session-seconds'],
388
+ };
238
389
  }
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));
390
+ responseData.severity = eventData.severity || 'permanent';
391
+ break;
392
+
393
+ case "complained":
394
+ console.log(`[MailgunWebhook:${correlationId}] 🚨 Spam complaint:`, recipient);
395
+ console.log(`[MailgunWebhook:${correlationId}] Message ID:`, messageId);
396
+ responseData.status = "complained";
397
+ responseData.complainedAt = typeof timestamp === 'number' ? new Date(timestamp * 1000).toISOString() : timestamp;
398
+ break;
399
+
400
+ case "failed":
401
+ console.log(`[MailgunWebhook:${correlationId}] âš ī¸ Email failed:`, recipient);
402
+ console.log(`[MailgunWebhook:${correlationId}] Message ID:`, messageId);
403
+ responseData.status = "failed";
404
+ responseData.reason = reason;
405
+ responseData.failedAt = typeof timestamp === 'number' ? new Date(timestamp * 1000).toISOString() : timestamp;
406
+ if (eventData['delivery-status']) {
407
+ responseData.deliveryStatus = {
408
+ code: eventData['delivery-status'].code,
409
+ message: eventData['delivery-status'].message,
410
+ description: eventData['delivery-status'].description,
411
+ attemptNo: eventData['delivery-status']['attempt-no'],
412
+ sessionSeconds: eventData['delivery-status']['session-seconds'],
413
+ };
253
414
  }
415
+ break;
416
+
417
+ case "unsubscribed":
418
+ console.log(`[MailgunWebhook:${correlationId}] 📤 User unsubscribed:`, recipient);
419
+ console.log(`[MailgunWebhook:${correlationId}] Message ID:`, messageId);
420
+ responseData.status = "unsubscribed";
421
+ responseData.unsubscribedAt = typeof timestamp === 'number' ? new Date(timestamp * 1000).toISOString() : timestamp;
422
+ break;
423
+
424
+ case "stored":
425
+ console.log(`[MailgunWebhook:${correlationId}] 💾 Email stored:`, recipient);
426
+ console.log(`[MailgunWebhook:${correlationId}] Message ID:`, messageId);
427
+ responseData.status = "stored";
428
+ responseData.storedAt = typeof timestamp === 'number' ? new Date(timestamp * 1000).toISOString() : timestamp;
429
+ break;
430
+
431
+ default:
432
+ console.log(`[MailgunWebhook:${correlationId}] â„šī¸ Other event:`, event);
433
+ console.log(`[MailgunWebhook:${correlationId}] Message ID:`, messageId);
434
+ responseData.status = "unknown";
435
+ responseData.fullEventData = eventData;
436
+ }
254
437
 
255
- res.status(200).json({ received: true });
438
+ // Log successful processing
439
+ const duration = Date.now() - startTime;
440
+ console.log(`[MailgunWebhook:${correlationId}] ✅ Webhook processed successfully`, {
441
+ event,
442
+ eventId,
443
+ duration: `${duration}ms`,
444
+ });
256
445
 
257
- } catch (error) {
258
- console.error("Inbound email processing error:", {
259
- error: error.message,
260
- stack: error.stack,
261
- timestamp: new Date().toISOString(),
262
- });
446
+ // ✅ MUST return 200 OK with event data for manual saving
447
+ res.status(200).json(responseData);
448
+
449
+ // Return event data so caller can save it manually
450
+ return responseData;
451
+
452
+ } catch (error) {
453
+ const duration = Date.now() - startTime;
454
+ console.error(`[MailgunWebhook:${correlationId}] ❌ Webhook Error:`, {
455
+ error: error.message,
456
+ stack: error.stack,
457
+ duration: `${duration}ms`,
458
+ });
263
459
 
264
- // Mailgun MUST receive 200 or it will retry
265
- res.status(200).json({ received: true });
266
- }
267
- }
268
- ];
460
+ // âš ī¸ Still return 200 so Mailgun doesn't retry forever
461
+ const errorResponse = {
462
+ received: true,
463
+ error: 'Processing failed but webhook acknowledged',
464
+ correlationId,
465
+ timestamp: new Date().toISOString(),
466
+ };
467
+ res.status(200).json(errorResponse);
468
+ return null;
469
+ }
269
470
  }
270
471
 
271
- // Export utilities and main functions
472
+ // Export utility functions for manual processing
272
473
  module.exports = {
273
- createMailgunInboundRouter,
274
- createMailgunInboundMiddleware,
275
474
  processEmailData,
276
- verifyMailgunSignature,
475
+ verifyRequestSignature, // Automatic signature verification (recommended)
476
+ verifyMailgunSignature, // Manual signature verification (advanced)
477
+ mailgunWebhook, // Production-ready event webhook handler
277
478
  extractEmail,
278
479
  extractEmails,
279
480
  cleanMessageId,
280
481
  parseHeaders,
281
482
  };
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.1.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
  }