@bernierllc/sender-identity-verification 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/.env.test +8 -0
  2. package/.eslintrc.js +30 -0
  3. package/README.md +376 -0
  4. package/__tests__/SenderIdentityVerification.test.ts +461 -0
  5. package/__tests__/__mocks__/fetch-mock.ts +156 -0
  6. package/__tests__/additional-coverage.test.ts +129 -0
  7. package/__tests__/additional-error-coverage.test.ts +483 -0
  8. package/__tests__/branch-coverage.test.ts +509 -0
  9. package/__tests__/config.test.ts +119 -0
  10. package/__tests__/error-handling.test.ts +321 -0
  11. package/__tests__/final-branch-coverage.test.ts +372 -0
  12. package/__tests__/integration.real-api.test.ts +295 -0
  13. package/__tests__/providers.test.ts +331 -0
  14. package/__tests__/service-coverage.test.ts +412 -0
  15. package/dist/SenderIdentityVerification.d.ts +72 -0
  16. package/dist/SenderIdentityVerification.js +643 -0
  17. package/dist/config.d.ts +31 -0
  18. package/dist/config.js +38 -0
  19. package/dist/errors.d.ts +27 -0
  20. package/dist/errors.js +61 -0
  21. package/dist/index.d.ts +4 -0
  22. package/dist/index.js +21 -0
  23. package/dist/providers/MailgunProvider.d.ts +13 -0
  24. package/dist/providers/MailgunProvider.js +35 -0
  25. package/dist/providers/SESProvider.d.ts +12 -0
  26. package/dist/providers/SESProvider.js +47 -0
  27. package/dist/providers/SMTPProvider.d.ts +12 -0
  28. package/dist/providers/SMTPProvider.js +30 -0
  29. package/dist/providers/SendGridProvider.d.ts +19 -0
  30. package/dist/providers/SendGridProvider.js +98 -0
  31. package/dist/templates/verification-email.d.ts +9 -0
  32. package/dist/templates/verification-email.js +67 -0
  33. package/dist/types.d.ts +139 -0
  34. package/dist/types.js +33 -0
  35. package/dist/utils/domain-extractor.d.ts +4 -0
  36. package/dist/utils/domain-extractor.js +20 -0
  37. package/jest.config.cjs +33 -0
  38. package/package.json +60 -0
  39. package/src/SenderIdentityVerification.ts +796 -0
  40. package/src/config.ts +81 -0
  41. package/src/errors.ts +64 -0
  42. package/src/global.d.ts +24 -0
  43. package/src/index.ts +24 -0
  44. package/src/providers/MailgunProvider.ts +35 -0
  45. package/src/providers/SESProvider.ts +51 -0
  46. package/src/providers/SMTPProvider.ts +29 -0
  47. package/src/providers/SendGridProvider.ts +108 -0
  48. package/src/templates/verification-email.ts +67 -0
  49. package/src/types.ts +163 -0
  50. package/src/utils/domain-extractor.ts +18 -0
  51. package/tsconfig.json +22 -0
@@ -0,0 +1,796 @@
1
+ /*
2
+ Copyright (c) 2025 Bernier LLC
3
+
4
+ This file is licensed to the client under a limited-use license.
5
+ The client may use and modify this code *only within the scope of the project it was delivered for*.
6
+ Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
7
+ */
8
+
9
+ import {
10
+ SenderIdentity,
11
+ SenderStatus,
12
+ EmailProvider,
13
+ CreateSenderInput,
14
+ UpdateSenderInput,
15
+ VerificationResult,
16
+ ComplianceCheckResult,
17
+ ListSendersOptions,
18
+ SenderIdentityResult,
19
+ IEmailDomainVerification
20
+ } from './types.js';
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
+ import { buildVerificationEmailHtml, buildVerificationEmailText } from './templates/verification-email.js';
27
+ import { extractDomain } from './utils/domain-extractor.js';
28
+
29
+ // Import types for dependencies (actual imports would be from npm packages)
30
+ type EmailSender = {
31
+ sendEmail(options: {
32
+ to: string;
33
+ subject: string;
34
+ html: string;
35
+ text: string;
36
+ from: string;
37
+ fromName: string;
38
+ }): Promise<{ success: boolean; error?: string }>;
39
+ };
40
+
41
+ type CryptoUtils = {
42
+ generateSecureToken(length: number): string;
43
+ };
44
+
45
+ type Logger = {
46
+ info(message: string, meta?: Record<string, unknown>): void;
47
+ warn(message: string, meta?: Record<string, unknown>): void;
48
+ error(message: string, meta?: Record<string, unknown>): void;
49
+ };
50
+
51
+ type EmailValidator = {
52
+ validate(email: string): { isValid: boolean; errors: string[] };
53
+ };
54
+
55
+ type NeverHubAdapter = {
56
+ register(config: {
57
+ type: string;
58
+ name: string;
59
+ version: string;
60
+ capabilities: Array<{
61
+ type: string;
62
+ name: string;
63
+ version: string;
64
+ metadata?: Record<string, unknown>;
65
+ }>;
66
+ dependencies: string[];
67
+ discoveryEnabled: boolean;
68
+ discoverySubscriptions: Array<{
69
+ subscriptionType: string;
70
+ filterCriteria: { capabilityTypes: string[] };
71
+ }>;
72
+ }): Promise<void>;
73
+ publishEvent(event: { type: string; data: Record<string, unknown> }): Promise<void>;
74
+ subscribe(eventType: string, handler: (event: { data: Record<string, unknown> }) => Promise<void>): Promise<void>;
75
+ };
76
+
77
+ /**
78
+ * Sender identity verification service
79
+ */
80
+ export class SenderIdentityVerification {
81
+ private senders: Map<string, SenderIdentity> = new Map();
82
+ private emailSender?: EmailSender;
83
+ private domainVerification?: IEmailDomainVerification;
84
+ private cryptoUtils?: CryptoUtils;
85
+ private logger?: Logger;
86
+ private emailValidator?: EmailValidator;
87
+ private neverhub?: NeverHubAdapter;
88
+
89
+ constructor(private config: SenderIdentityConfig) {
90
+ // Initialize domain verification from config if provided
91
+ if (config.domainVerificationConfig?.instance) {
92
+ this.domainVerification = config.domainVerificationConfig.instance;
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Initialize service and register with NeverHub
98
+ */
99
+ 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();
103
+
104
+ this.logger?.info('Sender identity verification service initialized');
105
+ }
106
+
107
+ /**
108
+ * Create a new sender identity
109
+ */
110
+ async createSender(input: CreateSenderInput): Promise<SenderIdentityResult<SenderIdentity>> {
111
+ try {
112
+ // 1. Validate email format
113
+ if (!this.isValidEmail(input.email)) {
114
+ return {
115
+ success: false,
116
+ error: 'Invalid email format'
117
+ };
118
+ }
119
+
120
+ // 2. Extract domain
121
+ const domain = extractDomain(input.email);
122
+
123
+ // 3. Check if domain is verified (stub for missing dependency)
124
+ if (this.domainVerification) {
125
+ const domainStatus = await this.domainVerification.getDomainStatus(domain);
126
+ if (!domainStatus.isVerified) {
127
+ return {
128
+ success: false,
129
+ error: `Domain ${domain} is not verified. Please verify the domain first.`,
130
+ errors: [`Domain verification required`]
131
+ };
132
+ }
133
+ }
134
+
135
+ // 4. Check for duplicate sender
136
+ const existing = await this.findSenderByEmail(input.email);
137
+ if (existing) {
138
+ return {
139
+ success: false,
140
+ error: `Sender with email ${input.email} already exists (ID: ${existing.id})`
141
+ };
142
+ }
143
+
144
+ // 5. Create sender record
145
+ const sender: SenderIdentity = {
146
+ id: this.generateSenderId(),
147
+ email: input.email,
148
+ name: input.name,
149
+ replyToEmail: input.replyToEmail || input.email,
150
+ replyToName: input.replyToName || input.name,
151
+ domain,
152
+ provider: input.provider,
153
+ status: input.skipVerification ? SenderStatus.VERIFIED : SenderStatus.PENDING,
154
+ isDefault: input.isDefault || false,
155
+ isActive: true,
156
+ verificationAttempts: 0,
157
+ createdAt: new Date(),
158
+ updatedAt: new Date(),
159
+ };
160
+
161
+ // If setting as default, unset other defaults for this provider
162
+ if (input.isDefault) {
163
+ await this.unsetOtherDefaults(input.provider);
164
+ }
165
+
166
+ // 6. Persist to database
167
+ await this.saveSenderToDatabase(sender);
168
+ this.senders.set(sender.id, sender);
169
+
170
+ // 7. Send verification email (unless skipped)
171
+ if (!input.skipVerification) {
172
+ await this.sendVerificationEmail(sender);
173
+ }
174
+
175
+ // 8. Publish event
176
+ if (this.neverhub) {
177
+ await this.neverhub.publishEvent({
178
+ type: 'sender-identity.created',
179
+ data: { senderId: sender.id, email: sender.email, provider: sender.provider }
180
+ });
181
+ }
182
+
183
+ this.logger?.info('Sender identity created', { senderId: sender.id, email: sender.email });
184
+
185
+ return {
186
+ success: true,
187
+ data: sender
188
+ };
189
+ } catch (error) {
190
+ this.logger?.error('Failed to create sender identity', { error, input });
191
+ return {
192
+ success: false,
193
+ error: error instanceof Error ? error.message : 'Unknown error creating sender'
194
+ };
195
+ }
196
+ }
197
+
198
+ /**
199
+ * Send verification email to sender
200
+ */
201
+ async sendVerificationEmail(sender: SenderIdentity): Promise<SenderIdentityResult<void>> {
202
+ try {
203
+ // 1. Generate verification token
204
+ const token = this.cryptoUtils?.generateSecureToken(32) || this.fallbackGenerateToken(32);
205
+ const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000); // 24 hours
206
+
207
+ // 2. Update sender record
208
+ sender.verificationToken = token;
209
+ sender.verificationSentAt = new Date();
210
+ sender.verificationExpiresAt = expiresAt;
211
+ sender.status = SenderStatus.VERIFICATION_SENT;
212
+ sender.verificationAttempts++;
213
+ sender.updatedAt = new Date();
214
+
215
+ await this.saveSenderToDatabase(sender);
216
+
217
+ // 3. Build verification URL
218
+ const verificationUrl = `${this.config.verificationBaseUrl}/verify-sender?token=${token}`;
219
+
220
+ // 4. Send verification email
221
+ if (this.emailSender) {
222
+ const emailResult = await this.emailSender.sendEmail({
223
+ to: sender.email,
224
+ subject: 'Verify your sender email address',
225
+ html: buildVerificationEmailHtml(sender, verificationUrl),
226
+ text: buildVerificationEmailText(sender, verificationUrl),
227
+ from: this.config.verificationFromEmail,
228
+ fromName: this.config.verificationFromName,
229
+ });
230
+
231
+ if (!emailResult.success) {
232
+ throw new Error(`Failed to send verification email: ${emailResult.error}`);
233
+ }
234
+ }
235
+
236
+ // 5. Publish event
237
+ if (this.neverhub) {
238
+ await this.neverhub.publishEvent({
239
+ type: 'sender-identity.verification-sent',
240
+ data: { senderId: sender.id, email: sender.email, expiresAt }
241
+ });
242
+ }
243
+
244
+ this.logger?.info('Verification email sent', { senderId: sender.id, email: sender.email });
245
+
246
+ return { success: true };
247
+ } catch (error) {
248
+ this.logger?.error('Failed to send verification email', { error, senderId: sender.id });
249
+ return {
250
+ success: false,
251
+ error: error instanceof Error ? error.message : 'Failed to send verification email'
252
+ };
253
+ }
254
+ }
255
+
256
+ /**
257
+ * Verify sender email with token
258
+ */
259
+ async verifySender(token: string): Promise<VerificationResult> {
260
+ try {
261
+ // 1. Find sender by token
262
+ const sender = await this.findSenderByToken(token);
263
+ if (!sender) {
264
+ return {
265
+ success: false,
266
+ senderId: '',
267
+ status: SenderStatus.FAILED,
268
+ message: 'Invalid verification token',
269
+ errors: ['Token not found or already used']
270
+ };
271
+ }
272
+
273
+ // 2. Check if already verified
274
+ if (sender.status === SenderStatus.VERIFIED) {
275
+ return {
276
+ success: true,
277
+ senderId: sender.id,
278
+ status: sender.status,
279
+ message: 'Sender already verified',
280
+ verifiedAt: sender.verifiedAt
281
+ };
282
+ }
283
+
284
+ // 3. Check if token expired
285
+ if (sender.verificationExpiresAt && sender.verificationExpiresAt < new Date()) {
286
+ sender.status = SenderStatus.EXPIRED;
287
+ sender.updatedAt = new Date();
288
+ await this.saveSenderToDatabase(sender);
289
+
290
+ return {
291
+ success: false,
292
+ senderId: sender.id,
293
+ status: SenderStatus.EXPIRED,
294
+ message: 'Verification token expired. Please request a new verification email.',
295
+ errors: ['Token expired']
296
+ };
297
+ }
298
+
299
+ // 4. Check if locked
300
+ if (sender.status === SenderStatus.LOCKED) {
301
+ return {
302
+ success: false,
303
+ senderId: sender.id,
304
+ status: SenderStatus.LOCKED,
305
+ message: 'Sender locked due to too many failed verification attempts',
306
+ errors: ['Account locked']
307
+ };
308
+ }
309
+
310
+ // 5. Verify with provider
311
+ const providerResult = await this.verifyWithProvider(sender);
312
+ if (!providerResult.success) {
313
+ sender.status = SenderStatus.FAILED;
314
+ sender.validationErrors = providerResult.errors || [providerResult.error || 'Provider verification failed'];
315
+ sender.updatedAt = new Date();
316
+ await this.saveSenderToDatabase(sender);
317
+
318
+ return {
319
+ success: false,
320
+ senderId: sender.id,
321
+ status: SenderStatus.FAILED,
322
+ message: 'Provider verification failed',
323
+ errors: sender.validationErrors
324
+ };
325
+ }
326
+
327
+ // 6. Mark as verified
328
+ sender.status = SenderStatus.VERIFIED;
329
+ sender.verifiedAt = new Date();
330
+ sender.verificationToken = undefined;
331
+ sender.providerSenderId = providerResult.data?.providerId as string | undefined;
332
+ sender.providerMetadata = providerResult.data?.metadata as Record<string, unknown> | undefined;
333
+ sender.lastValidated = new Date();
334
+ sender.validationErrors = undefined;
335
+ sender.updatedAt = new Date();
336
+
337
+ await this.saveSenderToDatabase(sender);
338
+
339
+ // 7. Publish event
340
+ if (this.neverhub) {
341
+ await this.neverhub.publishEvent({
342
+ type: 'sender-identity.verified',
343
+ data: {
344
+ senderId: sender.id,
345
+ email: sender.email,
346
+ provider: sender.provider,
347
+ verifiedAt: sender.verifiedAt
348
+ }
349
+ });
350
+ }
351
+
352
+ this.logger?.info('Sender verified successfully', { senderId: sender.id, email: sender.email });
353
+
354
+ return {
355
+ success: true,
356
+ senderId: sender.id,
357
+ status: sender.status,
358
+ message: 'Sender verified successfully',
359
+ verifiedAt: sender.verifiedAt
360
+ };
361
+ } catch (error) {
362
+ this.logger?.error('Failed to verify sender', { error, token });
363
+ return {
364
+ success: false,
365
+ senderId: '',
366
+ status: SenderStatus.FAILED,
367
+ message: error instanceof Error ? error.message : 'Verification failed',
368
+ errors: [error instanceof Error ? error.message : 'Unknown error']
369
+ };
370
+ }
371
+ }
372
+
373
+ /**
374
+ * Verify sender with email provider
375
+ */
376
+ private async verifyWithProvider(sender: SenderIdentity): Promise<SenderIdentityResult<{
377
+ providerId?: string;
378
+ metadata?: Record<string, unknown>;
379
+ }>> {
380
+ switch (sender.provider) {
381
+ case EmailProvider.SENDGRID:
382
+ if (this.config.sendgridApiKey) {
383
+ const provider = new SendGridProvider(this.config.sendgridApiKey);
384
+ return provider.verifySender(sender);
385
+ }
386
+ return { success: false, error: 'SendGrid API key not configured' };
387
+
388
+ case EmailProvider.MAILGUN:
389
+ if (this.config.mailgunApiKey) {
390
+ const provider = new MailgunProvider(this.config.mailgunApiKey);
391
+ return provider.verifySender(sender);
392
+ }
393
+ return { success: true, data: {} }; // Mailgun doesn't require verification
394
+
395
+ case EmailProvider.SES:
396
+ if (this.config.sesAccessKey && this.config.sesSecretKey && this.config.sesRegion) {
397
+ const provider = new SESProvider(
398
+ this.config.sesAccessKey,
399
+ this.config.sesSecretKey,
400
+ this.config.sesRegion
401
+ );
402
+ return provider.verifySender(sender);
403
+ }
404
+ return { success: false, error: 'SES credentials not configured' };
405
+
406
+ case EmailProvider.SMTP: {
407
+ const provider = new SMTPProvider();
408
+ return provider.verifySender(sender);
409
+ }
410
+
411
+ default:
412
+ return {
413
+ success: false,
414
+ error: `Unsupported provider: ${sender.provider}`
415
+ };
416
+ }
417
+ }
418
+
419
+ /**
420
+ * Get sender by ID
421
+ */
422
+ async getSender(senderId: string): Promise<SenderIdentityResult<SenderIdentity>> {
423
+ const sender = this.senders.get(senderId);
424
+ if (!sender) {
425
+ return {
426
+ success: false,
427
+ error: `Sender not found: ${senderId}`
428
+ };
429
+ }
430
+
431
+ return {
432
+ success: true,
433
+ data: sender
434
+ };
435
+ }
436
+
437
+ /**
438
+ * List senders
439
+ */
440
+ async listSenders(options: ListSendersOptions = {}): Promise<SenderIdentityResult<SenderIdentity[]>> {
441
+ try {
442
+ let senders = Array.from(this.senders.values());
443
+
444
+ // Filter by provider
445
+ if (options.provider) {
446
+ senders = senders.filter(s => s.provider === options.provider);
447
+ }
448
+
449
+ // Filter by status
450
+ if (options.status) {
451
+ senders = senders.filter(s => s.status === options.status);
452
+ }
453
+
454
+ // Filter by active
455
+ if (options.isActive !== undefined) {
456
+ senders = senders.filter(s => s.isActive === options.isActive);
457
+ }
458
+
459
+ // Filter by domain
460
+ if (options.domain) {
461
+ senders = senders.filter(s => s.domain === options.domain);
462
+ }
463
+
464
+ // Sort by created date (newest first)
465
+ senders.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
466
+
467
+ // Pagination
468
+ const limit = options.limit || 50;
469
+ const offset = options.offset || 0;
470
+ senders = senders.slice(offset, offset + limit);
471
+
472
+ return {
473
+ success: true,
474
+ data: senders
475
+ };
476
+ } catch (error) {
477
+ return {
478
+ success: false,
479
+ error: error instanceof Error ? error.message : 'Failed to list senders'
480
+ };
481
+ }
482
+ }
483
+
484
+ /**
485
+ * Update sender
486
+ */
487
+ async updateSender(senderId: string, input: UpdateSenderInput): Promise<SenderIdentityResult<SenderIdentity>> {
488
+ try {
489
+ const sender = this.senders.get(senderId);
490
+ if (!sender) {
491
+ return {
492
+ success: false,
493
+ error: `Sender not found: ${senderId}`
494
+ };
495
+ }
496
+
497
+ // Update fields
498
+ if (input.name !== undefined) sender.name = input.name;
499
+ if (input.replyToEmail !== undefined) sender.replyToEmail = input.replyToEmail;
500
+ if (input.replyToName !== undefined) sender.replyToName = input.replyToName;
501
+ if (input.isDefault !== undefined) {
502
+ // If setting as default, unset other defaults
503
+ if (input.isDefault) {
504
+ await this.unsetOtherDefaults(sender.provider);
505
+ }
506
+ sender.isDefault = input.isDefault;
507
+ }
508
+ if (input.isActive !== undefined) sender.isActive = input.isActive;
509
+
510
+ sender.updatedAt = new Date();
511
+
512
+ // If changing email-related fields, require re-verification
513
+ if (input.replyToEmail && input.replyToEmail !== sender.email) {
514
+ sender.status = SenderStatus.PENDING;
515
+ sender.verifiedAt = undefined;
516
+ sender.lastValidated = undefined;
517
+ }
518
+
519
+ await this.saveSenderToDatabase(sender);
520
+
521
+ // Publish event
522
+ if (this.neverhub) {
523
+ await this.neverhub.publishEvent({
524
+ type: 'sender-identity.updated',
525
+ data: { senderId: sender.id, updates: input }
526
+ });
527
+ }
528
+
529
+ this.logger?.info('Sender updated', { senderId, updates: input });
530
+
531
+ return {
532
+ success: true,
533
+ data: sender
534
+ };
535
+ } catch (error) {
536
+ this.logger?.error('Failed to update sender', { error, senderId, input });
537
+ return {
538
+ success: false,
539
+ error: error instanceof Error ? error.message : 'Failed to update sender'
540
+ };
541
+ }
542
+ }
543
+
544
+ /**
545
+ * Delete sender
546
+ */
547
+ async deleteSender(senderId: string): Promise<SenderIdentityResult<void>> {
548
+ try {
549
+ const sender = this.senders.get(senderId);
550
+ if (!sender) {
551
+ return {
552
+ success: false,
553
+ error: `Sender not found: ${senderId}`
554
+ };
555
+ }
556
+
557
+ // Soft delete
558
+ sender.deletedAt = new Date();
559
+ sender.isActive = false;
560
+ sender.updatedAt = new Date();
561
+
562
+ await this.saveSenderToDatabase(sender);
563
+ this.senders.delete(senderId);
564
+
565
+ // Publish event
566
+ if (this.neverhub) {
567
+ await this.neverhub.publishEvent({
568
+ type: 'sender-identity.deleted',
569
+ data: { senderId, email: sender.email }
570
+ });
571
+ }
572
+
573
+ this.logger?.info('Sender deleted', { senderId, email: sender.email });
574
+
575
+ return { success: true };
576
+ } catch (error) {
577
+ this.logger?.error('Failed to delete sender', { error, senderId });
578
+ return {
579
+ success: false,
580
+ error: error instanceof Error ? error.message : 'Failed to delete sender'
581
+ };
582
+ }
583
+ }
584
+
585
+ /**
586
+ * Check provider compliance for sender
587
+ */
588
+ async checkCompliance(senderId: string): Promise<SenderIdentityResult<ComplianceCheckResult>> {
589
+ try {
590
+ const sender = this.senders.get(senderId);
591
+ if (!sender) {
592
+ return {
593
+ success: false,
594
+ error: `Sender not found: ${senderId}`
595
+ };
596
+ }
597
+
598
+ const errors: string[] = [];
599
+ const warnings: string[] = [];
600
+
601
+ // 1. Check email format
602
+ const emailFormat = this.isValidEmail(sender.email);
603
+ if (!emailFormat) {
604
+ errors.push('Invalid email format');
605
+ }
606
+
607
+ // 2. Check domain verification (stub)
608
+ let domainVerified = true;
609
+ let spfValid = true;
610
+ let dkimValid = true;
611
+
612
+ if (this.domainVerification) {
613
+ const domainStatus = await this.domainVerification.getDomainStatus(sender.domain);
614
+ domainVerified = domainStatus.isVerified;
615
+ spfValid = domainStatus.dnsRecords?.spf?.isValid || false;
616
+ dkimValid = domainStatus.dnsRecords?.dkim?.isValid || false;
617
+
618
+ if (!domainVerified) {
619
+ errors.push(`Domain ${sender.domain} is not verified`);
620
+ }
621
+ if (!spfValid) {
622
+ errors.push('SPF record not configured or invalid');
623
+ }
624
+ if (!dkimValid) {
625
+ errors.push('DKIM record not configured or invalid');
626
+ }
627
+ }
628
+
629
+ // 3. Provider-specific checks
630
+ if (sender.provider === EmailProvider.SENDGRID) {
631
+ if (!sender.providerSenderId) {
632
+ warnings.push('SendGrid sender ID not set - may need re-verification');
633
+ }
634
+ }
635
+
636
+ const isCompliant = errors.length === 0;
637
+
638
+ const result: ComplianceCheckResult = {
639
+ isCompliant,
640
+ checks: {
641
+ domainVerified,
642
+ spfValid,
643
+ dkimValid,
644
+ emailFormat
645
+ },
646
+ errors,
647
+ warnings
648
+ };
649
+
650
+ // Update sender validation
651
+ sender.lastValidated = new Date();
652
+ sender.validationErrors = errors.length > 0 ? errors : undefined;
653
+ await this.saveSenderToDatabase(sender);
654
+
655
+ return {
656
+ success: true,
657
+ data: result
658
+ };
659
+ } catch (error) {
660
+ this.logger?.error('Failed to check compliance', { error, senderId });
661
+ return {
662
+ success: false,
663
+ error: error instanceof Error ? error.message : 'Failed to check compliance'
664
+ };
665
+ }
666
+ }
667
+
668
+ /**
669
+ * Get default sender for provider
670
+ */
671
+ async getDefaultSender(provider: EmailProvider): Promise<SenderIdentityResult<SenderIdentity>> {
672
+ const senders = Array.from(this.senders.values());
673
+ const defaultSender = senders.find(s =>
674
+ s.provider === provider &&
675
+ s.isDefault &&
676
+ s.status === SenderStatus.VERIFIED &&
677
+ s.isActive
678
+ );
679
+
680
+ if (!defaultSender) {
681
+ return {
682
+ success: false,
683
+ error: `No default sender found for provider ${provider}`
684
+ };
685
+ }
686
+
687
+ return {
688
+ success: true,
689
+ data: defaultSender
690
+ };
691
+ }
692
+
693
+ /**
694
+ * Resend verification email
695
+ */
696
+ async resendVerification(senderId: string): Promise<SenderIdentityResult<void>> {
697
+ try {
698
+ const sender = this.senders.get(senderId);
699
+ if (!sender) {
700
+ return {
701
+ success: false,
702
+ error: `Sender not found: ${senderId}`
703
+ };
704
+ }
705
+
706
+ if (sender.status === SenderStatus.VERIFIED) {
707
+ return {
708
+ success: false,
709
+ error: 'Sender already verified'
710
+ };
711
+ }
712
+
713
+ if (sender.status === SenderStatus.LOCKED) {
714
+ return {
715
+ success: false,
716
+ error: 'Sender locked due to too many attempts'
717
+ };
718
+ }
719
+
720
+ // Check rate limiting (max 3 emails per hour)
721
+ if (sender.verificationSentAt) {
722
+ const hourAgo = new Date(Date.now() - 60 * 60 * 1000);
723
+ if (sender.verificationSentAt > hourAgo && sender.verificationAttempts >= 3) {
724
+ return {
725
+ success: false,
726
+ error: 'Too many verification emails sent. Please try again later.'
727
+ };
728
+ }
729
+ }
730
+
731
+ return this.sendVerificationEmail(sender);
732
+ } catch (error) {
733
+ this.logger?.error('Failed to resend verification', { error, senderId });
734
+ return {
735
+ success: false,
736
+ error: error instanceof Error ? error.message : 'Failed to resend verification'
737
+ };
738
+ }
739
+ }
740
+
741
+ // ============================================
742
+ // Private Helper Methods
743
+ // ============================================
744
+
745
+ private generateSenderId(): string {
746
+ return this.cryptoUtils?.generateSecureToken(16) || this.fallbackGenerateToken(16);
747
+ }
748
+
749
+ private fallbackGenerateToken(length: number): string {
750
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
751
+ let result = '';
752
+ for (let i = 0; i < length; i++) {
753
+ result += chars.charAt(Math.floor(Math.random() * chars.length));
754
+ }
755
+ return result;
756
+ }
757
+
758
+ private isValidEmail(email: string): boolean {
759
+ if (this.emailValidator) {
760
+ return this.emailValidator.validate(email).isValid;
761
+ }
762
+ // Fallback basic validation
763
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
764
+ return emailRegex.test(email);
765
+ }
766
+
767
+ private async findSenderByEmail(email: string): Promise<SenderIdentity | undefined> {
768
+ return Array.from(this.senders.values()).find(s => s.email === email && !s.deletedAt);
769
+ }
770
+
771
+ private async findSenderByToken(token: string): Promise<SenderIdentity | undefined> {
772
+ return Array.from(this.senders.values()).find(s => s.verificationToken === token);
773
+ }
774
+
775
+ private async unsetOtherDefaults(provider: EmailProvider): Promise<void> {
776
+ const senders = Array.from(this.senders.values())
777
+ .filter(s => s.provider === provider && s.isDefault);
778
+
779
+ for (const sender of senders) {
780
+ sender.isDefault = false;
781
+ sender.updatedAt = new Date();
782
+ await this.saveSenderToDatabase(sender);
783
+ }
784
+ }
785
+
786
+ private async loadSendersFromDatabase(): Promise<void> {
787
+ // TODO: Implement database loading
788
+ // For now, no-op
789
+ }
790
+
791
+ private async saveSenderToDatabase(sender: SenderIdentity): Promise<void> {
792
+ // TODO: Implement database persistence
793
+ // For now, just update in-memory map
794
+ this.senders.set(sender.id, sender);
795
+ }
796
+ }