@bernierllc/sender-identity-verification 1.3.1 → 1.4.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/CHANGELOG.md +11 -0
- package/dist/SenderIdentityVerification.d.ts +37 -2
- package/dist/SenderIdentityVerification.js +393 -62
- package/dist/config.d.ts +3 -0
- package/dist/config.js +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -1
- package/dist/manual-instructions.d.ts +9 -0
- package/dist/manual-instructions.js +54 -0
- package/dist/types.d.ts +5 -0
- package/jest.config.cjs +2 -1
- package/package.json +5 -4
- package/src/SenderIdentityVerification.ts +395 -58
- package/src/config.ts +10 -1
- package/src/index.ts +2 -0
- package/src/manual-instructions.ts +61 -0
- package/src/types.ts +5 -0
|
@@ -19,12 +19,19 @@ import {
|
|
|
19
19
|
IEmailDomainVerification
|
|
20
20
|
} from './types.js';
|
|
21
21
|
import { SenderIdentityConfig } from './config.js';
|
|
22
|
-
import { SendGridProvider } from './providers/SendGridProvider.js';
|
|
23
|
-
import { MailgunProvider } from './providers/MailgunProvider.js';
|
|
24
|
-
import { SESProvider } from './providers/SESProvider.js';
|
|
25
|
-
import { SMTPProvider } from './providers/SMTPProvider.js';
|
|
26
22
|
import { buildVerificationEmailHtml, buildVerificationEmailText } from './templates/verification-email.js';
|
|
27
23
|
import { extractDomain } from './utils/domain-extractor.js';
|
|
24
|
+
import { isManualVerificationProvider, getManualInstructions } from './manual-instructions.js';
|
|
25
|
+
import type {
|
|
26
|
+
EmailSenderManager,
|
|
27
|
+
SenderProviderPlugin,
|
|
28
|
+
SenderConfiguration,
|
|
29
|
+
CreateSenderRequest as CoreCreateSenderRequest,
|
|
30
|
+
} from '@bernierllc/email-sender-manager';
|
|
31
|
+
import {
|
|
32
|
+
isManagedSenderProvider,
|
|
33
|
+
isVerifiableSenderProvider,
|
|
34
|
+
} from '@bernierllc/email-sender-manager';
|
|
28
35
|
|
|
29
36
|
// Import types for dependencies (actual imports would be from npm packages)
|
|
30
37
|
type EmailSender = {
|
|
@@ -75,10 +82,23 @@ type NeverHubAdapter = {
|
|
|
75
82
|
};
|
|
76
83
|
|
|
77
84
|
/**
|
|
78
|
-
* Sender identity verification service
|
|
85
|
+
* Sender identity verification service.
|
|
86
|
+
*
|
|
87
|
+
* When configured with an `EmailSenderManager` (via config.senderManager) and
|
|
88
|
+
* provider plugins (via config.providerPlugins or registerProviderPlugin()),
|
|
89
|
+
* all persistence and provider API calls are delegated. Without those, the
|
|
90
|
+
* service falls back to the legacy in-memory store.
|
|
79
91
|
*/
|
|
80
92
|
export class SenderIdentityVerification {
|
|
93
|
+
// Legacy in-memory store — used only when no senderManager is configured
|
|
81
94
|
private senders: Map<string, SenderIdentity> = new Map();
|
|
95
|
+
|
|
96
|
+
// Provider plugin registry (keyed by providerId)
|
|
97
|
+
private providerPlugins: Map<string, SenderProviderPlugin> = new Map();
|
|
98
|
+
|
|
99
|
+
// Core persistence layer (optional, new path)
|
|
100
|
+
private senderManager?: EmailSenderManager;
|
|
101
|
+
|
|
82
102
|
private emailSender?: EmailSender;
|
|
83
103
|
private domainVerification?: IEmailDomainVerification;
|
|
84
104
|
private cryptoUtils?: CryptoUtils;
|
|
@@ -91,15 +111,43 @@ export class SenderIdentityVerification {
|
|
|
91
111
|
if (config.domainVerificationConfig?.instance) {
|
|
92
112
|
this.domainVerification = config.domainVerificationConfig.instance;
|
|
93
113
|
}
|
|
114
|
+
|
|
115
|
+
// Wire core sender manager if provided
|
|
116
|
+
if (config.senderManager) {
|
|
117
|
+
this.senderManager = config.senderManager;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Register initial provider plugins
|
|
121
|
+
if (config.providerPlugins) {
|
|
122
|
+
for (const plugin of config.providerPlugins) {
|
|
123
|
+
this.providerPlugins.set(plugin.providerId, plugin);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Register a provider plugin at runtime.
|
|
130
|
+
*/
|
|
131
|
+
registerProviderPlugin(plugin: SenderProviderPlugin): void {
|
|
132
|
+
this.providerPlugins.set(plugin.providerId, plugin);
|
|
133
|
+
this.logger?.info('Provider plugin registered', { providerId: plugin.providerId, name: plugin.name });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Get a registered provider plugin by providerId.
|
|
138
|
+
*/
|
|
139
|
+
getProviderPlugin(providerId: string): SenderProviderPlugin | undefined {
|
|
140
|
+
return this.providerPlugins.get(providerId);
|
|
94
141
|
}
|
|
95
142
|
|
|
96
143
|
/**
|
|
97
144
|
* Initialize service and register with NeverHub
|
|
98
145
|
*/
|
|
99
146
|
async initialize(): Promise<void> {
|
|
100
|
-
//
|
|
101
|
-
|
|
102
|
-
|
|
147
|
+
// Load from database if no senderManager (legacy path)
|
|
148
|
+
if (!this.senderManager) {
|
|
149
|
+
await this.loadSendersFromDatabase();
|
|
150
|
+
}
|
|
103
151
|
|
|
104
152
|
this.logger?.info('Sender identity verification service initialized');
|
|
105
153
|
}
|
|
@@ -120,7 +168,7 @@ export class SenderIdentityVerification {
|
|
|
120
168
|
// 2. Extract domain
|
|
121
169
|
const domain = extractDomain(input.email);
|
|
122
170
|
|
|
123
|
-
// 3. Check if domain is verified
|
|
171
|
+
// 3. Check if domain is verified
|
|
124
172
|
if (this.domainVerification) {
|
|
125
173
|
const domainStatus = await this.domainVerification.getDomainStatus(domain);
|
|
126
174
|
if (!domainStatus.isVerified) {
|
|
@@ -163,16 +211,52 @@ export class SenderIdentityVerification {
|
|
|
163
211
|
await this.unsetOtherDefaults(input.provider);
|
|
164
212
|
}
|
|
165
213
|
|
|
166
|
-
// 6. Persist to
|
|
167
|
-
|
|
168
|
-
|
|
214
|
+
// 6. Persist — delegate to senderManager if available, else legacy
|
|
215
|
+
if (this.senderManager) {
|
|
216
|
+
const coreRequest = this.toCoreSenderRequest(input, sender);
|
|
217
|
+
const created = await this.senderManager.createSender(coreRequest);
|
|
218
|
+
sender.id = created.id;
|
|
219
|
+
sender.providerSenderId = created.providerSenderId;
|
|
220
|
+
} else {
|
|
221
|
+
await this.saveSenderToDatabase(sender);
|
|
222
|
+
this.senders.set(sender.id, sender);
|
|
223
|
+
}
|
|
169
224
|
|
|
170
|
-
// 7.
|
|
171
|
-
|
|
225
|
+
// 7. Delegate to provider plugin for API-level creation
|
|
226
|
+
const plugin = this.providerPlugins.get(input.provider);
|
|
227
|
+
if (plugin && isManagedSenderProvider(plugin) && !input.skipVerification) {
|
|
228
|
+
const providerResult = await plugin.createSender({
|
|
229
|
+
fromEmail: input.email,
|
|
230
|
+
fromName: input.name,
|
|
231
|
+
replyToEmail: input.replyToEmail,
|
|
232
|
+
replyToName: input.replyToName,
|
|
233
|
+
providerFields: input.providerFields,
|
|
234
|
+
});
|
|
235
|
+
if (providerResult.success) {
|
|
236
|
+
sender.providerSenderId = providerResult.providerSenderId;
|
|
237
|
+
sender.providerMetadata = providerResult.metadata;
|
|
238
|
+
// Persist the provider sender ID
|
|
239
|
+
if (this.senderManager) {
|
|
240
|
+
await this.senderManager.updateSender(sender.id, {
|
|
241
|
+
providerSenderId: providerResult.providerSenderId,
|
|
242
|
+
providerMetadata: providerResult.metadata,
|
|
243
|
+
lastModifiedBy: input.createdBy || 'system',
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
} else {
|
|
247
|
+
this.logger?.warn('Provider creation failed, sender saved locally', {
|
|
248
|
+
senderId: sender.id,
|
|
249
|
+
error: providerResult.error?.message,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// 8. Send verification email (unless skipped or provider handles it)
|
|
255
|
+
if (!input.skipVerification && !plugin) {
|
|
172
256
|
await this.sendVerificationEmail(sender);
|
|
173
257
|
}
|
|
174
258
|
|
|
175
|
-
//
|
|
259
|
+
// 9. Publish event
|
|
176
260
|
if (this.neverhub) {
|
|
177
261
|
await this.neverhub.publishEvent({
|
|
178
262
|
type: 'sender-identity.created',
|
|
@@ -307,7 +391,7 @@ export class SenderIdentityVerification {
|
|
|
307
391
|
};
|
|
308
392
|
}
|
|
309
393
|
|
|
310
|
-
// 5. Verify with provider
|
|
394
|
+
// 5. Verify with provider — use plugin system first, fall back to legacy
|
|
311
395
|
const providerResult = await this.verifyWithProvider(sender);
|
|
312
396
|
if (!providerResult.success) {
|
|
313
397
|
sender.status = SenderStatus.FAILED;
|
|
@@ -336,6 +420,18 @@ export class SenderIdentityVerification {
|
|
|
336
420
|
|
|
337
421
|
await this.saveSenderToDatabase(sender);
|
|
338
422
|
|
|
423
|
+
// Update core layer if available
|
|
424
|
+
if (this.senderManager) {
|
|
425
|
+
await this.senderManager.updateSender(sender.id, {
|
|
426
|
+
isVerified: true,
|
|
427
|
+
verificationStatus: 'verified',
|
|
428
|
+
lastVerifiedAt: sender.verifiedAt,
|
|
429
|
+
providerSenderId: sender.providerSenderId,
|
|
430
|
+
providerMetadata: sender.providerMetadata,
|
|
431
|
+
lastModifiedBy: 'system',
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
|
|
339
435
|
// 7. Publish event
|
|
340
436
|
if (this.neverhub) {
|
|
341
437
|
await this.neverhub.publishEvent({
|
|
@@ -371,15 +467,74 @@ export class SenderIdentityVerification {
|
|
|
371
467
|
}
|
|
372
468
|
|
|
373
469
|
/**
|
|
374
|
-
* Verify sender with email provider
|
|
470
|
+
* Verify sender with email provider.
|
|
471
|
+
*
|
|
472
|
+
* Uses the plugin registry first. If no plugin is registered for the
|
|
473
|
+
* provider, falls back to the legacy provider classes.
|
|
375
474
|
*/
|
|
376
475
|
private async verifyWithProvider(sender: SenderIdentity): Promise<SenderIdentityResult<{
|
|
377
476
|
providerId?: string;
|
|
378
477
|
metadata?: Record<string, unknown>;
|
|
379
478
|
}>> {
|
|
479
|
+
// Try plugin-based verification
|
|
480
|
+
const plugin = this.providerPlugins.get(sender.provider);
|
|
481
|
+
if (plugin) {
|
|
482
|
+
if (isVerifiableSenderProvider(plugin) && sender.providerSenderId) {
|
|
483
|
+
const result = await plugin.checkVerificationStatus(sender.providerSenderId);
|
|
484
|
+
if (result.status === 'verified') {
|
|
485
|
+
return {
|
|
486
|
+
success: true,
|
|
487
|
+
data: {
|
|
488
|
+
providerId: result.providerSenderId,
|
|
489
|
+
metadata: { verificationType: result.verificationType },
|
|
490
|
+
},
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
return {
|
|
494
|
+
success: false,
|
|
495
|
+
error: result.error?.message || `Verification status: ${result.status}`,
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Plugin exists but is not verifiable — validate only
|
|
500
|
+
const validation = await plugin.validateSender(sender.email);
|
|
501
|
+
if (validation.isVerified) {
|
|
502
|
+
return {
|
|
503
|
+
success: true,
|
|
504
|
+
data: { metadata: validation.metadata },
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
return { success: true, data: {} };
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Manual verification providers
|
|
511
|
+
if (isManualVerificationProvider(sender.provider)) {
|
|
512
|
+
const instructions = getManualInstructions(sender.provider);
|
|
513
|
+
return {
|
|
514
|
+
success: true,
|
|
515
|
+
data: {
|
|
516
|
+
metadata: instructions ? { manualInstructions: instructions } : {},
|
|
517
|
+
},
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Legacy fallback — for backward compatibility when no plugins are registered
|
|
522
|
+
return this.legacyVerifyWithProvider(sender);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Legacy provider verification using inline provider classes.
|
|
527
|
+
* Preserved for backward compatibility when no plugins are configured.
|
|
528
|
+
*/
|
|
529
|
+
private async legacyVerifyWithProvider(sender: SenderIdentity): Promise<SenderIdentityResult<{
|
|
530
|
+
providerId?: string;
|
|
531
|
+
metadata?: Record<string, unknown>;
|
|
532
|
+
}>> {
|
|
533
|
+
// Dynamic imports to avoid breaking if legacy providers are removed later
|
|
380
534
|
switch (sender.provider) {
|
|
381
535
|
case EmailProvider.SENDGRID:
|
|
382
536
|
if (this.config.sendgridApiKey) {
|
|
537
|
+
const { SendGridProvider } = await import('./providers/SendGridProvider.js');
|
|
383
538
|
const provider = new SendGridProvider(this.config.sendgridApiKey);
|
|
384
539
|
return provider.verifySender(sender);
|
|
385
540
|
}
|
|
@@ -387,13 +542,15 @@ export class SenderIdentityVerification {
|
|
|
387
542
|
|
|
388
543
|
case EmailProvider.MAILGUN:
|
|
389
544
|
if (this.config.mailgunApiKey) {
|
|
545
|
+
const { MailgunProvider } = await import('./providers/MailgunProvider.js');
|
|
390
546
|
const provider = new MailgunProvider(this.config.mailgunApiKey);
|
|
391
547
|
return provider.verifySender(sender);
|
|
392
548
|
}
|
|
393
|
-
return { success: true, data: {} };
|
|
549
|
+
return { success: true, data: {} };
|
|
394
550
|
|
|
395
551
|
case EmailProvider.SES:
|
|
396
552
|
if (this.config.sesAccessKey && this.config.sesSecretKey && this.config.sesRegion) {
|
|
553
|
+
const { SESProvider } = await import('./providers/SESProvider.js');
|
|
397
554
|
const provider = new SESProvider(
|
|
398
555
|
this.config.sesAccessKey,
|
|
399
556
|
this.config.sesSecretKey,
|
|
@@ -404,6 +561,7 @@ export class SenderIdentityVerification {
|
|
|
404
561
|
return { success: false, error: 'SES credentials not configured' };
|
|
405
562
|
|
|
406
563
|
case EmailProvider.SMTP: {
|
|
564
|
+
const { SMTPProvider } = await import('./providers/SMTPProvider.js');
|
|
407
565
|
const provider = new SMTPProvider();
|
|
408
566
|
return provider.verifySender(sender);
|
|
409
567
|
}
|
|
@@ -411,8 +569,6 @@ export class SenderIdentityVerification {
|
|
|
411
569
|
case EmailProvider.POSTMARK:
|
|
412
570
|
case EmailProvider.MANDRILL:
|
|
413
571
|
case EmailProvider.SMTP2GO:
|
|
414
|
-
// These providers handle sender verification through their own dashboards.
|
|
415
|
-
// Return success to allow the sender to be registered locally.
|
|
416
572
|
return { success: true, data: {} };
|
|
417
573
|
|
|
418
574
|
default:
|
|
@@ -427,6 +583,14 @@ export class SenderIdentityVerification {
|
|
|
427
583
|
* Get sender by ID
|
|
428
584
|
*/
|
|
429
585
|
async getSender(senderId: string): Promise<SenderIdentityResult<SenderIdentity>> {
|
|
586
|
+
if (this.senderManager) {
|
|
587
|
+
const config = await this.senderManager.getSender(senderId);
|
|
588
|
+
if (!config) {
|
|
589
|
+
return { success: false, error: `Sender not found: ${senderId}` };
|
|
590
|
+
}
|
|
591
|
+
return { success: true, data: this.coreConfigToIdentity(config) };
|
|
592
|
+
}
|
|
593
|
+
|
|
430
594
|
const sender = this.senders.get(senderId);
|
|
431
595
|
if (!sender) {
|
|
432
596
|
return {
|
|
@@ -446,6 +610,22 @@ export class SenderIdentityVerification {
|
|
|
446
610
|
*/
|
|
447
611
|
async listSenders(options: ListSendersOptions = {}): Promise<SenderIdentityResult<SenderIdentity[]>> {
|
|
448
612
|
try {
|
|
613
|
+
if (this.senderManager) {
|
|
614
|
+
const result = await this.senderManager.listSenders({
|
|
615
|
+
provider: options.provider,
|
|
616
|
+
isActive: options.isActive,
|
|
617
|
+
domain: options.domain,
|
|
618
|
+
offset: options.offset,
|
|
619
|
+
limit: options.limit,
|
|
620
|
+
});
|
|
621
|
+
const identities = result.items.map((c: SenderConfiguration) => this.coreConfigToIdentity(c));
|
|
622
|
+
// Apply status filter locally (core layer doesn't have SenderStatus enum)
|
|
623
|
+
const filtered = options.status
|
|
624
|
+
? identities.filter((s: SenderIdentity) => s.status === options.status)
|
|
625
|
+
: identities;
|
|
626
|
+
return { success: true, data: filtered };
|
|
627
|
+
}
|
|
628
|
+
|
|
449
629
|
let senders = Array.from(this.senders.values());
|
|
450
630
|
|
|
451
631
|
// Filter by provider
|
|
@@ -493,13 +673,12 @@ export class SenderIdentityVerification {
|
|
|
493
673
|
*/
|
|
494
674
|
async updateSender(senderId: string, input: UpdateSenderInput): Promise<SenderIdentityResult<SenderIdentity>> {
|
|
495
675
|
try {
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
error: `Sender not found: ${senderId}`
|
|
501
|
-
};
|
|
676
|
+
// Fetch from appropriate store
|
|
677
|
+
const getResult = await this.getSender(senderId);
|
|
678
|
+
if (!getResult.success || !getResult.data) {
|
|
679
|
+
return { success: false, error: `Sender not found: ${senderId}` };
|
|
502
680
|
}
|
|
681
|
+
const sender = getResult.data;
|
|
503
682
|
|
|
504
683
|
// Update fields
|
|
505
684
|
if (input.name !== undefined) sender.name = input.name;
|
|
@@ -523,7 +702,32 @@ export class SenderIdentityVerification {
|
|
|
523
702
|
sender.lastValidated = undefined;
|
|
524
703
|
}
|
|
525
704
|
|
|
526
|
-
|
|
705
|
+
// Persist
|
|
706
|
+
if (this.senderManager) {
|
|
707
|
+
await this.senderManager.updateSender(senderId, {
|
|
708
|
+
name: input.name,
|
|
709
|
+
fromName: input.name,
|
|
710
|
+
replyToEmail: input.replyToEmail,
|
|
711
|
+
replyToName: input.replyToName,
|
|
712
|
+
isDefault: input.isDefault,
|
|
713
|
+
isActive: input.isActive,
|
|
714
|
+
lastModifiedBy: input.lastModifiedBy || 'system',
|
|
715
|
+
});
|
|
716
|
+
} else {
|
|
717
|
+
await this.saveSenderToDatabase(sender);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// Delegate to provider plugin if available
|
|
721
|
+
const plugin = this.providerPlugins.get(sender.provider);
|
|
722
|
+
if (plugin && isManagedSenderProvider(plugin) && sender.providerSenderId) {
|
|
723
|
+
await plugin.updateSender({
|
|
724
|
+
providerSenderId: sender.providerSenderId,
|
|
725
|
+
fromName: input.name,
|
|
726
|
+
replyToEmail: input.replyToEmail,
|
|
727
|
+
replyToName: input.replyToName,
|
|
728
|
+
providerFields: input.providerFields,
|
|
729
|
+
});
|
|
730
|
+
}
|
|
527
731
|
|
|
528
732
|
// Publish event
|
|
529
733
|
if (this.neverhub) {
|
|
@@ -553,21 +757,28 @@ export class SenderIdentityVerification {
|
|
|
553
757
|
*/
|
|
554
758
|
async deleteSender(senderId: string): Promise<SenderIdentityResult<void>> {
|
|
555
759
|
try {
|
|
556
|
-
const
|
|
557
|
-
if (!
|
|
558
|
-
return {
|
|
559
|
-
success: false,
|
|
560
|
-
error: `Sender not found: ${senderId}`
|
|
561
|
-
};
|
|
760
|
+
const getResult = await this.getSender(senderId);
|
|
761
|
+
if (!getResult.success || !getResult.data) {
|
|
762
|
+
return { success: false, error: `Sender not found: ${senderId}` };
|
|
562
763
|
}
|
|
764
|
+
const sender = getResult.data;
|
|
563
765
|
|
|
564
|
-
//
|
|
565
|
-
|
|
566
|
-
sender.
|
|
567
|
-
|
|
766
|
+
// Delete from provider if plugin supports it
|
|
767
|
+
const plugin = this.providerPlugins.get(sender.provider);
|
|
768
|
+
if (plugin && isManagedSenderProvider(plugin) && sender.providerSenderId) {
|
|
769
|
+
await plugin.deleteSender(sender.providerSenderId);
|
|
770
|
+
}
|
|
568
771
|
|
|
569
|
-
|
|
570
|
-
this.
|
|
772
|
+
// Persist deletion
|
|
773
|
+
if (this.senderManager) {
|
|
774
|
+
await this.senderManager.deleteSender(senderId);
|
|
775
|
+
} else {
|
|
776
|
+
sender.deletedAt = new Date();
|
|
777
|
+
sender.isActive = false;
|
|
778
|
+
sender.updatedAt = new Date();
|
|
779
|
+
await this.saveSenderToDatabase(sender);
|
|
780
|
+
this.senders.delete(senderId);
|
|
781
|
+
}
|
|
571
782
|
|
|
572
783
|
// Publish event
|
|
573
784
|
if (this.neverhub) {
|
|
@@ -594,13 +805,11 @@ export class SenderIdentityVerification {
|
|
|
594
805
|
*/
|
|
595
806
|
async checkCompliance(senderId: string): Promise<SenderIdentityResult<ComplianceCheckResult>> {
|
|
596
807
|
try {
|
|
597
|
-
const
|
|
598
|
-
if (!
|
|
599
|
-
return {
|
|
600
|
-
success: false,
|
|
601
|
-
error: `Sender not found: ${senderId}`
|
|
602
|
-
};
|
|
808
|
+
const getResult = await this.getSender(senderId);
|
|
809
|
+
if (!getResult.success || !getResult.data) {
|
|
810
|
+
return { success: false, error: `Sender not found: ${senderId}` };
|
|
603
811
|
}
|
|
812
|
+
const sender = getResult.data;
|
|
604
813
|
|
|
605
814
|
const errors: string[] = [];
|
|
606
815
|
const warnings: string[] = [];
|
|
@@ -611,7 +820,7 @@ export class SenderIdentityVerification {
|
|
|
611
820
|
errors.push('Invalid email format');
|
|
612
821
|
}
|
|
613
822
|
|
|
614
|
-
// 2. Check domain verification
|
|
823
|
+
// 2. Check domain verification
|
|
615
824
|
let domainVerified = true;
|
|
616
825
|
let spfValid = true;
|
|
617
826
|
let dkimValid = true;
|
|
@@ -633,11 +842,15 @@ export class SenderIdentityVerification {
|
|
|
633
842
|
}
|
|
634
843
|
}
|
|
635
844
|
|
|
636
|
-
// 3. Provider-specific checks
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
845
|
+
// 3. Provider-specific checks via plugin
|
|
846
|
+
const plugin = this.providerPlugins.get(sender.provider);
|
|
847
|
+
if (plugin && isVerifiableSenderProvider(plugin) && sender.providerSenderId) {
|
|
848
|
+
const verificationResult = await plugin.checkVerificationStatus(sender.providerSenderId);
|
|
849
|
+
if (verificationResult.status !== 'verified') {
|
|
850
|
+
warnings.push(`Provider verification status: ${verificationResult.status}`);
|
|
640
851
|
}
|
|
852
|
+
} else if (sender.provider === EmailProvider.SENDGRID && !sender.providerSenderId) {
|
|
853
|
+
warnings.push('SendGrid sender ID not set - may need re-verification');
|
|
641
854
|
}
|
|
642
855
|
|
|
643
856
|
const isCompliant = errors.length === 0;
|
|
@@ -676,6 +889,19 @@ export class SenderIdentityVerification {
|
|
|
676
889
|
* Get default sender for provider
|
|
677
890
|
*/
|
|
678
891
|
async getDefaultSender(provider: EmailProvider): Promise<SenderIdentityResult<SenderIdentity>> {
|
|
892
|
+
if (this.senderManager) {
|
|
893
|
+
const result = await this.senderManager.listSenders({
|
|
894
|
+
provider,
|
|
895
|
+
isActive: true,
|
|
896
|
+
isVerified: true,
|
|
897
|
+
});
|
|
898
|
+
const defaultSender = result.items.find((s: SenderConfiguration) => s.isDefault);
|
|
899
|
+
if (!defaultSender) {
|
|
900
|
+
return { success: false, error: `No default sender found for provider ${provider}` };
|
|
901
|
+
}
|
|
902
|
+
return { success: true, data: this.coreConfigToIdentity(defaultSender) };
|
|
903
|
+
}
|
|
904
|
+
|
|
679
905
|
const senders = Array.from(this.senders.values());
|
|
680
906
|
const defaultSender = senders.find(s =>
|
|
681
907
|
s.provider === provider &&
|
|
@@ -702,13 +928,11 @@ export class SenderIdentityVerification {
|
|
|
702
928
|
*/
|
|
703
929
|
async resendVerification(senderId: string): Promise<SenderIdentityResult<void>> {
|
|
704
930
|
try {
|
|
705
|
-
const
|
|
706
|
-
if (!
|
|
707
|
-
return {
|
|
708
|
-
success: false,
|
|
709
|
-
error: `Sender not found: ${senderId}`
|
|
710
|
-
};
|
|
931
|
+
const getResult = await this.getSender(senderId);
|
|
932
|
+
if (!getResult.success || !getResult.data) {
|
|
933
|
+
return { success: false, error: `Sender not found: ${senderId}` };
|
|
711
934
|
}
|
|
935
|
+
const sender = getResult.data;
|
|
712
936
|
|
|
713
937
|
if (sender.status === SenderStatus.VERIFIED) {
|
|
714
938
|
return {
|
|
@@ -735,6 +959,15 @@ export class SenderIdentityVerification {
|
|
|
735
959
|
}
|
|
736
960
|
}
|
|
737
961
|
|
|
962
|
+
// If provider supports verification, use it
|
|
963
|
+
const plugin = this.providerPlugins.get(sender.provider);
|
|
964
|
+
if (plugin && isVerifiableSenderProvider(plugin) && sender.providerSenderId) {
|
|
965
|
+
const result = await plugin.initiateVerification(sender.providerSenderId);
|
|
966
|
+
if (result.success) {
|
|
967
|
+
return { success: true };
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
|
|
738
971
|
return this.sendVerificationEmail(sender);
|
|
739
972
|
} catch (error) {
|
|
740
973
|
this.logger?.error('Failed to resend verification', { error, senderId });
|
|
@@ -745,6 +978,42 @@ export class SenderIdentityVerification {
|
|
|
745
978
|
}
|
|
746
979
|
}
|
|
747
980
|
|
|
981
|
+
/**
|
|
982
|
+
* Check verification status for a sender using the provider plugin.
|
|
983
|
+
*/
|
|
984
|
+
async checkVerificationStatus(senderId: string): Promise<SenderIdentityResult<{
|
|
985
|
+
status: string;
|
|
986
|
+
verificationType?: string;
|
|
987
|
+
dnsRecords?: unknown[];
|
|
988
|
+
}>> {
|
|
989
|
+
const getResult = await this.getSender(senderId);
|
|
990
|
+
if (!getResult.success || !getResult.data) {
|
|
991
|
+
return { success: false, error: `Sender not found: ${senderId}` };
|
|
992
|
+
}
|
|
993
|
+
const sender = getResult.data;
|
|
994
|
+
|
|
995
|
+
const plugin = this.providerPlugins.get(sender.provider);
|
|
996
|
+
if (!plugin || !isVerifiableSenderProvider(plugin) || !sender.providerSenderId) {
|
|
997
|
+
return {
|
|
998
|
+
success: true,
|
|
999
|
+
data: {
|
|
1000
|
+
status: sender.status,
|
|
1001
|
+
verificationType: 'manual',
|
|
1002
|
+
},
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
const result = await plugin.checkVerificationStatus(sender.providerSenderId);
|
|
1007
|
+
return {
|
|
1008
|
+
success: result.success,
|
|
1009
|
+
data: {
|
|
1010
|
+
status: result.status,
|
|
1011
|
+
verificationType: result.verificationType,
|
|
1012
|
+
dnsRecords: result.dnsRecords,
|
|
1013
|
+
},
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
1016
|
+
|
|
748
1017
|
// ============================================
|
|
749
1018
|
// Private Helper Methods
|
|
750
1019
|
// ============================================
|
|
@@ -772,14 +1041,34 @@ export class SenderIdentityVerification {
|
|
|
772
1041
|
}
|
|
773
1042
|
|
|
774
1043
|
private async findSenderByEmail(email: string): Promise<SenderIdentity | undefined> {
|
|
1044
|
+
if (this.senderManager) {
|
|
1045
|
+
const result = await this.senderManager.listSenders({ limit: 1000 });
|
|
1046
|
+
return result.items
|
|
1047
|
+
.map((c: SenderConfiguration) => this.coreConfigToIdentity(c))
|
|
1048
|
+
.find((s: SenderIdentity) => s.email === email && !s.deletedAt);
|
|
1049
|
+
}
|
|
775
1050
|
return Array.from(this.senders.values()).find(s => s.email === email && !s.deletedAt);
|
|
776
1051
|
}
|
|
777
1052
|
|
|
778
1053
|
private async findSenderByToken(token: string): Promise<SenderIdentity | undefined> {
|
|
1054
|
+
// Token lookup is always in-memory (tokens live in the service layer)
|
|
779
1055
|
return Array.from(this.senders.values()).find(s => s.verificationToken === token);
|
|
780
1056
|
}
|
|
781
1057
|
|
|
782
1058
|
private async unsetOtherDefaults(provider: EmailProvider): Promise<void> {
|
|
1059
|
+
if (this.senderManager) {
|
|
1060
|
+
const result = await this.senderManager.listSenders({ provider });
|
|
1061
|
+
for (const sender of result.items) {
|
|
1062
|
+
if (sender.isDefault) {
|
|
1063
|
+
await this.senderManager.updateSender(sender.id, {
|
|
1064
|
+
isDefault: false,
|
|
1065
|
+
lastModifiedBy: 'system',
|
|
1066
|
+
});
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
783
1072
|
const senders = Array.from(this.senders.values())
|
|
784
1073
|
.filter(s => s.provider === provider && s.isDefault);
|
|
785
1074
|
|
|
@@ -791,13 +1080,61 @@ export class SenderIdentityVerification {
|
|
|
791
1080
|
}
|
|
792
1081
|
|
|
793
1082
|
private async loadSendersFromDatabase(): Promise<void> {
|
|
794
|
-
// TODO: Implement database loading
|
|
1083
|
+
// TODO: Implement database loading for legacy path
|
|
795
1084
|
// For now, no-op
|
|
796
1085
|
}
|
|
797
1086
|
|
|
798
1087
|
private async saveSenderToDatabase(sender: SenderIdentity): Promise<void> {
|
|
799
|
-
// TODO: Implement database persistence
|
|
1088
|
+
// TODO: Implement database persistence for legacy path
|
|
800
1089
|
// For now, just update in-memory map
|
|
801
1090
|
this.senders.set(sender.id, sender);
|
|
802
1091
|
}
|
|
1092
|
+
|
|
1093
|
+
// ============================================
|
|
1094
|
+
// Mapping helpers between service and core types
|
|
1095
|
+
// ============================================
|
|
1096
|
+
|
|
1097
|
+
private toCoreSenderRequest(input: CreateSenderInput, sender: SenderIdentity): CoreCreateSenderRequest {
|
|
1098
|
+
return {
|
|
1099
|
+
name: input.name,
|
|
1100
|
+
fromEmail: input.email,
|
|
1101
|
+
fromName: input.name,
|
|
1102
|
+
replyToEmail: input.replyToEmail,
|
|
1103
|
+
replyToName: input.replyToName,
|
|
1104
|
+
provider: input.provider,
|
|
1105
|
+
allowedDomains: [sender.domain],
|
|
1106
|
+
createdBy: input.createdBy || 'system',
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
private coreConfigToIdentity(config: SenderConfiguration): SenderIdentity {
|
|
1111
|
+
return {
|
|
1112
|
+
id: config.id,
|
|
1113
|
+
email: config.fromEmail,
|
|
1114
|
+
name: config.fromName,
|
|
1115
|
+
replyToEmail: config.replyToEmail,
|
|
1116
|
+
replyToName: config.replyToName,
|
|
1117
|
+
domain: config.domain,
|
|
1118
|
+
provider: config.provider as EmailProvider,
|
|
1119
|
+
status: this.mapVerificationStatus(config.verificationStatus),
|
|
1120
|
+
isDefault: config.isDefault,
|
|
1121
|
+
isActive: config.isActive,
|
|
1122
|
+
verificationAttempts: 0,
|
|
1123
|
+
verifiedAt: config.lastVerifiedAt,
|
|
1124
|
+
providerSenderId: config.providerSenderId,
|
|
1125
|
+
providerMetadata: config.providerMetadata,
|
|
1126
|
+
createdAt: config.createdAt,
|
|
1127
|
+
updatedAt: config.updatedAt,
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
private mapVerificationStatus(status: string): SenderStatus {
|
|
1132
|
+
switch (status) {
|
|
1133
|
+
case 'verified': return SenderStatus.VERIFIED;
|
|
1134
|
+
case 'failed': return SenderStatus.FAILED;
|
|
1135
|
+
case 'expired': return SenderStatus.EXPIRED;
|
|
1136
|
+
case 'pending':
|
|
1137
|
+
default: return SenderStatus.PENDING;
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
803
1140
|
}
|