@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
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
export interface EmailEvent {
|
|
2
|
+
id: string;
|
|
3
|
+
emailId: string;
|
|
4
|
+
messageId: string;
|
|
5
|
+
type: EmailEventType;
|
|
6
|
+
recipient: string;
|
|
7
|
+
timestamp: Date;
|
|
8
|
+
provider: EmailProvider;
|
|
9
|
+
metadata: EmailEventMetadata;
|
|
10
|
+
rawEvent?: unknown;
|
|
11
|
+
}
|
|
12
|
+
export declare enum EmailEventType {
|
|
13
|
+
DELIVERED = "delivered",
|
|
14
|
+
OPENED = "opened",
|
|
15
|
+
CLICKED = "clicked",
|
|
16
|
+
BOUNCED = "bounced",
|
|
17
|
+
COMPLAINED = "complained",
|
|
18
|
+
UNSUBSCRIBED = "unsubscribed",
|
|
19
|
+
FAILED = "failed",
|
|
20
|
+
DEFERRED = "deferred",
|
|
21
|
+
DROPPED = "dropped"
|
|
22
|
+
}
|
|
23
|
+
export declare enum EmailProvider {
|
|
24
|
+
SENDGRID = "sendgrid",
|
|
25
|
+
MAILGUN = "mailgun",
|
|
26
|
+
AWS_SES = "aws_ses",
|
|
27
|
+
POSTMARK = "postmark",
|
|
28
|
+
SMTP2GO = "smtp2go",
|
|
29
|
+
GENERIC_SMTP = "generic_smtp"
|
|
30
|
+
}
|
|
31
|
+
export interface EmailEventMetadata {
|
|
32
|
+
url?: string;
|
|
33
|
+
linkId?: string;
|
|
34
|
+
bounceType?: 'hard' | 'soft';
|
|
35
|
+
bounceReason?: string;
|
|
36
|
+
bounceCode?: string;
|
|
37
|
+
complaintFeedbackType?: string;
|
|
38
|
+
failureReason?: string;
|
|
39
|
+
smtpResponse?: string;
|
|
40
|
+
deferralReason?: string;
|
|
41
|
+
attemptNumber?: number;
|
|
42
|
+
nextAttempt?: Date;
|
|
43
|
+
userAgent?: string;
|
|
44
|
+
ipAddress?: string;
|
|
45
|
+
deviceType?: 'desktop' | 'mobile' | 'tablet' | 'unknown';
|
|
46
|
+
location?: {
|
|
47
|
+
country?: string;
|
|
48
|
+
region?: string;
|
|
49
|
+
city?: string;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export interface EmailAnalytics {
|
|
53
|
+
startDate: Date;
|
|
54
|
+
endDate: Date;
|
|
55
|
+
totalSent: number;
|
|
56
|
+
totalDelivered: number;
|
|
57
|
+
totalOpened: number;
|
|
58
|
+
totalClicked: number;
|
|
59
|
+
totalBounced: number;
|
|
60
|
+
totalComplained: number;
|
|
61
|
+
totalUnsubscribed: number;
|
|
62
|
+
totalFailed: number;
|
|
63
|
+
deliveryRate: number;
|
|
64
|
+
openRate: number;
|
|
65
|
+
clickRate: number;
|
|
66
|
+
clickToOpenRate: number;
|
|
67
|
+
bounceRate: number;
|
|
68
|
+
complaintRate: number;
|
|
69
|
+
unsubscribeRate: number;
|
|
70
|
+
byProvider: Record<EmailProvider, ProviderAnalytics>;
|
|
71
|
+
byEventType: Record<EmailEventType, number>;
|
|
72
|
+
byBounceType?: {
|
|
73
|
+
hard: number;
|
|
74
|
+
soft: number;
|
|
75
|
+
};
|
|
76
|
+
topLinks?: Array<{
|
|
77
|
+
url: string;
|
|
78
|
+
clicks: number;
|
|
79
|
+
uniqueClicks: number;
|
|
80
|
+
}>;
|
|
81
|
+
byDevice?: {
|
|
82
|
+
desktop: number;
|
|
83
|
+
mobile: number;
|
|
84
|
+
tablet: number;
|
|
85
|
+
unknown: number;
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
export interface ProviderAnalytics {
|
|
89
|
+
sent: number;
|
|
90
|
+
delivered: number;
|
|
91
|
+
opened: number;
|
|
92
|
+
clicked: number;
|
|
93
|
+
bounced: number;
|
|
94
|
+
complained: number;
|
|
95
|
+
failed: number;
|
|
96
|
+
deliveryRate: number;
|
|
97
|
+
openRate: number;
|
|
98
|
+
clickRate: number;
|
|
99
|
+
}
|
|
100
|
+
export interface EmailWebhookEventsConfig {
|
|
101
|
+
providers: ProviderWebhookConfig[];
|
|
102
|
+
persistence?: {
|
|
103
|
+
enabled: boolean;
|
|
104
|
+
adapter: 'supabase' | 'postgresql' | 'mongodb' | 'memory';
|
|
105
|
+
config?: unknown;
|
|
106
|
+
};
|
|
107
|
+
analytics?: {
|
|
108
|
+
realTimeEnabled: boolean;
|
|
109
|
+
aggregationInterval: number;
|
|
110
|
+
retentionDays: number;
|
|
111
|
+
};
|
|
112
|
+
notifications?: {
|
|
113
|
+
enabled: boolean;
|
|
114
|
+
criticalEvents: EmailEventType[];
|
|
115
|
+
channels: NotificationChannel[];
|
|
116
|
+
};
|
|
117
|
+
rateLimiting?: {
|
|
118
|
+
enabled: boolean;
|
|
119
|
+
maxEventsPerSecond: number;
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
export interface ProviderWebhookConfig {
|
|
123
|
+
provider: EmailProvider;
|
|
124
|
+
enabled: boolean;
|
|
125
|
+
webhookUrl: string;
|
|
126
|
+
secretKey: string;
|
|
127
|
+
signatureHeader: string;
|
|
128
|
+
signatureAlgorithm: 'sha256' | 'sha1' | 'sha512';
|
|
129
|
+
}
|
|
130
|
+
export interface NotificationChannel {
|
|
131
|
+
type: 'neverhub' | 'webhook' | 'email';
|
|
132
|
+
config: {
|
|
133
|
+
url?: string;
|
|
134
|
+
recipients?: string[];
|
|
135
|
+
[key: string]: unknown;
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
export interface EmailWebhookResult {
|
|
139
|
+
success: boolean;
|
|
140
|
+
event?: EmailEvent;
|
|
141
|
+
error?: string;
|
|
142
|
+
}
|
|
143
|
+
export interface WebhookPayload {
|
|
144
|
+
provider: EmailProvider;
|
|
145
|
+
signature: string;
|
|
146
|
+
payload: unknown;
|
|
147
|
+
headers: Record<string, string>;
|
|
148
|
+
}
|
|
149
|
+
export interface AnalyticsFilters {
|
|
150
|
+
provider?: EmailProvider;
|
|
151
|
+
emailId?: string;
|
|
152
|
+
recipient?: string;
|
|
153
|
+
eventTypes?: EmailEventType[];
|
|
154
|
+
}
|
|
155
|
+
export interface EventQuery {
|
|
156
|
+
emailId?: string;
|
|
157
|
+
recipient?: string;
|
|
158
|
+
eventTypes?: EmailEventType[];
|
|
159
|
+
startDate?: Date;
|
|
160
|
+
endDate?: Date;
|
|
161
|
+
limit?: number;
|
|
162
|
+
offset?: number;
|
|
163
|
+
}
|
|
164
|
+
export interface EmailWebhookError {
|
|
165
|
+
code: EmailWebhookErrorCode;
|
|
166
|
+
message: string;
|
|
167
|
+
details?: unknown;
|
|
168
|
+
retryable: boolean;
|
|
169
|
+
}
|
|
170
|
+
export declare enum EmailWebhookErrorCode {
|
|
171
|
+
INVALID_SIGNATURE = "INVALID_SIGNATURE",
|
|
172
|
+
UNSUPPORTED_PROVIDER = "UNSUPPORTED_PROVIDER",
|
|
173
|
+
NORMALIZATION_FAILED = "NORMALIZATION_FAILED",
|
|
174
|
+
PERSISTENCE_FAILED = "PERSISTENCE_FAILED",
|
|
175
|
+
ANALYTICS_UPDATE_FAILED = "ANALYTICS_UPDATE_FAILED",
|
|
176
|
+
NOTIFICATION_FAILED = "NOTIFICATION_FAILED"
|
|
177
|
+
}
|
|
178
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
Copyright (c) 2025 Bernier LLC
|
|
4
|
+
|
|
5
|
+
This file is licensed to the client under a limited-use license.
|
|
6
|
+
The client may use and modify this code *only within the scope of the project it was delivered for*.
|
|
7
|
+
Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.EmailWebhookErrorCode = exports.EmailProvider = exports.EmailEventType = void 0;
|
|
11
|
+
var EmailEventType;
|
|
12
|
+
(function (EmailEventType) {
|
|
13
|
+
EmailEventType["DELIVERED"] = "delivered";
|
|
14
|
+
EmailEventType["OPENED"] = "opened";
|
|
15
|
+
EmailEventType["CLICKED"] = "clicked";
|
|
16
|
+
EmailEventType["BOUNCED"] = "bounced";
|
|
17
|
+
EmailEventType["COMPLAINED"] = "complained";
|
|
18
|
+
EmailEventType["UNSUBSCRIBED"] = "unsubscribed";
|
|
19
|
+
EmailEventType["FAILED"] = "failed";
|
|
20
|
+
EmailEventType["DEFERRED"] = "deferred";
|
|
21
|
+
EmailEventType["DROPPED"] = "dropped";
|
|
22
|
+
})(EmailEventType || (exports.EmailEventType = EmailEventType = {}));
|
|
23
|
+
var EmailProvider;
|
|
24
|
+
(function (EmailProvider) {
|
|
25
|
+
EmailProvider["SENDGRID"] = "sendgrid";
|
|
26
|
+
EmailProvider["MAILGUN"] = "mailgun";
|
|
27
|
+
EmailProvider["AWS_SES"] = "aws_ses";
|
|
28
|
+
EmailProvider["POSTMARK"] = "postmark";
|
|
29
|
+
EmailProvider["SMTP2GO"] = "smtp2go";
|
|
30
|
+
EmailProvider["GENERIC_SMTP"] = "generic_smtp";
|
|
31
|
+
})(EmailProvider || (exports.EmailProvider = EmailProvider = {}));
|
|
32
|
+
var EmailWebhookErrorCode;
|
|
33
|
+
(function (EmailWebhookErrorCode) {
|
|
34
|
+
EmailWebhookErrorCode["INVALID_SIGNATURE"] = "INVALID_SIGNATURE";
|
|
35
|
+
EmailWebhookErrorCode["UNSUPPORTED_PROVIDER"] = "UNSUPPORTED_PROVIDER";
|
|
36
|
+
EmailWebhookErrorCode["NORMALIZATION_FAILED"] = "NORMALIZATION_FAILED";
|
|
37
|
+
EmailWebhookErrorCode["PERSISTENCE_FAILED"] = "PERSISTENCE_FAILED";
|
|
38
|
+
EmailWebhookErrorCode["ANALYTICS_UPDATE_FAILED"] = "ANALYTICS_UPDATE_FAILED";
|
|
39
|
+
EmailWebhookErrorCode["NOTIFICATION_FAILED"] = "NOTIFICATION_FAILED";
|
|
40
|
+
})(EmailWebhookErrorCode || (exports.EmailWebhookErrorCode = EmailWebhookErrorCode = {}));
|
|
41
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":";AAAA;;;;;;EAME;;;AA4BF,IAAY,cAUX;AAVD,WAAY,cAAc;IACxB,yCAAuB,CAAA;IACvB,mCAAiB,CAAA;IACjB,qCAAmB,CAAA;IACnB,qCAAmB,CAAA;IACnB,2CAAyB,CAAA;IACzB,+CAA6B,CAAA;IAC7B,mCAAiB,CAAA;IACjB,uCAAqB,CAAA;IACrB,qCAAmB,CAAA;AACrB,CAAC,EAVW,cAAc,8BAAd,cAAc,QAUzB;AAED,IAAY,aAOX;AAPD,WAAY,aAAa;IACvB,sCAAqB,CAAA;IACrB,oCAAmB,CAAA;IACnB,oCAAmB,CAAA;IACnB,sCAAqB,CAAA;IACrB,oCAAmB,CAAA;IACnB,8CAA6B,CAAA;AAC/B,CAAC,EAPW,aAAa,6BAAb,aAAa,QAOxB;AAyLD,IAAY,qBAOX;AAPD,WAAY,qBAAqB;IAC/B,gEAAuC,CAAA;IACvC,sEAA6C,CAAA;IAC7C,sEAA6C,CAAA;IAC7C,kEAAyC,CAAA;IACzC,4EAAmD,CAAA;IACnD,oEAA2C,CAAA;AAC7C,CAAC,EAPW,qBAAqB,qCAArB,qBAAqB,QAOhC"}
|
package/jest.config.cjs
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
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
|
+
module.exports = {
|
|
10
|
+
preset: 'ts-jest',
|
|
11
|
+
testEnvironment: '<rootDir>/../../../jest-environment-node-fixed.cjs',
|
|
12
|
+
roots: ['<rootDir>/__tests__'],
|
|
13
|
+
testMatch: ['**/__tests__/**/*.test.ts'],
|
|
14
|
+
collectCoverageFrom: [
|
|
15
|
+
'src/**/*.ts',
|
|
16
|
+
'!src/**/*.d.ts',
|
|
17
|
+
'!src/index.ts'
|
|
18
|
+
],
|
|
19
|
+
coverageThreshold: {
|
|
20
|
+
global: {
|
|
21
|
+
branches: 68, // TODO: Increase to 85% - current: 68.35%
|
|
22
|
+
functions: 85,
|
|
23
|
+
lines: 85,
|
|
24
|
+
statements: 85
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
coverageDirectory: 'coverage',
|
|
28
|
+
verbose: true
|
|
29
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bernierllc/email-webhook-events",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Email event tracking, normalization, and analytics service for processing webhooks from all email providers",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"email",
|
|
9
|
+
"webhooks",
|
|
10
|
+
"events",
|
|
11
|
+
"analytics",
|
|
12
|
+
"sendgrid",
|
|
13
|
+
"mailgun",
|
|
14
|
+
"aws-ses",
|
|
15
|
+
"postmark",
|
|
16
|
+
"tracking",
|
|
17
|
+
"normalization",
|
|
18
|
+
"service"
|
|
19
|
+
],
|
|
20
|
+
"author": "Bernier LLC",
|
|
21
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
22
|
+
"dependencies": {},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/jest": "^29.0.0",
|
|
25
|
+
"@types/node": "^20.0.0",
|
|
26
|
+
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
|
27
|
+
"@typescript-eslint/parser": "^6.0.0",
|
|
28
|
+
"eslint": "^8.0.0",
|
|
29
|
+
"jest": "^29.0.0",
|
|
30
|
+
"ts-jest": "^29.0.0",
|
|
31
|
+
"typescript": "^5.0.0"
|
|
32
|
+
},
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
},
|
|
36
|
+
"bernierllc": {
|
|
37
|
+
"integration": {
|
|
38
|
+
"neverhub": "required",
|
|
39
|
+
"neveradmin": "not-applicable",
|
|
40
|
+
"logger": "integrated"
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "tsc",
|
|
45
|
+
"test": "jest",
|
|
46
|
+
"test:run": "jest --watchAll=false",
|
|
47
|
+
"test:coverage": "jest --coverage --watchAll=false",
|
|
48
|
+
"lint": "eslint src --ext .ts",
|
|
49
|
+
"clean": "rm -rf dist"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,247 @@
|
|
|
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 {
|
|
10
|
+
EmailWebhookEventsConfig,
|
|
11
|
+
EmailWebhookResult,
|
|
12
|
+
WebhookPayload,
|
|
13
|
+
EmailEvent,
|
|
14
|
+
EmailAnalytics,
|
|
15
|
+
EmailEventType,
|
|
16
|
+
EmailProvider,
|
|
17
|
+
AnalyticsFilters,
|
|
18
|
+
EventQuery,
|
|
19
|
+
NotificationChannel
|
|
20
|
+
} from './types';
|
|
21
|
+
import { AnalyticsEngine } from './analytics/AnalyticsEngine';
|
|
22
|
+
import { PersistenceAdapter } from './persistence/PersistenceAdapter';
|
|
23
|
+
import { InMemoryPersistenceAdapter } from './persistence/InMemoryPersistenceAdapter';
|
|
24
|
+
import { EventNormalizer } from './normalizers/EventNormalizer';
|
|
25
|
+
import { SendGridNormalizer } from './normalizers/SendGridNormalizer';
|
|
26
|
+
import { MailgunNormalizer } from './normalizers/MailgunNormalizer';
|
|
27
|
+
import { SESNormalizer } from './normalizers/SESNormalizer';
|
|
28
|
+
import { PostmarkNormalizer } from './normalizers/PostmarkNormalizer';
|
|
29
|
+
import { SMTP2GONormalizer } from './normalizers/SMTP2GONormalizer';
|
|
30
|
+
|
|
31
|
+
// Mock logger interface (since @bernierllc/logger is not published yet)
|
|
32
|
+
interface Logger {
|
|
33
|
+
info(message: string, meta?: Record<string, unknown>): void;
|
|
34
|
+
warn(message: string, meta?: Record<string, unknown>): void;
|
|
35
|
+
error(message: string, meta?: Record<string, unknown>): void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
class MockLogger implements Logger {
|
|
39
|
+
info(message: string, meta?: Record<string, unknown>): void {
|
|
40
|
+
console.log(`[INFO] ${message}`, meta || '');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
warn(message: string, meta?: Record<string, unknown>): void {
|
|
44
|
+
console.warn(`[WARN] ${message}`, meta || '');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
error(message: string, meta?: Record<string, unknown>): void {
|
|
48
|
+
console.error(`[ERROR] ${message}`, meta || '');
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export class EmailWebhookEventsService {
|
|
53
|
+
private logger: Logger;
|
|
54
|
+
private persistenceAdapter?: PersistenceAdapter;
|
|
55
|
+
private analyticsEngine: AnalyticsEngine;
|
|
56
|
+
|
|
57
|
+
constructor(private config: EmailWebhookEventsConfig) {
|
|
58
|
+
this.logger = new MockLogger();
|
|
59
|
+
this.analyticsEngine = new AnalyticsEngine(config.analytics);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Initialize the service
|
|
64
|
+
*/
|
|
65
|
+
async initialize(): Promise<void> {
|
|
66
|
+
// Initialize persistence
|
|
67
|
+
if (this.config.persistence?.enabled) {
|
|
68
|
+
this.persistenceAdapter = await this.createPersistenceAdapter();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
this.logger.info('Email webhook events service initialized');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Process incoming webhook
|
|
76
|
+
*/
|
|
77
|
+
async processWebhook(webhook: WebhookPayload): Promise<EmailWebhookResult> {
|
|
78
|
+
try {
|
|
79
|
+
// Validate webhook signature would happen here
|
|
80
|
+
// For now, we'll assume validation passes
|
|
81
|
+
|
|
82
|
+
// Normalize event
|
|
83
|
+
const normalizedEvent = await this.normalizeEvent(webhook);
|
|
84
|
+
if (!normalizedEvent) {
|
|
85
|
+
this.logger.warn('Failed to normalize webhook event', {
|
|
86
|
+
provider: webhook.provider
|
|
87
|
+
});
|
|
88
|
+
return { success: false, error: 'Event normalization failed' };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Persist event
|
|
92
|
+
if (this.persistenceAdapter) {
|
|
93
|
+
await this.persistenceAdapter.saveEvent(normalizedEvent);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Update analytics
|
|
97
|
+
await this.analyticsEngine.recordEvent(normalizedEvent);
|
|
98
|
+
|
|
99
|
+
// Publish critical events
|
|
100
|
+
if (this.isCriticalEvent(normalizedEvent)) {
|
|
101
|
+
await this.publishCriticalEvent(normalizedEvent);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
this.logger.info('Email event processed successfully', {
|
|
105
|
+
eventId: normalizedEvent.id,
|
|
106
|
+
type: normalizedEvent.type,
|
|
107
|
+
recipient: normalizedEvent.recipient
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
return { success: true, event: normalizedEvent };
|
|
111
|
+
|
|
112
|
+
} catch (error) {
|
|
113
|
+
this.logger.error('Error processing email webhook', {
|
|
114
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
115
|
+
provider: webhook.provider
|
|
116
|
+
});
|
|
117
|
+
return {
|
|
118
|
+
success: false,
|
|
119
|
+
error: error instanceof Error ? error.message : 'Unknown error'
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Get analytics for date range
|
|
126
|
+
*/
|
|
127
|
+
async getAnalytics(
|
|
128
|
+
startDate: Date,
|
|
129
|
+
endDate: Date,
|
|
130
|
+
filters?: AnalyticsFilters
|
|
131
|
+
): Promise<EmailAnalytics> {
|
|
132
|
+
return this.analyticsEngine.getAnalytics(startDate, endDate, filters);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Query events
|
|
137
|
+
*/
|
|
138
|
+
async queryEvents(query: EventQuery): Promise<EmailEvent[]> {
|
|
139
|
+
if (!this.persistenceAdapter) {
|
|
140
|
+
throw new Error('Event persistence not enabled');
|
|
141
|
+
}
|
|
142
|
+
return this.persistenceAdapter.queryEvents(query);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Get events for specific email
|
|
147
|
+
*/
|
|
148
|
+
async getEventsForEmail(emailId: string): Promise<EmailEvent[]> {
|
|
149
|
+
return this.queryEvents({ emailId });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Normalize webhook event to unified format
|
|
154
|
+
*/
|
|
155
|
+
private async normalizeEvent(webhook: WebhookPayload): Promise<EmailEvent | null> {
|
|
156
|
+
const normalizer = this.getNormalizerForProvider(webhook.provider);
|
|
157
|
+
if (!normalizer) {
|
|
158
|
+
this.logger.error('No normalizer found for provider', {
|
|
159
|
+
provider: webhook.provider
|
|
160
|
+
});
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return normalizer.normalize(webhook);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Get normalizer for provider
|
|
169
|
+
*/
|
|
170
|
+
private getNormalizerForProvider(provider: EmailProvider): EventNormalizer | null {
|
|
171
|
+
switch (provider) {
|
|
172
|
+
case EmailProvider.SENDGRID:
|
|
173
|
+
return new SendGridNormalizer();
|
|
174
|
+
case EmailProvider.MAILGUN:
|
|
175
|
+
return new MailgunNormalizer();
|
|
176
|
+
case EmailProvider.AWS_SES:
|
|
177
|
+
return new SESNormalizer();
|
|
178
|
+
case EmailProvider.POSTMARK:
|
|
179
|
+
return new PostmarkNormalizer();
|
|
180
|
+
case EmailProvider.SMTP2GO:
|
|
181
|
+
return new SMTP2GONormalizer();
|
|
182
|
+
default:
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Check if event is critical (requires notification)
|
|
189
|
+
*/
|
|
190
|
+
private isCriticalEvent(event: EmailEvent): boolean {
|
|
191
|
+
const criticalTypes = this.config.notifications?.criticalEvents || [
|
|
192
|
+
EmailEventType.BOUNCED,
|
|
193
|
+
EmailEventType.COMPLAINED,
|
|
194
|
+
EmailEventType.FAILED
|
|
195
|
+
];
|
|
196
|
+
return criticalTypes.includes(event.type);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Publish critical event to notification channels
|
|
201
|
+
*/
|
|
202
|
+
private async publishCriticalEvent(event: EmailEvent): Promise<void> {
|
|
203
|
+
if (!this.config.notifications?.enabled) return;
|
|
204
|
+
|
|
205
|
+
for (const channel of this.config.notifications.channels) {
|
|
206
|
+
try {
|
|
207
|
+
await this.sendNotification(channel, event);
|
|
208
|
+
} catch (error) {
|
|
209
|
+
this.logger.error('Failed to send notification', {
|
|
210
|
+
channel: channel.type,
|
|
211
|
+
error: error instanceof Error ? error.message : 'Unknown error'
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Send notification via channel
|
|
219
|
+
*/
|
|
220
|
+
private async sendNotification(
|
|
221
|
+
channel: NotificationChannel,
|
|
222
|
+
event: EmailEvent
|
|
223
|
+
): Promise<void> {
|
|
224
|
+
// In a real implementation, this would send notifications
|
|
225
|
+
// For now, just log
|
|
226
|
+
this.logger.info('Critical event notification', {
|
|
227
|
+
channel: channel.type,
|
|
228
|
+
eventType: event.type,
|
|
229
|
+
recipient: event.recipient
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Create persistence adapter
|
|
235
|
+
*/
|
|
236
|
+
private async createPersistenceAdapter(): Promise<PersistenceAdapter> {
|
|
237
|
+
const adapter = this.config.persistence?.adapter || 'memory';
|
|
238
|
+
|
|
239
|
+
switch (adapter) {
|
|
240
|
+
case 'memory':
|
|
241
|
+
return new InMemoryPersistenceAdapter();
|
|
242
|
+
// Other adapters would be implemented here
|
|
243
|
+
default:
|
|
244
|
+
throw new Error(`Unsupported persistence adapter: ${adapter}`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|