@bernierllc/email-manager 0.2.0 → 0.4.1

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 (68) hide show
  1. package/README.md +653 -0
  2. package/dist/capabilities/degradation-logger.d.ts +59 -0
  3. package/dist/capabilities/degradation-logger.d.ts.map +1 -0
  4. package/dist/capabilities/degradation-logger.js +106 -0
  5. package/dist/capabilities/degradation-logger.js.map +1 -0
  6. package/dist/capabilities/feature-router.d.ts +55 -0
  7. package/dist/capabilities/feature-router.d.ts.map +1 -0
  8. package/dist/capabilities/feature-router.js +143 -0
  9. package/dist/capabilities/feature-router.js.map +1 -0
  10. package/dist/capabilities/in-memory-resolver.d.ts +27 -0
  11. package/dist/capabilities/in-memory-resolver.d.ts.map +1 -0
  12. package/dist/capabilities/in-memory-resolver.js +79 -0
  13. package/dist/capabilities/in-memory-resolver.js.map +1 -0
  14. package/dist/capabilities/index.d.ts +13 -0
  15. package/dist/capabilities/index.d.ts.map +1 -0
  16. package/dist/capabilities/index.js +21 -0
  17. package/dist/capabilities/index.js.map +1 -0
  18. package/dist/capabilities/matrix.d.ts +66 -0
  19. package/dist/capabilities/matrix.d.ts.map +1 -0
  20. package/dist/capabilities/matrix.js +247 -0
  21. package/dist/capabilities/matrix.js.map +1 -0
  22. package/dist/capabilities/redis-resolver.d.ts +95 -0
  23. package/dist/capabilities/redis-resolver.d.ts.map +1 -0
  24. package/dist/capabilities/redis-resolver.js +227 -0
  25. package/dist/capabilities/redis-resolver.js.map +1 -0
  26. package/dist/capabilities/resolver-factory.d.ts +30 -0
  27. package/dist/capabilities/resolver-factory.d.ts.map +1 -0
  28. package/dist/capabilities/resolver-factory.js +70 -0
  29. package/dist/capabilities/resolver-factory.js.map +1 -0
  30. package/dist/capabilities/resolver.d.ts +40 -0
  31. package/dist/capabilities/resolver.d.ts.map +1 -0
  32. package/dist/capabilities/resolver.js +18 -0
  33. package/dist/capabilities/resolver.js.map +1 -0
  34. package/dist/capabilities/routing-metadata.d.ts +16 -0
  35. package/dist/capabilities/routing-metadata.d.ts.map +1 -0
  36. package/dist/capabilities/routing-metadata.js +17 -0
  37. package/dist/capabilities/routing-metadata.js.map +1 -0
  38. package/dist/capabilities/safe-resolver.d.ts +24 -0
  39. package/dist/capabilities/safe-resolver.d.ts.map +1 -0
  40. package/dist/capabilities/safe-resolver.js +48 -0
  41. package/dist/capabilities/safe-resolver.js.map +1 -0
  42. package/dist/config/schema.d.ts +99 -4
  43. package/dist/config/schema.d.ts.map +1 -1
  44. package/dist/config/schema.js +17 -0
  45. package/dist/config/schema.js.map +1 -1
  46. package/dist/email-manager.d.ts.map +1 -1
  47. package/dist/email-manager.js +28 -1
  48. package/dist/email-manager.js.map +1 -1
  49. package/dist/enhanced-email-manager.d.ts +163 -1
  50. package/dist/enhanced-email-manager.d.ts.map +1 -1
  51. package/dist/enhanced-email-manager.js +412 -8
  52. package/dist/enhanced-email-manager.js.map +1 -1
  53. package/dist/errors.d.ts +11 -0
  54. package/dist/errors.d.ts.map +1 -1
  55. package/dist/errors.js +13 -0
  56. package/dist/errors.js.map +1 -1
  57. package/dist/index.d.ts +13 -1
  58. package/dist/index.d.ts.map +1 -1
  59. package/dist/index.js +7 -1
  60. package/dist/index.js.map +1 -1
  61. package/dist/managers/provider-manager.d.ts +43 -0
  62. package/dist/managers/provider-manager.d.ts.map +1 -1
  63. package/dist/managers/provider-manager.js +102 -4
  64. package/dist/managers/provider-manager.js.map +1 -1
  65. package/dist/types.d.ts +66 -0
  66. package/dist/types.d.ts.map +1 -1
  67. package/dist/types.js.map +1 -1
  68. package/package.json +35 -22
package/README.md CHANGED
@@ -529,6 +529,659 @@ emailManager.shutdown();
529
529
 
530
530
  This stops the scheduler and cleans up resources.
531
531
 
532
+ ---
533
+
534
+ ## Enhanced Features
535
+
536
+ The `EnhancedEmailManager` extends the base `EmailManager` with calendar invites, batch sending, subscription management, unsubscribe flows, and more. All enhanced features are available through a single import:
537
+
538
+ ```typescript
539
+ import { EnhancedEmailManager, EnhancedEmailManagerConfig } from '@bernierllc/email-manager';
540
+ ```
541
+
542
+ ### Calendar Invites
543
+
544
+ Send calendar invitations (ICS) and cancellations as email attachments. The manager generates valid ICS content using the `@bernierllc/email-calendar` package and attaches it automatically.
545
+
546
+ #### Send a Calendar Invite
547
+
548
+ ```typescript
549
+ import { EnhancedEmailManager, CalendarEvent } from '@bernierllc/email-manager';
550
+
551
+ const event: CalendarEvent = {
552
+ uid: 'meeting-2025-q2@example.com',
553
+ title: 'Q2 Business Review',
554
+ description: 'Quarterly review of business metrics.',
555
+ start: new Date('2025-07-15T14:00:00Z'),
556
+ end: new Date('2025-07-15T15:30:00Z'),
557
+ location: 'Conference Room A',
558
+ organizer: { email: 'organizer@example.com', name: 'Alice Johnson' },
559
+ attendees: [
560
+ { email: 'bob@example.com', name: 'Bob Smith', role: 'REQ-PARTICIPANT' },
561
+ { email: 'carol@example.com', name: 'Carol Lee', role: 'OPT-PARTICIPANT' },
562
+ ],
563
+ };
564
+
565
+ const result = await manager.sendCalendarInvite(
566
+ event,
567
+ ['bob@example.com', 'carol@example.com'],
568
+ );
569
+
570
+ console.log('Invite sent:', result.success);
571
+ ```
572
+
573
+ You can customize the subject and body:
574
+
575
+ ```typescript
576
+ const result = await manager.sendCalendarInvite(event, recipients, {
577
+ subject: 'You are invited: Q2 Business Review',
578
+ htmlBody: '<h2>Q2 Review</h2><p>Please join us for the quarterly review.</p>',
579
+ textBody: 'Please join us for the Q2 quarterly review.',
580
+ metadata: { campaign: 'internal-meetings' },
581
+ });
582
+ ```
583
+
584
+ #### Cancel a Calendar Event
585
+
586
+ ```typescript
587
+ import { CalendarAttendee } from '@bernierllc/email-manager';
588
+
589
+ const organizer: CalendarAttendee = { email: 'organizer@example.com', name: 'Alice' };
590
+ const attendees: CalendarAttendee[] = [
591
+ { email: 'bob@example.com', name: 'Bob' },
592
+ { email: 'carol@example.com', name: 'Carol' },
593
+ ];
594
+
595
+ const result = await manager.sendCalendarCancel(
596
+ 'meeting-2025-q2@example.com', // UID of the original event
597
+ organizer,
598
+ attendees,
599
+ {
600
+ subject: 'Cancelled: Q2 Business Review',
601
+ htmlBody: '<p>The Q2 Business Review has been cancelled.</p>',
602
+ },
603
+ );
604
+ ```
605
+
606
+ > See [`examples/calendar-invites.ts`](examples/calendar-invites.ts) for a complete working example.
607
+
608
+ ---
609
+
610
+ ### Batch Sending
611
+
612
+ Send large volumes of email with built-in concurrency control, rate limiting, retry logic, and progress tracking. Powered by `@bernierllc/email-batch-sender`.
613
+
614
+ #### Basic Batch Send
615
+
616
+ ```typescript
617
+ import { BatchEmailInput } from '@bernierllc/email-manager';
618
+
619
+ const emails: BatchEmailInput[] = [
620
+ {
621
+ toEmail: 'alice@example.com',
622
+ subject: 'Newsletter - July',
623
+ htmlContent: '<h1>Newsletter</h1><p>Hello Alice...</p>',
624
+ textContent: 'Newsletter\n\nHello Alice...',
625
+ },
626
+ {
627
+ toEmail: 'bob@example.com',
628
+ subject: 'Newsletter - July',
629
+ htmlContent: '<h1>Newsletter</h1><p>Hello Bob...</p>',
630
+ },
631
+ ];
632
+
633
+ const result = await manager.sendBatch(emails);
634
+ console.log(`Sent: ${result.succeeded}/${result.total}, Failed: ${result.failed}`);
635
+ ```
636
+
637
+ #### Batch Send with Rate Limiting and Retry
638
+
639
+ ```typescript
640
+ import { BatchSenderOptions } from '@bernierllc/email-manager';
641
+
642
+ const options: BatchSenderOptions = {
643
+ concurrency: 5, // Up to 5 emails in parallel
644
+ rateLimit: {
645
+ maxPerSecond: 10, // Respect provider rate limits
646
+ },
647
+ retry: {
648
+ maxRetries: 3, // Retry failed sends
649
+ initialDelayMs: 1000, // 1 second initial delay
650
+ backoffMultiplier: 2, // Exponential backoff
651
+ },
652
+ failureStrategy: 'continue', // Keep sending even if some fail
653
+ };
654
+
655
+ const result = await manager.sendBatch(emails, options);
656
+ ```
657
+
658
+ #### Batch Send with Progress Tracking
659
+
660
+ ```typescript
661
+ const result = await manager.sendBatch(emails, {
662
+ concurrency: 3,
663
+ onProgress: (progress) => {
664
+ const pct = ((progress.completed / progress.total) * 100).toFixed(0);
665
+ console.log(`Progress: ${pct}% - ${progress.succeeded} sent, ${progress.failed} failed`);
666
+ },
667
+ });
668
+ ```
669
+
670
+ #### Abort on Failure
671
+
672
+ For critical emails where partial delivery is unacceptable:
673
+
674
+ ```typescript
675
+ const result = await manager.sendBatch(criticalEmails, {
676
+ concurrency: 1,
677
+ failureStrategy: 'abort', // Stop immediately on first failure
678
+ retry: { maxRetries: 5 },
679
+ });
680
+
681
+ if (result.aborted) {
682
+ console.error('Batch was aborted due to a failure');
683
+ }
684
+ ```
685
+
686
+ > See [`examples/batch-sending.ts`](examples/batch-sending.ts) for complete examples.
687
+
688
+ ---
689
+
690
+ ### Subscription Management
691
+
692
+ Manage subscriber lists, add/remove subscribers, and handle bulk imports. The subscription system uses `@bernierllc/email-subscription` under the hood.
693
+
694
+ #### Create Subscriber Lists
695
+
696
+ ```typescript
697
+ const newsletter = await manager.createSubscriberList('Monthly Newsletter', {
698
+ description: 'Monthly product newsletter subscribers',
699
+ metadata: { category: 'marketing' },
700
+ });
701
+
702
+ console.log('List ID:', newsletter.id);
703
+ ```
704
+
705
+ #### Add Subscribers
706
+
707
+ ```typescript
708
+ // Individual subscriber
709
+ await manager.addSubscriber(newsletter.id, {
710
+ email: 'alice@example.com',
711
+ name: 'Alice Johnson',
712
+ });
713
+
714
+ // Bulk add (skips duplicates and suppressed emails)
715
+ const result = await manager.addSubscribers(newsletter.id, [
716
+ { email: 'user1@example.com', name: 'User One' },
717
+ { email: 'user2@example.com', name: 'User Two' },
718
+ { email: 'user3@example.com', name: 'User Three' },
719
+ ]);
720
+
721
+ console.log(`Added: ${result.added}, Skipped: ${result.skipped}`);
722
+ ```
723
+
724
+ #### Look Up and Remove Subscribers
725
+
726
+ ```typescript
727
+ // Look up a subscriber
728
+ const subscriber = await manager.getSubscriber(newsletter.id, 'alice@example.com');
729
+ if (subscriber) {
730
+ console.log('Status:', subscriber.status);
731
+ }
732
+
733
+ // Remove a subscriber
734
+ const removed = await manager.removeSubscriber(newsletter.id, 'alice@example.com');
735
+ ```
736
+
737
+ #### Delete a List
738
+
739
+ ```typescript
740
+ const deleted = await manager.deleteSubscriberList(newsletter.id);
741
+ ```
742
+
743
+ ---
744
+
745
+ ### Unsubscribe Flows
746
+
747
+ Generate signed unsubscribe URLs and process unsubscribe requests. Requires the `subscription.unsubscribe` configuration.
748
+
749
+ #### Configuration
750
+
751
+ ```typescript
752
+ const config: EnhancedEmailManagerConfig = {
753
+ providers: [/* ... */],
754
+ subscription: {
755
+ enabled: true,
756
+ unsubscribe: {
757
+ baseUrl: 'https://example.com/unsubscribe',
758
+ secret: process.env.UNSUBSCRIBE_SECRET!,
759
+ expiresIn: 30 * 24 * 60 * 60, // 30 days
760
+ },
761
+ },
762
+ };
763
+ ```
764
+
765
+ #### Generate Unsubscribe URL
766
+
767
+ ```typescript
768
+ const url = manager.generateUnsubscribeUrl('alice@example.com', listId);
769
+ // Returns: https://example.com/unsubscribe?token=<signed-jwt>
770
+ ```
771
+
772
+ #### Process Unsubscribe Request
773
+
774
+ In your web server's unsubscribe endpoint:
775
+
776
+ ```typescript
777
+ app.get('/unsubscribe', async (req, res) => {
778
+ const token = req.query.token as string;
779
+
780
+ try {
781
+ await manager.processUnsubscribe(token);
782
+ res.send('You have been unsubscribed successfully.');
783
+ } catch (error) {
784
+ res.status(400).send('Invalid or expired unsubscribe link.');
785
+ }
786
+ });
787
+ ```
788
+
789
+ #### Send Email with List-Unsubscribe Headers
790
+
791
+ Automatically generate and attach RFC 2369 / RFC 8058 `List-Unsubscribe` headers:
792
+
793
+ ```typescript
794
+ const result = await manager.sendWithUnsubscribe(
795
+ {
796
+ to: 'alice@example.com',
797
+ subject: 'Monthly Newsletter',
798
+ html: '<p>Newsletter content...</p>',
799
+ },
800
+ listId, // Subscriber list ID
801
+ 'alice@example.com', // Recipient for token generation
802
+ true, // Enable one-click unsubscribe (RFC 8058)
803
+ );
804
+ ```
805
+
806
+ > See [`examples/subscription-management.ts`](examples/subscription-management.ts) for the full lifecycle.
807
+
808
+ ---
809
+
810
+ ### Suppression
811
+
812
+ Check if an email address is suppressed before sending. Emails become suppressed when a subscriber unsubscribes or when bounces/complaints are processed.
813
+
814
+ ```typescript
815
+ // Check global suppression
816
+ const isSuppressed = await manager.isSuppressed('alice@example.com');
817
+
818
+ // Check list-specific suppression
819
+ const isListSuppressed = await manager.isSuppressed('alice@example.com', listId);
820
+
821
+ if (isSuppressed) {
822
+ console.log('Skipping send - email is suppressed');
823
+ }
824
+ ```
825
+
826
+ ---
827
+
828
+ ### Custom Subscription Store
829
+
830
+ By default, the manager uses an in-memory subscription store. For production, replace it with a database-backed implementation:
831
+
832
+ ```typescript
833
+ import { SubscriptionStore } from '@bernierllc/email-manager';
834
+
835
+ class DatabaseSubscriptionStore implements SubscriptionStore {
836
+ // Implement all SubscriptionStore methods using your database
837
+ // (createList, getList, addSubscriber, etc.)
838
+ }
839
+
840
+ const store = new DatabaseSubscriptionStore();
841
+ manager.setSubscriptionStore(store);
842
+ ```
843
+
844
+ ---
845
+
846
+ ### Custom Headers
847
+
848
+ Add custom email headers for threading, read receipts, and other purposes by including them in the `metadata.headers` field.
849
+
850
+ #### Email Threading
851
+
852
+ ```typescript
853
+ const result = await manager.sendEmail({
854
+ to: 'colleague@example.com',
855
+ subject: 'Re: Project Discussion',
856
+ html: '<p>I agree with the proposed approach.</p>',
857
+ metadata: {
858
+ headers: {
859
+ 'In-Reply-To': '<original-message-id@example.com>',
860
+ 'References': '<original-message-id@example.com>',
861
+ },
862
+ },
863
+ });
864
+ ```
865
+
866
+ #### List-Unsubscribe Headers (Standalone)
867
+
868
+ Use the `createListUnsubscribeHeaders` utility directly:
869
+
870
+ ```typescript
871
+ import { createListUnsubscribeHeaders } from '@bernierllc/email-manager';
872
+
873
+ const headers = createListUnsubscribeHeaders(
874
+ 'https://example.com/unsubscribe?id=123', // HTTPS unsubscribe URL
875
+ 'mailto:unsub@example.com?subject=unsub', // Optional mailto fallback
876
+ true, // One-click unsubscribe (RFC 8058)
877
+ );
878
+
879
+ const result = await manager.sendEmail({
880
+ to: 'subscriber@example.com',
881
+ subject: 'Weekly Digest',
882
+ html: '<p>Your digest...</p>',
883
+ metadata: { headers },
884
+ });
885
+ ```
886
+
887
+ #### MDN (Read Receipt) Request
888
+
889
+ ```typescript
890
+ const result = await manager.sendEmail({
891
+ to: 'recipient@example.com',
892
+ subject: 'Contract for Review',
893
+ html: '<p>Please confirm receipt of the attached contract.</p>',
894
+ metadata: {
895
+ headers: {
896
+ 'Disposition-Notification-To': 'sender@example.com',
897
+ },
898
+ },
899
+ });
900
+ ```
901
+
902
+ > See [`examples/attachments-and-headers.ts`](examples/attachments-and-headers.ts) for complete examples.
903
+
904
+ ---
905
+
906
+ ### Attachments
907
+
908
+ Send emails with file attachments, inline images, or both.
909
+
910
+ #### File Attachment
911
+
912
+ ```typescript
913
+ const result = await manager.sendEmail({
914
+ to: 'recipient@example.com',
915
+ subject: 'Report Attached',
916
+ html: '<p>Please find the report attached.</p>',
917
+ attachments: [
918
+ {
919
+ filename: 'report.pdf',
920
+ content: fs.readFileSync('/path/to/report.pdf'),
921
+ contentType: 'application/pdf',
922
+ },
923
+ ],
924
+ });
925
+ ```
926
+
927
+ #### String Content Attachment
928
+
929
+ ```typescript
930
+ const csvData = 'Name,Email\nAlice,alice@example.com\nBob,bob@example.com\n';
931
+
932
+ const result = await manager.sendEmail({
933
+ to: 'manager@example.com',
934
+ subject: 'User Export',
935
+ html: '<p>User export attached.</p>',
936
+ attachments: [
937
+ {
938
+ filename: 'users.csv',
939
+ content: csvData, // String content is also supported
940
+ contentType: 'text/csv',
941
+ },
942
+ ],
943
+ });
944
+ ```
945
+
946
+ #### Inline / Embedded Image
947
+
948
+ Reference inline images using a Content-ID (CID):
949
+
950
+ ```typescript
951
+ const result = await manager.sendEmail({
952
+ to: 'customer@example.com',
953
+ subject: 'Order Confirmation',
954
+ html: `
955
+ <img src="cid:company-logo" alt="Logo" />
956
+ <h2>Order Confirmed!</h2>
957
+ `,
958
+ attachments: [
959
+ {
960
+ filename: 'logo.png',
961
+ content: fs.readFileSync('/path/to/logo.png'),
962
+ contentType: 'image/png',
963
+ contentDisposition: 'inline',
964
+ cid: 'company-logo', // Matches src="cid:company-logo" in HTML
965
+ },
966
+ ],
967
+ });
968
+ ```
969
+
970
+ #### Multiple Attachments
971
+
972
+ ```typescript
973
+ const result = await manager.sendEmail({
974
+ to: 'team@example.com',
975
+ subject: 'Brand Kit Assets',
976
+ html: '<p>Attached are the brand assets.</p>',
977
+ attachments: [
978
+ {
979
+ filename: 'guidelines.pdf',
980
+ content: fs.readFileSync('/path/to/guidelines.pdf'),
981
+ contentType: 'application/pdf',
982
+ },
983
+ {
984
+ filename: 'logo.png',
985
+ content: fs.readFileSync('/path/to/logo.png'),
986
+ contentType: 'image/png',
987
+ },
988
+ {
989
+ filename: 'palette.json',
990
+ content: JSON.stringify({ primary: '#0066CC' }, null, 2),
991
+ contentType: 'application/json',
992
+ },
993
+ ],
994
+ });
995
+ ```
996
+
997
+ > See [`examples/attachments-and-headers.ts`](examples/attachments-and-headers.ts) for complete examples.
998
+
999
+ ---
1000
+
1001
+ ## Capability Matrix & Smart Routing
1002
+
1003
+ The email manager includes a full capability matrix that maps every feature to every provider, enabling smart routing decisions and graceful degradation.
1004
+
1005
+ ### Capability Matrix
1006
+
1007
+ Each feature is classified per provider with one of four source types:
1008
+
1009
+ | Source | Meaning |
1010
+ |--------|---------|
1011
+ | `provider` | Natively supported by the email provider's API |
1012
+ | `platform` | Implemented by the platform (polyfilled locally) |
1013
+ | `enhanced` | Built on top of provider primitives with added value |
1014
+ | `unsupported` | Not available for this provider |
1015
+
1016
+ #### Provider Feature Support
1017
+
1018
+ | Feature | SendGrid | Mailgun | Postmark | SES | SMTP |
1019
+ |---------|----------|---------|----------|-----|------|
1020
+ | sendEmail | provider | provider | provider | provider | provider |
1021
+ | batchSend | provider | provider | provider | provider | platform |
1022
+ | providerTemplates | provider | provider | provider | provider | unsupported |
1023
+ | localTemplates | platform | platform | platform | platform | platform |
1024
+ | calendarInvites | platform | platform | platform | platform | platform |
1025
+ | calendarEventMgmt | platform | platform | platform | platform | platform |
1026
+ | webhooksReceive | provider | provider | provider | provider | unsupported |
1027
+ | webhookNormalization | enhanced | enhanced | enhanced | enhanced | unsupported |
1028
+ | deliveryTracking | provider | provider | provider | provider | unsupported |
1029
+ | inboundEmailParsing | enhanced | enhanced | enhanced | enhanced | unsupported |
1030
+ | subscriptionMgmt | enhanced | enhanced | platform | platform | platform |
1031
+ | suppressionLists | provider | provider | provider | provider | platform |
1032
+ | unsubscribeUrlGeneration | platform | platform | platform | platform | platform |
1033
+ | scheduledSend | provider | provider | platform | platform | platform |
1034
+ | openClickTracking | provider | provider | provider* | unsupported | unsupported |
1035
+ | emailContentParsing | platform | platform | platform | platform | platform |
1036
+ | emailHeaderMgmt | platform | platform | platform | platform | platform |
1037
+ | attachments | provider | provider | provider | provider | provider |
1038
+ | advancedAttachmentProcessing | platform | platform | platform | platform | platform |
1039
+ | dkimSpf | provider | provider | provider | provider | unsupported |
1040
+ | linkBranding | provider | provider | unsupported | unsupported | unsupported |
1041
+ | retryResilience | platform | platform | platform | platform | platform |
1042
+ | multiProviderFailover | platform | platform | platform | platform | platform |
1043
+
1044
+ *Postmark supports open tracking only; click tracking is not available.
1045
+
1046
+ Notable provider-specific limitations:
1047
+ - **SendGrid** `scheduledSend`: 72-hour maximum scheduling window
1048
+ - **Mailgun** `scheduledSend`: 3-day maximum scheduling window
1049
+ - **SES** `webhooksReceive` / `deliveryTracking`: Requires SNS topic configuration
1050
+
1051
+ ### Routing Configuration
1052
+
1053
+ Capability-aware routing is opt-in. Enable it by adding a `routing` section to your config:
1054
+
1055
+ ```typescript
1056
+ import { EmailManager, EmailManagerConfig } from '@bernierllc/email-manager';
1057
+
1058
+ const config: EmailManagerConfig = {
1059
+ providers: [
1060
+ { id: 'sg', name: 'SendGrid', type: 'sendgrid', config: { apiKey: '...' }, isActive: true, priority: 1 },
1061
+ { id: 'mg', name: 'Mailgun', type: 'mailgun', config: { apiKey: '...', domain: '...' }, isActive: true, priority: 2 },
1062
+ ],
1063
+ routing: {
1064
+ strategy: 'primary-failover',
1065
+ featureOverrides: {
1066
+ // Always use platform implementation for local templates
1067
+ localTemplates: { behavior: 'platform-always' },
1068
+ // Throw an error if no native provider supports the feature
1069
+ openClickTracking: { behavior: 'error' },
1070
+ },
1071
+ },
1072
+ degradation: {
1073
+ logLevel: 'warn',
1074
+ includeDocLinks: true,
1075
+ docsBaseUrl: 'https://docs.example.com/email',
1076
+ },
1077
+ };
1078
+
1079
+ const manager = new EmailManager(config);
1080
+ ```
1081
+
1082
+ #### Feature Override Behaviors
1083
+
1084
+ | Behavior | Description |
1085
+ |----------|-------------|
1086
+ | `native-first` | (Default) Prefer providers with native or enhanced support; fall back to platform if none found |
1087
+ | `platform-always` | Always use the platform implementation, even if the provider supports it natively |
1088
+ | `error` | Throw `FeatureUnsupportedError` if no provider has native or enhanced support |
1089
+
1090
+ ### Degradation Behavior
1091
+
1092
+ When a feature is routed to a provider via platform polyfill (not native), the system records degradation information:
1093
+
1094
+ - **`platform` source features** (e.g., SMTP `batchSend`): Emails are sent sequentially rather than in a true batch. The `SendResult.routing` field indicates `degraded: true` with a `degradationReason`.
1095
+ - **`unsupported` features** (e.g., SMTP `webhooksReceive`): The router skips the provider entirely and tries the next in priority order. If no provider supports the feature, a `FeatureUnsupportedError` is thrown.
1096
+ - **Degradation logging**: Controlled by `degradation.logLevel` (`'silent'` | `'info'` | `'warn'` | `'error'`). When `includeDocLinks` is `true`, log messages include links to degradation documentation.
1097
+
1098
+ ### Redis Resolver Setup
1099
+
1100
+ By default, the capability matrix is resolved from an in-memory snapshot. For shared or dynamic capability data across multiple instances, use the Redis-backed resolver:
1101
+
1102
+ ```bash
1103
+ # Set the environment variable to enable Redis resolver auto-detection
1104
+ export EMAIL_MANAGER_REDIS_URL="redis://localhost:6379"
1105
+ ```
1106
+
1107
+ The factory (`createCapabilityResolver`) automatically detects the environment variable and creates a `SafeCapabilityResolver` that wraps a Redis resolver with an in-memory fallback. If `ioredis` is not installed or Redis is unavailable, it falls back to the in-memory resolver silently.
1108
+
1109
+ #### Redis Resolver Options
1110
+
1111
+ ```typescript
1112
+ import { createCapabilityResolver } from '@bernierllc/email-manager';
1113
+
1114
+ const resolver = await createCapabilityResolver({
1115
+ namespace: 'myapp:capabilities', // Redis key namespace (default: 'email-manager:capabilities')
1116
+ ttl: 3600, // Cache TTL in seconds (default: varies by implementation)
1117
+ });
1118
+ ```
1119
+
1120
+ You can also provide your own resolver implementation:
1121
+
1122
+ ```typescript
1123
+ const config: EmailManagerConfig = {
1124
+ providers: [/* ... */],
1125
+ routing: {
1126
+ strategy: 'primary-failover',
1127
+ capabilityResolver: myCustomResolver, // Must implement CapabilityResolver interface
1128
+ },
1129
+ };
1130
+ ```
1131
+
1132
+ ### RoutingMetadata in Responses
1133
+
1134
+ When routing is active, `SendResult` includes a `routing` field:
1135
+
1136
+ ```typescript
1137
+ const result = await manager.sendEmail(emailData);
1138
+
1139
+ if (result.routing) {
1140
+ console.log('Provider used:', result.routing.provider);
1141
+ console.log('Feature:', result.routing.feature);
1142
+ console.log('Source:', result.routing.source); // 'provider' | 'platform' | 'enhanced' | 'unsupported'
1143
+ console.log('Degraded:', result.routing.degraded); // true if platform polyfill was used
1144
+ console.log('Reason:', result.routing.degradationReason);
1145
+ console.log('Tried:', result.routing.attemptedProviders); // providers checked before selecting
1146
+ }
1147
+ ```
1148
+
1149
+ The `RoutingMetadata` interface:
1150
+
1151
+ ```typescript
1152
+ interface RoutingMetadata {
1153
+ provider: string;
1154
+ feature: string;
1155
+ source: 'provider' | 'platform' | 'enhanced' | 'unsupported';
1156
+ degraded: boolean;
1157
+ degradationReason?: string;
1158
+ attemptedProviders?: string[];
1159
+ }
1160
+ ```
1161
+
1162
+ ### Migration Guide from v0.4.x
1163
+
1164
+ The capability matrix and routing system is fully backward compatible. Existing code continues to work without changes:
1165
+
1166
+ - **No routing config**: When `routing` is omitted from the config, the manager uses the same provider selection logic as before (priority-based failover).
1167
+ - **No degradation config**: When `degradation` is omitted, degradation events are not logged.
1168
+ - **Opt-in**: To enable capability-aware routing, add a `routing` section to your config. The `RoutingMetadata` field on `SendResult` only appears when routing is active.
1169
+ - **No new required dependencies**: The Redis resolver is optional and only activated when `EMAIL_MANAGER_REDIS_URL` is set and `ioredis` is installed.
1170
+
1171
+ ---
1172
+
1173
+ ## Re-exported Types
1174
+
1175
+ For convenience, the email-manager re-exports types and utilities from its sub-packages so consumers do not need direct dependencies:
1176
+
1177
+ | Package | Re-exported Items |
1178
+ |---------|-------------------|
1179
+ | `@bernierllc/email-calendar` | `CalendarEvent`, `CalendarAttendee`, `CalendarMethod`, `createCalendarInvite`, `createCalendarCancel`, `toCalendarAttachment` |
1180
+ | `@bernierllc/email-batch-sender` | `BatchSenderOptions`, `BatchResult`, `BatchEmailInput`, `BatchProgress`, `BatchSender`, `createBatchSender` |
1181
+ | `@bernierllc/email-subscription` | `SubscriberList`, `Subscriber`, `SubscriptionStore`, `AddSubscriberInput`, `CreateListOptions`, `BulkAddResult`, `UnsubscribeManager`, `SuppressionManager`, `InMemorySubscriptionStore` |
1182
+ | `@bernierllc/email-headers` | `EmailHeaders`, `ThreadingHeaders`, `ListUnsubscribeHeaders`, `createListUnsubscribeHeaders` |
1183
+ | `@bernierllc/email-attachments` | `AttachmentInput`, `AttachmentLimits`, `AttachmentValidationResult` |
1184
+
532
1185
  ## License
533
1186
 
534
1187
  Copyright (c) 2025 Bernier LLC. All rights reserved.