or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

authentication.mdconnection.mdindex.mdplugins.mdsending.mdtesting.mdtransports.md

plugins.mddocs/

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

```