@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.
- package/.eslintrc.cjs +29 -0
- package/README.md +349 -0
- package/__tests__/EmailWebhookEventsService.test.ts +247 -0
- package/__tests__/analytics/AnalyticsEngine.test.ts +244 -0
- package/__tests__/normalizers/MailgunNormalizer.test.ts +149 -0
- package/__tests__/normalizers/PostmarkNormalizer.test.ts +112 -0
- package/__tests__/normalizers/SESNormalizer.test.ts +168 -0
- package/__tests__/normalizers/SMTP2GONormalizer.test.ts +83 -0
- package/__tests__/normalizers/SendGridNormalizer.test.ts +181 -0
- package/__tests__/persistence/PersistenceAdapter.test.ts +103 -0
- package/coverage/clover.xml +328 -0
- package/coverage/coverage-final.json +10 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +161 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +210 -0
- package/coverage/lcov-report/src/EmailWebhookEventsService.ts.html +826 -0
- package/coverage/lcov-report/src/analytics/AnalyticsEngine.ts.html +775 -0
- package/coverage/lcov-report/src/analytics/index.html +116 -0
- package/coverage/lcov-report/src/index.html +131 -0
- package/coverage/lcov-report/src/normalizers/MailgunNormalizer.ts.html +301 -0
- package/coverage/lcov-report/src/normalizers/PostmarkNormalizer.ts.html +301 -0
- package/coverage/lcov-report/src/normalizers/SESNormalizer.ts.html +436 -0
- package/coverage/lcov-report/src/normalizers/SMTP2GONormalizer.ts.html +247 -0
- package/coverage/lcov-report/src/normalizers/SendGridNormalizer.ts.html +274 -0
- package/coverage/lcov-report/src/normalizers/index.html +176 -0
- package/coverage/lcov-report/src/persistence/InMemoryPersistenceAdapter.ts.html +289 -0
- package/coverage/lcov-report/src/persistence/index.html +116 -0
- package/coverage/lcov-report/src/types.ts.html +823 -0
- package/coverage/lcov.info +710 -0
- package/dist/EmailWebhookEventsService.d.ts +53 -0
- package/dist/EmailWebhookEventsService.d.ts.map +1 -0
- package/dist/EmailWebhookEventsService.js +198 -0
- package/dist/EmailWebhookEventsService.js.map +1 -0
- package/dist/analytics/AnalyticsEngine.d.ts +20 -0
- package/dist/analytics/AnalyticsEngine.d.ts.map +1 -0
- package/dist/analytics/AnalyticsEngine.js +160 -0
- package/dist/analytics/AnalyticsEngine.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/normalizers/EventNormalizer.d.ts +5 -0
- package/dist/normalizers/EventNormalizer.d.ts.map +1 -0
- package/dist/normalizers/EventNormalizer.js +10 -0
- package/dist/normalizers/EventNormalizer.js.map +1 -0
- package/dist/normalizers/MailgunNormalizer.d.ts +9 -0
- package/dist/normalizers/MailgunNormalizer.d.ts.map +1 -0
- package/dist/normalizers/MailgunNormalizer.js +73 -0
- package/dist/normalizers/MailgunNormalizer.js.map +1 -0
- package/dist/normalizers/PostmarkNormalizer.d.ts +10 -0
- package/dist/normalizers/PostmarkNormalizer.d.ts.map +1 -0
- package/dist/normalizers/PostmarkNormalizer.js +75 -0
- package/dist/normalizers/PostmarkNormalizer.js.map +1 -0
- package/dist/normalizers/SESNormalizer.d.ts +16 -0
- package/dist/normalizers/SESNormalizer.d.ts.map +1 -0
- package/dist/normalizers/SESNormalizer.js +107 -0
- package/dist/normalizers/SESNormalizer.js.map +1 -0
- package/dist/normalizers/SMTP2GONormalizer.d.ts +9 -0
- package/dist/normalizers/SMTP2GONormalizer.d.ts.map +1 -0
- package/dist/normalizers/SMTP2GONormalizer.js +55 -0
- package/dist/normalizers/SMTP2GONormalizer.js.map +1 -0
- package/dist/normalizers/SendGridNormalizer.d.ts +9 -0
- package/dist/normalizers/SendGridNormalizer.d.ts.map +1 -0
- package/dist/normalizers/SendGridNormalizer.js +64 -0
- package/dist/normalizers/SendGridNormalizer.js.map +1 -0
- package/dist/persistence/InMemoryPersistenceAdapter.d.ts +12 -0
- package/dist/persistence/InMemoryPersistenceAdapter.d.ts.map +1 -0
- package/dist/persistence/InMemoryPersistenceAdapter.js +58 -0
- package/dist/persistence/InMemoryPersistenceAdapter.js.map +1 -0
- package/dist/persistence/PersistenceAdapter.d.ts +7 -0
- package/dist/persistence/PersistenceAdapter.d.ts.map +1 -0
- package/dist/persistence/PersistenceAdapter.js +10 -0
- package/dist/persistence/PersistenceAdapter.js.map +1 -0
- package/dist/types.d.ts +178 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +41 -0
- package/dist/types.js.map +1 -0
- package/jest.config.cjs +29 -0
- package/package.json +51 -0
- package/src/EmailWebhookEventsService.ts +247 -0
- package/src/analytics/AnalyticsEngine.ts +230 -0
- package/src/index.ts +39 -0
- package/src/normalizers/EventNormalizer.ts +13 -0
- package/src/normalizers/MailgunNormalizer.ts +72 -0
- package/src/normalizers/PostmarkNormalizer.ts +72 -0
- package/src/normalizers/SESNormalizer.ts +117 -0
- package/src/normalizers/SMTP2GONormalizer.ts +54 -0
- package/src/normalizers/SendGridNormalizer.ts +63 -0
- package/src/persistence/InMemoryPersistenceAdapter.ts +68 -0
- package/src/persistence/PersistenceAdapter.ts +15 -0
- package/src/types.ts +246 -0
- 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
|
+
}
|