@strapi/provider-email-nodemailer 5.37.1 → 5.38.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/README.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # @strapi/provider-email-nodemailer
2
2
 
3
+ A feature-rich Nodemailer email provider for Strapi with support for DKIM, OAuth2, connection pooling, calendar invitations, newsletters, and more.
4
+
5
+ ## Features
6
+
7
+ | Category | Features |
8
+ | ------------------ | ------------------------------------------------------------------------------ |
9
+ | **Sending** | Priority, custom headers, attachments, embedded images, AMP4Email |
10
+ | **Security** | DKIM signing, OAuth2, requireTLS, file/URL access restrictions |
11
+ | **Performance** | Connection pooling, rate limiting |
12
+ | **Deliverability** | List-Unsubscribe headers (Gmail/Outlook), DSN bounce tracking, custom envelope |
13
+ | **Rich Content** | Calendar invitations (iCalendar), AMP4Email interactive emails |
14
+ | **Connectivity** | SOCKS/HTTP proxy support, NTLM and custom auth mechanisms |
15
+ | **Utilities** | RFC 5322/2047/6531 email address parsing and formatting |
16
+
3
17
  ## Resources
4
18
 
5
19
  - [LICENSE](LICENSE)
@@ -127,17 +141,716 @@ await strapi.plugin('email').service('email').send({
127
141
 
128
142
  The following fields are supported:
129
143
 
130
- | Field | Description |
131
- | ----------- | ----------------------------------------------------------------- |
132
- | from | Email address of the sender |
133
- | to | Comma separated list or an array of recipients |
134
- | replyTo | Email address to which replies are sent |
135
- | cc | Comma separated list or an array of recipients |
136
- | bcc | Comma separated list or an array of recipients |
137
- | subject | Subject of the email |
138
- | text | Plaintext version of the message |
139
- | html | HTML version of the message |
140
- | attachments | Array of objects See: https://nodemailer.com/message/attachments/ |
144
+ | Field | Description |
145
+ | ------------------ | ----------------------------------------------------------------------------- |
146
+ | **Core** | |
147
+ | from | Email address of the sender |
148
+ | to | Comma separated list or an array of recipients |
149
+ | cc | Comma separated list or an array of recipients |
150
+ | bcc | Comma separated list or an array of recipients |
151
+ | replyTo | Email address to which replies are sent |
152
+ | sender | Address for the Sender: field (for "on behalf of" emails) |
153
+ | subject | Subject of the email |
154
+ | **Content** | |
155
+ | text | Plaintext version of the message |
156
+ | html | HTML version of the message |
157
+ | watchHtml | Apple Watch specific HTML version |
158
+ | amp | AMP4Email content for interactive emails |
159
+ | attachments | Array of attachment objects. See: https://nodemailer.com/message/attachments/ |
160
+ | alternatives | Array of alternative content (e.g. Markdown alongside HTML) |
161
+ | **Headers & Meta** | |
162
+ | headers | Custom SMTP headers object (e.g. `{ 'X-Custom': 'value' }`) |
163
+ | priority | Email priority: `'high'`, `'normal'`, or `'low'` |
164
+ | messageId | Custom Message-ID (random generated if not set) |
165
+ | date | Custom Date value (current UTC if not set) |
166
+ | xMailer | Control X-Mailer header (`false` to remove, string to override) |
167
+ | **Threading** | |
168
+ | inReplyTo | Message-ID of the email being replied to |
169
+ | references | Message-ID list this email references (array or space-separated string) |
170
+ | **Encoding** | |
171
+ | textEncoding | Force content-transfer-encoding: `'quoted-printable'` or `'base64'` |
172
+ | encoding | Encoding for the message content |
173
+ | normalizeHeaderKey | Function to normalize header key casing |
174
+ | **Advanced** | |
175
+ | icalEvent | Calendar event invitation (iCalendar format) |
176
+ | list | RFC 2369 List-\* headers - enables one-click unsubscribe in Gmail/Outlook |
177
+ | envelope | Custom SMTP envelope for bounce handling (MAIL FROM / RCPT TO) |
178
+ | dkim | Per-message DKIM signing options (overrides transport-level DKIM) |
179
+ | attachDataUrls | Convert `data:` URIs in HTML to embedded CID attachments (`true`/`false`) |
180
+ | dsn | Delivery Status Notification - request bounce/success reports |
181
+ | auth | Per-message OAuth2 credentials for multi-user sending |
182
+ | **Security** | |
183
+ | disableUrlAccess | Fail if content tries to load from a URL (`true`/`false`) |
184
+ | disableFileAccess | Fail if content tries to load from a file path (`true`/`false`) |
185
+ | **Raw MIME** | |
186
+ | raw | Pre-built MIME message (skips message generation, envelope must be set) |
187
+
188
+ Every field listed above is explicitly allowlisted. Unknown properties are silently dropped to prevent injection.
189
+
190
+ ### Sending with priority and custom headers
191
+
192
+ ```js
193
+ await strapi
194
+ .plugin('email')
195
+ .service('email')
196
+ .send({
197
+ to: 'someone@example.com',
198
+ subject: 'Urgent',
199
+ text: 'Please respond ASAP',
200
+ priority: 'high',
201
+ headers: {
202
+ 'X-Custom-Header': 'my-value',
203
+ },
204
+ });
205
+ ```
206
+
207
+ ### Calendar invitations
208
+
209
+ ```js
210
+ await strapi
211
+ .plugin('email')
212
+ .service('email')
213
+ .send({
214
+ to: 'someone@example.com',
215
+ subject: 'Meeting Invitation',
216
+ text: 'You are invited to a meeting',
217
+ icalEvent: {
218
+ method: 'REQUEST',
219
+ content: `BEGIN:VCALENDAR
220
+ VERSION:2.0
221
+ BEGIN:VEVENT
222
+ DTSTART:20260130T100000Z
223
+ DTEND:20260130T110000Z
224
+ SUMMARY:Team Meeting
225
+ END:VEVENT
226
+ END:VCALENDAR`,
227
+ },
228
+ });
229
+ ```
230
+
231
+ ### Newsletter with List-Unsubscribe
232
+
233
+ When sending newsletters, include List-Unsubscribe headers so Gmail and Outlook show a one-click "Unsubscribe" button:
234
+
235
+ ```js
236
+ await strapi
237
+ .plugin('email')
238
+ .service('email')
239
+ .send({
240
+ to: 'subscriber@example.com',
241
+ subject: 'Weekly Newsletter',
242
+ html: '<h1>This week in tech...</h1>',
243
+ list: {
244
+ unsubscribe: {
245
+ url: 'https://example.com/unsubscribe?id=123',
246
+ comment: 'Unsubscribe',
247
+ },
248
+ help: 'support@example.com?subject=help',
249
+ },
250
+ });
251
+ ```
252
+
253
+ ### Delivery Status Notifications (DSN)
254
+
255
+ Request bounce reports or delivery confirmations:
256
+
257
+ ```js
258
+ await strapi
259
+ .plugin('email')
260
+ .service('email')
261
+ .send({
262
+ to: 'someone@example.com',
263
+ subject: 'Important document',
264
+ text: 'Please confirm receipt',
265
+ dsn: {
266
+ id: 'msg-unique-123',
267
+ return: 'headers',
268
+ notify: ['success', 'failure'],
269
+ recipient: 'bounce-handler@example.com',
270
+ },
271
+ });
272
+ ```
273
+
274
+ ### Custom SMTP Envelope (Bounce Handling)
275
+
276
+ Control the MAIL FROM address independently from the visible From header, useful for tracking bounces:
277
+
278
+ ```js
279
+ await strapi
280
+ .plugin('email')
281
+ .service('email')
282
+ .send({
283
+ from: 'Newsletter <newsletter@example.com>',
284
+ to: 'subscriber@example.com',
285
+ subject: 'Newsletter',
286
+ text: 'Hello!',
287
+ envelope: {
288
+ from: 'bounce+subscriber=example.com@example.com',
289
+ to: 'subscriber@example.com',
290
+ },
291
+ });
292
+ ```
293
+
294
+ ### AMP4Email (Interactive Emails)
295
+
296
+ Send interactive AMP-powered emails (supported by Gmail):
297
+
298
+ ```js
299
+ await strapi
300
+ .plugin('email')
301
+ .service('email')
302
+ .send({
303
+ to: 'someone@example.com',
304
+ subject: 'Interactive Email',
305
+ text: 'Fallback for non-AMP clients',
306
+ html: '<p>Fallback for non-AMP clients</p>',
307
+ amp: `<!doctype html>
308
+ <html ⚡4email>
309
+ <head>
310
+ <meta charset="utf-8">
311
+ <style amp4email-boilerplate>body{visibility:hidden}</style>
312
+ <script async src="https://cdn.ampproject.org/v0.js"></script>
313
+ </head>
314
+ <body>
315
+ <p>This is an interactive AMP email!</p>
316
+ </body>
317
+ </html>`,
318
+ });
319
+ ```
320
+
321
+ ### Email threading (replies & conversations)
322
+
323
+ ```js
324
+ await strapi
325
+ .plugin('email')
326
+ .service('email')
327
+ .send({
328
+ to: 'someone@example.com',
329
+ subject: 'Re: Project Update',
330
+ text: 'Thanks for the update!',
331
+ inReplyTo: '<original-msg-id@example.com>',
332
+ references: ['<original-msg-id@example.com>', '<prev-msg-id@example.com>'],
333
+ });
334
+ ```
335
+
336
+ ### Sending on behalf of (Sender field)
337
+
338
+ ```js
339
+ await strapi.plugin('email').service('email').send({
340
+ from: 'CEO <ceo@example.com>',
341
+ sender: 'assistant@example.com',
342
+ to: 'board@example.com',
343
+ subject: 'Quarterly Report',
344
+ text: 'Please find the report attached',
345
+ });
346
+ ```
347
+
348
+ ### Per-message DKIM signing
349
+
350
+ Override transport-level DKIM settings for a specific message:
351
+
352
+ ```js
353
+ await strapi
354
+ .plugin('email')
355
+ .service('email')
356
+ .send({
357
+ to: 'someone@example.com',
358
+ subject: 'Signed Email',
359
+ text: 'This email has a specific DKIM signature',
360
+ dkim: {
361
+ domainName: 'special.example.com',
362
+ keySelector: 'mail2026',
363
+ privateKey: process.env.DKIM_PRIVATE_KEY_SPECIAL,
364
+ },
365
+ });
366
+ ```
367
+
368
+ ### Alternative content formats
369
+
370
+ Provide multiple representations of the same content (email clients pick the best match):
371
+
372
+ ```js
373
+ await strapi
374
+ .plugin('email')
375
+ .service('email')
376
+ .send({
377
+ to: 'someone@example.com',
378
+ subject: 'Multi-format Email',
379
+ text: 'Plain text version',
380
+ html: '<p>HTML version</p>',
381
+ alternatives: [
382
+ {
383
+ contentType: 'text/x-web-markdown',
384
+ content: '**Markdown** version',
385
+ },
386
+ ],
387
+ });
388
+ ```
389
+
390
+ ### Raw MIME passthrough
391
+
392
+ Send a pre-built MIME message directly (skips all message generation):
393
+
394
+ ```js
395
+ await strapi
396
+ .plugin('email')
397
+ .service('email')
398
+ .send({
399
+ to: 'someone@example.com',
400
+ subject: 'ignored',
401
+ text: 'ignored',
402
+ envelope: {
403
+ from: 'sender@example.com',
404
+ to: 'someone@example.com',
405
+ },
406
+ raw: 'From: sender@example.com\r\nTo: someone@example.com\r\nSubject: Raw\r\n\r\nPre-built body',
407
+ });
408
+ ```
409
+
410
+ ### Embedded images
411
+
412
+ ```js
413
+ await strapi
414
+ .plugin('email')
415
+ .service('email')
416
+ .send({
417
+ to: 'someone@example.com',
418
+ subject: 'Newsletter',
419
+ html: '<p>Logo: <img src="cid:logo@company"/></p>',
420
+ attachments: [
421
+ {
422
+ filename: 'logo.png',
423
+ path: '/path/to/logo.png',
424
+ cid: 'logo@company',
425
+ },
426
+ ],
427
+ });
428
+ ```
429
+
430
+ ## Advanced configuration
431
+
432
+ ### OAuth2 authentication
433
+
434
+ For services like Gmail or Outlook, you can use OAuth2 instead of passwords:
435
+
436
+ ```js
437
+ module.exports = ({ env }) => ({
438
+ email: {
439
+ config: {
440
+ provider: 'nodemailer',
441
+ providerOptions: {
442
+ host: 'smtp.gmail.com',
443
+ port: 465,
444
+ secure: true,
445
+ auth: {
446
+ type: 'OAuth2',
447
+ user: env('SMTP_USER'),
448
+ clientId: env('OAUTH_CLIENT_ID'),
449
+ clientSecret: env('OAUTH_CLIENT_SECRET'),
450
+ refreshToken: env('OAUTH_REFRESH_TOKEN'),
451
+ },
452
+ },
453
+ settings: {
454
+ defaultFrom: env('SMTP_USER'),
455
+ defaultReplyTo: env('SMTP_USER'),
456
+ },
457
+ },
458
+ },
459
+ });
460
+ ```
461
+
462
+ #### Per-message OAuth2 (multi-user)
463
+
464
+ You can send emails on behalf of different users through a single transporter. Configure the transporter with shared OAuth2 credentials, then pass user-specific tokens per message:
465
+
466
+ ```js
467
+ // config/plugins.js - shared transporter with OAuth2
468
+ module.exports = ({ env }) => ({
469
+ email: {
470
+ config: {
471
+ provider: 'nodemailer',
472
+ providerOptions: {
473
+ host: 'smtp.gmail.com',
474
+ port: 465,
475
+ secure: true,
476
+ auth: {
477
+ type: 'OAuth2',
478
+ clientId: env('OAUTH_CLIENT_ID'),
479
+ clientSecret: env('OAUTH_CLIENT_SECRET'),
480
+ },
481
+ },
482
+ settings: {
483
+ defaultFrom: 'noreply@example.com',
484
+ defaultReplyTo: 'support@example.com',
485
+ },
486
+ },
487
+ },
488
+ });
489
+ ```
490
+
491
+ ```js
492
+ // Send as a specific user
493
+ await strapi
494
+ .plugin('email')
495
+ .service('email')
496
+ .send({
497
+ to: 'recipient@example.com',
498
+ subject: 'Hello from user',
499
+ text: 'Sent on behalf of a specific user',
500
+ auth: {
501
+ user: 'specific-user@gmail.com',
502
+ refreshToken: userRefreshToken,
503
+ accessToken: userAccessToken,
504
+ },
505
+ });
506
+ ```
507
+
508
+ See [nodemailer OAuth2 documentation](https://nodemailer.com/smtp/oauth2/) for details.
509
+
510
+ ### Connection pooling
511
+
512
+ For better performance when sending multiple emails:
513
+
514
+ ```js
515
+ module.exports = ({ env }) => ({
516
+ email: {
517
+ config: {
518
+ provider: 'nodemailer',
519
+ providerOptions: {
520
+ host: env('SMTP_HOST'),
521
+ port: 465,
522
+ secure: true,
523
+ pool: true,
524
+ maxConnections: 5,
525
+ maxMessages: 100,
526
+ auth: {
527
+ user: env('SMTP_USERNAME'),
528
+ pass: env('SMTP_PASSWORD'),
529
+ },
530
+ },
531
+ settings: {
532
+ defaultFrom: 'hello@example.com',
533
+ defaultReplyTo: 'hello@example.com',
534
+ },
535
+ },
536
+ },
537
+ });
538
+ ```
539
+
540
+ ### DKIM signing
541
+
542
+ Add DKIM signatures to improve email deliverability:
543
+
544
+ ```js
545
+ module.exports = ({ env }) => ({
546
+ email: {
547
+ config: {
548
+ provider: 'nodemailer',
549
+ providerOptions: {
550
+ host: env('SMTP_HOST'),
551
+ port: 587,
552
+ auth: {
553
+ user: env('SMTP_USERNAME'),
554
+ pass: env('SMTP_PASSWORD'),
555
+ },
556
+ dkim: {
557
+ domainName: 'example.com',
558
+ keySelector: 'mail',
559
+ privateKey: env('DKIM_PRIVATE_KEY'),
560
+ },
561
+ },
562
+ settings: {
563
+ defaultFrom: 'hello@example.com',
564
+ defaultReplyTo: 'hello@example.com',
565
+ },
566
+ },
567
+ },
568
+ });
569
+ ```
570
+
571
+ ### Rate limiting
572
+
573
+ Limit the number of emails sent per time interval to avoid being flagged as spam:
574
+
575
+ ```js
576
+ module.exports = ({ env }) => ({
577
+ email: {
578
+ config: {
579
+ provider: 'nodemailer',
580
+ providerOptions: {
581
+ host: env('SMTP_HOST'),
582
+ port: 465,
583
+ secure: true,
584
+ pool: true,
585
+ maxConnections: 5,
586
+ maxMessages: 100,
587
+ rateDelta: 1000, // Time interval in ms (1 second)
588
+ rateLimit: 5, // Max messages per rateDelta interval
589
+ auth: {
590
+ user: env('SMTP_USERNAME'),
591
+ pass: env('SMTP_PASSWORD'),
592
+ },
593
+ },
594
+ settings: {
595
+ defaultFrom: 'hello@example.com',
596
+ defaultReplyTo: 'hello@example.com',
597
+ },
598
+ },
599
+ },
600
+ });
601
+ ```
602
+
603
+ ### Proxy support
604
+
605
+ Route SMTP connections through a SOCKS or HTTP proxy:
606
+
607
+ ```js
608
+ module.exports = ({ env }) => ({
609
+ email: {
610
+ config: {
611
+ provider: 'nodemailer',
612
+ providerOptions: {
613
+ host: env('SMTP_HOST'),
614
+ port: 465,
615
+ secure: true,
616
+ proxy: env('SMTP_PROXY', 'socks5://127.0.0.1:1080'), // or 'http://proxy:3128'
617
+ auth: {
618
+ user: env('SMTP_USERNAME'),
619
+ pass: env('SMTP_PASSWORD'),
620
+ },
621
+ },
622
+ settings: {
623
+ defaultFrom: 'hello@example.com',
624
+ defaultReplyTo: 'hello@example.com',
625
+ },
626
+ },
627
+ },
628
+ });
629
+ ```
630
+
631
+ For SOCKS proxy, install the `socks` package: `yarn add socks`.
632
+
633
+ ### Require TLS
634
+
635
+ Force TLS encryption and refuse to send if the server doesn't support it:
636
+
637
+ ```js
638
+ providerOptions: {
639
+ host: env('SMTP_HOST'),
640
+ port: 587,
641
+ requireTLS: true, // Fail if STARTTLS is not available
642
+ auth: { user: env('SMTP_USERNAME'), pass: env('SMTP_PASSWORD') },
643
+ },
644
+ ```
645
+
646
+ ### Security options
647
+
648
+ Restrict file and URL access at the transport level (applies to all messages):
649
+
650
+ ```js
651
+ module.exports = ({ env }) => ({
652
+ email: {
653
+ config: {
654
+ provider: 'nodemailer',
655
+ providerOptions: {
656
+ host: env('SMTP_HOST'),
657
+ port: 587,
658
+ auth: {
659
+ user: env('SMTP_USERNAME'),
660
+ pass: env('SMTP_PASSWORD'),
661
+ },
662
+ disableFileAccess: true,
663
+ disableUrlAccess: true,
664
+ },
665
+ settings: {
666
+ defaultFrom: 'hello@example.com',
667
+ defaultReplyTo: 'hello@example.com',
668
+ },
669
+ },
670
+ },
671
+ });
672
+ ```
673
+
674
+ You can also set these per message when processing content from untrusted sources:
675
+
676
+ ```js
677
+ await strapi.plugin('email').service('email').send({
678
+ to: 'someone@example.com',
679
+ subject: 'User-generated content',
680
+ html: userProvidedHtml,
681
+ disableUrlAccess: true,
682
+ disableFileAccess: true,
683
+ });
684
+ ```
685
+
686
+ **Security note:** This provider uses an explicit allowlist for all message fields. Unknown or unsupported properties are silently dropped, preventing property injection attacks.
687
+
688
+ ## Provider methods
689
+
690
+ ### verify
691
+
692
+ Verify your SMTP configuration without sending an email:
693
+
694
+ ```js
695
+ const emailProvider = strapi.plugin('email').provider;
696
+
697
+ try {
698
+ await emailProvider.verify();
699
+ console.log('SMTP connection is working');
700
+ } catch (error) {
701
+ console.error('SMTP configuration error:', error.message);
702
+ }
703
+ ```
704
+
705
+ This tests DNS resolution, TCP connection, TLS upgrade (if applicable), and authentication.
706
+
707
+ ### isIdle
708
+
709
+ Check if the transporter has available capacity (useful with connection pooling):
710
+
711
+ ```js
712
+ if (emailProvider.isIdle()) {
713
+ // Safe to send more emails
714
+ }
715
+ ```
716
+
717
+ ### close
718
+
719
+ Close all connections gracefully (recommended when using connection pooling):
720
+
721
+ ```js
722
+ // On application shutdown
723
+ emailProvider.close();
724
+ ```
725
+
726
+ ## Email Address Utilities
727
+
728
+ This package includes RFC-compliant utilities for parsing and formatting email addresses.
729
+
730
+ ### Import
731
+
732
+ ```js
733
+ import {
734
+ parseEmailAddress,
735
+ formatEmailAddress,
736
+ parseMultipleEmailAddresses,
737
+ isValidEmail,
738
+ decodeRfc2047,
739
+ encodeRfc2047Base64,
740
+ } from '@strapi/provider-email-nodemailer/utils';
741
+ ```
742
+
743
+ ### Parsing Email Addresses
744
+
745
+ Parse email addresses in various RFC 5322 formats:
746
+
747
+ ```js
748
+ // Simple email
749
+ parseEmailAddress('test@example.com');
750
+ // { name: null, email: 'test@example.com', original: '...' }
751
+
752
+ // Name with angle brackets
753
+ parseEmailAddress('John Doe <john@example.com>');
754
+ // { name: 'John Doe', email: 'john@example.com', original: '...' }
755
+
756
+ // Quoted name (RFC 5322)
757
+ parseEmailAddress('"Doe, John" <john@example.com>');
758
+ // { name: 'Doe, John', email: 'john@example.com', original: '...' }
759
+
760
+ // RFC 2047 encoded name (non-ASCII characters)
761
+ parseEmailAddress('=?UTF-8?B?TcO8bGxlcg==?= <mueller@example.com>');
762
+ // { name: 'Müller', email: 'mueller@example.com', original: '...' }
763
+
764
+ // Comment format (RFC 5322)
765
+ parseEmailAddress('support@example.com (Support Team)');
766
+ // { name: 'Support Team', email: 'support@example.com', original: '...' }
767
+ ```
768
+
769
+ ### Formatting Email Addresses
770
+
771
+ Create properly formatted email address strings:
772
+
773
+ ```js
774
+ // Simple format
775
+ formatEmailAddress('John Doe', 'john@example.com');
776
+ // 'John Doe <john@example.com>'
777
+
778
+ // Auto-quotes special characters
779
+ formatEmailAddress('Doe, John', 'john@example.com');
780
+ // '"Doe, John" <john@example.com>'
781
+
782
+ // Auto-encodes non-ASCII characters (RFC 2047)
783
+ formatEmailAddress('Müller', 'mueller@example.com');
784
+ // '=?UTF-8?B?TcO8bGxlcg==?= <mueller@example.com>'
785
+
786
+ // Skip encoding if needed
787
+ formatEmailAddress('Müller', 'mueller@example.com', { encodeNonAscii: false });
788
+ // 'Müller <mueller@example.com>'
789
+ ```
790
+
791
+ ### Multiple Addresses
792
+
793
+ Parse comma-separated email addresses (handles quoted strings with commas):
794
+
795
+ ```js
796
+ parseMultipleEmailAddresses('a@example.com, "Doe, John" <b@example.com>');
797
+ // [
798
+ // { name: null, email: 'a@example.com', ... },
799
+ // { name: 'Doe, John', email: 'b@example.com', ... }
800
+ // ]
801
+ ```
802
+
803
+ ### RFC 2047 Encoding/Decoding
804
+
805
+ Handle MIME encoded-words for non-ASCII characters:
806
+
807
+ ```js
808
+ // Decode Base64 or Quoted-Printable
809
+ decodeRfc2047('=?UTF-8?B?U3RyYXBp?=');
810
+ // 'Strapi'
811
+
812
+ decodeRfc2047('=?UTF-8?Q?M=C3=BCller?=');
813
+ // 'Müller'
814
+
815
+ // Encode for MIME headers
816
+ encodeRfc2047Base64('Müller');
817
+ // '=?UTF-8?B?TcO8bGxlcg==?='
818
+ ```
819
+
820
+ ### Validation
821
+
822
+ ```js
823
+ isValidEmail('user@example.com'); // true
824
+ isValidEmail('invalid'); // false
825
+ ```
826
+
827
+ ### Normalization
828
+
829
+ All email addresses are automatically normalized to lowercase when parsed or formatted (per RFC 5321 section 2.4):
830
+
831
+ ```js
832
+ parseEmailAddress('User@Example.COM');
833
+ // { name: null, email: 'user@example.com', original: 'User@Example.COM' }
834
+
835
+ formatEmailAddress('Admin', 'Admin@EXAMPLE.ORG');
836
+ // 'Admin <admin@example.org>'
837
+
838
+ normalizeEmail('Mixed.Case@Example.COM');
839
+ // 'mixed.case@example.com'
840
+ ```
841
+
842
+ ### Supported RFC Standards
843
+
844
+ | RFC | Description |
845
+ | -------- | ---------------------------------------------------------------- |
846
+ | RFC 5321 | SMTP protocol - email address case-insensitive normalization |
847
+ | RFC 5322 | Internet Message Format (name <email>, quoted strings, comments) |
848
+ | RFC 2047 | MIME encoded-words (=?charset?encoding?text?=) |
849
+ | RFC 2369 | List-\* headers (List-Unsubscribe, List-Help, etc.) |
850
+ | RFC 3461 | Delivery Status Notifications (DSN) |
851
+ | RFC 6376 | DomainKeys Identified Mail (DKIM) signatures |
852
+ | RFC 6531 | Internationalized Email (UTF-8 in local parts) |
853
+ | RFC 6532 | Internationalized Email Headers (UTF-8 in header fields) |
141
854
 
142
855
  ## Troubleshooting
143
856