@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.
@@ -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
- // Initialize dependencies (would import from actual packages)
101
- // For now, we'll use stubs since we're missing dependencies
102
- await this.loadSendersFromDatabase();
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 (stub for missing dependency)
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 database
167
- await this.saveSenderToDatabase(sender);
168
- this.senders.set(sender.id, sender);
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. Send verification email (unless skipped)
171
- if (!input.skipVerification) {
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
- // 8. Publish event
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: {} }; // Mailgun doesn't require verification
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
- const sender = this.senders.get(senderId);
497
- if (!sender) {
498
- return {
499
- success: false,
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
- await this.saveSenderToDatabase(sender);
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 sender = this.senders.get(senderId);
557
- if (!sender) {
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
- // Soft delete
565
- sender.deletedAt = new Date();
566
- sender.isActive = false;
567
- sender.updatedAt = new Date();
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
- await this.saveSenderToDatabase(sender);
570
- this.senders.delete(senderId);
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 sender = this.senders.get(senderId);
598
- if (!sender) {
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 (stub)
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
- if (sender.provider === EmailProvider.SENDGRID) {
638
- if (!sender.providerSenderId) {
639
- warnings.push('SendGrid sender ID not set - may need re-verification');
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 sender = this.senders.get(senderId);
706
- if (!sender) {
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
  }