@bernierllc/email-webhook-events 1.0.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.
Files changed (97) hide show
  1. package/.eslintrc.cjs +29 -0
  2. package/README.md +349 -0
  3. package/__tests__/EmailWebhookEventsService.test.ts +247 -0
  4. package/__tests__/analytics/AnalyticsEngine.test.ts +244 -0
  5. package/__tests__/normalizers/MailgunNormalizer.test.ts +149 -0
  6. package/__tests__/normalizers/PostmarkNormalizer.test.ts +112 -0
  7. package/__tests__/normalizers/SESNormalizer.test.ts +168 -0
  8. package/__tests__/normalizers/SMTP2GONormalizer.test.ts +83 -0
  9. package/__tests__/normalizers/SendGridNormalizer.test.ts +181 -0
  10. package/__tests__/persistence/PersistenceAdapter.test.ts +103 -0
  11. package/coverage/clover.xml +328 -0
  12. package/coverage/coverage-final.json +10 -0
  13. package/coverage/lcov-report/base.css +224 -0
  14. package/coverage/lcov-report/block-navigation.js +87 -0
  15. package/coverage/lcov-report/favicon.png +0 -0
  16. package/coverage/lcov-report/index.html +161 -0
  17. package/coverage/lcov-report/prettify.css +1 -0
  18. package/coverage/lcov-report/prettify.js +2 -0
  19. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  20. package/coverage/lcov-report/sorter.js +210 -0
  21. package/coverage/lcov-report/src/EmailWebhookEventsService.ts.html +826 -0
  22. package/coverage/lcov-report/src/analytics/AnalyticsEngine.ts.html +775 -0
  23. package/coverage/lcov-report/src/analytics/index.html +116 -0
  24. package/coverage/lcov-report/src/index.html +131 -0
  25. package/coverage/lcov-report/src/normalizers/MailgunNormalizer.ts.html +301 -0
  26. package/coverage/lcov-report/src/normalizers/PostmarkNormalizer.ts.html +301 -0
  27. package/coverage/lcov-report/src/normalizers/SESNormalizer.ts.html +436 -0
  28. package/coverage/lcov-report/src/normalizers/SMTP2GONormalizer.ts.html +247 -0
  29. package/coverage/lcov-report/src/normalizers/SendGridNormalizer.ts.html +274 -0
  30. package/coverage/lcov-report/src/normalizers/index.html +176 -0
  31. package/coverage/lcov-report/src/persistence/InMemoryPersistenceAdapter.ts.html +289 -0
  32. package/coverage/lcov-report/src/persistence/index.html +116 -0
  33. package/coverage/lcov-report/src/types.ts.html +823 -0
  34. package/coverage/lcov.info +710 -0
  35. package/dist/EmailWebhookEventsService.d.ts +53 -0
  36. package/dist/EmailWebhookEventsService.d.ts.map +1 -0
  37. package/dist/EmailWebhookEventsService.js +198 -0
  38. package/dist/EmailWebhookEventsService.js.map +1 -0
  39. package/dist/analytics/AnalyticsEngine.d.ts +20 -0
  40. package/dist/analytics/AnalyticsEngine.d.ts.map +1 -0
  41. package/dist/analytics/AnalyticsEngine.js +160 -0
  42. package/dist/analytics/AnalyticsEngine.js.map +1 -0
  43. package/dist/index.d.ts +12 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +31 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/normalizers/EventNormalizer.d.ts +5 -0
  48. package/dist/normalizers/EventNormalizer.d.ts.map +1 -0
  49. package/dist/normalizers/EventNormalizer.js +10 -0
  50. package/dist/normalizers/EventNormalizer.js.map +1 -0
  51. package/dist/normalizers/MailgunNormalizer.d.ts +9 -0
  52. package/dist/normalizers/MailgunNormalizer.d.ts.map +1 -0
  53. package/dist/normalizers/MailgunNormalizer.js +73 -0
  54. package/dist/normalizers/MailgunNormalizer.js.map +1 -0
  55. package/dist/normalizers/PostmarkNormalizer.d.ts +10 -0
  56. package/dist/normalizers/PostmarkNormalizer.d.ts.map +1 -0
  57. package/dist/normalizers/PostmarkNormalizer.js +75 -0
  58. package/dist/normalizers/PostmarkNormalizer.js.map +1 -0
  59. package/dist/normalizers/SESNormalizer.d.ts +16 -0
  60. package/dist/normalizers/SESNormalizer.d.ts.map +1 -0
  61. package/dist/normalizers/SESNormalizer.js +107 -0
  62. package/dist/normalizers/SESNormalizer.js.map +1 -0
  63. package/dist/normalizers/SMTP2GONormalizer.d.ts +9 -0
  64. package/dist/normalizers/SMTP2GONormalizer.d.ts.map +1 -0
  65. package/dist/normalizers/SMTP2GONormalizer.js +55 -0
  66. package/dist/normalizers/SMTP2GONormalizer.js.map +1 -0
  67. package/dist/normalizers/SendGridNormalizer.d.ts +9 -0
  68. package/dist/normalizers/SendGridNormalizer.d.ts.map +1 -0
  69. package/dist/normalizers/SendGridNormalizer.js +64 -0
  70. package/dist/normalizers/SendGridNormalizer.js.map +1 -0
  71. package/dist/persistence/InMemoryPersistenceAdapter.d.ts +12 -0
  72. package/dist/persistence/InMemoryPersistenceAdapter.d.ts.map +1 -0
  73. package/dist/persistence/InMemoryPersistenceAdapter.js +58 -0
  74. package/dist/persistence/InMemoryPersistenceAdapter.js.map +1 -0
  75. package/dist/persistence/PersistenceAdapter.d.ts +7 -0
  76. package/dist/persistence/PersistenceAdapter.d.ts.map +1 -0
  77. package/dist/persistence/PersistenceAdapter.js +10 -0
  78. package/dist/persistence/PersistenceAdapter.js.map +1 -0
  79. package/dist/types.d.ts +178 -0
  80. package/dist/types.d.ts.map +1 -0
  81. package/dist/types.js +41 -0
  82. package/dist/types.js.map +1 -0
  83. package/jest.config.cjs +29 -0
  84. package/package.json +51 -0
  85. package/src/EmailWebhookEventsService.ts +247 -0
  86. package/src/analytics/AnalyticsEngine.ts +230 -0
  87. package/src/index.ts +39 -0
  88. package/src/normalizers/EventNormalizer.ts +13 -0
  89. package/src/normalizers/MailgunNormalizer.ts +72 -0
  90. package/src/normalizers/PostmarkNormalizer.ts +72 -0
  91. package/src/normalizers/SESNormalizer.ts +117 -0
  92. package/src/normalizers/SMTP2GONormalizer.ts +54 -0
  93. package/src/normalizers/SendGridNormalizer.ts +63 -0
  94. package/src/persistence/InMemoryPersistenceAdapter.ts +68 -0
  95. package/src/persistence/PersistenceAdapter.ts +15 -0
  96. package/src/types.ts +246 -0
  97. package/tsconfig.json +31 -0
@@ -0,0 +1,68 @@
1
+ /*
2
+ Copyright (c) 2025 Bernier LLC
3
+
4
+ This file is licensed to the client under a limited-use license.
5
+ The client may use and modify this code *only within the scope of the project it was delivered for*.
6
+ Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
7
+ */
8
+
9
+ import { PersistenceAdapter } from './PersistenceAdapter';
10
+ import { EmailEvent, EventQuery } from '../types';
11
+
12
+ export class InMemoryPersistenceAdapter implements PersistenceAdapter {
13
+ private events: EmailEvent[] = [];
14
+ private sentEmails: unknown[] = [];
15
+
16
+ async saveEvent(event: EmailEvent): Promise<void> {
17
+ this.events.push(event);
18
+ }
19
+
20
+ async queryEvents(query: EventQuery): Promise<EmailEvent[]> {
21
+ let results = [...this.events];
22
+
23
+ if (query.emailId) {
24
+ results = results.filter(e => e.emailId === query.emailId);
25
+ }
26
+
27
+ if (query.recipient) {
28
+ results = results.filter(e => e.recipient === query.recipient);
29
+ }
30
+
31
+ if (query.eventTypes && query.eventTypes.length > 0) {
32
+ results = results.filter(e => query.eventTypes!.includes(e.type));
33
+ }
34
+
35
+ if (query.startDate) {
36
+ results = results.filter(e => e.timestamp >= query.startDate!);
37
+ }
38
+
39
+ if (query.endDate) {
40
+ results = results.filter(e => e.timestamp <= query.endDate!);
41
+ }
42
+
43
+ // Apply offset and limit
44
+ if (query.offset) {
45
+ results = results.slice(query.offset);
46
+ }
47
+
48
+ if (query.limit) {
49
+ results = results.slice(0, query.limit);
50
+ }
51
+
52
+ return results;
53
+ }
54
+
55
+ async saveSentEmail(emailData: unknown): Promise<void> {
56
+ this.sentEmails.push(emailData);
57
+ }
58
+
59
+ // Test helper methods
60
+ getEventCount(): number {
61
+ return this.events.length;
62
+ }
63
+
64
+ clearEvents(): void {
65
+ this.events = [];
66
+ this.sentEmails = [];
67
+ }
68
+ }
@@ -0,0 +1,15 @@
1
+ /*
2
+ Copyright (c) 2025 Bernier LLC
3
+
4
+ This file is licensed to the client under a limited-use license.
5
+ The client may use and modify this code *only within the scope of the project it was delivered for*.
6
+ Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
7
+ */
8
+
9
+ import { EmailEvent, EventQuery } from '../types';
10
+
11
+ export interface PersistenceAdapter {
12
+ saveEvent(event: EmailEvent): Promise<void>;
13
+ queryEvents(query: EventQuery): Promise<EmailEvent[]>;
14
+ saveSentEmail?(emailData: unknown): Promise<void>;
15
+ }
package/src/types.ts ADDED
@@ -0,0 +1,246 @@
1
+ /*
2
+ Copyright (c) 2025 Bernier LLC
3
+
4
+ This file is licensed to the client under a limited-use license.
5
+ The client may use and modify this code *only within the scope of the project it was delivered for*.
6
+ Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
7
+ */
8
+
9
+ // Unified email event format
10
+ export interface EmailEvent {
11
+ // Event identification
12
+ id: string;
13
+ emailId: string;
14
+ messageId: string;
15
+
16
+ // Event type
17
+ type: EmailEventType;
18
+
19
+ // Recipient information
20
+ recipient: string;
21
+
22
+ // Timestamp
23
+ timestamp: Date;
24
+
25
+ // Provider information
26
+ provider: EmailProvider;
27
+
28
+ // Event-specific data
29
+ metadata: EmailEventMetadata;
30
+
31
+ // Original webhook data (for debugging)
32
+ rawEvent?: unknown;
33
+ }
34
+
35
+ export enum EmailEventType {
36
+ DELIVERED = 'delivered',
37
+ OPENED = 'opened',
38
+ CLICKED = 'clicked',
39
+ BOUNCED = 'bounced',
40
+ COMPLAINED = 'complained',
41
+ UNSUBSCRIBED = 'unsubscribed',
42
+ FAILED = 'failed',
43
+ DEFERRED = 'deferred',
44
+ DROPPED = 'dropped'
45
+ }
46
+
47
+ export enum EmailProvider {
48
+ SENDGRID = 'sendgrid',
49
+ MAILGUN = 'mailgun',
50
+ AWS_SES = 'aws_ses',
51
+ POSTMARK = 'postmark',
52
+ SMTP2GO = 'smtp2go',
53
+ GENERIC_SMTP = 'generic_smtp'
54
+ }
55
+
56
+ export interface EmailEventMetadata {
57
+ // For clicked events
58
+ url?: string;
59
+ linkId?: string;
60
+
61
+ // For bounced events
62
+ bounceType?: 'hard' | 'soft';
63
+ bounceReason?: string;
64
+ bounceCode?: string;
65
+
66
+ // For complained events
67
+ complaintFeedbackType?: string;
68
+
69
+ // For failed events
70
+ failureReason?: string;
71
+ smtpResponse?: string;
72
+
73
+ // For deferred events
74
+ deferralReason?: string;
75
+ attemptNumber?: number;
76
+ nextAttempt?: Date;
77
+
78
+ // Common metadata
79
+ userAgent?: string;
80
+ ipAddress?: string;
81
+ deviceType?: 'desktop' | 'mobile' | 'tablet' | 'unknown';
82
+ location?: {
83
+ country?: string;
84
+ region?: string;
85
+ city?: string;
86
+ };
87
+ }
88
+
89
+ // Analytics aggregation
90
+ export interface EmailAnalytics {
91
+ // Time range
92
+ startDate: Date;
93
+ endDate: Date;
94
+
95
+ // Counts
96
+ totalSent: number;
97
+ totalDelivered: number;
98
+ totalOpened: number;
99
+ totalClicked: number;
100
+ totalBounced: number;
101
+ totalComplained: number;
102
+ totalUnsubscribed: number;
103
+ totalFailed: number;
104
+
105
+ // Rates
106
+ deliveryRate: number;
107
+ openRate: number;
108
+ clickRate: number;
109
+ clickToOpenRate: number;
110
+ bounceRate: number;
111
+ complaintRate: number;
112
+ unsubscribeRate: number;
113
+
114
+ // Breakdowns
115
+ byProvider: Record<EmailProvider, ProviderAnalytics>;
116
+ byEventType: Record<EmailEventType, number>;
117
+ byBounceType?: {
118
+ hard: number;
119
+ soft: number;
120
+ };
121
+
122
+ // Top links (for clicked events)
123
+ topLinks?: Array<{
124
+ url: string;
125
+ clicks: number;
126
+ uniqueClicks: number;
127
+ }>;
128
+
129
+ // Device breakdown
130
+ byDevice?: {
131
+ desktop: number;
132
+ mobile: number;
133
+ tablet: number;
134
+ unknown: number;
135
+ };
136
+ }
137
+
138
+ export interface ProviderAnalytics {
139
+ sent: number;
140
+ delivered: number;
141
+ opened: number;
142
+ clicked: number;
143
+ bounced: number;
144
+ complained: number;
145
+ failed: number;
146
+ deliveryRate: number;
147
+ openRate: number;
148
+ clickRate: number;
149
+ }
150
+
151
+ // Configuration
152
+ export interface EmailWebhookEventsConfig {
153
+ // Provider configurations
154
+ providers: ProviderWebhookConfig[];
155
+
156
+ // Event persistence
157
+ persistence?: {
158
+ enabled: boolean;
159
+ adapter: 'supabase' | 'postgresql' | 'mongodb' | 'memory';
160
+ config?: unknown;
161
+ };
162
+
163
+ // Analytics
164
+ analytics?: {
165
+ realTimeEnabled: boolean;
166
+ aggregationInterval: number;
167
+ retentionDays: number;
168
+ };
169
+
170
+ // Notifications
171
+ notifications?: {
172
+ enabled: boolean;
173
+ criticalEvents: EmailEventType[];
174
+ channels: NotificationChannel[];
175
+ };
176
+
177
+ // Rate limiting
178
+ rateLimiting?: {
179
+ enabled: boolean;
180
+ maxEventsPerSecond: number;
181
+ };
182
+ }
183
+
184
+ export interface ProviderWebhookConfig {
185
+ provider: EmailProvider;
186
+ enabled: boolean;
187
+ webhookUrl: string;
188
+ secretKey: string;
189
+ signatureHeader: string;
190
+ signatureAlgorithm: 'sha256' | 'sha1' | 'sha512';
191
+ }
192
+
193
+ export interface NotificationChannel {
194
+ type: 'neverhub' | 'webhook' | 'email';
195
+ config: {
196
+ url?: string;
197
+ recipients?: string[];
198
+ [key: string]: unknown;
199
+ };
200
+ }
201
+
202
+ export interface EmailWebhookResult {
203
+ success: boolean;
204
+ event?: EmailEvent;
205
+ error?: string;
206
+ }
207
+
208
+ export interface WebhookPayload {
209
+ provider: EmailProvider;
210
+ signature: string;
211
+ payload: unknown;
212
+ headers: Record<string, string>;
213
+ }
214
+
215
+ export interface AnalyticsFilters {
216
+ provider?: EmailProvider;
217
+ emailId?: string;
218
+ recipient?: string;
219
+ eventTypes?: EmailEventType[];
220
+ }
221
+
222
+ export interface EventQuery {
223
+ emailId?: string;
224
+ recipient?: string;
225
+ eventTypes?: EmailEventType[];
226
+ startDate?: Date;
227
+ endDate?: Date;
228
+ limit?: number;
229
+ offset?: number;
230
+ }
231
+
232
+ export interface EmailWebhookError {
233
+ code: EmailWebhookErrorCode;
234
+ message: string;
235
+ details?: unknown;
236
+ retryable: boolean;
237
+ }
238
+
239
+ export enum EmailWebhookErrorCode {
240
+ INVALID_SIGNATURE = 'INVALID_SIGNATURE',
241
+ UNSUPPORTED_PROVIDER = 'UNSUPPORTED_PROVIDER',
242
+ NORMALIZATION_FAILED = 'NORMALIZATION_FAILED',
243
+ PERSISTENCE_FAILED = 'PERSISTENCE_FAILED',
244
+ ANALYTICS_UPDATE_FAILED = 'ANALYTICS_UPDATE_FAILED',
245
+ NOTIFICATION_FAILED = 'NOTIFICATION_FAILED'
246
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "commonjs",
5
+ "lib": ["ES2020"],
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "declaration": true,
9
+ "declarationMap": true,
10
+ "sourceMap": true,
11
+ "strict": true,
12
+ "esModuleInterop": true,
13
+ "skipLibCheck": true,
14
+ "forceConsistentCasingInFileNames": true,
15
+ "resolveJsonModule": true,
16
+ "moduleResolution": "node",
17
+ "noImplicitAny": true,
18
+ "strictNullChecks": true,
19
+ "strictFunctionTypes": true,
20
+ "strictBindCallApply": true,
21
+ "strictPropertyInitialization": true,
22
+ "noImplicitThis": true,
23
+ "alwaysStrict": true,
24
+ "noUnusedLocals": true,
25
+ "noUnusedParameters": true,
26
+ "noImplicitReturns": true,
27
+ "noFallthroughCasesInSwitch": true
28
+ },
29
+ "include": ["src/**/*"],
30
+ "exclude": ["node_modules", "dist", "__tests__"]
31
+ }