@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.
Files changed (58) hide show
  1. package/.eslintrc.js +27 -0
  2. package/README.md +432 -0
  3. package/__tests__/api/rate-limiter.test.ts +125 -0
  4. package/__tests__/converters/sender-converter.test.ts +148 -0
  5. package/__tests__/converters/template-converter.test.ts +230 -0
  6. package/__tests__/plugin/sender-manager.test.ts +135 -0
  7. package/__tests__/plugin/sendgrid-plugin.test.ts +195 -0
  8. package/__tests__/plugin/template-manager.test.ts +110 -0
  9. package/__tests__/safety/ownership-validator.test.ts +130 -0
  10. package/__tests__/safety/prefix-enforcer.test.ts +136 -0
  11. package/dist/api/errors.d.ts +20 -0
  12. package/dist/api/errors.js +50 -0
  13. package/dist/api/rate-limiter.d.ts +17 -0
  14. package/dist/api/rate-limiter.js +58 -0
  15. package/dist/api/sendgrid-client.d.ts +21 -0
  16. package/dist/api/sendgrid-client.js +67 -0
  17. package/dist/converters/sender-converter.d.ts +6 -0
  18. package/dist/converters/sender-converter.js +40 -0
  19. package/dist/converters/template-converter.d.ts +11 -0
  20. package/dist/converters/template-converter.js +76 -0
  21. package/dist/index.d.ts +14 -0
  22. package/dist/index.js +42 -0
  23. package/dist/plugin/sender-manager.d.ts +20 -0
  24. package/dist/plugin/sender-manager.js +124 -0
  25. package/dist/plugin/sendgrid-plugin.d.ts +60 -0
  26. package/dist/plugin/sendgrid-plugin.js +204 -0
  27. package/dist/plugin/sync-manager.d.ts +19 -0
  28. package/dist/plugin/sync-manager.js +94 -0
  29. package/dist/plugin/template-manager.d.ts +25 -0
  30. package/dist/plugin/template-manager.js +152 -0
  31. package/dist/safety/ownership-validator.d.ts +15 -0
  32. package/dist/safety/ownership-validator.js +42 -0
  33. package/dist/safety/prefix-enforcer.d.ts +8 -0
  34. package/dist/safety/prefix-enforcer.js +34 -0
  35. package/dist/types/config-types.d.ts +24 -0
  36. package/dist/types/config-types.js +9 -0
  37. package/dist/types/plugin-types.d.ts +79 -0
  38. package/dist/types/plugin-types.js +9 -0
  39. package/dist/types/sendgrid-types.d.ts +56 -0
  40. package/dist/types/sendgrid-types.js +9 -0
  41. package/jest.config.js +25 -0
  42. package/package.json +42 -0
  43. package/src/api/errors.ts +49 -0
  44. package/src/api/rate-limiter.ts +79 -0
  45. package/src/api/sendgrid-client.ts +86 -0
  46. package/src/converters/sender-converter.ts +41 -0
  47. package/src/converters/template-converter.ts +97 -0
  48. package/src/index.ts +58 -0
  49. package/src/plugin/sender-manager.ts +166 -0
  50. package/src/plugin/sendgrid-plugin.ts +316 -0
  51. package/src/plugin/sync-manager.ts +115 -0
  52. package/src/plugin/template-manager.ts +240 -0
  53. package/src/safety/ownership-validator.ts +55 -0
  54. package/src/safety/prefix-enforcer.ts +37 -0
  55. package/src/types/config-types.ts +34 -0
  56. package/src/types/plugin-types.ts +97 -0
  57. package/src/types/sendgrid-types.ts +69 -0
  58. package/tsconfig.json +18 -0
@@ -0,0 +1,58 @@
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.SimpleRateLimiter = void 0;
11
+ const errors_1 = require("./errors");
12
+ class SimpleRateLimiter {
13
+ constructor(config) {
14
+ this.requestTimestamps = [];
15
+ this.maxRequestsPerSecond = config.maxRequestsPerSecond;
16
+ this.maxRequestsPerMinute = config.maxRequestsPerMinute;
17
+ }
18
+ async acquire() {
19
+ const now = Date.now();
20
+ // Clean up old timestamps
21
+ this.requestTimestamps = this.requestTimestamps.filter(timestamp => now - timestamp < 60000 // Keep last minute
22
+ );
23
+ // Check per-second limit
24
+ const lastSecond = this.requestTimestamps.filter(timestamp => now - timestamp < 1000);
25
+ if (lastSecond.length >= this.maxRequestsPerSecond) {
26
+ const waitTime = 1000 - (now - lastSecond[0]);
27
+ await this.sleep(waitTime);
28
+ return this.acquire();
29
+ }
30
+ // Check per-minute limit
31
+ if (this.requestTimestamps.length >= this.maxRequestsPerMinute) {
32
+ const waitTime = 60000 - (now - this.requestTimestamps[0]);
33
+ if (waitTime > 5000) {
34
+ throw new errors_1.RateLimitError(`Rate limit exceeded, would need to wait ${Math.ceil(waitTime / 1000)}s`);
35
+ }
36
+ await this.sleep(waitTime);
37
+ return this.acquire();
38
+ }
39
+ // Add timestamp and proceed
40
+ this.requestTimestamps.push(now);
41
+ }
42
+ release() {
43
+ // No-op for simple implementation
44
+ }
45
+ sleep(ms) {
46
+ return new Promise(resolve => setTimeout(resolve, Math.max(0, ms)));
47
+ }
48
+ getRemainingCapacity() {
49
+ const now = Date.now();
50
+ const lastSecond = this.requestTimestamps.filter(timestamp => now - timestamp < 1000).length;
51
+ const lastMinute = this.requestTimestamps.length;
52
+ return {
53
+ perSecond: Math.max(0, this.maxRequestsPerSecond - lastSecond),
54
+ perMinute: Math.max(0, this.maxRequestsPerMinute - lastMinute)
55
+ };
56
+ }
57
+ }
58
+ exports.SimpleRateLimiter = SimpleRateLimiter;
@@ -0,0 +1,21 @@
1
+ import { ApiRequestOptions, ApiResponse } from '../types/config-types';
2
+ import { SimpleRateLimiter } from './rate-limiter';
3
+ export interface SendGridClientConfig {
4
+ apiKey: string;
5
+ rateLimiter?: SimpleRateLimiter;
6
+ logger?: {
7
+ debug: (message: string) => void;
8
+ error: (message: string) => void;
9
+ info: (message: string) => void;
10
+ };
11
+ }
12
+ export declare class SendGridClient {
13
+ private readonly apiKey;
14
+ private readonly rateLimiter?;
15
+ private readonly logger?;
16
+ private readonly baseUrl;
17
+ constructor(config: SendGridClientConfig);
18
+ request(endpoint: string, options: ApiRequestOptions): Promise<ApiResponse>;
19
+ isSuccessResponse(response: ApiResponse): boolean;
20
+ parseResponseBody<T>(response: ApiResponse): T;
21
+ }
@@ -0,0 +1,67 @@
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.SendGridClient = void 0;
11
+ const errors_1 = require("./errors");
12
+ class SendGridClient {
13
+ constructor(config) {
14
+ this.baseUrl = 'https://api.sendgrid.com/v3';
15
+ this.apiKey = config.apiKey;
16
+ this.rateLimiter = config.rateLimiter;
17
+ this.logger = config.logger;
18
+ }
19
+ async request(endpoint, options) {
20
+ // Apply rate limiting if configured
21
+ if (this.rateLimiter) {
22
+ await this.rateLimiter.acquire();
23
+ }
24
+ try {
25
+ const url = `${this.baseUrl}${endpoint}`;
26
+ const response = await fetch(url, {
27
+ method: options.method,
28
+ headers: {
29
+ 'Authorization': `Bearer ${this.apiKey}`,
30
+ 'Content-Type': 'application/json',
31
+ ...options.headers
32
+ },
33
+ body: options.body
34
+ });
35
+ const body = await response.text();
36
+ // Log all API calls for audit trail
37
+ this.logger?.debug(`SendGrid API ${options.method} ${endpoint}: ${response.status}`);
38
+ return {
39
+ statusCode: response.status,
40
+ headers: Object.fromEntries(response.headers.entries()),
41
+ body
42
+ };
43
+ }
44
+ catch (error) {
45
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
46
+ this.logger?.error(`SendGrid API error: ${errorMessage}`);
47
+ throw new errors_1.SendGridApiError(`API request failed: ${errorMessage}`, undefined, error);
48
+ }
49
+ finally {
50
+ if (this.rateLimiter) {
51
+ this.rateLimiter.release();
52
+ }
53
+ }
54
+ }
55
+ isSuccessResponse(response) {
56
+ return response.statusCode >= 200 && response.statusCode < 300;
57
+ }
58
+ parseResponseBody(response) {
59
+ try {
60
+ return JSON.parse(response.body);
61
+ }
62
+ catch (error) {
63
+ throw new errors_1.SendGridApiError('Failed to parse response body', response.statusCode, response.body);
64
+ }
65
+ }
66
+ }
67
+ exports.SendGridClient = SendGridClient;
@@ -0,0 +1,6 @@
1
+ import { VerifiedSender, SendGridVerifiedSenderData } from '../types/sendgrid-types';
2
+ export declare class SenderConverter {
3
+ convertToVerifiedSender(sendgridData: SendGridVerifiedSenderData): VerifiedSender;
4
+ extractDomain(email: string): string;
5
+ isDomainMatch(domain1: string, domain2: string): boolean;
6
+ }
@@ -0,0 +1,40 @@
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.SenderConverter = void 0;
11
+ class SenderConverter {
12
+ convertToVerifiedSender(sendgridData) {
13
+ const domain = this.extractDomain(sendgridData.from_email);
14
+ return {
15
+ id: sendgridData.id.toString(),
16
+ email: sendgridData.from_email,
17
+ name: sendgridData.from_name || sendgridData.nickname,
18
+ isVerified: sendgridData.verified,
19
+ domain,
20
+ verificationStatus: sendgridData.verified ? 'verified' : 'pending',
21
+ createdAt: new Date(), // SendGrid doesn't provide creation date
22
+ lastVerifiedAt: sendgridData.verified ? new Date() : undefined
23
+ };
24
+ }
25
+ extractDomain(email) {
26
+ return email.split('@')[1]?.toLowerCase() || '';
27
+ }
28
+ isDomainMatch(domain1, domain2) {
29
+ // Exact match
30
+ if (domain1 === domain2)
31
+ return true;
32
+ // Subdomain match (e.g., app.example.com matches example.com)
33
+ if (domain1.endsWith(`.${domain2}`))
34
+ return true;
35
+ if (domain2.endsWith(`.${domain1}`))
36
+ return true;
37
+ return false;
38
+ }
39
+ }
40
+ exports.SenderConverter = SenderConverter;
@@ -0,0 +1,11 @@
1
+ import { SendGridTemplateData, SendGridTemplateVersion } from '../types/sendgrid-types';
2
+ import { ProviderTemplate, Template } from '../types/plugin-types';
3
+ import { PrefixEnforcer } from '../safety/prefix-enforcer';
4
+ export declare class TemplateConverter {
5
+ private readonly prefixEnforcer;
6
+ constructor(prefixEnforcer: PrefixEnforcer);
7
+ convertToProviderTemplate(sendgridData: SendGridTemplateData, activeVersion?: SendGridTemplateVersion): ProviderTemplate;
8
+ convertToLocalTemplate(sendgridData: SendGridTemplateData, localId: string, activeVersion?: SendGridTemplateVersion): Template;
9
+ extractVariables(content: string): string[];
10
+ private findActiveVersion;
11
+ }
@@ -0,0 +1,76 @@
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.TemplateConverter = void 0;
11
+ class TemplateConverter {
12
+ constructor(prefixEnforcer) {
13
+ this.prefixEnforcer = prefixEnforcer;
14
+ }
15
+ convertToProviderTemplate(sendgridData, activeVersion) {
16
+ // Find active version if not provided
17
+ const version = activeVersion || this.findActiveVersion(sendgridData.versions || []);
18
+ return {
19
+ id: sendgridData.id,
20
+ name: this.prefixEnforcer.stripPrefix(sendgridData.name),
21
+ subject: version?.subject || '',
22
+ htmlContent: version?.html_content || '',
23
+ textContent: version?.plain_content,
24
+ version: version?.name || '1.0.0',
25
+ active: version?.active === 1,
26
+ createdAt: new Date(sendgridData.updated_at),
27
+ updatedAt: new Date(sendgridData.updated_at)
28
+ };
29
+ }
30
+ convertToLocalTemplate(sendgridData, localId, activeVersion) {
31
+ const providerTemplate = this.convertToProviderTemplate(sendgridData, activeVersion);
32
+ return {
33
+ id: localId,
34
+ name: providerTemplate.name,
35
+ subject: providerTemplate.subject,
36
+ htmlContent: providerTemplate.htmlContent,
37
+ textContent: providerTemplate.textContent,
38
+ category: 'synced',
39
+ tags: ['sendgrid', 'synced'],
40
+ version: providerTemplate.version,
41
+ isActive: providerTemplate.active,
42
+ requiredVariables: [],
43
+ providerTemplateIds: {
44
+ sendgrid: sendgridData.id
45
+ },
46
+ lastSyncedAt: {
47
+ sendgrid: new Date()
48
+ },
49
+ syncStatus: {
50
+ sendgrid: 'success'
51
+ },
52
+ createdAt: providerTemplate.createdAt,
53
+ updatedAt: providerTemplate.updatedAt,
54
+ createdBy: 'sendgrid-sync',
55
+ lastModifiedBy: 'sendgrid-sync'
56
+ };
57
+ }
58
+ extractVariables(content) {
59
+ // Extract Handlebars-style variables: {{variableName}}
60
+ const regex = /\{\{([^}]+)\}\}/g;
61
+ const matches = content.matchAll(regex);
62
+ const variables = new Set();
63
+ for (const match of matches) {
64
+ const variable = match[1].trim();
65
+ // Filter out Handlebars helpers
66
+ if (!variable.startsWith('#') && !variable.startsWith('/')) {
67
+ variables.add(variable);
68
+ }
69
+ }
70
+ return Array.from(variables);
71
+ }
72
+ findActiveVersion(versions) {
73
+ return versions.find(v => v.active === 1) || versions[0];
74
+ }
75
+ }
76
+ exports.TemplateConverter = TemplateConverter;
@@ -0,0 +1,14 @@
1
+ export { SendGridTemplatePlugin, TemplateProviderPlugin } from './plugin/sendgrid-plugin';
2
+ export { TemplateManager } from './plugin/template-manager';
3
+ export { SenderManager } from './plugin/sender-manager';
4
+ export { SyncManager } from './plugin/sync-manager';
5
+ export { SendGridClient } from './api/sendgrid-client';
6
+ export { SimpleRateLimiter } from './api/rate-limiter';
7
+ export { OwnershipValidator, SafetyMode } from './safety/ownership-validator';
8
+ export { PrefixEnforcer } from './safety/prefix-enforcer';
9
+ export { TemplateConverter } from './converters/template-converter';
10
+ export { SenderConverter } from './converters/sender-converter';
11
+ export { SendGridApiError, TemplateNotFoundError, UnauthorizedTemplateAccessError, TemplateConflictError, RateLimitError } from './api/errors';
12
+ export type { SendGridPluginConfig, ApiRequestOptions, ApiResponse } from './types/config-types';
13
+ export type { PluginConfig, ProviderCapability, Template, ProviderTemplate, TemplateVersion, SyncResult, ValidationResult, DateRange, TemplateAnalytics, FeatureConfig } from './types/plugin-types';
14
+ export type { VerifiedSender, SenderValidationResult, SendGridTemplateData, SendGridTemplateVersion, SendGridVerifiedSenderData } from './types/sendgrid-types';
package/dist/index.js ADDED
@@ -0,0 +1,42 @@
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.RateLimitError = exports.TemplateConflictError = exports.UnauthorizedTemplateAccessError = exports.TemplateNotFoundError = exports.SendGridApiError = exports.SenderConverter = exports.TemplateConverter = exports.PrefixEnforcer = exports.OwnershipValidator = exports.SimpleRateLimiter = exports.SendGridClient = exports.SyncManager = exports.SenderManager = exports.TemplateManager = exports.SendGridTemplatePlugin = void 0;
11
+ // Main plugin class
12
+ var sendgrid_plugin_1 = require("./plugin/sendgrid-plugin");
13
+ Object.defineProperty(exports, "SendGridTemplatePlugin", { enumerable: true, get: function () { return sendgrid_plugin_1.SendGridTemplatePlugin; } });
14
+ // Plugin components
15
+ var template_manager_1 = require("./plugin/template-manager");
16
+ Object.defineProperty(exports, "TemplateManager", { enumerable: true, get: function () { return template_manager_1.TemplateManager; } });
17
+ var sender_manager_1 = require("./plugin/sender-manager");
18
+ Object.defineProperty(exports, "SenderManager", { enumerable: true, get: function () { return sender_manager_1.SenderManager; } });
19
+ var sync_manager_1 = require("./plugin/sync-manager");
20
+ Object.defineProperty(exports, "SyncManager", { enumerable: true, get: function () { return sync_manager_1.SyncManager; } });
21
+ // API components
22
+ var sendgrid_client_1 = require("./api/sendgrid-client");
23
+ Object.defineProperty(exports, "SendGridClient", { enumerable: true, get: function () { return sendgrid_client_1.SendGridClient; } });
24
+ var rate_limiter_1 = require("./api/rate-limiter");
25
+ Object.defineProperty(exports, "SimpleRateLimiter", { enumerable: true, get: function () { return rate_limiter_1.SimpleRateLimiter; } });
26
+ // Safety components
27
+ var ownership_validator_1 = require("./safety/ownership-validator");
28
+ Object.defineProperty(exports, "OwnershipValidator", { enumerable: true, get: function () { return ownership_validator_1.OwnershipValidator; } });
29
+ var prefix_enforcer_1 = require("./safety/prefix-enforcer");
30
+ Object.defineProperty(exports, "PrefixEnforcer", { enumerable: true, get: function () { return prefix_enforcer_1.PrefixEnforcer; } });
31
+ // Converters
32
+ var template_converter_1 = require("./converters/template-converter");
33
+ Object.defineProperty(exports, "TemplateConverter", { enumerable: true, get: function () { return template_converter_1.TemplateConverter; } });
34
+ var sender_converter_1 = require("./converters/sender-converter");
35
+ Object.defineProperty(exports, "SenderConverter", { enumerable: true, get: function () { return sender_converter_1.SenderConverter; } });
36
+ // Errors
37
+ var errors_1 = require("./api/errors");
38
+ Object.defineProperty(exports, "SendGridApiError", { enumerable: true, get: function () { return errors_1.SendGridApiError; } });
39
+ Object.defineProperty(exports, "TemplateNotFoundError", { enumerable: true, get: function () { return errors_1.TemplateNotFoundError; } });
40
+ Object.defineProperty(exports, "UnauthorizedTemplateAccessError", { enumerable: true, get: function () { return errors_1.UnauthorizedTemplateAccessError; } });
41
+ Object.defineProperty(exports, "TemplateConflictError", { enumerable: true, get: function () { return errors_1.TemplateConflictError; } });
42
+ Object.defineProperty(exports, "RateLimitError", { enumerable: true, get: function () { return errors_1.RateLimitError; } });
@@ -0,0 +1,20 @@
1
+ import { SendGridClient } from '../api/sendgrid-client';
2
+ import { SenderConverter } from '../converters/sender-converter';
3
+ import { VerifiedSender, SenderValidationResult } from '../types/sendgrid-types';
4
+ export declare class SenderManager {
5
+ private readonly client;
6
+ private readonly converter;
7
+ private readonly logger?;
8
+ private sendersCache?;
9
+ private cacheTimestamp?;
10
+ private readonly cacheTTL;
11
+ constructor(client: SendGridClient, converter: SenderConverter, logger?: {
12
+ info: (message: string) => void;
13
+ debug: (message: string) => void;
14
+ } | undefined);
15
+ getVerifiedSenders(useCache?: boolean): Promise<VerifiedSender[]>;
16
+ validateSender(email: string): Promise<SenderValidationResult>;
17
+ selectBestSender(fromEmail: string): Promise<VerifiedSender | null>;
18
+ clearCache(): void;
19
+ private getSenderSuggestions;
20
+ }
@@ -0,0 +1,124 @@
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.SenderManager = void 0;
11
+ const errors_1 = require("../api/errors");
12
+ class SenderManager {
13
+ constructor(client, converter, logger) {
14
+ this.client = client;
15
+ this.converter = converter;
16
+ this.logger = logger;
17
+ this.cacheTTL = 300000; // 5 minutes
18
+ }
19
+ async getVerifiedSenders(useCache = true) {
20
+ // Return cached data if valid
21
+ if (useCache &&
22
+ this.sendersCache &&
23
+ this.cacheTimestamp &&
24
+ Date.now() - this.cacheTimestamp < this.cacheTTL) {
25
+ this.logger?.debug('Returning cached verified senders');
26
+ return this.sendersCache;
27
+ }
28
+ const response = await this.client.request('/verified_senders', {
29
+ method: 'GET'
30
+ });
31
+ if (!this.client.isSuccessResponse(response)) {
32
+ throw new errors_1.SendGridApiError('Failed to fetch verified senders', response.statusCode, response.body);
33
+ }
34
+ const data = this.client.parseResponseBody(response);
35
+ const senders = data.results.map(sender => this.converter.convertToVerifiedSender(sender));
36
+ // Update cache
37
+ this.sendersCache = senders;
38
+ this.cacheTimestamp = Date.now();
39
+ this.logger?.info(`Fetched ${senders.length} verified senders from SendGrid`);
40
+ return senders;
41
+ }
42
+ async validateSender(email) {
43
+ const verifiedSenders = await this.getVerifiedSenders();
44
+ const emailDomain = this.converter.extractDomain(email);
45
+ // Check for exact match
46
+ const exactMatch = verifiedSenders.find(sender => sender.email.toLowerCase() === email.toLowerCase() && sender.isVerified);
47
+ if (exactMatch) {
48
+ return {
49
+ isValid: true,
50
+ verifiedSender: exactMatch,
51
+ domain: emailDomain,
52
+ domainMatches: [exactMatch],
53
+ errors: [],
54
+ suggestions: []
55
+ };
56
+ }
57
+ // Check for domain matches
58
+ const domainMatches = verifiedSenders.filter(sender => {
59
+ const senderDomain = this.converter.extractDomain(sender.email);
60
+ return this.converter.isDomainMatch(emailDomain, senderDomain) && sender.isVerified;
61
+ });
62
+ if (domainMatches.length > 0) {
63
+ return {
64
+ isValid: true,
65
+ domain: emailDomain,
66
+ domainMatches,
67
+ errors: [],
68
+ suggestions: domainMatches.map(sender => sender.email)
69
+ };
70
+ }
71
+ // No matches found
72
+ return {
73
+ isValid: false,
74
+ domain: emailDomain,
75
+ domainMatches: [],
76
+ errors: [`No verified sender found for email ${email} or domain ${emailDomain}`],
77
+ suggestions: this.getSenderSuggestions(email, verifiedSenders)
78
+ };
79
+ }
80
+ async selectBestSender(fromEmail) {
81
+ const validation = await this.validateSender(fromEmail);
82
+ if (!validation.isValid || validation.domainMatches.length === 0) {
83
+ return null;
84
+ }
85
+ // Prefer exact match
86
+ if (validation.verifiedSender) {
87
+ return validation.verifiedSender;
88
+ }
89
+ // Fall back to best domain match
90
+ // Priority: most recently verified, then alphabetical
91
+ return validation.domainMatches.sort((a, b) => {
92
+ if (a.lastVerifiedAt && b.lastVerifiedAt) {
93
+ return b.lastVerifiedAt.getTime() - a.lastVerifiedAt.getTime();
94
+ }
95
+ if (a.lastVerifiedAt && !b.lastVerifiedAt)
96
+ return -1;
97
+ if (!a.lastVerifiedAt && b.lastVerifiedAt)
98
+ return 1;
99
+ return a.email.localeCompare(b.email);
100
+ })[0];
101
+ }
102
+ clearCache() {
103
+ this.sendersCache = undefined;
104
+ this.cacheTimestamp = undefined;
105
+ this.logger?.debug('Cleared verified senders cache');
106
+ }
107
+ getSenderSuggestions(email, senders) {
108
+ const emailDomain = this.converter.extractDomain(email);
109
+ // Find senders with similar domains
110
+ const suggestions = [];
111
+ for (const sender of senders) {
112
+ if (!sender.isVerified)
113
+ continue;
114
+ const senderDomain = this.converter.extractDomain(sender.email);
115
+ // Check for partial domain matches
116
+ if (emailDomain.includes(senderDomain) ||
117
+ senderDomain.includes(emailDomain)) {
118
+ suggestions.push(sender.email);
119
+ }
120
+ }
121
+ return suggestions.slice(0, 5); // Limit to 5 suggestions
122
+ }
123
+ }
124
+ exports.SenderManager = SenderManager;
@@ -0,0 +1,60 @@
1
+ import { SendGridPluginConfig } from '../types/config-types';
2
+ import { PluginConfig, ProviderCapability, Template, ProviderTemplate, TemplateVersion, SyncResult, ValidationResult, DateRange, TemplateAnalytics, FeatureConfig } from '../types/plugin-types';
3
+ import { VerifiedSender, SenderValidationResult } from '../types/sendgrid-types';
4
+ export interface TemplateProviderPlugin {
5
+ readonly name: string;
6
+ readonly version: string;
7
+ readonly providerId: string;
8
+ initialize(config: PluginConfig): Promise<void>;
9
+ destroy(): Promise<void>;
10
+ createTemplate(template: Template): Promise<string>;
11
+ updateTemplate(providerTemplateId: string, template: Template): Promise<void>;
12
+ deleteTemplate(providerTemplateId: string): Promise<void>;
13
+ getTemplate(providerTemplateId: string): Promise<ProviderTemplate>;
14
+ listTemplates(): Promise<ProviderTemplate[]>;
15
+ syncToProvider(template: Template): Promise<SyncResult>;
16
+ syncFromProvider(providerTemplateId: string): Promise<Template>;
17
+ getCapabilities(): ProviderCapability[];
18
+ supportsFeature(feature: string): boolean;
19
+ getFeatureConfig(feature: string): FeatureConfig | null;
20
+ }
21
+ export declare class SendGridTemplatePlugin implements TemplateProviderPlugin {
22
+ readonly name = "sendgrid-templates";
23
+ readonly version = "1.0.0";
24
+ readonly providerId = "sendgrid";
25
+ private client;
26
+ private rateLimiter?;
27
+ private ownershipValidator;
28
+ private prefixEnforcer;
29
+ private templateConverter;
30
+ private senderConverter;
31
+ private templateManager;
32
+ private senderManager;
33
+ private syncManager;
34
+ private readonly config;
35
+ private readonly logger?;
36
+ private initialized;
37
+ constructor(config: SendGridPluginConfig);
38
+ initialize(__pluginConfig: PluginConfig): Promise<void>;
39
+ destroy(): Promise<void>;
40
+ createTemplate(template: Template): Promise<string>;
41
+ updateTemplate(providerTemplateId: string, template: Template): Promise<void>;
42
+ deleteTemplate(providerTemplateId: string): Promise<void>;
43
+ getTemplate(providerTemplateId: string): Promise<ProviderTemplate>;
44
+ listTemplates(): Promise<ProviderTemplate[]>;
45
+ syncToProvider(template: Template): Promise<SyncResult>;
46
+ syncFromProvider(providerTemplateId: string): Promise<Template>;
47
+ getVerifiedSenders(): Promise<VerifiedSender[]>;
48
+ validateSender(email: string): Promise<SenderValidationResult>;
49
+ selectBestSender(email: string): Promise<VerifiedSender | null>;
50
+ createTemplateVersion(templateId: string, version: TemplateVersion): Promise<string>;
51
+ activateTemplateVersion(templateId: string, versionId: string): Promise<void>;
52
+ getTemplateAnalytics(__templateId: string, __dateRange: DateRange): Promise<TemplateAnalytics>;
53
+ batchSyncTemplates(templates: Template[]): Promise<SyncResult[]>;
54
+ getCapabilities(): ProviderCapability[];
55
+ supportsFeature(feature: string): boolean;
56
+ getFeatureConfig(feature: string): FeatureConfig | null;
57
+ validateRoute(__route: unknown): ValidationResult;
58
+ private loadExistingTemplates;
59
+ private ensureInitialized;
60
+ }