@bernierllc/email-manager 0.4.0 → 0.4.2
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 +653 -0
- package/dist/capabilities/degradation-logger.d.ts +59 -0
- package/dist/capabilities/degradation-logger.d.ts.map +1 -0
- package/dist/capabilities/degradation-logger.js +106 -0
- package/dist/capabilities/degradation-logger.js.map +1 -0
- package/dist/capabilities/feature-router.d.ts +55 -0
- package/dist/capabilities/feature-router.d.ts.map +1 -0
- package/dist/capabilities/feature-router.js +143 -0
- package/dist/capabilities/feature-router.js.map +1 -0
- package/dist/capabilities/in-memory-resolver.d.ts +27 -0
- package/dist/capabilities/in-memory-resolver.d.ts.map +1 -0
- package/dist/capabilities/in-memory-resolver.js +79 -0
- package/dist/capabilities/in-memory-resolver.js.map +1 -0
- package/dist/capabilities/index.d.ts +13 -0
- package/dist/capabilities/index.d.ts.map +1 -0
- package/dist/capabilities/index.js +21 -0
- package/dist/capabilities/index.js.map +1 -0
- package/dist/capabilities/matrix.d.ts +66 -0
- package/dist/capabilities/matrix.d.ts.map +1 -0
- package/dist/capabilities/matrix.js +247 -0
- package/dist/capabilities/matrix.js.map +1 -0
- package/dist/capabilities/redis-resolver.d.ts +95 -0
- package/dist/capabilities/redis-resolver.d.ts.map +1 -0
- package/dist/capabilities/redis-resolver.js +227 -0
- package/dist/capabilities/redis-resolver.js.map +1 -0
- package/dist/capabilities/resolver-factory.d.ts +30 -0
- package/dist/capabilities/resolver-factory.d.ts.map +1 -0
- package/dist/capabilities/resolver-factory.js +70 -0
- package/dist/capabilities/resolver-factory.js.map +1 -0
- package/dist/capabilities/resolver.d.ts +40 -0
- package/dist/capabilities/resolver.d.ts.map +1 -0
- package/dist/capabilities/resolver.js +18 -0
- package/dist/capabilities/resolver.js.map +1 -0
- package/dist/capabilities/routing-metadata.d.ts +16 -0
- package/dist/capabilities/routing-metadata.d.ts.map +1 -0
- package/dist/capabilities/routing-metadata.js +17 -0
- package/dist/capabilities/routing-metadata.js.map +1 -0
- package/dist/capabilities/safe-resolver.d.ts +24 -0
- package/dist/capabilities/safe-resolver.d.ts.map +1 -0
- package/dist/capabilities/safe-resolver.js +48 -0
- package/dist/capabilities/safe-resolver.js.map +1 -0
- package/dist/config/schema.d.ts +99 -4
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +17 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/email-manager.d.ts.map +1 -1
- package/dist/email-manager.js +28 -1
- package/dist/email-manager.js.map +1 -1
- package/dist/enhanced-email-manager.d.ts +163 -1
- package/dist/enhanced-email-manager.d.ts.map +1 -1
- package/dist/enhanced-email-manager.js +412 -8
- package/dist/enhanced-email-manager.js.map +1 -1
- package/dist/errors.d.ts +11 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +13 -0
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +13 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/managers/provider-manager.d.ts +43 -0
- package/dist/managers/provider-manager.d.ts.map +1 -1
- package/dist/managers/provider-manager.js +102 -4
- package/dist/managers/provider-manager.js.map +1 -1
- package/dist/types.d.ts +66 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +38 -25
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.
|