@bernierllc/email-sendgrid-plugin 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.js +27 -0
- package/README.md +432 -0
- package/__tests__/api/rate-limiter.test.ts +125 -0
- package/__tests__/converters/sender-converter.test.ts +148 -0
- package/__tests__/converters/template-converter.test.ts +230 -0
- package/__tests__/plugin/sender-manager.test.ts +135 -0
- package/__tests__/plugin/sendgrid-plugin.test.ts +195 -0
- package/__tests__/plugin/template-manager.test.ts +110 -0
- package/__tests__/safety/ownership-validator.test.ts +130 -0
- package/__tests__/safety/prefix-enforcer.test.ts +136 -0
- package/dist/api/errors.d.ts +20 -0
- package/dist/api/errors.js +50 -0
- package/dist/api/rate-limiter.d.ts +17 -0
- package/dist/api/rate-limiter.js +58 -0
- package/dist/api/sendgrid-client.d.ts +21 -0
- package/dist/api/sendgrid-client.js +67 -0
- package/dist/converters/sender-converter.d.ts +6 -0
- package/dist/converters/sender-converter.js +40 -0
- package/dist/converters/template-converter.d.ts +11 -0
- package/dist/converters/template-converter.js +76 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +42 -0
- package/dist/plugin/sender-manager.d.ts +20 -0
- package/dist/plugin/sender-manager.js +124 -0
- package/dist/plugin/sendgrid-plugin.d.ts +60 -0
- package/dist/plugin/sendgrid-plugin.js +204 -0
- package/dist/plugin/sync-manager.d.ts +19 -0
- package/dist/plugin/sync-manager.js +94 -0
- package/dist/plugin/template-manager.d.ts +25 -0
- package/dist/plugin/template-manager.js +152 -0
- package/dist/safety/ownership-validator.d.ts +15 -0
- package/dist/safety/ownership-validator.js +42 -0
- package/dist/safety/prefix-enforcer.d.ts +8 -0
- package/dist/safety/prefix-enforcer.js +34 -0
- package/dist/types/config-types.d.ts +24 -0
- package/dist/types/config-types.js +9 -0
- package/dist/types/plugin-types.d.ts +79 -0
- package/dist/types/plugin-types.js +9 -0
- package/dist/types/sendgrid-types.d.ts +56 -0
- package/dist/types/sendgrid-types.js +9 -0
- package/jest.config.js +25 -0
- package/package.json +42 -0
- package/src/api/errors.ts +49 -0
- package/src/api/rate-limiter.ts +79 -0
- package/src/api/sendgrid-client.ts +86 -0
- package/src/converters/sender-converter.ts +41 -0
- package/src/converters/template-converter.ts +97 -0
- package/src/index.ts +58 -0
- package/src/plugin/sender-manager.ts +166 -0
- package/src/plugin/sendgrid-plugin.ts +316 -0
- package/src/plugin/sync-manager.ts +115 -0
- package/src/plugin/template-manager.ts +240 -0
- package/src/safety/ownership-validator.ts +55 -0
- package/src/safety/prefix-enforcer.ts +37 -0
- package/src/types/config-types.ts +34 -0
- package/src/types/plugin-types.ts +97 -0
- package/src/types/sendgrid-types.ts +69 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,166 @@
|
|
|
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 { SendGridClient } from '../api/sendgrid-client';
|
|
10
|
+
import { SendGridApiError } from '../api/errors';
|
|
11
|
+
import { SenderConverter } from '../converters/sender-converter';
|
|
12
|
+
import {
|
|
13
|
+
VerifiedSender,
|
|
14
|
+
SenderValidationResult,
|
|
15
|
+
SendGridVerifiedSenderData
|
|
16
|
+
} from '../types/sendgrid-types';
|
|
17
|
+
|
|
18
|
+
export class SenderManager {
|
|
19
|
+
private sendersCache?: VerifiedSender[];
|
|
20
|
+
private cacheTimestamp?: number;
|
|
21
|
+
private readonly cacheTTL = 300000; // 5 minutes
|
|
22
|
+
|
|
23
|
+
constructor(
|
|
24
|
+
private readonly client: SendGridClient,
|
|
25
|
+
private readonly converter: SenderConverter,
|
|
26
|
+
private readonly logger?: {
|
|
27
|
+
info: (message: string) => void;
|
|
28
|
+
debug: (message: string) => void;
|
|
29
|
+
}
|
|
30
|
+
) {}
|
|
31
|
+
|
|
32
|
+
async getVerifiedSenders(useCache = true): Promise<VerifiedSender[]> {
|
|
33
|
+
// Return cached data if valid
|
|
34
|
+
if (
|
|
35
|
+
useCache &&
|
|
36
|
+
this.sendersCache &&
|
|
37
|
+
this.cacheTimestamp &&
|
|
38
|
+
Date.now() - this.cacheTimestamp < this.cacheTTL
|
|
39
|
+
) {
|
|
40
|
+
this.logger?.debug('Returning cached verified senders');
|
|
41
|
+
return this.sendersCache;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const response = await this.client.request('/verified_senders', {
|
|
45
|
+
method: 'GET'
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
if (!this.client.isSuccessResponse(response)) {
|
|
49
|
+
throw new SendGridApiError(
|
|
50
|
+
'Failed to fetch verified senders',
|
|
51
|
+
response.statusCode,
|
|
52
|
+
response.body
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const data = this.client.parseResponseBody<{ results: SendGridVerifiedSenderData[] }>(response);
|
|
57
|
+
const senders = data.results.map(sender => this.converter.convertToVerifiedSender(sender));
|
|
58
|
+
|
|
59
|
+
// Update cache
|
|
60
|
+
this.sendersCache = senders;
|
|
61
|
+
this.cacheTimestamp = Date.now();
|
|
62
|
+
|
|
63
|
+
this.logger?.info(`Fetched ${senders.length} verified senders from SendGrid`);
|
|
64
|
+
|
|
65
|
+
return senders;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async validateSender(email: string): Promise<SenderValidationResult> {
|
|
69
|
+
const verifiedSenders = await this.getVerifiedSenders();
|
|
70
|
+
const emailDomain = this.converter.extractDomain(email);
|
|
71
|
+
|
|
72
|
+
// Check for exact match
|
|
73
|
+
const exactMatch = verifiedSenders.find(
|
|
74
|
+
sender => sender.email.toLowerCase() === email.toLowerCase() && sender.isVerified
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
if (exactMatch) {
|
|
78
|
+
return {
|
|
79
|
+
isValid: true,
|
|
80
|
+
verifiedSender: exactMatch,
|
|
81
|
+
domain: emailDomain,
|
|
82
|
+
domainMatches: [exactMatch],
|
|
83
|
+
errors: [],
|
|
84
|
+
suggestions: []
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Check for domain matches
|
|
89
|
+
const domainMatches = verifiedSenders.filter(sender => {
|
|
90
|
+
const senderDomain = this.converter.extractDomain(sender.email);
|
|
91
|
+
return this.converter.isDomainMatch(emailDomain, senderDomain) && sender.isVerified;
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
if (domainMatches.length > 0) {
|
|
95
|
+
return {
|
|
96
|
+
isValid: true,
|
|
97
|
+
domain: emailDomain,
|
|
98
|
+
domainMatches,
|
|
99
|
+
errors: [],
|
|
100
|
+
suggestions: domainMatches.map(sender => sender.email)
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// No matches found
|
|
105
|
+
return {
|
|
106
|
+
isValid: false,
|
|
107
|
+
domain: emailDomain,
|
|
108
|
+
domainMatches: [],
|
|
109
|
+
errors: [`No verified sender found for email ${email} or domain ${emailDomain}`],
|
|
110
|
+
suggestions: this.getSenderSuggestions(email, verifiedSenders)
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async selectBestSender(fromEmail: string): Promise<VerifiedSender | null> {
|
|
115
|
+
const validation = await this.validateSender(fromEmail);
|
|
116
|
+
|
|
117
|
+
if (!validation.isValid || validation.domainMatches.length === 0) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Prefer exact match
|
|
122
|
+
if (validation.verifiedSender) {
|
|
123
|
+
return validation.verifiedSender;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Fall back to best domain match
|
|
127
|
+
// Priority: most recently verified, then alphabetical
|
|
128
|
+
return validation.domainMatches.sort((a, b) => {
|
|
129
|
+
if (a.lastVerifiedAt && b.lastVerifiedAt) {
|
|
130
|
+
return b.lastVerifiedAt.getTime() - a.lastVerifiedAt.getTime();
|
|
131
|
+
}
|
|
132
|
+
if (a.lastVerifiedAt && !b.lastVerifiedAt) return -1;
|
|
133
|
+
if (!a.lastVerifiedAt && b.lastVerifiedAt) return 1;
|
|
134
|
+
return a.email.localeCompare(b.email);
|
|
135
|
+
})[0];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
clearCache(): void {
|
|
139
|
+
this.sendersCache = undefined;
|
|
140
|
+
this.cacheTimestamp = undefined;
|
|
141
|
+
this.logger?.debug('Cleared verified senders cache');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private getSenderSuggestions(email: string, senders: VerifiedSender[]): string[] {
|
|
145
|
+
const emailDomain = this.converter.extractDomain(email);
|
|
146
|
+
|
|
147
|
+
// Find senders with similar domains
|
|
148
|
+
const suggestions: string[] = [];
|
|
149
|
+
|
|
150
|
+
for (const sender of senders) {
|
|
151
|
+
if (!sender.isVerified) continue;
|
|
152
|
+
|
|
153
|
+
const senderDomain = this.converter.extractDomain(sender.email);
|
|
154
|
+
|
|
155
|
+
// Check for partial domain matches
|
|
156
|
+
if (
|
|
157
|
+
emailDomain.includes(senderDomain) ||
|
|
158
|
+
senderDomain.includes(emailDomain)
|
|
159
|
+
) {
|
|
160
|
+
suggestions.push(sender.email);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return suggestions.slice(0, 5); // Limit to 5 suggestions
|
|
165
|
+
}
|
|
166
|
+
}
|
|
@@ -0,0 +1,316 @@
|
|
|
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 { SendGridPluginConfig } from '../types/config-types';
|
|
10
|
+
import {
|
|
11
|
+
PluginConfig,
|
|
12
|
+
ProviderCapability,
|
|
13
|
+
Template,
|
|
14
|
+
ProviderTemplate,
|
|
15
|
+
TemplateVersion,
|
|
16
|
+
SyncResult,
|
|
17
|
+
ValidationResult,
|
|
18
|
+
DateRange,
|
|
19
|
+
TemplateAnalytics,
|
|
20
|
+
FeatureConfig
|
|
21
|
+
} from '../types/plugin-types';
|
|
22
|
+
import { VerifiedSender, SenderValidationResult } from '../types/sendgrid-types';
|
|
23
|
+
import { SendGridClient } from '../api/sendgrid-client';
|
|
24
|
+
import { SimpleRateLimiter } from '../api/rate-limiter';
|
|
25
|
+
import { OwnershipValidator } from '../safety/ownership-validator';
|
|
26
|
+
import { PrefixEnforcer } from '../safety/prefix-enforcer';
|
|
27
|
+
import { TemplateConverter } from '../converters/template-converter';
|
|
28
|
+
import { SenderConverter } from '../converters/sender-converter';
|
|
29
|
+
import { TemplateManager } from './template-manager';
|
|
30
|
+
import { SenderManager } from './sender-manager';
|
|
31
|
+
import { SyncManager } from './sync-manager';
|
|
32
|
+
|
|
33
|
+
export interface TemplateProviderPlugin {
|
|
34
|
+
readonly name: string;
|
|
35
|
+
readonly version: string;
|
|
36
|
+
readonly providerId: string;
|
|
37
|
+
|
|
38
|
+
initialize(config: PluginConfig): Promise<void>;
|
|
39
|
+
destroy(): Promise<void>;
|
|
40
|
+
|
|
41
|
+
// Template operations
|
|
42
|
+
createTemplate(template: Template): Promise<string>;
|
|
43
|
+
updateTemplate(providerTemplateId: string, template: Template): Promise<void>;
|
|
44
|
+
deleteTemplate(providerTemplateId: string): Promise<void>;
|
|
45
|
+
getTemplate(providerTemplateId: string): Promise<ProviderTemplate>;
|
|
46
|
+
listTemplates(): Promise<ProviderTemplate[]>;
|
|
47
|
+
|
|
48
|
+
// Synchronization
|
|
49
|
+
syncToProvider(template: Template): Promise<SyncResult>;
|
|
50
|
+
syncFromProvider(providerTemplateId: string): Promise<Template>;
|
|
51
|
+
|
|
52
|
+
// Capabilities
|
|
53
|
+
getCapabilities(): ProviderCapability[];
|
|
54
|
+
supportsFeature(feature: string): boolean;
|
|
55
|
+
getFeatureConfig(feature: string): FeatureConfig | null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export class SendGridTemplatePlugin implements TemplateProviderPlugin {
|
|
59
|
+
readonly name = 'sendgrid-templates';
|
|
60
|
+
readonly version = '1.0.0';
|
|
61
|
+
readonly providerId = 'sendgrid';
|
|
62
|
+
|
|
63
|
+
private client!: SendGridClient;
|
|
64
|
+
private rateLimiter?: SimpleRateLimiter;
|
|
65
|
+
private ownershipValidator!: OwnershipValidator;
|
|
66
|
+
private prefixEnforcer!: PrefixEnforcer;
|
|
67
|
+
private templateConverter!: TemplateConverter;
|
|
68
|
+
private senderConverter!: SenderConverter;
|
|
69
|
+
private templateManager!: TemplateManager;
|
|
70
|
+
private senderManager!: SenderManager;
|
|
71
|
+
private syncManager!: SyncManager;
|
|
72
|
+
|
|
73
|
+
private readonly config: SendGridPluginConfig;
|
|
74
|
+
private readonly logger?: {
|
|
75
|
+
info: (message: string) => void;
|
|
76
|
+
debug: (message: string) => void;
|
|
77
|
+
error: (message: string) => void;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
private initialized = false;
|
|
81
|
+
|
|
82
|
+
constructor(config: SendGridPluginConfig) {
|
|
83
|
+
this.config = config;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async initialize(__pluginConfig: PluginConfig): Promise<void> {
|
|
87
|
+
if (this.initialized) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Initialize rate limiter if configured
|
|
92
|
+
if (this.config.rateLimits) {
|
|
93
|
+
this.rateLimiter = new SimpleRateLimiter({
|
|
94
|
+
maxRequestsPerSecond: this.config.rateLimits.apiCallsPerSecond,
|
|
95
|
+
maxRequestsPerMinute: this.config.rateLimits.templatesPerMinute
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Initialize SendGrid client
|
|
100
|
+
this.client = new SendGridClient({
|
|
101
|
+
apiKey: this.config.apiKey,
|
|
102
|
+
rateLimiter: this.rateLimiter,
|
|
103
|
+
logger: this.logger
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Initialize safety components
|
|
107
|
+
this.ownershipValidator = new OwnershipValidator(
|
|
108
|
+
this.config.safetyMode,
|
|
109
|
+
this.logger
|
|
110
|
+
);
|
|
111
|
+
this.prefixEnforcer = new PrefixEnforcer(this.config.templatePrefix);
|
|
112
|
+
|
|
113
|
+
// Initialize converters
|
|
114
|
+
this.templateConverter = new TemplateConverter(this.prefixEnforcer);
|
|
115
|
+
this.senderConverter = new SenderConverter();
|
|
116
|
+
|
|
117
|
+
// Initialize managers
|
|
118
|
+
this.templateManager = new TemplateManager(
|
|
119
|
+
this.client,
|
|
120
|
+
this.ownershipValidator,
|
|
121
|
+
this.prefixEnforcer,
|
|
122
|
+
this.templateConverter,
|
|
123
|
+
this.logger
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
this.senderManager = new SenderManager(
|
|
127
|
+
this.client,
|
|
128
|
+
this.senderConverter,
|
|
129
|
+
this.logger
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
this.syncManager = new SyncManager(
|
|
133
|
+
this.templateManager,
|
|
134
|
+
this.templateConverter,
|
|
135
|
+
this.client,
|
|
136
|
+
this.logger
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
// Load existing templates
|
|
140
|
+
await this.loadExistingTemplates();
|
|
141
|
+
|
|
142
|
+
this.initialized = true;
|
|
143
|
+
this.logger?.info('SendGrid plugin initialized successfully');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async destroy(): Promise<void> {
|
|
147
|
+
if (!this.initialized) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
this.ownershipValidator.clearRegistrations();
|
|
152
|
+
this.senderManager.clearCache();
|
|
153
|
+
|
|
154
|
+
this.initialized = false;
|
|
155
|
+
this.logger?.info('SendGrid plugin destroyed');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Template operations
|
|
159
|
+
async createTemplate(template: Template): Promise<string> {
|
|
160
|
+
this.ensureInitialized();
|
|
161
|
+
return this.templateManager.createTemplate(template);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async updateTemplate(providerTemplateId: string, template: Template): Promise<void> {
|
|
165
|
+
this.ensureInitialized();
|
|
166
|
+
return this.templateManager.updateTemplate(providerTemplateId, template);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async deleteTemplate(providerTemplateId: string): Promise<void> {
|
|
170
|
+
this.ensureInitialized();
|
|
171
|
+
return this.templateManager.deleteTemplate(providerTemplateId);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async getTemplate(providerTemplateId: string): Promise<ProviderTemplate> {
|
|
175
|
+
this.ensureInitialized();
|
|
176
|
+
return this.templateManager.getTemplate(providerTemplateId);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async listTemplates(): Promise<ProviderTemplate[]> {
|
|
180
|
+
this.ensureInitialized();
|
|
181
|
+
return this.templateManager.listTemplates();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Synchronization
|
|
185
|
+
async syncToProvider(template: Template): Promise<SyncResult> {
|
|
186
|
+
this.ensureInitialized();
|
|
187
|
+
return this.syncManager.syncToProvider(template);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async syncFromProvider(providerTemplateId: string): Promise<Template> {
|
|
191
|
+
this.ensureInitialized();
|
|
192
|
+
return this.syncManager.syncFromProvider(providerTemplateId);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// SendGrid-specific features
|
|
196
|
+
async getVerifiedSenders(): Promise<VerifiedSender[]> {
|
|
197
|
+
this.ensureInitialized();
|
|
198
|
+
return this.senderManager.getVerifiedSenders();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async validateSender(email: string): Promise<SenderValidationResult> {
|
|
202
|
+
this.ensureInitialized();
|
|
203
|
+
return this.senderManager.validateSender(email);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async selectBestSender(email: string): Promise<VerifiedSender | null> {
|
|
207
|
+
this.ensureInitialized();
|
|
208
|
+
return this.senderManager.selectBestSender(email);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async createTemplateVersion(
|
|
212
|
+
templateId: string,
|
|
213
|
+
version: TemplateVersion
|
|
214
|
+
): Promise<string> {
|
|
215
|
+
this.ensureInitialized();
|
|
216
|
+
return this.templateManager.createTemplateVersion(templateId, version);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async activateTemplateVersion(templateId: string, versionId: string): Promise<void> {
|
|
220
|
+
this.ensureInitialized();
|
|
221
|
+
return this.templateManager.activateTemplateVersion(templateId, versionId);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async getTemplateAnalytics(
|
|
225
|
+
__templateId: string,
|
|
226
|
+
__dateRange: DateRange
|
|
227
|
+
): Promise<TemplateAnalytics> {
|
|
228
|
+
this.ensureInitialized();
|
|
229
|
+
// TODO: Implement analytics fetching from SendGrid
|
|
230
|
+
throw new Error('Template analytics not yet implemented');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async batchSyncTemplates(templates: Template[]): Promise<SyncResult[]> {
|
|
234
|
+
this.ensureInitialized();
|
|
235
|
+
return this.syncManager.batchSync(templates);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Capabilities
|
|
239
|
+
getCapabilities(): ProviderCapability[] {
|
|
240
|
+
return [
|
|
241
|
+
{
|
|
242
|
+
feature: 'template-management',
|
|
243
|
+
description: 'Full CRUD operations for SendGrid templates',
|
|
244
|
+
methods: ['createTemplate', 'updateTemplate', 'deleteTemplate', 'getTemplate', 'listTemplates'],
|
|
245
|
+
config: {
|
|
246
|
+
supportsVersioning: true,
|
|
247
|
+
supportsActivation: true,
|
|
248
|
+
maxNameLength: 100,
|
|
249
|
+
templatePrefix: this.config.templatePrefix
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
feature: 'sender-verification',
|
|
254
|
+
description: 'SendGrid verified sender management',
|
|
255
|
+
methods: ['getVerifiedSenders', 'validateSender', 'selectBestSender'],
|
|
256
|
+
config: {
|
|
257
|
+
supportsDomainMatching: true,
|
|
258
|
+
supportsAutoSelection: true
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
feature: 'template-analytics',
|
|
263
|
+
description: 'SendGrid template usage and performance analytics',
|
|
264
|
+
methods: ['getTemplateAnalytics'],
|
|
265
|
+
config: {
|
|
266
|
+
supportsRealTime: false,
|
|
267
|
+
historicalDataDays: 30,
|
|
268
|
+
metricsAvailable: ['opens', 'clicks', 'bounces', 'deliveries']
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
feature: 'batch-operations',
|
|
273
|
+
description: 'Efficient bulk template operations',
|
|
274
|
+
methods: ['batchSyncTemplates'],
|
|
275
|
+
config: {
|
|
276
|
+
maxBatchSize: 50,
|
|
277
|
+
supportsTransactions: false
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
];
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
supportsFeature(feature: string): boolean {
|
|
284
|
+
return this.getCapabilities().some(cap => cap.feature === feature);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
getFeatureConfig(feature: string): FeatureConfig | null {
|
|
288
|
+
const capability = this.getCapabilities().find(cap => cap.feature === feature);
|
|
289
|
+
return capability?.config || null;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
validateRoute(__route: unknown): ValidationResult {
|
|
293
|
+
// Placeholder validation
|
|
294
|
+
return {
|
|
295
|
+
isValid: true,
|
|
296
|
+
errors: []
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Private methods
|
|
301
|
+
private async loadExistingTemplates(): Promise<void> {
|
|
302
|
+
try {
|
|
303
|
+
const templates = await this.templateManager.listTemplates();
|
|
304
|
+
this.logger?.info(`Loaded ${templates.length} existing SendGrid templates`);
|
|
305
|
+
} catch (error) {
|
|
306
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
307
|
+
this.logger?.error(`Failed to load existing templates: ${errorMessage}`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
private ensureInitialized(): void {
|
|
312
|
+
if (!this.initialized) {
|
|
313
|
+
throw new Error('SendGrid plugin not initialized. Call initialize() first.');
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
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 { TemplateManager } from './template-manager';
|
|
10
|
+
import { TemplateConverter } from '../converters/template-converter';
|
|
11
|
+
import { SendGridClient } from '../api/sendgrid-client';
|
|
12
|
+
import { Template, SyncResult } from '../types/plugin-types';
|
|
13
|
+
import { SendGridTemplateData } from '../types/sendgrid-types';
|
|
14
|
+
|
|
15
|
+
export class SyncManager {
|
|
16
|
+
constructor(
|
|
17
|
+
private readonly templateManager: TemplateManager,
|
|
18
|
+
private readonly converter: TemplateConverter,
|
|
19
|
+
private readonly client: SendGridClient,
|
|
20
|
+
private readonly logger?: {
|
|
21
|
+
info: (message: string) => void;
|
|
22
|
+
error: (message: string) => void;
|
|
23
|
+
}
|
|
24
|
+
) {}
|
|
25
|
+
|
|
26
|
+
async syncToProvider(template: Template): Promise<SyncResult> {
|
|
27
|
+
try {
|
|
28
|
+
const existingProviderId = template.providerTemplateIds?.sendgrid;
|
|
29
|
+
|
|
30
|
+
if (existingProviderId) {
|
|
31
|
+
// Update existing template
|
|
32
|
+
await this.templateManager.updateTemplate(existingProviderId, template);
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
success: true,
|
|
36
|
+
action: 'updated',
|
|
37
|
+
providerTemplateId: existingProviderId,
|
|
38
|
+
localTemplateId: template.id,
|
|
39
|
+
metadata: {
|
|
40
|
+
newVersion: template.version
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
} else {
|
|
44
|
+
// Create new template
|
|
45
|
+
const newProviderId = await this.templateManager.createTemplate(template);
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
success: true,
|
|
49
|
+
action: 'created',
|
|
50
|
+
providerTemplateId: newProviderId,
|
|
51
|
+
localTemplateId: template.id,
|
|
52
|
+
metadata: {
|
|
53
|
+
version: template.version
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
} catch (error) {
|
|
58
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
59
|
+
this.logger?.error(`Template sync failed: ${errorMessage}`);
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
success: false,
|
|
63
|
+
action: 'failed',
|
|
64
|
+
localTemplateId: template.id,
|
|
65
|
+
error: errorMessage,
|
|
66
|
+
metadata: {
|
|
67
|
+
errorType: error instanceof Error ? error.constructor.name : 'UnknownError',
|
|
68
|
+
timestamp: new Date()
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async syncFromProvider(providerTemplateId: string): Promise<Template> {
|
|
75
|
+
const response = await this.client.request(`/templates/${providerTemplateId}`, {
|
|
76
|
+
method: 'GET'
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
if (!this.client.isSuccessResponse(response)) {
|
|
80
|
+
throw new Error(`Failed to fetch template from SendGrid: ${response.statusCode}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const templateData = this.client.parseResponseBody<SendGridTemplateData>(response);
|
|
84
|
+
const localId = this.generateLocalId();
|
|
85
|
+
|
|
86
|
+
return this.converter.convertToLocalTemplate(templateData, localId);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async batchSync(templates: Template[]): Promise<SyncResult[]> {
|
|
90
|
+
this.logger?.info(`Starting batch sync of ${templates.length} templates`);
|
|
91
|
+
|
|
92
|
+
const results: SyncResult[] = [];
|
|
93
|
+
|
|
94
|
+
for (const template of templates) {
|
|
95
|
+
const result = await this.syncToProvider(template);
|
|
96
|
+
results.push(result);
|
|
97
|
+
|
|
98
|
+
// Add delay to respect rate limits
|
|
99
|
+
await this.sleep(100);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const successful = results.filter(r => r.success).length;
|
|
103
|
+
this.logger?.info(`Batch sync completed: ${successful}/${templates.length} successful`);
|
|
104
|
+
|
|
105
|
+
return results;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private generateLocalId(): string {
|
|
109
|
+
return `local_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private sleep(ms: number): Promise<void> {
|
|
113
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
114
|
+
}
|
|
115
|
+
}
|