0
# Plugin Development
1
2
Extensible plugin architecture for customizing email processing, adding functionality, and integrating with external services during the email composition and sending pipeline.
3
4
## Capabilities
5
6
### Plugin System Overview
7
8
Nodemailer provides a two-step plugin system that allows custom processing during email compilation and streaming phases.
9
10
```javascript { .api }
11
/**
12
* Adds a plugin to the mail processing pipeline
13
* @param {string} step - Processing step ('compile' or 'stream')
14
* @param {Function} plugin - Plugin function
15
* @returns {Mailer} this (chainable)
16
*/
17
use(step, plugin);
18
19
/**
20
* Plugin function signature
21
* @param {MailMessage} mail - Mail message object
22
* @param {Function} callback - Callback function (error) => void
23
*/
24
type PluginFunction = (mail: MailMessage, callback: Function) => void;
25
```
26
27
**Plugin Steps:**
28
- `compile`: Runs before message compilation, allows modification of mail data
29
- `stream`: Runs after compilation, allows modification of the message stream
30
31
### Compile Step Plugins
32
33
Compile step plugins run before the email message is compiled into MIME format, allowing modification of message data, headers, and content.
34
35
```javascript { .api }
36
/**
37
* Compile step plugin for modifying mail data before compilation
38
* @param {MailMessage} mail - Mail message object with data property
39
* @param {Function} callback - Callback function
40
*/
41
function compilePlugin(mail, callback) {
42
// Access and modify mail.data
43
// mail.data contains: from, to, subject, text, html, attachments, etc.
44
45
// Perform modifications
46
// ...
47
48
// Call callback when done
49
callback();
50
}
51
52
interface MailMessage {
53
data: MailOptions; // Original mail options
54
message?: CompiledMessage; // Compiled message (available in stream step)
55
resolveContent(obj, key, callback): void; // Resolve content streams/buffers
56
}
57
```
58
59
**Usage Examples:**
60
61
Add custom headers:
62
```javascript
63
const transporter = nodemailer.createTransport(/* transport config */);
64
65
// Plugin to add tracking headers
66
transporter.use('compile', (mail, callback) => {
67
mail.data.headers = mail.data.headers || {};
68
mail.data.headers['X-Message-ID'] = generateUniqueId();
69
mail.data.headers['X-Sent-Time'] = new Date().toISOString();
70
callback();
71
});
72
```
73
74
Modify subject line:
75
```javascript
76
// Plugin to add environment prefix to subject
77
transporter.use('compile', (mail, callback) => {
78
const env = process.env.NODE_ENV || 'development';
79
if (env !== 'production') {
80
mail.data.subject = `[${env.toUpperCase()}] ${mail.data.subject}`;
81
}
82
callback();
83
});
84
```
85
86
Content validation:
87
```javascript
88
// Plugin to validate email content
89
transporter.use('compile', (mail, callback) => {
90
// Check for required fields
91
if (!mail.data.subject) {
92
return callback(new Error('Subject is required'));
93
}
94
95
if (!mail.data.text && !mail.data.html) {
96
return callback(new Error('Either text or HTML content is required'));
97
}
98
99
// Validate email addresses
100
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
101
const recipients = [].concat(mail.data.to || []);
102
103
for (const recipient of recipients) {
104
const email = typeof recipient === 'string' ? recipient : recipient.address;
105
if (!emailRegex.test(email)) {
106
return callback(new Error(`Invalid email address: ${email}`));
107
}
108
}
109
110
callback();
111
});
112
```
113
114
Template processing:
115
```javascript
116
// Plugin to process templates
117
transporter.use('compile', (mail, callback) => {
118
if (mail.data.template) {
119
const templateData = mail.data.templateData || {};
120
121
// Process template (example using a hypothetical template engine)
122
processTemplate(mail.data.template, templateData, (error, result) => {
123
if (error) {
124
return callback(error);
125
}
126
127
// Replace content with processed template
128
mail.data.html = result.html;
129
mail.data.text = result.text;
130
mail.data.subject = result.subject || mail.data.subject;
131
132
// Clean up template-specific data
133
delete mail.data.template;
134
delete mail.data.templateData;
135
136
callback();
137
});
138
} else {
139
callback();
140
}
141
});
142
```
143
144
### Stream Step Plugins
145
146
Stream step plugins run after message compilation, allowing modification of the final MIME message stream.
147
148
```javascript { .api }
149
/**
150
* Stream step plugin for modifying compiled message stream
151
* @param {MailMessage} mail - Mail message object with compiled message
152
* @param {Function} callback - Callback function
153
*/
154
function streamPlugin(mail, callback) {
155
// Access compiled message: mail.message
156
// mail.message is a readable stream with additional methods
157
158
// Modify the message stream
159
// ...
160
161
callback();
162
}
163
164
interface CompiledMessage extends NodeJS.ReadableStream {
165
messageId(): string; // Get message ID
166
getEnvelope(): Envelope; // Get SMTP envelope
167
createReadStream(): NodeJS.ReadableStream; // Create new read stream
168
processFunc(processor): void; // Add stream processor function
169
}
170
```
171
172
**Usage Examples:**
173
174
Message signing:
175
```javascript
176
// Plugin to add custom message signature
177
transporter.use('stream', (mail, callback) => {
178
mail.message.processFunc((input) => {
179
// Add custom signature to message
180
const signature = '\n\n--\nSent via Custom Mailer\n';
181
return input + signature;
182
});
183
callback();
184
});
185
```
186
187
Content modification:
188
```javascript
189
// Plugin to modify message content in stream
190
transporter.use('stream', (mail, callback) => {
191
mail.message.processFunc((input) => {
192
// Replace placeholder text in the entire message
193
return input.replace(/\{COMPANY_NAME\}/g, 'Acme Corporation');
194
});
195
callback();
196
});
197
```
198
199
Logging and analytics:
200
```javascript
201
// Plugin to log message details
202
transporter.use('stream', (mail, callback) => {
203
const messageId = mail.message.messageId();
204
const envelope = mail.message.getEnvelope();
205
206
console.log('Sending message:', {
207
messageId: messageId,
208
from: envelope.from,
209
to: envelope.to,
210
timestamp: new Date().toISOString()
211
});
212
213
// Log to external service
214
logToAnalytics({
215
event: 'email_sent',
216
messageId: messageId,
217
recipients: envelope.to.length
218
});
219
220
callback();
221
});
222
```
223
224
### Advanced Plugin Examples
225
226
Complex plugins demonstrating advanced functionality and integration patterns.
227
228
**Attachment Processing Plugin:**
229
```javascript
230
// Plugin to process and optimize attachments
231
transporter.use('compile', (mail, callback) => {
232
if (!mail.data.attachments || !Array.isArray(mail.data.attachments)) {
233
return callback();
234
}
235
236
let processed = 0;
237
const attachments = mail.data.attachments;
238
239
if (attachments.length === 0) {
240
return callback();
241
}
242
243
attachments.forEach((attachment, index) => {
244
if (attachment.contentType && attachment.contentType.startsWith('image/')) {
245
// Optimize image attachments
246
optimizeImage(attachment, (error, optimized) => {
247
if (error) {
248
console.warn('Image optimization failed:', error.message);
249
} else {
250
attachments[index] = optimized;
251
}
252
253
processed++;
254
if (processed === attachments.length) {
255
callback();
256
}
257
});
258
} else {
259
processed++;
260
if (processed === attachments.length) {
261
callback();
262
}
263
}
264
});
265
});
266
```
267
268
**Database Integration Plugin:**
269
```javascript
270
// Plugin to log emails to database
271
transporter.use('compile', (mail, callback) => {
272
const emailRecord = {
273
to: Array.isArray(mail.data.to) ? mail.data.to.join(', ') : mail.data.to,
274
from: mail.data.from,
275
subject: mail.data.subject,
276
created_at: new Date(),
277
status: 'pending'
278
};
279
280
// Save to database
281
database.emails.create(emailRecord, (error, record) => {
282
if (error) {
283
console.error('Failed to log email to database:', error);
284
// Continue anyway - don't fail the email send
285
} else {
286
// Store record ID for later reference
287
mail.data.headers = mail.data.headers || {};
288
mail.data.headers['X-Database-ID'] = record.id;
289
}
290
callback();
291
});
292
});
293
294
// Stream plugin to update status after sending
295
transporter.use('stream', (mail, callback) => {
296
const databaseId = mail.data.headers && mail.data.headers['X-Database-ID'];
297
if (databaseId) {
298
database.emails.update(databaseId, { status: 'sent' }, (error) => {
299
if (error) {
300
console.error('Failed to update email status:', error);
301
}
302
});
303
}
304
callback();
305
});
306
```
307
308
**Content Security Plugin:**
309
```javascript
310
// Plugin to scan content for security issues
311
transporter.use('compile', (mail, callback) => {
312
const scanContent = (content) => {
313
if (!content) return true;
314
315
// Check for suspicious patterns
316
const suspiciousPatterns = [
317
/javascript:/i,
318
/<script/i,
319
/onclick=/i,
320
/onerror=/i
321
];
322
323
return !suspiciousPatterns.some(pattern => pattern.test(content));
324
};
325
326
// Scan HTML content
327
if (mail.data.html && !scanContent(mail.data.html)) {
328
return callback(new Error('Suspicious content detected in HTML'));
329
}
330
331
// Scan text content
332
if (mail.data.text && !scanContent(mail.data.text)) {
333
return callback(new Error('Suspicious content detected in text'));
334
}
335
336
// Scan subject
337
if (mail.data.subject && !scanContent(mail.data.subject)) {
338
return callback(new Error('Suspicious content detected in subject'));
339
}
340
341
callback();
342
});
343
```
344
345
### Plugin Chaining and Order
346
347
Multiple plugins can be added to the same step and will be executed in the order they were added.
348
349
```javascript { .api }
350
/**
351
* Plugin execution order
352
* - Plugins are executed in the order they were added with use()
353
* - Each plugin must call callback() to continue to the next plugin
354
* - If any plugin calls callback(error), the entire process stops
355
*/
356
357
// Example of plugin chaining
358
transporter
359
.use('compile', plugin1) // Runs first
360
.use('compile', plugin2) // Runs second
361
.use('compile', plugin3) // Runs third
362
.use('stream', streamPlugin1) // Runs first in stream phase
363
.use('stream', streamPlugin2); // Runs second in stream phase
364
```
365
366
**Usage Examples:**
367
368
```javascript
369
// Plugin execution order example
370
const transporter = nodemailer.createTransport(transportConfig);
371
372
// Add multiple compile plugins
373
transporter
374
.use('compile', (mail, callback) => {
375
console.log('Plugin 1: Adding timestamp');
376
mail.data.headers = mail.data.headers || {};
377
mail.data.headers['X-Timestamp'] = Date.now();
378
callback();
379
})
380
.use('compile', (mail, callback) => {
381
console.log('Plugin 2: Validating content');
382
if (!mail.data.subject) {
383
return callback(new Error('Subject required'));
384
}
385
callback();
386
})
387
.use('compile', (mail, callback) => {
388
console.log('Plugin 3: Processing templates');
389
// Template processing logic...
390
callback();
391
});
392
393
// Output when sending email:
394
// Plugin 1: Adding timestamp
395
// Plugin 2: Validating content
396
// Plugin 3: Processing templates
397
```
398
399
### Error Handling in Plugins
400
401
Proper error handling patterns for robust plugin development.
402
403
```javascript { .api }
404
/**
405
* Plugin error handling best practices
406
*/
407
408
// Always handle errors properly
409
transporter.use('compile', (mail, callback) => {
410
try {
411
// Plugin logic here
412
processMailData(mail.data);
413
callback(); // Success
414
} catch (error) {
415
callback(error); // Pass error to stop processing
416
}
417
});
418
419
// Async operations with proper error handling
420
transporter.use('compile', (mail, callback) => {
421
asyncOperation(mail.data, (error, result) => {
422
if (error) {
423
return callback(error);
424
}
425
426
// Use result
427
mail.data.processedContent = result;
428
callback();
429
});
430
});
431
432
// Promise-based async operations
433
transporter.use('compile', (mail, callback) => {
434
promiseBasedOperation(mail.data)
435
.then((result) => {
436
mail.data.result = result;
437
callback();
438
})
439
.catch((error) => {
440
callback(error);
441
});
442
});
443
```