@astralibx/email-account-manager 2.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.
package/dist/index.mjs ADDED
@@ -0,0 +1,2999 @@
1
+ import { z } from 'zod';
2
+ import { baseRedisSchema, baseDbSchema, loggerSchema, AlxError } from '@astralibx/core';
3
+ import { Schema, Types } from 'mongoose';
4
+ import crypto from 'crypto';
5
+ import { Queue, Worker } from 'bullmq';
6
+ import nodemailer from 'nodemailer';
7
+ import https from 'https';
8
+ import { Router, text, json } from 'express';
9
+
10
+ // src/validation/config.schema.ts
11
+ var AlxAccountError = class extends AlxError {
12
+ constructor(message, code) {
13
+ super(message, code);
14
+ this.code = code;
15
+ this.name = "AlxAccountError";
16
+ }
17
+ };
18
+ var ConfigValidationError = class extends AlxAccountError {
19
+ constructor(message, field) {
20
+ super(message, "CONFIG_VALIDATION");
21
+ this.field = field;
22
+ this.name = "ConfigValidationError";
23
+ }
24
+ };
25
+ var AccountDisabledError = class extends AlxAccountError {
26
+ constructor(accountId, reason) {
27
+ super(`Account ${accountId} is disabled: ${reason}`, "ACCOUNT_DISABLED");
28
+ this.accountId = accountId;
29
+ this.reason = reason;
30
+ this.name = "AccountDisabledError";
31
+ }
32
+ };
33
+ var NoAvailableAccountError = class extends AlxAccountError {
34
+ constructor() {
35
+ super("No available accounts with remaining capacity", "NO_AVAILABLE_ACCOUNT");
36
+ this.name = "NoAvailableAccountError";
37
+ }
38
+ };
39
+ var SmtpConnectionError = class extends AlxAccountError {
40
+ constructor(accountId, originalError) {
41
+ super(`SMTP connection failed for account ${accountId}: ${originalError.message}`, "SMTP_CONNECTION");
42
+ this.accountId = accountId;
43
+ this.originalError = originalError;
44
+ this.name = "SmtpConnectionError";
45
+ }
46
+ };
47
+ var InvalidTokenError = class extends AlxAccountError {
48
+ constructor(tokenType) {
49
+ super(`Invalid or expired ${tokenType} token`, "INVALID_TOKEN");
50
+ this.tokenType = tokenType;
51
+ this.name = "InvalidTokenError";
52
+ }
53
+ };
54
+ var QuotaExceededError = class extends AlxAccountError {
55
+ constructor(accountId, dailyMax, currentSent) {
56
+ super(
57
+ `Account ${accountId} exceeded daily quota: ${currentSent}/${dailyMax}`,
58
+ "QUOTA_EXCEEDED"
59
+ );
60
+ this.accountId = accountId;
61
+ this.dailyMax = dailyMax;
62
+ this.currentSent = currentSent;
63
+ this.name = "QuotaExceededError";
64
+ }
65
+ };
66
+ var SnsSignatureError = class extends AlxAccountError {
67
+ constructor() {
68
+ super("SNS message signature verification failed", "SNS_SIGNATURE_INVALID");
69
+ this.name = "SnsSignatureError";
70
+ }
71
+ };
72
+ var AccountNotFoundError = class extends AlxAccountError {
73
+ constructor(accountId) {
74
+ super(`Account not found: ${accountId}`, "ACCOUNT_NOT_FOUND");
75
+ this.accountId = accountId;
76
+ this.name = "AccountNotFoundError";
77
+ }
78
+ };
79
+ var DraftNotFoundError = class extends AlxAccountError {
80
+ constructor(draftId) {
81
+ super(`Draft not found: ${draftId}`, "DRAFT_NOT_FOUND");
82
+ this.draftId = draftId;
83
+ this.name = "DraftNotFoundError";
84
+ }
85
+ };
86
+
87
+ // src/validation/config.schema.ts
88
+ var warmupPhaseSchema = z.object({
89
+ days: z.tuple([z.number().int().min(0), z.number().int().min(0)]),
90
+ dailyLimit: z.number().int().positive(),
91
+ delayMinMs: z.number().int().min(0),
92
+ delayMaxMs: z.number().int().min(0)
93
+ });
94
+ var configSchema = z.object({
95
+ db: baseDbSchema,
96
+ redis: baseRedisSchema,
97
+ logger: loggerSchema.optional(),
98
+ options: z.object({
99
+ warmup: z.object({
100
+ defaultSchedule: z.array(warmupPhaseSchema).min(1)
101
+ }).optional(),
102
+ healthDefaults: z.object({
103
+ minScore: z.number().min(0).max(100).optional(),
104
+ maxBounceRate: z.number().min(0).max(100).optional(),
105
+ maxConsecutiveErrors: z.number().int().positive().optional()
106
+ }).optional(),
107
+ ses: z.object({
108
+ enabled: z.boolean(),
109
+ validateSignature: z.boolean().optional(),
110
+ allowedTopicArns: z.array(z.string()).optional()
111
+ }).optional(),
112
+ unsubscribe: z.object({
113
+ builtin: z.object({
114
+ enabled: z.boolean(),
115
+ secret: z.string().min(1),
116
+ baseUrl: z.string().url(),
117
+ tokenExpiryDays: z.number().int().positive().optional()
118
+ }).optional(),
119
+ generateUrl: z.function().optional()
120
+ }).optional(),
121
+ queues: z.object({
122
+ sendQueueName: z.string().optional(),
123
+ approvalQueueName: z.string().optional()
124
+ }).optional()
125
+ }).optional(),
126
+ hooks: z.object({
127
+ onAccountDisabled: z.function().optional(),
128
+ onWarmupComplete: z.function().optional(),
129
+ onHealthDegraded: z.function().optional(),
130
+ onSend: z.function().optional(),
131
+ onSendError: z.function().optional(),
132
+ onBounce: z.function().optional(),
133
+ onUnsubscribe: z.function().optional(),
134
+ onDelivery: z.function().optional(),
135
+ onComplaint: z.function().optional(),
136
+ onOpen: z.function().optional(),
137
+ onClick: z.function().optional(),
138
+ onDraftCreated: z.function().optional(),
139
+ onDraftApproved: z.function().optional(),
140
+ onDraftRejected: z.function().optional()
141
+ }).optional()
142
+ });
143
+ function validateConfig(raw) {
144
+ const result = configSchema.safeParse(raw);
145
+ if (!result.success) {
146
+ const issues = result.error.issues.map((i) => ` ${i.path.join(".")}: ${i.message}`).join("\n");
147
+ throw new ConfigValidationError(
148
+ `Invalid EmailAccountManagerConfig:
149
+ ${issues}`,
150
+ result.error.issues[0]?.path.join(".") ?? ""
151
+ );
152
+ }
153
+ }
154
+
155
+ // src/constants/index.ts
156
+ var ACCOUNT_PROVIDER = {
157
+ Gmail: "gmail",
158
+ Ses: "ses"
159
+ };
160
+ var ACCOUNT_STATUS = {
161
+ Active: "active",
162
+ Disabled: "disabled",
163
+ QuotaExceeded: "quota_exceeded",
164
+ Error: "error",
165
+ Warmup: "warmup"
166
+ };
167
+ var IDENTIFIER_STATUS = {
168
+ Active: "active",
169
+ Bounced: "bounced",
170
+ Unsubscribed: "unsubscribed",
171
+ Blocked: "blocked",
172
+ Invalid: "invalid"
173
+ };
174
+ var BOUNCE_TYPE = {
175
+ Hard: "hard",
176
+ Soft: "soft",
177
+ InboxFull: "inbox_full",
178
+ InvalidEmail: "invalid_email"
179
+ };
180
+ var DRAFT_STATUS = {
181
+ Pending: "pending",
182
+ Approved: "approved",
183
+ Rejected: "rejected",
184
+ Queued: "queued",
185
+ Sent: "sent",
186
+ Failed: "failed"
187
+ };
188
+ var EMAIL_EVENT_TYPE = {
189
+ Sent: "sent",
190
+ Failed: "failed",
191
+ Delivered: "delivered",
192
+ Bounced: "bounced",
193
+ Complained: "complained",
194
+ Opened: "opened",
195
+ Clicked: "clicked",
196
+ Unsubscribed: "unsubscribed"
197
+ };
198
+ var SES_BOUNCE_TYPE = {
199
+ Permanent: "Permanent",
200
+ Transient: "Transient",
201
+ Undetermined: "Undetermined"
202
+ };
203
+ var SES_COMPLAINT_TYPE = {
204
+ Abuse: "abuse",
205
+ AuthFailure: "auth-failure",
206
+ Fraud: "fraud",
207
+ NotSpam: "not-spam",
208
+ Other: "other",
209
+ Virus: "virus"
210
+ };
211
+ var SNS_MESSAGE_TYPE = {
212
+ Notification: "Notification",
213
+ SubscriptionConfirmation: "SubscriptionConfirmation",
214
+ UnsubscribeConfirmation: "UnsubscribeConfirmation"
215
+ };
216
+ var SES_NOTIFICATION_TYPE = {
217
+ Bounce: "Bounce",
218
+ Complaint: "Complaint",
219
+ Delivery: "Delivery",
220
+ Send: "Send",
221
+ Open: "Open",
222
+ Click: "Click"
223
+ };
224
+
225
+ // src/schemas/email-account.schema.ts
226
+ function createEmailAccountSchema(options) {
227
+ const schema = new Schema(
228
+ {
229
+ email: { type: String, required: true, unique: true, lowercase: true },
230
+ senderName: { type: String, required: true },
231
+ provider: { type: String, enum: Object.values(ACCOUNT_PROVIDER), required: true },
232
+ status: {
233
+ type: String,
234
+ enum: Object.values(ACCOUNT_STATUS),
235
+ default: ACCOUNT_STATUS.Active,
236
+ index: true
237
+ },
238
+ // WARNING: SMTP/IMAP credentials are stored as plaintext in the database.
239
+ // Consumers MUST encrypt `smtp.pass` and `imap.pass` at the application layer
240
+ // before storing, and decrypt after retrieval. A built-in encryption layer
241
+ // is planned for a future version.
242
+ smtp: {
243
+ type: {
244
+ host: { type: String, required: true },
245
+ port: { type: Number, required: true },
246
+ user: { type: String, required: true },
247
+ pass: { type: String, required: true }
248
+ },
249
+ required: true,
250
+ _id: false
251
+ },
252
+ imap: {
253
+ type: {
254
+ host: { type: String, required: true },
255
+ port: { type: Number, required: true },
256
+ user: { type: String, required: true },
257
+ pass: { type: String, required: true }
258
+ },
259
+ _id: false
260
+ },
261
+ ses: {
262
+ type: {
263
+ region: { type: String, required: true },
264
+ configurationSet: String
265
+ },
266
+ _id: false
267
+ },
268
+ limits: {
269
+ type: {
270
+ dailyMax: { type: Number, required: true, default: 100 }
271
+ },
272
+ required: true,
273
+ _id: false
274
+ },
275
+ health: {
276
+ type: {
277
+ score: { type: Number, default: 100, min: 0, max: 100 },
278
+ consecutiveErrors: { type: Number, default: 0 },
279
+ bounceCount: { type: Number, default: 0 },
280
+ thresholds: {
281
+ type: {
282
+ minScore: { type: Number, default: 50 },
283
+ maxBounceRate: { type: Number, default: 5 },
284
+ maxConsecutiveErrors: { type: Number, default: 10 }
285
+ },
286
+ _id: false
287
+ }
288
+ },
289
+ required: true,
290
+ _id: false
291
+ },
292
+ warmup: {
293
+ type: {
294
+ enabled: { type: Boolean, default: true },
295
+ startedAt: Date,
296
+ completedAt: Date,
297
+ currentDay: { type: Number, default: 1 },
298
+ schedule: [{
299
+ days: { type: [Number], required: true },
300
+ dailyLimit: { type: Number, required: true },
301
+ delayMinMs: { type: Number, required: true },
302
+ delayMaxMs: { type: Number, required: true },
303
+ _id: false
304
+ }]
305
+ },
306
+ required: true,
307
+ _id: false
308
+ },
309
+ totalEmailsSent: { type: Number, default: 0 },
310
+ lastSuccessfulSendAt: Date,
311
+ lastImapCheckAt: Date
312
+ },
313
+ {
314
+ timestamps: true,
315
+ collection: options?.collectionName || "email_accounts",
316
+ statics: {
317
+ findActive() {
318
+ return this.find({
319
+ status: { $in: [ACCOUNT_STATUS.Active, ACCOUNT_STATUS.Warmup] }
320
+ });
321
+ },
322
+ findByProvider(provider) {
323
+ return this.find({ provider });
324
+ },
325
+ findByEmail(email) {
326
+ return this.findOne({ email: email.toLowerCase().trim() });
327
+ },
328
+ async getBestAvailable() {
329
+ const accounts = await this.find({
330
+ status: { $in: [ACCOUNT_STATUS.Active, ACCOUNT_STATUS.Warmup] }
331
+ });
332
+ if (accounts.length === 0) return null;
333
+ return accounts.sort((a, b) => b.health.score - a.health.score)[0] || null;
334
+ }
335
+ }
336
+ }
337
+ );
338
+ schema.index({ status: 1, "health.score": -1 });
339
+ schema.index({ "health.score": -1 });
340
+ schema.index({ provider: 1 });
341
+ return schema;
342
+ }
343
+ function createEmailDailyStatsSchema(options) {
344
+ const schema = new Schema(
345
+ {
346
+ accountId: { type: Schema.Types.ObjectId, required: true, index: true },
347
+ date: { type: String, required: true, index: true },
348
+ sent: { type: Number, default: 0 },
349
+ failed: { type: Number, default: 0 },
350
+ bounced: { type: Number, default: 0 },
351
+ delivered: { type: Number, default: 0 },
352
+ complained: { type: Number, default: 0 },
353
+ opened: { type: Number, default: 0 },
354
+ clicked: { type: Number, default: 0 },
355
+ unsubscribed: { type: Number, default: 0 }
356
+ },
357
+ {
358
+ timestamps: true,
359
+ collection: options?.collectionName || "email_daily_stats",
360
+ statics: {
361
+ incrementStat(accountId, field, count = 1, date) {
362
+ const targetDate = date || (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
363
+ const update = {};
364
+ update[field] = count;
365
+ return this.findOneAndUpdate(
366
+ { accountId: new Types.ObjectId(accountId), date: targetDate },
367
+ { $inc: update },
368
+ { upsert: true, new: true }
369
+ );
370
+ },
371
+ getForAccount(accountId, days = 30) {
372
+ const startDate = /* @__PURE__ */ new Date();
373
+ startDate.setDate(startDate.getDate() - days);
374
+ const startDateStr = startDate.toISOString().split("T")[0];
375
+ return this.find({
376
+ accountId: new Types.ObjectId(accountId),
377
+ date: { $gte: startDateStr }
378
+ }).sort({ date: -1 });
379
+ },
380
+ getForDate(date) {
381
+ return this.find({ date });
382
+ }
383
+ }
384
+ }
385
+ );
386
+ schema.index({ accountId: 1, date: -1 }, { unique: true });
387
+ return schema;
388
+ }
389
+ function createEmailIdentifierSchema(options) {
390
+ const schema = new Schema(
391
+ {
392
+ email: { type: String, required: true, unique: true, lowercase: true },
393
+ status: {
394
+ type: String,
395
+ enum: Object.values(IDENTIFIER_STATUS),
396
+ default: IDENTIFIER_STATUS.Active,
397
+ index: true
398
+ },
399
+ sentCount: { type: Number, default: 0 },
400
+ bounceCount: { type: Number, default: 0 },
401
+ lastSentAt: Date,
402
+ lastBouncedAt: Date,
403
+ bounceType: { type: String, enum: Object.values(BOUNCE_TYPE) },
404
+ unsubscribedAt: Date,
405
+ metadata: { type: Schema.Types.Mixed }
406
+ },
407
+ {
408
+ timestamps: true,
409
+ collection: options?.collectionName || "email_identifiers",
410
+ statics: {
411
+ async findOrCreate(email) {
412
+ const normalized = email.toLowerCase().trim();
413
+ const existing = await this.findOne({ email: normalized });
414
+ if (existing) {
415
+ return { identifier: existing, created: false };
416
+ }
417
+ const identifier = await this.findOneAndUpdate(
418
+ { email: normalized },
419
+ {
420
+ $setOnInsert: {
421
+ email: normalized,
422
+ status: IDENTIFIER_STATUS.Active,
423
+ sentCount: 0,
424
+ bounceCount: 0
425
+ }
426
+ },
427
+ { upsert: true, new: true }
428
+ );
429
+ return { identifier, created: true };
430
+ },
431
+ markBounced(email, bounceType) {
432
+ const normalized = email.toLowerCase().trim();
433
+ return this.findOneAndUpdate(
434
+ { email: normalized },
435
+ {
436
+ $set: {
437
+ status: IDENTIFIER_STATUS.Bounced,
438
+ bounceType,
439
+ lastBouncedAt: /* @__PURE__ */ new Date()
440
+ },
441
+ $inc: { bounceCount: 1 }
442
+ },
443
+ { new: true }
444
+ );
445
+ },
446
+ markUnsubscribed(email) {
447
+ const normalized = email.toLowerCase().trim();
448
+ return this.findOneAndUpdate(
449
+ { email: normalized },
450
+ {
451
+ $set: {
452
+ status: IDENTIFIER_STATUS.Unsubscribed,
453
+ unsubscribedAt: /* @__PURE__ */ new Date()
454
+ }
455
+ },
456
+ { new: true }
457
+ );
458
+ },
459
+ incrementSentCount(email) {
460
+ const normalized = email.toLowerCase().trim();
461
+ return this.findOneAndUpdate(
462
+ { email: normalized },
463
+ {
464
+ $inc: { sentCount: 1 },
465
+ $set: { lastSentAt: /* @__PURE__ */ new Date() }
466
+ },
467
+ { new: true, upsert: true }
468
+ );
469
+ },
470
+ findByEmail(email) {
471
+ const normalized = email.toLowerCase().trim();
472
+ return this.findOne({ email: normalized });
473
+ }
474
+ }
475
+ }
476
+ );
477
+ schema.index({ email: 1 }, { unique: true });
478
+ schema.index({ status: 1 });
479
+ return schema;
480
+ }
481
+ function createEmailDraftSchema(options) {
482
+ const schema = new Schema(
483
+ {
484
+ to: { type: String, required: true, lowercase: true },
485
+ subject: { type: String, required: true },
486
+ htmlBody: { type: String, required: true },
487
+ textBody: String,
488
+ accountId: { type: Schema.Types.ObjectId, required: true, index: true },
489
+ status: {
490
+ type: String,
491
+ enum: Object.values(DRAFT_STATUS),
492
+ default: DRAFT_STATUS.Pending,
493
+ index: true
494
+ },
495
+ approvedAt: Date,
496
+ rejectedAt: Date,
497
+ rejectionReason: String,
498
+ sentAt: Date,
499
+ scheduledAt: Date,
500
+ failureReason: String,
501
+ metadata: { type: Schema.Types.Mixed }
502
+ },
503
+ {
504
+ timestamps: true,
505
+ collection: options?.collectionName || "email_drafts",
506
+ statics: {
507
+ findPending(limit = 50) {
508
+ return this.find({ status: DRAFT_STATUS.Pending }).sort({ createdAt: -1 }).limit(limit);
509
+ },
510
+ findByStatus(status, limit = 50) {
511
+ return this.find({ status }).sort({ createdAt: -1 }).limit(limit);
512
+ },
513
+ async countByStatus() {
514
+ const results = await this.aggregate([
515
+ { $group: { _id: "$status", count: { $sum: 1 } } }
516
+ ]);
517
+ const counts = {};
518
+ for (const status of Object.values(DRAFT_STATUS)) {
519
+ counts[status] = 0;
520
+ }
521
+ for (const result of results) {
522
+ counts[result._id] = result.count;
523
+ }
524
+ return counts;
525
+ }
526
+ }
527
+ }
528
+ );
529
+ schema.index({ status: 1, createdAt: -1 });
530
+ schema.index({ accountId: 1, status: 1 });
531
+ return schema;
532
+ }
533
+ var DEFAULT_SETTINGS = {
534
+ timezone: "UTC",
535
+ devMode: {
536
+ enabled: false,
537
+ testEmails: []
538
+ },
539
+ imap: {
540
+ enabled: false,
541
+ pollIntervalMs: 3e5,
542
+ searchSince: "last_check",
543
+ bounceSenders: ["mailer-daemon@googlemail.com"]
544
+ },
545
+ ses: {
546
+ configurationSet: void 0,
547
+ trackOpens: true,
548
+ trackClicks: true
549
+ },
550
+ approval: {
551
+ enabled: false,
552
+ defaultMode: "manual",
553
+ autoApproveDelayMs: 0,
554
+ sendWindow: {
555
+ timezone: "UTC",
556
+ startHour: 9,
557
+ endHour: 21
558
+ },
559
+ spreadStrategy: "random",
560
+ maxSpreadMinutes: 120
561
+ },
562
+ unsubscribePage: {
563
+ companyName: "",
564
+ logoUrl: void 0,
565
+ accentColor: void 0
566
+ },
567
+ queues: {
568
+ sendConcurrency: 3,
569
+ sendAttempts: 3,
570
+ sendBackoffMs: 5e3,
571
+ approvalConcurrency: 1,
572
+ approvalAttempts: 3,
573
+ approvalBackoffMs: 1e4
574
+ }
575
+ };
576
+ function createGlobalSettingsSchema(options) {
577
+ const schema = new Schema(
578
+ {
579
+ _id: { type: String, default: "global" },
580
+ timezone: { type: String, default: DEFAULT_SETTINGS.timezone },
581
+ devMode: {
582
+ type: {
583
+ enabled: { type: Boolean, default: false },
584
+ testEmails: [{ type: String }]
585
+ },
586
+ default: () => ({ ...DEFAULT_SETTINGS.devMode }),
587
+ _id: false
588
+ },
589
+ imap: {
590
+ type: {
591
+ enabled: { type: Boolean, default: false },
592
+ pollIntervalMs: { type: Number, default: 3e5 },
593
+ searchSince: { type: String, enum: ["last_check", "last_24h", "last_7d"], default: "last_check" },
594
+ bounceSenders: [{ type: String }]
595
+ },
596
+ default: () => ({ ...DEFAULT_SETTINGS.imap }),
597
+ _id: false
598
+ },
599
+ ses: {
600
+ type: {
601
+ configurationSet: String,
602
+ trackOpens: { type: Boolean, default: true },
603
+ trackClicks: { type: Boolean, default: true }
604
+ },
605
+ default: () => ({ ...DEFAULT_SETTINGS.ses }),
606
+ _id: false
607
+ },
608
+ approval: {
609
+ type: {
610
+ enabled: { type: Boolean, default: false },
611
+ defaultMode: { type: String, enum: ["manual", "auto"], default: "manual" },
612
+ autoApproveDelayMs: { type: Number, default: 0 },
613
+ sendWindow: {
614
+ type: {
615
+ timezone: { type: String, default: "UTC" },
616
+ startHour: { type: Number, default: 9, min: 0, max: 23 },
617
+ endHour: { type: Number, default: 21, min: 0, max: 23 }
618
+ },
619
+ _id: false
620
+ },
621
+ spreadStrategy: { type: String, enum: ["random", "even"], default: "random" },
622
+ maxSpreadMinutes: { type: Number, default: 120 }
623
+ },
624
+ default: () => ({ ...DEFAULT_SETTINGS.approval, sendWindow: { ...DEFAULT_SETTINGS.approval.sendWindow } }),
625
+ _id: false
626
+ },
627
+ unsubscribePage: {
628
+ type: {
629
+ companyName: { type: String, default: "" },
630
+ logoUrl: String,
631
+ accentColor: String
632
+ },
633
+ default: () => ({ ...DEFAULT_SETTINGS.unsubscribePage }),
634
+ _id: false
635
+ },
636
+ queues: {
637
+ type: {
638
+ sendConcurrency: { type: Number, default: 3 },
639
+ sendAttempts: { type: Number, default: 3 },
640
+ sendBackoffMs: { type: Number, default: 5e3 },
641
+ approvalConcurrency: { type: Number, default: 1 },
642
+ approvalAttempts: { type: Number, default: 3 },
643
+ approvalBackoffMs: { type: Number, default: 1e4 }
644
+ },
645
+ default: () => ({ ...DEFAULT_SETTINGS.queues }),
646
+ _id: false
647
+ }
648
+ },
649
+ {
650
+ timestamps: { createdAt: false, updatedAt: true },
651
+ collection: options?.collectionName || "global_settings",
652
+ statics: {
653
+ async getSettings() {
654
+ let doc = await this.findById("global");
655
+ if (!doc) {
656
+ doc = await this.create({ _id: "global", ...DEFAULT_SETTINGS });
657
+ }
658
+ return doc;
659
+ },
660
+ async updateSettings(partial) {
661
+ const flatUpdate = {};
662
+ for (const [key, value] of Object.entries(partial)) {
663
+ if (value !== void 0 && typeof value === "object" && !Array.isArray(value)) {
664
+ for (const [subKey, subValue] of Object.entries(value)) {
665
+ flatUpdate[`${key}.${subKey}`] = subValue;
666
+ }
667
+ } else {
668
+ flatUpdate[key] = value;
669
+ }
670
+ }
671
+ const doc = await this.findOneAndUpdate(
672
+ { _id: "global" },
673
+ { $set: flatUpdate },
674
+ { new: true, upsert: true }
675
+ );
676
+ return doc;
677
+ }
678
+ }
679
+ }
680
+ );
681
+ return schema;
682
+ }
683
+
684
+ // src/services/settings.service.ts
685
+ var DEFAULTS = {
686
+ timezone: "UTC",
687
+ devMode: { enabled: false, testEmails: [] },
688
+ imap: {
689
+ enabled: false,
690
+ pollIntervalMs: 3e5,
691
+ searchSince: "last_check",
692
+ bounceSenders: ["mailer-daemon@googlemail.com"]
693
+ },
694
+ ses: { trackOpens: true, trackClicks: true },
695
+ approval: {
696
+ enabled: false,
697
+ defaultMode: "manual",
698
+ autoApproveDelayMs: 0,
699
+ sendWindow: { timezone: "UTC", startHour: 9, endHour: 21 },
700
+ spreadStrategy: "random",
701
+ maxSpreadMinutes: 120
702
+ },
703
+ unsubscribePage: { companyName: "" },
704
+ queues: {
705
+ sendConcurrency: 3,
706
+ sendAttempts: 3,
707
+ sendBackoffMs: 5e3,
708
+ approvalConcurrency: 1,
709
+ approvalAttempts: 3,
710
+ approvalBackoffMs: 1e4
711
+ }
712
+ };
713
+ var SettingsService = class {
714
+ constructor(GlobalSettings, logger) {
715
+ this.GlobalSettings = GlobalSettings;
716
+ this.logger = logger;
717
+ }
718
+ cache = null;
719
+ async get() {
720
+ if (this.cache) return this.cache;
721
+ let doc = await this.GlobalSettings.findById("global").lean();
722
+ if (!doc) {
723
+ doc = await this.GlobalSettings.create({
724
+ _id: "global",
725
+ ...DEFAULTS,
726
+ updatedAt: /* @__PURE__ */ new Date()
727
+ });
728
+ this.logger.info("GlobalSettings created with defaults");
729
+ }
730
+ this.cache = doc;
731
+ return doc;
732
+ }
733
+ async update(partial) {
734
+ const flattened = flattenObject({ ...partial, updatedAt: /* @__PURE__ */ new Date() });
735
+ const doc = await this.GlobalSettings.findByIdAndUpdate(
736
+ "global",
737
+ { $set: flattened },
738
+ { new: true, upsert: true }
739
+ ).lean();
740
+ this.cache = doc;
741
+ this.logger.info("GlobalSettings updated", { sections: Object.keys(partial) });
742
+ return doc;
743
+ }
744
+ async updateSection(section, data) {
745
+ const setFields = { updatedAt: /* @__PURE__ */ new Date() };
746
+ if (typeof data === "object" && data !== null) {
747
+ for (const [key, value] of Object.entries(data)) {
748
+ setFields[`${section}.${key}`] = value;
749
+ }
750
+ } else {
751
+ setFields[section] = data;
752
+ }
753
+ const doc = await this.GlobalSettings.findByIdAndUpdate(
754
+ "global",
755
+ { $set: setFields },
756
+ { new: true, upsert: true }
757
+ ).lean();
758
+ this.cache = doc;
759
+ this.logger.info("GlobalSettings section updated", { section });
760
+ return doc;
761
+ }
762
+ invalidateCache() {
763
+ this.cache = null;
764
+ }
765
+ };
766
+ function flattenObject(obj, prefix = "") {
767
+ const result = {};
768
+ for (const [key, value] of Object.entries(obj)) {
769
+ const fullKey = prefix ? `${prefix}.${key}` : key;
770
+ if (value && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date)) {
771
+ Object.assign(result, flattenObject(value, fullKey));
772
+ } else {
773
+ result[fullKey] = value;
774
+ }
775
+ }
776
+ return result;
777
+ }
778
+
779
+ // src/services/identifier.service.ts
780
+ var IdentifierService = class {
781
+ constructor(EmailIdentifier, logger, hooks) {
782
+ this.EmailIdentifier = EmailIdentifier;
783
+ this.logger = logger;
784
+ this.hooks = hooks;
785
+ }
786
+ async findOrCreate(email) {
787
+ const normalized = email.toLowerCase().trim();
788
+ const doc = await this.EmailIdentifier.findOneAndUpdate(
789
+ { email: normalized },
790
+ {
791
+ $setOnInsert: {
792
+ email: normalized,
793
+ status: IDENTIFIER_STATUS.Active,
794
+ sentCount: 0,
795
+ bounceCount: 0
796
+ }
797
+ },
798
+ { upsert: true, new: true }
799
+ );
800
+ return doc;
801
+ }
802
+ async findByEmail(email) {
803
+ return this.EmailIdentifier.findOne({ email: email.toLowerCase().trim() });
804
+ }
805
+ async markBounced(email, bounceType) {
806
+ const normalized = email.toLowerCase().trim();
807
+ await this.EmailIdentifier.findOneAndUpdate(
808
+ { email: normalized },
809
+ {
810
+ $set: {
811
+ status: IDENTIFIER_STATUS.Bounced,
812
+ bounceType,
813
+ lastBouncedAt: /* @__PURE__ */ new Date()
814
+ },
815
+ $inc: { bounceCount: 1 }
816
+ },
817
+ { upsert: true }
818
+ );
819
+ this.logger.warn("Identifier marked bounced", { email: normalized, bounceType });
820
+ this.hooks?.onBounce?.({ accountId: "", email: normalized, bounceType, provider: "" });
821
+ }
822
+ async markUnsubscribed(email) {
823
+ const normalized = email.toLowerCase().trim();
824
+ await this.EmailIdentifier.findOneAndUpdate(
825
+ { email: normalized },
826
+ {
827
+ $set: {
828
+ status: IDENTIFIER_STATUS.Unsubscribed,
829
+ unsubscribedAt: /* @__PURE__ */ new Date()
830
+ }
831
+ },
832
+ { upsert: true }
833
+ );
834
+ this.logger.info("Identifier marked unsubscribed", { email: normalized });
835
+ this.hooks?.onUnsubscribe?.({ email: normalized });
836
+ }
837
+ async updateStatus(email, status) {
838
+ await this.EmailIdentifier.findOneAndUpdate(
839
+ { email: email.toLowerCase().trim() },
840
+ { $set: { status } }
841
+ );
842
+ }
843
+ async incrementSentCount(email) {
844
+ await this.EmailIdentifier.findOneAndUpdate(
845
+ { email: email.toLowerCase().trim() },
846
+ {
847
+ $inc: { sentCount: 1 },
848
+ $set: { lastSentAt: /* @__PURE__ */ new Date() }
849
+ }
850
+ );
851
+ }
852
+ async merge(sourceEmail, targetEmail) {
853
+ const source = await this.findByEmail(sourceEmail);
854
+ const target = await this.findByEmail(targetEmail);
855
+ if (!source) {
856
+ this.logger.warn("Merge source not found", { sourceEmail });
857
+ return;
858
+ }
859
+ if (!target) {
860
+ await this.EmailIdentifier.findOneAndUpdate(
861
+ { email: sourceEmail.toLowerCase().trim() },
862
+ { $set: { email: targetEmail.toLowerCase().trim() } }
863
+ );
864
+ return;
865
+ }
866
+ await this.EmailIdentifier.findByIdAndUpdate(target._id, {
867
+ $inc: {
868
+ sentCount: source.sentCount || 0,
869
+ bounceCount: source.bounceCount || 0
870
+ },
871
+ $set: {
872
+ ...source.lastSentAt && (!target.lastSentAt || source.lastSentAt > target.lastSentAt) ? { lastSentAt: source.lastSentAt } : {},
873
+ ...source.metadata ? { metadata: { ...source.metadata, ...target.metadata || {} } } : {}
874
+ }
875
+ });
876
+ await this.EmailIdentifier.findByIdAndDelete(source._id);
877
+ this.logger.info("Identifiers merged", { sourceEmail, targetEmail });
878
+ }
879
+ async list(filters, page = 1, limit = 50) {
880
+ const query = {};
881
+ if (filters?.status) query.status = filters.status;
882
+ const [items, total] = await Promise.all([
883
+ this.EmailIdentifier.find(query).sort({ updatedAt: -1 }).skip((page - 1) * limit).limit(limit),
884
+ this.EmailIdentifier.countDocuments(query)
885
+ ]);
886
+ return { items, total };
887
+ }
888
+ };
889
+
890
+ // src/services/health-tracker.ts
891
+ var HealthTracker = class {
892
+ constructor(EmailAccount, EmailDailyStats, settings, logger, hooks) {
893
+ this.EmailAccount = EmailAccount;
894
+ this.EmailDailyStats = EmailDailyStats;
895
+ this.settings = settings;
896
+ this.logger = logger;
897
+ this.hooks = hooks;
898
+ }
899
+ async recordSuccess(accountId) {
900
+ const updated = await this.EmailAccount.findByIdAndUpdate(
901
+ accountId,
902
+ [
903
+ {
904
+ $set: {
905
+ "health.score": { $min: [100, { $add: ["$health.score", 1] }] },
906
+ "health.consecutiveErrors": 0,
907
+ lastSuccessfulSendAt: /* @__PURE__ */ new Date(),
908
+ totalEmailsSent: { $add: ["$totalEmailsSent", 1] }
909
+ }
910
+ }
911
+ ],
912
+ { new: true }
913
+ );
914
+ if (!updated) return;
915
+ const dateStr = await this.getTodayDateString();
916
+ await this.incrementDailyStat(accountId, "sent", 1, dateStr);
917
+ }
918
+ async recordError(accountId, error) {
919
+ const updated = await this.EmailAccount.findByIdAndUpdate(
920
+ accountId,
921
+ [
922
+ {
923
+ $set: {
924
+ "health.score": { $max: [0, { $subtract: ["$health.score", 5] }] },
925
+ "health.consecutiveErrors": { $add: ["$health.consecutiveErrors", 1] }
926
+ }
927
+ }
928
+ ],
929
+ { new: true }
930
+ );
931
+ if (!updated) return;
932
+ const acct = updated;
933
+ const newScore = acct.health.score;
934
+ const newErrors = acct.health.consecutiveErrors;
935
+ this.logger.warn("Account health degraded", {
936
+ accountId,
937
+ error,
938
+ score: newScore,
939
+ consecutiveErrors: newErrors
940
+ });
941
+ this.hooks?.onHealthDegraded?.({ accountId, healthScore: newScore });
942
+ const thresholds = acct.health.thresholds;
943
+ const shouldDisable = newScore < thresholds.minScore || newErrors > thresholds.maxConsecutiveErrors;
944
+ if (shouldDisable) {
945
+ const reason = newScore < thresholds.minScore ? `Health score ${newScore} below minimum ${thresholds.minScore}` : `${newErrors} consecutive errors exceeds maximum ${thresholds.maxConsecutiveErrors}`;
946
+ await this.EmailAccount.findByIdAndUpdate(accountId, {
947
+ $set: { status: ACCOUNT_STATUS.Disabled }
948
+ });
949
+ this.logger.error("Account auto-disabled", { accountId, reason });
950
+ this.hooks?.onAccountDisabled?.({ accountId, reason });
951
+ }
952
+ }
953
+ async recordBounce(accountId, email, bounceType) {
954
+ const updated = await this.EmailAccount.findByIdAndUpdate(
955
+ accountId,
956
+ [
957
+ {
958
+ $set: {
959
+ "health.score": { $max: [0, { $subtract: ["$health.score", 10] }] },
960
+ "health.bounceCount": { $add: ["$health.bounceCount", 1] }
961
+ }
962
+ }
963
+ ],
964
+ { new: true }
965
+ );
966
+ if (!updated) return;
967
+ const acct = updated;
968
+ const newScore = acct.health.score;
969
+ const newBounceCount = acct.health.bounceCount;
970
+ const totalSent = acct.totalEmailsSent || 1;
971
+ const bounceRate = newBounceCount / totalSent * 100;
972
+ const dateStr = await this.getTodayDateString();
973
+ await this.incrementDailyStat(accountId, "bounced", 1, dateStr);
974
+ this.logger.warn("Bounce recorded", { accountId, email, bounceType, score: newScore });
975
+ this.hooks?.onBounce?.({
976
+ accountId,
977
+ email,
978
+ bounceType,
979
+ provider: acct.provider
980
+ });
981
+ if (bounceRate > acct.health.thresholds.maxBounceRate) {
982
+ const reason = `Bounce rate ${bounceRate.toFixed(1)}% exceeds maximum ${acct.health.thresholds.maxBounceRate}%`;
983
+ await this.EmailAccount.findByIdAndUpdate(accountId, {
984
+ $set: { status: ACCOUNT_STATUS.Disabled }
985
+ });
986
+ this.logger.error("Account auto-disabled due to bounce rate", { accountId, reason });
987
+ this.hooks?.onAccountDisabled?.({ accountId, reason });
988
+ }
989
+ }
990
+ async getHealth(accountId) {
991
+ const account = await this.EmailAccount.findById(accountId);
992
+ if (!account) return null;
993
+ return this.toAccountHealth(account);
994
+ }
995
+ async getAllHealth() {
996
+ const accounts = await this.EmailAccount.find();
997
+ return accounts.map((a) => this.toAccountHealth(a));
998
+ }
999
+ toAccountHealth(account) {
1000
+ return {
1001
+ accountId: account._id.toString(),
1002
+ email: account.email,
1003
+ score: account.health.score,
1004
+ consecutiveErrors: account.health.consecutiveErrors,
1005
+ bounceCount: account.health.bounceCount,
1006
+ thresholds: account.health.thresholds,
1007
+ status: account.status
1008
+ };
1009
+ }
1010
+ async getTodayDateString() {
1011
+ const globalSettings = await this.settings.get();
1012
+ const tz = globalSettings.timezone || "UTC";
1013
+ const now = /* @__PURE__ */ new Date();
1014
+ const formatted = now.toLocaleDateString("en-CA", { timeZone: tz });
1015
+ return formatted;
1016
+ }
1017
+ async incrementDailyStat(accountId, field, amount, date) {
1018
+ await this.EmailDailyStats.findOneAndUpdate(
1019
+ { accountId, date },
1020
+ { $inc: { [field]: amount } },
1021
+ { upsert: true }
1022
+ );
1023
+ }
1024
+ };
1025
+
1026
+ // src/services/warmup-manager.ts
1027
+ var WarmupManager = class {
1028
+ constructor(EmailAccount, config, logger, hooks) {
1029
+ this.EmailAccount = EmailAccount;
1030
+ this.config = config;
1031
+ this.logger = logger;
1032
+ this.hooks = hooks;
1033
+ }
1034
+ async startWarmup(accountId) {
1035
+ const defaultSchedule = this.config.options?.warmup?.defaultSchedule;
1036
+ await this.EmailAccount.findByIdAndUpdate(accountId, {
1037
+ $set: {
1038
+ "warmup.enabled": true,
1039
+ "warmup.startedAt": /* @__PURE__ */ new Date(),
1040
+ "warmup.completedAt": null,
1041
+ "warmup.currentDay": 1,
1042
+ status: ACCOUNT_STATUS.Warmup,
1043
+ ...defaultSchedule ? { "warmup.schedule": defaultSchedule } : {}
1044
+ }
1045
+ });
1046
+ this.logger.info("Warmup started", { accountId });
1047
+ }
1048
+ async completeWarmup(accountId) {
1049
+ const account = await this.EmailAccount.findByIdAndUpdate(
1050
+ accountId,
1051
+ {
1052
+ $set: {
1053
+ "warmup.enabled": false,
1054
+ "warmup.completedAt": /* @__PURE__ */ new Date(),
1055
+ status: ACCOUNT_STATUS.Active
1056
+ }
1057
+ },
1058
+ { new: true }
1059
+ );
1060
+ if (account) {
1061
+ this.logger.info("Warmup completed", { accountId, email: account.email });
1062
+ this.hooks?.onWarmupComplete?.({
1063
+ accountId,
1064
+ email: account.email
1065
+ });
1066
+ }
1067
+ }
1068
+ async resetWarmup(accountId) {
1069
+ await this.EmailAccount.findByIdAndUpdate(accountId, {
1070
+ $set: {
1071
+ "warmup.enabled": true,
1072
+ "warmup.startedAt": /* @__PURE__ */ new Date(),
1073
+ "warmup.completedAt": null,
1074
+ "warmup.currentDay": 1,
1075
+ status: ACCOUNT_STATUS.Warmup
1076
+ }
1077
+ });
1078
+ this.logger.info("Warmup reset", { accountId });
1079
+ }
1080
+ async getCurrentPhase(accountId) {
1081
+ const account = await this.EmailAccount.findById(accountId);
1082
+ if (!account) return null;
1083
+ const warmup = account.warmup;
1084
+ if (!warmup?.enabled || !warmup.schedule) return null;
1085
+ return this.findPhaseForDay(warmup.schedule, warmup.currentDay);
1086
+ }
1087
+ async getRecommendedDelay(accountId) {
1088
+ const phase = await this.getCurrentPhase(accountId);
1089
+ if (!phase) return 0;
1090
+ return Math.floor(Math.random() * (phase.delayMaxMs - phase.delayMinMs + 1)) + phase.delayMinMs;
1091
+ }
1092
+ async getDailyLimit(accountId) {
1093
+ const account = await this.EmailAccount.findById(accountId);
1094
+ if (!account) return 0;
1095
+ const warmup = account.warmup;
1096
+ if (!warmup?.enabled) return account.limits.dailyMax;
1097
+ const phase = this.findPhaseForDay(warmup.schedule, warmup.currentDay);
1098
+ return phase ? phase.dailyLimit : account.limits.dailyMax;
1099
+ }
1100
+ async advanceDay(accountId) {
1101
+ const account = await this.EmailAccount.findById(accountId);
1102
+ if (!account) return;
1103
+ const warmup = account.warmup;
1104
+ if (!warmup?.enabled) return;
1105
+ const nextDay = warmup.currentDay + 1;
1106
+ const maxDay = this.getMaxDay(warmup.schedule);
1107
+ if (maxDay > 0 && nextDay > maxDay) {
1108
+ await this.completeWarmup(accountId);
1109
+ } else {
1110
+ await this.EmailAccount.findByIdAndUpdate(accountId, {
1111
+ $set: { "warmup.currentDay": nextDay }
1112
+ });
1113
+ }
1114
+ }
1115
+ async getStatus(accountId) {
1116
+ const account = await this.EmailAccount.findById(accountId);
1117
+ if (!account) return null;
1118
+ const acct = account;
1119
+ const warmup = acct.warmup;
1120
+ const phase = warmup?.schedule ? this.findPhaseForDay(warmup.schedule, warmup.currentDay) : null;
1121
+ return {
1122
+ accountId: acct._id.toString(),
1123
+ email: acct.email,
1124
+ enabled: warmup?.enabled ?? false,
1125
+ currentDay: warmup?.currentDay ?? 0,
1126
+ startedAt: warmup?.startedAt,
1127
+ completedAt: warmup?.completedAt,
1128
+ currentPhase: phase,
1129
+ dailyLimit: phase ? phase.dailyLimit : acct.limits.dailyMax,
1130
+ delayRange: phase ? { min: phase.delayMinMs, max: phase.delayMaxMs } : { min: 0, max: 0 }
1131
+ };
1132
+ }
1133
+ async updateSchedule(accountId, schedule) {
1134
+ await this.EmailAccount.findByIdAndUpdate(accountId, {
1135
+ $set: { "warmup.schedule": schedule }
1136
+ });
1137
+ this.logger.info("Warmup schedule updated", { accountId, phases: schedule.length });
1138
+ }
1139
+ findPhaseForDay(schedule, day) {
1140
+ for (const phase of schedule) {
1141
+ const [start, end] = phase.days;
1142
+ if (end === 0) {
1143
+ if (day >= start) return phase;
1144
+ } else {
1145
+ if (day >= start && day <= end) return phase;
1146
+ }
1147
+ }
1148
+ return null;
1149
+ }
1150
+ getMaxDay(schedule) {
1151
+ let max = 0;
1152
+ for (const phase of schedule) {
1153
+ if (phase.days[1] === 0) return 0;
1154
+ if (phase.days[1] > max) max = phase.days[1];
1155
+ }
1156
+ return max;
1157
+ }
1158
+ };
1159
+
1160
+ // src/services/capacity-manager.ts
1161
+ var CapacityManager = class {
1162
+ constructor(EmailAccount, EmailDailyStats, warmupManager, settings, logger) {
1163
+ this.EmailAccount = EmailAccount;
1164
+ this.EmailDailyStats = EmailDailyStats;
1165
+ this.warmupManager = warmupManager;
1166
+ this.settings = settings;
1167
+ this.logger = logger;
1168
+ }
1169
+ async getBestAccount() {
1170
+ const accounts = await this.EmailAccount.find({
1171
+ status: { $in: [ACCOUNT_STATUS.Active, ACCOUNT_STATUS.Warmup] }
1172
+ }).sort({ "health.score": -1 });
1173
+ for (const account of accounts) {
1174
+ const acct = account;
1175
+ const capacity = await this.getAccountCapacity(acct._id.toString());
1176
+ if (capacity.remaining > 0) {
1177
+ return account;
1178
+ }
1179
+ }
1180
+ return null;
1181
+ }
1182
+ async getAccountCapacity(accountId) {
1183
+ const account = await this.EmailAccount.findById(accountId);
1184
+ if (!account) {
1185
+ return {
1186
+ accountId,
1187
+ email: "",
1188
+ provider: "gmail",
1189
+ dailyMax: 0,
1190
+ sentToday: 0,
1191
+ remaining: 0,
1192
+ usagePercent: 0
1193
+ };
1194
+ }
1195
+ const acct = account;
1196
+ const sentToday = await this.getSentToday(accountId);
1197
+ const dailyMax = await this.getDailyLimit(acct);
1198
+ const remaining = Math.max(0, dailyMax - sentToday);
1199
+ const usagePercent = dailyMax > 0 ? Math.round(sentToday / dailyMax * 100) : 0;
1200
+ return {
1201
+ accountId: acct._id.toString(),
1202
+ email: acct.email,
1203
+ provider: acct.provider,
1204
+ dailyMax,
1205
+ sentToday,
1206
+ remaining,
1207
+ usagePercent
1208
+ };
1209
+ }
1210
+ async getAllCapacity() {
1211
+ const accounts = await this.EmailAccount.find({
1212
+ status: { $in: [ACCOUNT_STATUS.Active, ACCOUNT_STATUS.Warmup] }
1213
+ });
1214
+ const capacities = await Promise.all(
1215
+ accounts.map((a) => this.getAccountCapacity(a._id.toString()))
1216
+ );
1217
+ const totalRemaining = capacities.reduce((sum, c) => sum + c.remaining, 0);
1218
+ return { accounts: capacities, totalRemaining };
1219
+ }
1220
+ async getSentToday(accountId) {
1221
+ const dateStr = await this.getTodayDateString();
1222
+ const stat = await this.EmailDailyStats.findOne({ accountId, date: dateStr });
1223
+ return stat?.sent || 0;
1224
+ }
1225
+ async getDailyLimit(account) {
1226
+ if (account.warmup?.enabled) {
1227
+ const warmupLimit = await this.warmupManager.getDailyLimit(account._id.toString());
1228
+ return warmupLimit;
1229
+ }
1230
+ return account.limits.dailyMax;
1231
+ }
1232
+ async getTodayDateString() {
1233
+ const globalSettings = await this.settings.get();
1234
+ const tz = globalSettings.timezone || "UTC";
1235
+ return (/* @__PURE__ */ new Date()).toLocaleDateString("en-CA", { timeZone: tz });
1236
+ }
1237
+ };
1238
+ var SEPARATOR = "|";
1239
+ var UnsubscribeService = class {
1240
+ constructor(EmailIdentifier, config, logger, hooks) {
1241
+ this.EmailIdentifier = EmailIdentifier;
1242
+ this.config = config;
1243
+ this.logger = logger;
1244
+ this.hooks = hooks;
1245
+ }
1246
+ generateUrl(email, accountId) {
1247
+ const customGenerator = this.config.options?.unsubscribe?.generateUrl;
1248
+ if (customGenerator) {
1249
+ return customGenerator(email, accountId || "");
1250
+ }
1251
+ const builtin = this.config.options?.unsubscribe?.builtin;
1252
+ if (!builtin?.enabled || !builtin.baseUrl) {
1253
+ return "";
1254
+ }
1255
+ const token = this.generateToken(email);
1256
+ return `${builtin.baseUrl}?token=${token}`;
1257
+ }
1258
+ generateToken(email) {
1259
+ const secret = this.config.options?.unsubscribe?.builtin?.secret;
1260
+ if (!secret) return "";
1261
+ const timestamp = Date.now().toString();
1262
+ const payload = `${email.toLowerCase()}${SEPARATOR}${timestamp}`;
1263
+ const signature = crypto.createHmac("sha256", secret).update(payload).digest("base64url");
1264
+ const token = `${payload}${SEPARATOR}${signature}`;
1265
+ return Buffer.from(token).toString("base64url");
1266
+ }
1267
+ verifyToken(email, token) {
1268
+ try {
1269
+ const secret = this.config.options?.unsubscribe?.builtin?.secret;
1270
+ if (!secret) return false;
1271
+ const decoded = Buffer.from(token, "base64url").toString("utf-8");
1272
+ const parts = decoded.split(SEPARATOR);
1273
+ if (parts.length !== 3) return false;
1274
+ const [tokenEmail, timestampStr, providedSignature] = parts;
1275
+ const payload = `${tokenEmail}${SEPARATOR}${timestampStr}`;
1276
+ const expectedSignature = crypto.createHmac("sha256", secret).update(payload).digest("base64url");
1277
+ const sig1 = Buffer.from(providedSignature);
1278
+ const sig2 = Buffer.from(expectedSignature);
1279
+ if (sig1.length !== sig2.length || !crypto.timingSafeEqual(sig1, sig2)) {
1280
+ return false;
1281
+ }
1282
+ if (email && tokenEmail !== email.toLowerCase()) {
1283
+ return false;
1284
+ }
1285
+ const expiryDays = this.config.options?.unsubscribe?.builtin?.tokenExpiryDays;
1286
+ if (expiryDays) {
1287
+ const timestamp = parseInt(timestampStr, 10);
1288
+ const expiresAt = timestamp + expiryDays * 24 * 60 * 60 * 1e3;
1289
+ if (Date.now() > expiresAt) return false;
1290
+ }
1291
+ return true;
1292
+ } catch {
1293
+ return false;
1294
+ }
1295
+ }
1296
+ async handleUnsubscribe(email, token) {
1297
+ const decoded = Buffer.from(token, "base64url").toString("utf-8");
1298
+ const parts = decoded.split(SEPARATOR);
1299
+ const tokenEmail = parts.length === 3 ? parts[0] : email;
1300
+ if (!this.verifyToken(tokenEmail, token)) {
1301
+ return { success: false, error: "Invalid or expired unsubscribe link" };
1302
+ }
1303
+ const identifier = await this.EmailIdentifier.findOne({
1304
+ email: tokenEmail.toLowerCase()
1305
+ });
1306
+ if (!identifier) {
1307
+ return { success: true, email: tokenEmail };
1308
+ }
1309
+ if (identifier.status === IDENTIFIER_STATUS.Unsubscribed) {
1310
+ return { success: true, email: tokenEmail };
1311
+ }
1312
+ await this.EmailIdentifier.findByIdAndUpdate(identifier._id, {
1313
+ $set: {
1314
+ status: IDENTIFIER_STATUS.Unsubscribed,
1315
+ unsubscribedAt: /* @__PURE__ */ new Date()
1316
+ }
1317
+ });
1318
+ this.logger.info("Unsubscribe processed", { email: tokenEmail });
1319
+ this.hooks?.onUnsubscribe?.({ email: tokenEmail });
1320
+ return { success: true, email: tokenEmail };
1321
+ }
1322
+ getConfirmationHtml(email, success) {
1323
+ const statusIcon = success ? "&#10003;" : "&#10007;";
1324
+ const statusColor = success ? "#22c55e" : "#ef4444";
1325
+ const title = success ? "Unsubscribed" : "Error";
1326
+ const message = success ? `${email ? email + " has" : "You have"} been unsubscribed successfully.` : "Invalid or expired unsubscribe link. Please try again.";
1327
+ return `<!DOCTYPE html>
1328
+ <html lang="en">
1329
+ <head>
1330
+ <meta charset="UTF-8">
1331
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1332
+ <title>Unsubscribe</title>
1333
+ <style>
1334
+ body {
1335
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
1336
+ display: flex;
1337
+ align-items: center;
1338
+ justify-content: center;
1339
+ min-height: 100vh;
1340
+ margin: 0;
1341
+ background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
1342
+ }
1343
+ .card {
1344
+ background: white;
1345
+ padding: 40px 50px;
1346
+ border-radius: 16px;
1347
+ box-shadow: 0 10px 40px rgba(0,0,0,0.1);
1348
+ text-align: center;
1349
+ max-width: 400px;
1350
+ }
1351
+ .icon {
1352
+ font-size: 48px;
1353
+ color: ${statusColor};
1354
+ margin-bottom: 20px;
1355
+ }
1356
+ h1 { color: #1f2937; font-size: 24px; margin: 0 0 12px; }
1357
+ p { color: #6b7280; font-size: 16px; margin: 0; line-height: 1.5; }
1358
+ </style>
1359
+ </head>
1360
+ <body>
1361
+ <div class="card">
1362
+ <div class="icon">${statusIcon}</div>
1363
+ <h1>${title}</h1>
1364
+ <p>${message}</p>
1365
+ </div>
1366
+ </body>
1367
+ </html>`;
1368
+ }
1369
+ };
1370
+ var QueueService = class {
1371
+ constructor(redis, config, settings, logger) {
1372
+ this.redis = redis;
1373
+ this.config = config;
1374
+ this.settings = settings;
1375
+ this.logger = logger;
1376
+ }
1377
+ sendQueue;
1378
+ approvalQueue;
1379
+ sendWorker;
1380
+ approvalWorker;
1381
+ async init(processors) {
1382
+ const keyPrefix = this.config.redis.keyPrefix || "eam:";
1383
+ const sendQueueName = this.config.options?.queues?.sendQueueName || "email-send";
1384
+ const approvalQueueName = this.config.options?.queues?.approvalQueueName || "email-approved";
1385
+ const globalSettings = await this.settings.get();
1386
+ const queueSettings = globalSettings.queues;
1387
+ const redisConnection = this.redis;
1388
+ this.sendQueue = new Queue(sendQueueName, {
1389
+ connection: redisConnection,
1390
+ prefix: keyPrefix
1391
+ });
1392
+ this.approvalQueue = new Queue(approvalQueueName, {
1393
+ connection: redisConnection,
1394
+ prefix: keyPrefix
1395
+ });
1396
+ this.sendWorker = new Worker(
1397
+ sendQueueName,
1398
+ processors.sendProcessor,
1399
+ {
1400
+ connection: redisConnection,
1401
+ prefix: keyPrefix,
1402
+ concurrency: queueSettings.sendConcurrency
1403
+ }
1404
+ );
1405
+ this.approvalWorker = new Worker(
1406
+ approvalQueueName,
1407
+ processors.approvalProcessor,
1408
+ {
1409
+ connection: redisConnection,
1410
+ prefix: keyPrefix,
1411
+ concurrency: queueSettings.approvalConcurrency
1412
+ }
1413
+ );
1414
+ this.sendWorker.on("failed", (job, err) => {
1415
+ this.logger.error("Send job failed", {
1416
+ jobId: job?.id,
1417
+ error: err.message
1418
+ });
1419
+ });
1420
+ this.approvalWorker.on("failed", (job, err) => {
1421
+ this.logger.error("Approval job failed", {
1422
+ jobId: job?.id,
1423
+ error: err.message
1424
+ });
1425
+ });
1426
+ this.logger.info("Queue service initialized", {
1427
+ sendQueue: sendQueueName,
1428
+ approvalQueue: approvalQueueName
1429
+ });
1430
+ }
1431
+ async enqueueSend(data) {
1432
+ const globalSettings = await this.settings.get();
1433
+ const queueSettings = globalSettings.queues;
1434
+ const job = await this.sendQueue.add("send", data, {
1435
+ attempts: queueSettings.sendAttempts,
1436
+ backoff: {
1437
+ type: "exponential",
1438
+ delay: queueSettings.sendBackoffMs
1439
+ }
1440
+ });
1441
+ return job.id || "";
1442
+ }
1443
+ async enqueueApproval(data) {
1444
+ const globalSettings = await this.settings.get();
1445
+ const queueSettings = globalSettings.queues;
1446
+ const delay = data.scheduledAt ? Math.max(0, new Date(data.scheduledAt).getTime() - Date.now()) : 0;
1447
+ const job = await this.approvalQueue.add("approved", data, {
1448
+ attempts: queueSettings.approvalAttempts,
1449
+ backoff: {
1450
+ type: "exponential",
1451
+ delay: queueSettings.approvalBackoffMs
1452
+ },
1453
+ delay
1454
+ });
1455
+ return job.id || "";
1456
+ }
1457
+ async getStats() {
1458
+ const [sendCounts, approvalCounts] = await Promise.all([
1459
+ this.sendQueue.getJobCounts("waiting", "active", "completed", "failed", "delayed"),
1460
+ this.approvalQueue.getJobCounts("waiting", "active", "completed", "failed", "delayed")
1461
+ ]);
1462
+ return {
1463
+ send: sendCounts,
1464
+ approval: approvalCounts
1465
+ };
1466
+ }
1467
+ async pause(queue) {
1468
+ const target = queue === "send" ? this.sendQueue : this.approvalQueue;
1469
+ await target.pause();
1470
+ this.logger.info("Queue paused", { queue });
1471
+ }
1472
+ async resume(queue) {
1473
+ const target = queue === "send" ? this.sendQueue : this.approvalQueue;
1474
+ await target.resume();
1475
+ this.logger.info("Queue resumed", { queue });
1476
+ }
1477
+ async close() {
1478
+ await Promise.all([
1479
+ this.sendWorker?.close(),
1480
+ this.approvalWorker?.close(),
1481
+ this.sendQueue?.close(),
1482
+ this.approvalQueue?.close()
1483
+ ]);
1484
+ this.logger.info("Queue service closed");
1485
+ }
1486
+ };
1487
+ var SmtpService = class {
1488
+ constructor(EmailAccount, capacityManager, healthTracker, identifierService, unsubscribeService, queueService, settings, config, logger, hooks) {
1489
+ this.EmailAccount = EmailAccount;
1490
+ this.capacityManager = capacityManager;
1491
+ this.healthTracker = healthTracker;
1492
+ this.identifierService = identifierService;
1493
+ this.unsubscribeService = unsubscribeService;
1494
+ this.queueService = queueService;
1495
+ this.settings = settings;
1496
+ this.config = config;
1497
+ this.logger = logger;
1498
+ this.hooks = hooks;
1499
+ }
1500
+ devRoundRobinIndex = 0;
1501
+ transporterPool = /* @__PURE__ */ new Map();
1502
+ async send(params) {
1503
+ const globalSettings = await this.settings.get();
1504
+ if (globalSettings.approval.enabled) {
1505
+ return { success: true, draftId: "approval-required" };
1506
+ }
1507
+ const account = params.accountId ? await this.EmailAccount.findById(params.accountId) : await this.capacityManager.getBestAccount();
1508
+ if (!account) {
1509
+ return { success: false, error: "No available email account" };
1510
+ }
1511
+ const acct = account;
1512
+ const unsubscribeUrl = this.unsubscribeService.generateUrl(params.to, acct._id.toString());
1513
+ const jobId = await this.queueService.enqueueSend({
1514
+ accountId: acct._id.toString(),
1515
+ to: params.to,
1516
+ subject: params.subject,
1517
+ html: params.html,
1518
+ text: params.text || "",
1519
+ unsubscribeUrl: unsubscribeUrl || void 0,
1520
+ metadata: params.metadata
1521
+ });
1522
+ return { success: true, messageId: jobId };
1523
+ }
1524
+ async testConnection(accountId) {
1525
+ const account = await this.EmailAccount.findById(accountId);
1526
+ if (!account) return { success: false, error: "Account not found" };
1527
+ try {
1528
+ const acct = account;
1529
+ const transporter = this.createTransporter(acct);
1530
+ await transporter.verify();
1531
+ transporter.close();
1532
+ return { success: true };
1533
+ } catch (err) {
1534
+ const message = err instanceof Error ? err.message : "Unknown error";
1535
+ return { success: false, error: message };
1536
+ }
1537
+ }
1538
+ async executeSend(accountId, to, subject, html, text2, unsubscribeUrl) {
1539
+ const account = await this.EmailAccount.findById(accountId);
1540
+ if (!account) return { success: false, error: "Account not found" };
1541
+ const acct = account;
1542
+ try {
1543
+ const globalSettings = await this.settings.get();
1544
+ let recipientEmail = to;
1545
+ if (globalSettings.devMode.enabled && globalSettings.devMode.testEmails.length > 0) {
1546
+ const testEmails = globalSettings.devMode.testEmails;
1547
+ const devIndex = this.devRoundRobinIndex++ % testEmails.length;
1548
+ recipientEmail = testEmails[devIndex];
1549
+ this.logger.info("Dev mode: email redirected", { original: to, redirected: recipientEmail });
1550
+ }
1551
+ const transporter = this.getOrCreateTransporter(accountId, acct);
1552
+ const headers = {};
1553
+ if (acct.provider === ACCOUNT_PROVIDER.Ses && acct.ses?.configurationSet) {
1554
+ headers["X-SES-CONFIGURATION-SET"] = acct.ses.configurationSet;
1555
+ }
1556
+ if (unsubscribeUrl) {
1557
+ headers["List-Unsubscribe"] = `<${unsubscribeUrl}>`;
1558
+ headers["List-Unsubscribe-Post"] = "List-Unsubscribe=One-Click";
1559
+ }
1560
+ const mailOptions = {
1561
+ from: `${acct.senderName} <${acct.email}>`,
1562
+ to: recipientEmail,
1563
+ subject,
1564
+ text: text2,
1565
+ html,
1566
+ headers
1567
+ };
1568
+ const info = await transporter.sendMail(mailOptions);
1569
+ const messageId = info.messageId?.replace(/[<>]/g, "");
1570
+ await this.healthTracker.recordSuccess(accountId);
1571
+ await this.identifierService.incrementSentCount(to);
1572
+ this.hooks?.onSend?.({ accountId, email: to, messageId });
1573
+ this.logger.info("Email sent", { accountId, to, messageId });
1574
+ return { success: true, messageId };
1575
+ } catch (err) {
1576
+ const errorMsg = err instanceof Error ? err.message : "Unknown error";
1577
+ this.transporterPool.delete(accountId);
1578
+ await this.healthTracker.recordError(accountId, errorMsg);
1579
+ this.hooks?.onSendError?.({ accountId, email: to, error: errorMsg });
1580
+ this.logger.error("Email send failed", { accountId, to, error: errorMsg });
1581
+ return { success: false, error: errorMsg };
1582
+ }
1583
+ }
1584
+ closeAll() {
1585
+ for (const [id, transporter] of this.transporterPool) {
1586
+ try {
1587
+ transporter.close();
1588
+ } catch {
1589
+ }
1590
+ }
1591
+ this.transporterPool.clear();
1592
+ }
1593
+ getOrCreateTransporter(accountId, account) {
1594
+ const existing = this.transporterPool.get(accountId);
1595
+ if (existing) return existing;
1596
+ const transporter = this.createTransporter(account, true);
1597
+ this.transporterPool.set(accountId, transporter);
1598
+ return transporter;
1599
+ }
1600
+ createTransporter(account, pooled = false) {
1601
+ return nodemailer.createTransport({
1602
+ host: account.smtp.host,
1603
+ port: account.smtp.port || 587,
1604
+ secure: account.smtp.port === 465,
1605
+ auth: {
1606
+ user: account.smtp.user,
1607
+ pass: account.smtp.pass
1608
+ },
1609
+ ...pooled ? { pool: true, maxConnections: 5, maxMessages: 100 } : {}
1610
+ });
1611
+ }
1612
+ };
1613
+
1614
+ // src/services/approval.service.ts
1615
+ var ApprovalService = class {
1616
+ constructor(EmailDraft, queueService, settings, logger, hooks) {
1617
+ this.EmailDraft = EmailDraft;
1618
+ this.queueService = queueService;
1619
+ this.settings = settings;
1620
+ this.logger = logger;
1621
+ this.hooks = hooks;
1622
+ }
1623
+ async createDraft(input) {
1624
+ const draft = await this.EmailDraft.create({
1625
+ ...input,
1626
+ status: DRAFT_STATUS.Pending
1627
+ });
1628
+ this.logger.info("Draft created", { draftId: draft._id.toString(), to: input.to });
1629
+ this.hooks?.onDraftCreated?.({
1630
+ draftId: draft._id.toString(),
1631
+ to: input.to,
1632
+ subject: input.subject
1633
+ });
1634
+ return draft;
1635
+ }
1636
+ async approve(draftId) {
1637
+ const draft = await this.EmailDraft.findById(draftId);
1638
+ if (!draft) throw new DraftNotFoundError(draftId);
1639
+ const scheduledAt = await this.calculateScheduledTime(0, 1);
1640
+ await this.EmailDraft.findByIdAndUpdate(draftId, {
1641
+ $set: {
1642
+ status: DRAFT_STATUS.Approved,
1643
+ approvedAt: /* @__PURE__ */ new Date(),
1644
+ scheduledAt
1645
+ }
1646
+ });
1647
+ await this.queueService.enqueueApproval({
1648
+ draftId,
1649
+ scheduledAt: scheduledAt?.toISOString()
1650
+ });
1651
+ this.logger.info("Draft approved", { draftId });
1652
+ this.hooks?.onDraftApproved?.({
1653
+ draftId,
1654
+ to: draft.to,
1655
+ scheduledAt
1656
+ });
1657
+ }
1658
+ async reject(draftId, reason) {
1659
+ const draft = await this.EmailDraft.findById(draftId);
1660
+ if (!draft) throw new DraftNotFoundError(draftId);
1661
+ await this.EmailDraft.findByIdAndUpdate(draftId, {
1662
+ $set: {
1663
+ status: DRAFT_STATUS.Rejected,
1664
+ rejectedAt: /* @__PURE__ */ new Date(),
1665
+ ...reason ? { rejectionReason: reason } : {}
1666
+ }
1667
+ });
1668
+ this.logger.info("Draft rejected", { draftId, reason });
1669
+ this.hooks?.onDraftRejected?.({
1670
+ draftId,
1671
+ to: draft.to,
1672
+ reason
1673
+ });
1674
+ }
1675
+ async bulkApprove(draftIds) {
1676
+ const total = draftIds.length;
1677
+ for (let i = 0; i < total; i++) {
1678
+ const scheduledAt = await this.calculateScheduledTime(i, total);
1679
+ await this.EmailDraft.findByIdAndUpdate(draftIds[i], {
1680
+ $set: {
1681
+ status: DRAFT_STATUS.Approved,
1682
+ approvedAt: /* @__PURE__ */ new Date(),
1683
+ scheduledAt
1684
+ }
1685
+ });
1686
+ await this.queueService.enqueueApproval({
1687
+ draftId: draftIds[i],
1688
+ scheduledAt: scheduledAt?.toISOString()
1689
+ });
1690
+ }
1691
+ this.logger.info("Bulk approve completed", { count: total });
1692
+ }
1693
+ async bulkReject(draftIds, reason) {
1694
+ await this.EmailDraft.updateMany(
1695
+ { _id: { $in: draftIds } },
1696
+ {
1697
+ $set: {
1698
+ status: DRAFT_STATUS.Rejected,
1699
+ rejectedAt: /* @__PURE__ */ new Date(),
1700
+ ...reason ? { rejectionReason: reason } : {}
1701
+ }
1702
+ }
1703
+ );
1704
+ this.logger.info("Bulk reject completed", { count: draftIds.length, reason });
1705
+ }
1706
+ async sendNow(draftId) {
1707
+ const draft = await this.EmailDraft.findById(draftId);
1708
+ if (!draft) throw new DraftNotFoundError(draftId);
1709
+ await this.EmailDraft.findByIdAndUpdate(draftId, {
1710
+ $set: {
1711
+ status: DRAFT_STATUS.Approved,
1712
+ approvedAt: /* @__PURE__ */ new Date()
1713
+ }
1714
+ });
1715
+ await this.queueService.enqueueApproval({ draftId });
1716
+ this.logger.info("Draft sent immediately", { draftId });
1717
+ }
1718
+ async updateContent(draftId, content) {
1719
+ const updates = {};
1720
+ if (content.subject !== void 0) updates.subject = content.subject;
1721
+ if (content.htmlBody !== void 0) updates.htmlBody = content.htmlBody;
1722
+ if (content.textBody !== void 0) updates.textBody = content.textBody;
1723
+ const draft = await this.EmailDraft.findByIdAndUpdate(
1724
+ draftId,
1725
+ { $set: updates },
1726
+ { new: true }
1727
+ );
1728
+ if (!draft) throw new DraftNotFoundError(draftId);
1729
+ return draft;
1730
+ }
1731
+ async getDrafts(filters, page = 1, limit = 50) {
1732
+ const query = {};
1733
+ if (filters?.status) query.status = filters.status;
1734
+ const [items, total] = await Promise.all([
1735
+ this.EmailDraft.find(query).sort({ createdAt: -1 }).skip((page - 1) * limit).limit(limit),
1736
+ this.EmailDraft.countDocuments(query)
1737
+ ]);
1738
+ return { items, total };
1739
+ }
1740
+ async getDraftById(draftId) {
1741
+ return this.EmailDraft.findById(draftId);
1742
+ }
1743
+ async countByStatus() {
1744
+ const counts = await this.EmailDraft.aggregate([
1745
+ { $group: { _id: "$status", count: { $sum: 1 } } }
1746
+ ]);
1747
+ const result = {};
1748
+ for (const entry of counts) {
1749
+ result[entry._id] = entry.count;
1750
+ }
1751
+ return result;
1752
+ }
1753
+ async calculateScheduledTime(index, total) {
1754
+ const globalSettings = await this.settings.get();
1755
+ const approval = globalSettings.approval;
1756
+ if (!approval.sendWindow) {
1757
+ return /* @__PURE__ */ new Date();
1758
+ }
1759
+ const now = /* @__PURE__ */ new Date();
1760
+ const tz = approval.sendWindow.timezone || globalSettings.timezone || "UTC";
1761
+ const currentHour = parseInt(
1762
+ now.toLocaleString("en-US", { timeZone: tz, hour: "numeric", hour12: false }),
1763
+ 10
1764
+ );
1765
+ let scheduledDate = new Date(now);
1766
+ if (currentHour < approval.sendWindow.startHour) {
1767
+ const diff = approval.sendWindow.startHour - currentHour;
1768
+ scheduledDate = new Date(now.getTime() + diff * 60 * 60 * 1e3);
1769
+ } else if (currentHour >= approval.sendWindow.endHour) {
1770
+ const hoursUntilNextStart = 24 - currentHour + approval.sendWindow.startHour;
1771
+ scheduledDate = new Date(now.getTime() + hoursUntilNextStart * 60 * 60 * 1e3);
1772
+ }
1773
+ if (total <= 1) return scheduledDate;
1774
+ const maxSpreadMs = approval.maxSpreadMinutes * 60 * 1e3;
1775
+ if (approval.spreadStrategy === "even") {
1776
+ const interval = total > 1 ? maxSpreadMs / (total - 1) : 0;
1777
+ return new Date(scheduledDate.getTime() + index * interval);
1778
+ }
1779
+ const randomDelay = Math.floor(Math.random() * maxSpreadMs);
1780
+ return new Date(scheduledDate.getTime() + randomDelay);
1781
+ }
1782
+ };
1783
+
1784
+ // src/services/imap-bounce-checker.ts
1785
+ var BOUNCE_PATTERNS = [
1786
+ { pattern: /recipient inbox full|mailbox full|mail box full|delivery incomplete/i, type: BOUNCE_TYPE.InboxFull },
1787
+ { pattern: /address not found|user unknown|no such user|does not exist|invalid address|message blocked/i, type: BOUNCE_TYPE.InvalidEmail }
1788
+ ];
1789
+ var ImapBounceChecker = class {
1790
+ constructor(EmailAccount, healthTracker, identifierService, settings, logger, hooks) {
1791
+ this.EmailAccount = EmailAccount;
1792
+ this.healthTracker = healthTracker;
1793
+ this.identifierService = identifierService;
1794
+ this.settings = settings;
1795
+ this.logger = logger;
1796
+ this.hooks = hooks;
1797
+ }
1798
+ polling = null;
1799
+ async start() {
1800
+ const globalSettings = await this.settings.get();
1801
+ if (!globalSettings.imap.enabled) {
1802
+ this.logger.info("IMAP bounce checker disabled");
1803
+ return;
1804
+ }
1805
+ const intervalMs = globalSettings.imap.pollIntervalMs || 3e5;
1806
+ this.polling = setInterval(async () => {
1807
+ try {
1808
+ await this.checkNow();
1809
+ } catch (err) {
1810
+ const msg = err instanceof Error ? err.message : "Unknown error";
1811
+ this.logger.error("IMAP polling error", { error: msg });
1812
+ }
1813
+ }, intervalMs);
1814
+ this.logger.info("IMAP bounce checker started", { intervalMs });
1815
+ }
1816
+ stop() {
1817
+ if (this.polling) {
1818
+ clearInterval(this.polling);
1819
+ this.polling = null;
1820
+ this.logger.info("IMAP bounce checker stopped");
1821
+ }
1822
+ }
1823
+ async checkNow() {
1824
+ const accounts = await this.EmailAccount.find({
1825
+ provider: ACCOUNT_PROVIDER.Gmail,
1826
+ status: { $in: [ACCOUNT_STATUS.Active, ACCOUNT_STATUS.Warmup] },
1827
+ "imap.host": { $exists: true }
1828
+ });
1829
+ this.logger.info("IMAP bounce check starting", { accounts: accounts.length });
1830
+ for (const account of accounts) {
1831
+ try {
1832
+ await this.checkAccount(account._id.toString());
1833
+ } catch (err) {
1834
+ const msg = err instanceof Error ? err.message : "Unknown error";
1835
+ this.logger.error("IMAP check failed for account", {
1836
+ accountId: account._id.toString(),
1837
+ error: msg
1838
+ });
1839
+ }
1840
+ }
1841
+ }
1842
+ async checkAccount(accountId) {
1843
+ let ImapFlow;
1844
+ try {
1845
+ ImapFlow = (await import('imapflow')).ImapFlow;
1846
+ } catch {
1847
+ this.logger.warn("imapflow package not installed, skipping IMAP check");
1848
+ return { bouncesFound: 0 };
1849
+ }
1850
+ const account = await this.EmailAccount.findById(accountId);
1851
+ if (!account) return { bouncesFound: 0 };
1852
+ const acct = account;
1853
+ if (!acct.imap?.host || !acct.imap?.user || !acct.imap?.pass) {
1854
+ return { bouncesFound: 0 };
1855
+ }
1856
+ const globalSettings = await this.settings.get();
1857
+ const bounceSenders = globalSettings.imap.bounceSenders || ["mailer-daemon@googlemail.com"];
1858
+ const client = new ImapFlow({
1859
+ host: acct.imap.host,
1860
+ port: acct.imap.port || 993,
1861
+ secure: true,
1862
+ auth: {
1863
+ user: acct.imap.user,
1864
+ pass: acct.imap.pass
1865
+ },
1866
+ logger: false
1867
+ });
1868
+ let bouncesFound = 0;
1869
+ try {
1870
+ await client.connect();
1871
+ const lock = await client.getMailboxLock("INBOX");
1872
+ try {
1873
+ const searchDate = this.getSearchDate(globalSettings.imap.searchSince, acct.lastImapCheckAt);
1874
+ const senderQuery = bounceSenders.map((s) => ({ from: s }));
1875
+ const messages = client.fetch(
1876
+ {
1877
+ or: senderQuery,
1878
+ since: searchDate
1879
+ },
1880
+ { source: true }
1881
+ );
1882
+ for await (const msg of messages) {
1883
+ try {
1884
+ const source = msg.source?.toString() || "";
1885
+ const recipientMatch = source.match(
1886
+ /Original-Recipient:.*?;?\s*<?([^\s<>]+@[^\s<>]+)>?/i
1887
+ ) || source.match(
1888
+ /Final-Recipient:.*?;?\s*<?([^\s<>]+@[^\s<>]+)>?/i
1889
+ ) || source.match(
1890
+ /was\s+not\s+delivered\s+to\s+<?([^\s<>]+@[^\s<>]+)>?/i
1891
+ );
1892
+ if (!recipientMatch) continue;
1893
+ const recipientEmail = recipientMatch[1].toLowerCase();
1894
+ const bounceType = this.classifyBounce(source);
1895
+ await this.identifierService.markBounced(recipientEmail, bounceType);
1896
+ await this.healthTracker.recordBounce(accountId, recipientEmail, bounceType);
1897
+ this.hooks?.onBounce?.({
1898
+ accountId,
1899
+ email: recipientEmail,
1900
+ bounceType,
1901
+ provider: ACCOUNT_PROVIDER.Gmail
1902
+ });
1903
+ bouncesFound++;
1904
+ } catch (err) {
1905
+ const msg2 = err instanceof Error ? err.message : "Unknown error";
1906
+ this.logger.warn("Failed to parse bounce message", { error: msg2 });
1907
+ }
1908
+ }
1909
+ } finally {
1910
+ lock.release();
1911
+ }
1912
+ await client.logout();
1913
+ await this.EmailAccount.findByIdAndUpdate(accountId, {
1914
+ $set: { lastImapCheckAt: /* @__PURE__ */ new Date() }
1915
+ });
1916
+ } catch (err) {
1917
+ const msg = err instanceof Error ? err.message : "Unknown error";
1918
+ this.logger.error("IMAP connection failed", { accountId, error: msg });
1919
+ try {
1920
+ await client.logout();
1921
+ } catch {
1922
+ }
1923
+ }
1924
+ this.logger.info("IMAP bounce check completed", { accountId, bouncesFound });
1925
+ return { bouncesFound };
1926
+ }
1927
+ classifyBounce(messageBody) {
1928
+ for (const { pattern, type } of BOUNCE_PATTERNS) {
1929
+ if (pattern.test(messageBody)) return type;
1930
+ }
1931
+ return BOUNCE_TYPE.Soft;
1932
+ }
1933
+ getSearchDate(searchSince, lastImapCheckAt) {
1934
+ const now = /* @__PURE__ */ new Date();
1935
+ const fallback24h = new Date(now.getTime() - 24 * 60 * 60 * 1e3);
1936
+ switch (searchSince) {
1937
+ case "last_24h":
1938
+ return fallback24h;
1939
+ case "last_7d":
1940
+ return new Date(now.getTime() - 7 * 24 * 60 * 60 * 1e3);
1941
+ case "last_check":
1942
+ return lastImapCheckAt ? new Date(lastImapCheckAt) : fallback24h;
1943
+ default:
1944
+ return fallback24h;
1945
+ }
1946
+ }
1947
+ };
1948
+ var SNS_SIGNATURE_KEYS = [
1949
+ "Message",
1950
+ "MessageId",
1951
+ "Subject",
1952
+ "SubscribeURL",
1953
+ "Timestamp",
1954
+ "Token",
1955
+ "TopicArn",
1956
+ "Type"
1957
+ ];
1958
+ var SesWebhookHandler = class {
1959
+ constructor(healthTracker, identifierService, EmailAccount, config, logger, hooks) {
1960
+ this.healthTracker = healthTracker;
1961
+ this.identifierService = identifierService;
1962
+ this.EmailAccount = EmailAccount;
1963
+ this.config = config;
1964
+ this.logger = logger;
1965
+ this.hooks = hooks;
1966
+ }
1967
+ async handleSnsMessage(body) {
1968
+ const message = this.parseBody(body);
1969
+ if (!message || !message.Type) {
1970
+ return { processed: false, error: "Invalid SNS message format" };
1971
+ }
1972
+ const validateSignature = this.config.options?.ses?.validateSignature !== false;
1973
+ if (validateSignature && message.SignatureVersion) {
1974
+ const valid = await this.validateSignature(message);
1975
+ if (!valid) {
1976
+ throw new SnsSignatureError();
1977
+ }
1978
+ }
1979
+ const allowedTopics = this.config.options?.ses?.allowedTopicArns;
1980
+ if (allowedTopics?.length && !allowedTopics.includes(message.TopicArn)) {
1981
+ return { processed: false, error: "Topic ARN not allowed" };
1982
+ }
1983
+ if (message.Type === SNS_MESSAGE_TYPE.SubscriptionConfirmation) {
1984
+ return this.handleSubscriptionConfirmation(message);
1985
+ }
1986
+ if (message.Type === SNS_MESSAGE_TYPE.UnsubscribeConfirmation) {
1987
+ return { processed: true, type: "unsubscribe_confirmed" };
1988
+ }
1989
+ if (message.Type === SNS_MESSAGE_TYPE.Notification) {
1990
+ const notification = JSON.parse(message.Message);
1991
+ return this.processNotification(notification);
1992
+ }
1993
+ return { processed: true, type: "ignored" };
1994
+ }
1995
+ async handleSubscriptionConfirmation(message) {
1996
+ if (!message.SubscribeURL) {
1997
+ return { processed: false, error: "No SubscribeURL provided" };
1998
+ }
1999
+ try {
2000
+ const url = new URL(message.SubscribeURL);
2001
+ if (!url.hostname.endsWith(".amazonaws.com")) {
2002
+ this.logger.warn("Rejected SubscribeURL with non-AWS hostname", {
2003
+ hostname: url.hostname
2004
+ });
2005
+ return { processed: false, error: "SubscribeURL hostname is not from amazonaws.com" };
2006
+ }
2007
+ } catch {
2008
+ return { processed: false, error: "Invalid SubscribeURL" };
2009
+ }
2010
+ return new Promise((resolve) => {
2011
+ https.get(message.SubscribeURL, (res) => {
2012
+ resolve({
2013
+ processed: res.statusCode === 200,
2014
+ type: "subscription_confirmed"
2015
+ });
2016
+ }).on("error", (err) => {
2017
+ resolve({ processed: false, error: err.message });
2018
+ });
2019
+ });
2020
+ }
2021
+ async processNotification(notification) {
2022
+ const accountId = await this.findAccountIdBySource(notification.mail.source);
2023
+ switch (notification.notificationType) {
2024
+ case SES_NOTIFICATION_TYPE.Bounce:
2025
+ return this.processBounce(accountId, notification.bounce);
2026
+ case SES_NOTIFICATION_TYPE.Complaint:
2027
+ return this.processComplaint(accountId, notification.complaint);
2028
+ case SES_NOTIFICATION_TYPE.Delivery:
2029
+ if (notification.delivery) {
2030
+ for (const recipient of notification.delivery.recipients) {
2031
+ this.hooks?.onDelivery?.({ accountId: accountId || "", email: recipient });
2032
+ }
2033
+ }
2034
+ return { processed: true, type: "delivery" };
2035
+ case SES_NOTIFICATION_TYPE.Open:
2036
+ if (notification.open) {
2037
+ const recipients = notification.mail.destination || [];
2038
+ for (const email of recipients) {
2039
+ this.hooks?.onOpen?.({
2040
+ accountId: accountId || "",
2041
+ email,
2042
+ timestamp: new Date(notification.open.timestamp)
2043
+ });
2044
+ }
2045
+ }
2046
+ return { processed: true, type: "open" };
2047
+ case SES_NOTIFICATION_TYPE.Click:
2048
+ if (notification.click) {
2049
+ const recipients = notification.mail.destination || [];
2050
+ for (const email of recipients) {
2051
+ this.hooks?.onClick?.({
2052
+ accountId: accountId || "",
2053
+ email,
2054
+ link: notification.click.link
2055
+ });
2056
+ }
2057
+ }
2058
+ return { processed: true, type: "click" };
2059
+ default:
2060
+ return { processed: true, type: "unknown" };
2061
+ }
2062
+ }
2063
+ async processBounce(accountId, bounce) {
2064
+ const bounceType = bounce.bounceType === SES_BOUNCE_TYPE.Permanent ? BOUNCE_TYPE.Hard : BOUNCE_TYPE.Soft;
2065
+ for (const recipient of bounce.bouncedRecipients) {
2066
+ const email = recipient.emailAddress;
2067
+ if (bounce.bounceType === SES_BOUNCE_TYPE.Permanent) {
2068
+ const subType = bounce.bounceSubType?.toLowerCase();
2069
+ if (subType === "noemail" || subType === "general") {
2070
+ await this.identifierService.updateStatus(email, IDENTIFIER_STATUS.Invalid);
2071
+ } else {
2072
+ await this.identifierService.markBounced(email, BOUNCE_TYPE.Hard);
2073
+ }
2074
+ } else {
2075
+ if (bounce.bounceSubType?.toLowerCase() === "mailboxfull") {
2076
+ await this.identifierService.markBounced(email, BOUNCE_TYPE.InboxFull);
2077
+ } else {
2078
+ await this.identifierService.markBounced(email, BOUNCE_TYPE.Soft);
2079
+ }
2080
+ }
2081
+ if (accountId) {
2082
+ await this.healthTracker.recordBounce(accountId, email, bounceType);
2083
+ }
2084
+ this.hooks?.onBounce?.({
2085
+ accountId: accountId || "",
2086
+ email,
2087
+ bounceType,
2088
+ provider: ACCOUNT_PROVIDER.Ses
2089
+ });
2090
+ }
2091
+ return { processed: true, type: "bounce" };
2092
+ }
2093
+ async processComplaint(accountId, complaint) {
2094
+ for (const recipient of complaint.complainedRecipients) {
2095
+ await this.identifierService.updateStatus(
2096
+ recipient.emailAddress,
2097
+ IDENTIFIER_STATUS.Blocked
2098
+ );
2099
+ this.hooks?.onComplaint?.({
2100
+ accountId: accountId || "",
2101
+ email: recipient.emailAddress
2102
+ });
2103
+ }
2104
+ return { processed: true, type: "complaint" };
2105
+ }
2106
+ async validateSignature(message) {
2107
+ try {
2108
+ if (message.SignatureVersion !== "1") return false;
2109
+ const certificate = await this.fetchCertificate(message.SigningCertURL);
2110
+ const parts = [];
2111
+ const record = message;
2112
+ for (const key of SNS_SIGNATURE_KEYS) {
2113
+ const value = record[key];
2114
+ if (value !== void 0 && value !== null) {
2115
+ parts.push(key);
2116
+ parts.push(String(value));
2117
+ }
2118
+ }
2119
+ const stringToSign = parts.join("\n") + "\n";
2120
+ const verifier = crypto.createVerify("SHA1");
2121
+ verifier.update(stringToSign, "utf8");
2122
+ const signatureBuffer = Buffer.from(message.Signature, "base64");
2123
+ return verifier.verify(certificate, signatureBuffer);
2124
+ } catch {
2125
+ return false;
2126
+ }
2127
+ }
2128
+ async fetchCertificate(certUrl) {
2129
+ return new Promise((resolve, reject) => {
2130
+ const url = new URL(certUrl);
2131
+ if (!url.hostname.endsWith(".amazonaws.com")) {
2132
+ return reject(new Error("Invalid certificate URL: not from amazonaws.com"));
2133
+ }
2134
+ if (url.protocol !== "https:") {
2135
+ return reject(new Error("Invalid certificate URL: must be HTTPS"));
2136
+ }
2137
+ https.get(certUrl, (res) => {
2138
+ let data = "";
2139
+ res.on("data", (chunk) => {
2140
+ data += chunk;
2141
+ });
2142
+ res.on("end", () => resolve(data));
2143
+ res.on("error", reject);
2144
+ }).on("error", reject);
2145
+ });
2146
+ }
2147
+ parseBody(rawBody) {
2148
+ if (typeof rawBody === "string") {
2149
+ return JSON.parse(rawBody);
2150
+ }
2151
+ return rawBody;
2152
+ }
2153
+ async findAccountIdBySource(source) {
2154
+ const email = source.replace(/.*<(.+)>/, "$1").toLowerCase();
2155
+ const account = await this.EmailAccount.findOne({ email });
2156
+ return account ? account._id.toString() : null;
2157
+ }
2158
+ };
2159
+
2160
+ // src/queues/send.queue.ts
2161
+ function createSendProcessor(smtpService, logger) {
2162
+ return async (job) => {
2163
+ const { accountId, to, subject, html, text: text2, unsubscribeUrl } = job.data;
2164
+ logger.info("Processing send job", { jobId: job.id, accountId, to });
2165
+ const result = await smtpService.executeSend(accountId, to, subject, html, text2, unsubscribeUrl);
2166
+ if (!result.success) {
2167
+ throw new Error(result.error || "Send failed");
2168
+ }
2169
+ };
2170
+ }
2171
+
2172
+ // src/queues/approval.queue.ts
2173
+ function createApprovalProcessor(EmailDraft, smtpService, queueService, logger) {
2174
+ return async (job) => {
2175
+ const { draftId } = job.data;
2176
+ const draft = await EmailDraft.findById(draftId);
2177
+ if (!draft) {
2178
+ logger.warn("Approval job: draft not found", { draftId });
2179
+ return;
2180
+ }
2181
+ const d = draft;
2182
+ if (d.status !== DRAFT_STATUS.Approved) {
2183
+ logger.warn("Approval job: draft not in approved status", { draftId, status: d.status });
2184
+ return;
2185
+ }
2186
+ logger.info("Processing approved draft", { draftId, to: d.to });
2187
+ await EmailDraft.findByIdAndUpdate(draftId, {
2188
+ $set: { status: DRAFT_STATUS.Queued }
2189
+ });
2190
+ await queueService.enqueueSend({
2191
+ accountId: d.accountId.toString(),
2192
+ to: d.to,
2193
+ subject: d.subject,
2194
+ html: d.htmlBody,
2195
+ text: d.textBody || ""
2196
+ });
2197
+ };
2198
+ }
2199
+
2200
+ // src/controllers/account.controller.ts
2201
+ function createAccountController(EmailAccount, capacityManager, healthTracker, warmupManager, smtpService, imapBounceChecker, config) {
2202
+ return {
2203
+ async list(req, res) {
2204
+ try {
2205
+ const accounts = await EmailAccount.find().select("-smtp.pass -imap.pass").sort({ createdAt: -1 });
2206
+ res.json({ success: true, data: { accounts } });
2207
+ } catch (error) {
2208
+ const message = error instanceof Error ? error.message : "Unknown error";
2209
+ res.status(500).json({ success: false, error: message });
2210
+ }
2211
+ },
2212
+ async getById(req, res) {
2213
+ try {
2214
+ const account = await EmailAccount.findById(req.params.id).select("-smtp.pass -imap.pass");
2215
+ if (!account) {
2216
+ return res.status(404).json({ success: false, error: "Account not found" });
2217
+ }
2218
+ res.json({ success: true, data: { account } });
2219
+ } catch (error) {
2220
+ const message = error instanceof Error ? error.message : "Unknown error";
2221
+ res.status(500).json({ success: false, error: message });
2222
+ }
2223
+ },
2224
+ async create(req, res) {
2225
+ try {
2226
+ const input = req.body;
2227
+ if (!input.email || !input.senderName) {
2228
+ return res.status(400).json({ success: false, error: "email and senderName are required" });
2229
+ }
2230
+ const existing = await EmailAccount.findOne({ email: input.email.toLowerCase() });
2231
+ if (existing) {
2232
+ return res.status(400).json({ success: false, error: "Email account already exists" });
2233
+ }
2234
+ const healthDefaults = config.options?.healthDefaults;
2235
+ const warmupDefaults = config.options?.warmup?.defaultSchedule;
2236
+ const accountData = {
2237
+ email: input.email.toLowerCase(),
2238
+ senderName: input.senderName,
2239
+ provider: input.provider,
2240
+ smtp: input.smtp,
2241
+ ...input.imap ? { imap: input.imap } : {},
2242
+ ...input.ses ? { ses: input.ses } : {},
2243
+ limits: { dailyMax: input.limits?.dailyMax || 450 },
2244
+ health: {
2245
+ score: 100,
2246
+ consecutiveErrors: 0,
2247
+ bounceCount: 0,
2248
+ thresholds: {
2249
+ minScore: input.health?.thresholds?.minScore ?? healthDefaults?.minScore ?? 50,
2250
+ maxBounceRate: input.health?.thresholds?.maxBounceRate ?? healthDefaults?.maxBounceRate ?? 5,
2251
+ maxConsecutiveErrors: input.health?.thresholds?.maxConsecutiveErrors ?? healthDefaults?.maxConsecutiveErrors ?? 10
2252
+ }
2253
+ },
2254
+ warmup: {
2255
+ enabled: true,
2256
+ startedAt: /* @__PURE__ */ new Date(),
2257
+ currentDay: 1,
2258
+ schedule: input.warmup?.schedule || warmupDefaults || []
2259
+ },
2260
+ status: "warmup",
2261
+ totalEmailsSent: 0
2262
+ };
2263
+ const account = await EmailAccount.create(accountData);
2264
+ const saved = account.toObject();
2265
+ delete saved.smtp?.pass;
2266
+ delete saved.imap?.pass;
2267
+ res.status(201).json({ success: true, data: { account: saved } });
2268
+ } catch (error) {
2269
+ const message = error instanceof Error ? error.message : "Unknown error";
2270
+ res.status(500).json({ success: false, error: message });
2271
+ }
2272
+ },
2273
+ async update(req, res) {
2274
+ try {
2275
+ const input = req.body;
2276
+ const updates = {};
2277
+ if (input.senderName !== void 0) updates.senderName = input.senderName;
2278
+ if (input.status !== void 0) updates.status = input.status;
2279
+ if (input.smtp) {
2280
+ for (const [k, v] of Object.entries(input.smtp)) {
2281
+ if (v !== void 0 && (k !== "pass" || v !== "")) updates[`smtp.${k}`] = v;
2282
+ }
2283
+ }
2284
+ if (input.imap) {
2285
+ for (const [k, v] of Object.entries(input.imap)) {
2286
+ if (v !== void 0 && (k !== "pass" || v !== "")) updates[`imap.${k}`] = v;
2287
+ }
2288
+ }
2289
+ if (input.ses) {
2290
+ for (const [k, v] of Object.entries(input.ses)) {
2291
+ if (v !== void 0) updates[`ses.${k}`] = v;
2292
+ }
2293
+ }
2294
+ if (input.limits?.dailyMax !== void 0) updates["limits.dailyMax"] = input.limits.dailyMax;
2295
+ const account = await EmailAccount.findByIdAndUpdate(
2296
+ req.params.id,
2297
+ { $set: updates },
2298
+ { new: true }
2299
+ ).select("-smtp.pass -imap.pass");
2300
+ if (!account) {
2301
+ return res.status(404).json({ success: false, error: "Account not found" });
2302
+ }
2303
+ res.json({ success: true, data: { account } });
2304
+ } catch (error) {
2305
+ const message = error instanceof Error ? error.message : "Unknown error";
2306
+ res.status(500).json({ success: false, error: message });
2307
+ }
2308
+ },
2309
+ async remove(req, res) {
2310
+ try {
2311
+ const result = await EmailAccount.findByIdAndDelete(req.params.id);
2312
+ if (!result) {
2313
+ return res.status(404).json({ success: false, error: "Account not found" });
2314
+ }
2315
+ res.json({ success: true });
2316
+ } catch (error) {
2317
+ const message = error instanceof Error ? error.message : "Unknown error";
2318
+ res.status(500).json({ success: false, error: message });
2319
+ }
2320
+ },
2321
+ async bulkUpdate(req, res) {
2322
+ try {
2323
+ const { accountIds, updates } = req.body;
2324
+ if (!Array.isArray(accountIds) || accountIds.length === 0) {
2325
+ return res.status(400).json({ success: false, error: "accountIds array is required" });
2326
+ }
2327
+ const allowed = {};
2328
+ if (updates?.status !== void 0) allowed.status = updates.status;
2329
+ if (updates?.dailyMax !== void 0) allowed["limits.dailyMax"] = updates.dailyMax;
2330
+ if (Object.keys(allowed).length === 0) {
2331
+ return res.status(400).json({ success: false, error: "No valid updates provided" });
2332
+ }
2333
+ const result = await EmailAccount.updateMany(
2334
+ { _id: { $in: accountIds } },
2335
+ { $set: allowed }
2336
+ );
2337
+ res.json({ success: true, data: { matched: result.matchedCount, modified: result.modifiedCount } });
2338
+ } catch (error) {
2339
+ const message = error instanceof Error ? error.message : "Unknown error";
2340
+ res.status(500).json({ success: false, error: message });
2341
+ }
2342
+ },
2343
+ async getCapacity(_req, res) {
2344
+ try {
2345
+ const result = await capacityManager.getAllCapacity();
2346
+ res.json({ success: true, data: result });
2347
+ } catch (error) {
2348
+ const message = error instanceof Error ? error.message : "Unknown error";
2349
+ res.status(500).json({ success: false, error: message });
2350
+ }
2351
+ },
2352
+ async getHealth(_req, res) {
2353
+ try {
2354
+ const accounts = await healthTracker.getAllHealth();
2355
+ res.json({ success: true, data: { accounts } });
2356
+ } catch (error) {
2357
+ const message = error instanceof Error ? error.message : "Unknown error";
2358
+ res.status(500).json({ success: false, error: message });
2359
+ }
2360
+ },
2361
+ async getWarmupStatus(_req, res) {
2362
+ try {
2363
+ const accounts = await EmailAccount.find({ "warmup.enabled": true });
2364
+ const statuses = await Promise.all(
2365
+ accounts.map((a) => warmupManager.getStatus(a._id.toString()))
2366
+ );
2367
+ res.json({ success: true, data: { accounts: statuses.filter(Boolean) } });
2368
+ } catch (error) {
2369
+ const message = error instanceof Error ? error.message : "Unknown error";
2370
+ res.status(500).json({ success: false, error: message });
2371
+ }
2372
+ },
2373
+ async testConnection(req, res) {
2374
+ try {
2375
+ const result = await smtpService.testConnection(req.params.id);
2376
+ res.json({ success: true, data: result });
2377
+ } catch (error) {
2378
+ const message = error instanceof Error ? error.message : "Unknown error";
2379
+ res.status(500).json({ success: false, error: message });
2380
+ }
2381
+ },
2382
+ async checkBounces(req, res) {
2383
+ try {
2384
+ if (!imapBounceChecker) {
2385
+ return res.status(400).json({ success: false, error: "IMAP bounce checker not available" });
2386
+ }
2387
+ const result = await imapBounceChecker.checkAccount(req.params.id);
2388
+ res.json({ success: true, data: result });
2389
+ } catch (error) {
2390
+ const message = error instanceof Error ? error.message : "Unknown error";
2391
+ res.status(500).json({ success: false, error: message });
2392
+ }
2393
+ },
2394
+ async getWarmup(req, res) {
2395
+ try {
2396
+ const status = await warmupManager.getStatus(req.params.id);
2397
+ if (!status) {
2398
+ return res.status(404).json({ success: false, error: "Account not found" });
2399
+ }
2400
+ res.json({ success: true, data: status });
2401
+ } catch (error) {
2402
+ const message = error instanceof Error ? error.message : "Unknown error";
2403
+ res.status(500).json({ success: false, error: message });
2404
+ }
2405
+ },
2406
+ async updateWarmupSchedule(req, res) {
2407
+ try {
2408
+ const { schedule } = req.body;
2409
+ if (!Array.isArray(schedule)) {
2410
+ return res.status(400).json({ success: false, error: "schedule array is required" });
2411
+ }
2412
+ await warmupManager.updateSchedule(req.params.id, schedule);
2413
+ res.json({ success: true });
2414
+ } catch (error) {
2415
+ const message = error instanceof Error ? error.message : "Unknown error";
2416
+ res.status(500).json({ success: false, error: message });
2417
+ }
2418
+ },
2419
+ async startWarmup(req, res) {
2420
+ try {
2421
+ await warmupManager.startWarmup(req.params.id);
2422
+ res.json({ success: true });
2423
+ } catch (error) {
2424
+ const message = error instanceof Error ? error.message : "Unknown error";
2425
+ res.status(500).json({ success: false, error: message });
2426
+ }
2427
+ },
2428
+ async completeWarmup(req, res) {
2429
+ try {
2430
+ await warmupManager.completeWarmup(req.params.id);
2431
+ res.json({ success: true });
2432
+ } catch (error) {
2433
+ const message = error instanceof Error ? error.message : "Unknown error";
2434
+ res.status(500).json({ success: false, error: message });
2435
+ }
2436
+ },
2437
+ async resetWarmup(req, res) {
2438
+ try {
2439
+ await warmupManager.resetWarmup(req.params.id);
2440
+ res.json({ success: true });
2441
+ } catch (error) {
2442
+ const message = error instanceof Error ? error.message : "Unknown error";
2443
+ res.status(500).json({ success: false, error: message });
2444
+ }
2445
+ },
2446
+ async updateHealthThresholds(req, res) {
2447
+ try {
2448
+ const { thresholds } = req.body;
2449
+ if (!thresholds || typeof thresholds !== "object") {
2450
+ return res.status(400).json({ success: false, error: "thresholds object is required" });
2451
+ }
2452
+ const updates = {};
2453
+ if (thresholds.minScore !== void 0) updates["health.thresholds.minScore"] = thresholds.minScore;
2454
+ if (thresholds.maxBounceRate !== void 0) updates["health.thresholds.maxBounceRate"] = thresholds.maxBounceRate;
2455
+ if (thresholds.maxConsecutiveErrors !== void 0) updates["health.thresholds.maxConsecutiveErrors"] = thresholds.maxConsecutiveErrors;
2456
+ const account = await EmailAccount.findByIdAndUpdate(
2457
+ req.params.id,
2458
+ { $set: updates },
2459
+ { new: true }
2460
+ ).select("-smtp.pass -imap.pass");
2461
+ if (!account) {
2462
+ return res.status(404).json({ success: false, error: "Account not found" });
2463
+ }
2464
+ res.json({ success: true, data: { account } });
2465
+ } catch (error) {
2466
+ const message = error instanceof Error ? error.message : "Unknown error";
2467
+ res.status(500).json({ success: false, error: message });
2468
+ }
2469
+ }
2470
+ };
2471
+ }
2472
+
2473
+ // src/controllers/identifier.controller.ts
2474
+ function createIdentifierController(identifierService) {
2475
+ return {
2476
+ async list(req, res) {
2477
+ try {
2478
+ const status = req.query.status;
2479
+ const page = parseInt(req.query.page, 10) || 1;
2480
+ const limit = parseInt(req.query.limit, 10) || 50;
2481
+ const result = await identifierService.list(
2482
+ status ? { status } : void 0,
2483
+ page,
2484
+ limit
2485
+ );
2486
+ res.json({ success: true, data: result });
2487
+ } catch (error) {
2488
+ const message = error instanceof Error ? error.message : "Unknown error";
2489
+ res.status(500).json({ success: false, error: message });
2490
+ }
2491
+ },
2492
+ async getByEmail(req, res) {
2493
+ try {
2494
+ const identifier = await identifierService.findByEmail(req.params.email);
2495
+ if (!identifier) {
2496
+ return res.status(404).json({ success: false, error: "Identifier not found" });
2497
+ }
2498
+ res.json({ success: true, data: { identifier } });
2499
+ } catch (error) {
2500
+ const message = error instanceof Error ? error.message : "Unknown error";
2501
+ res.status(500).json({ success: false, error: message });
2502
+ }
2503
+ },
2504
+ async updateStatus(req, res) {
2505
+ try {
2506
+ const { status } = req.body;
2507
+ if (!status) {
2508
+ return res.status(400).json({ success: false, error: "status is required" });
2509
+ }
2510
+ await identifierService.updateStatus(req.params.email, status);
2511
+ res.json({ success: true });
2512
+ } catch (error) {
2513
+ const message = error instanceof Error ? error.message : "Unknown error";
2514
+ res.status(500).json({ success: false, error: message });
2515
+ }
2516
+ },
2517
+ async merge(req, res) {
2518
+ try {
2519
+ const { sourceEmail, targetEmail } = req.body;
2520
+ if (!sourceEmail || !targetEmail) {
2521
+ return res.status(400).json({ success: false, error: "sourceEmail and targetEmail are required" });
2522
+ }
2523
+ await identifierService.merge(sourceEmail, targetEmail);
2524
+ res.json({ success: true });
2525
+ } catch (error) {
2526
+ const message = error instanceof Error ? error.message : "Unknown error";
2527
+ res.status(500).json({ success: false, error: message });
2528
+ }
2529
+ }
2530
+ };
2531
+ }
2532
+
2533
+ // src/controllers/approval.controller.ts
2534
+ function createApprovalController(approvalService) {
2535
+ return {
2536
+ async getDrafts(req, res) {
2537
+ try {
2538
+ const status = req.query.status;
2539
+ const page = parseInt(req.query.page, 10) || 1;
2540
+ const limit = parseInt(req.query.limit, 10) || 50;
2541
+ const result = await approvalService.getDrafts(
2542
+ status ? { status } : void 0,
2543
+ page,
2544
+ limit
2545
+ );
2546
+ res.json({ success: true, data: result });
2547
+ } catch (error) {
2548
+ const message = error instanceof Error ? error.message : "Unknown error";
2549
+ res.status(500).json({ success: false, error: message });
2550
+ }
2551
+ },
2552
+ async getDraftById(req, res) {
2553
+ try {
2554
+ const draft = await approvalService.getDraftById(req.params.id);
2555
+ if (!draft) {
2556
+ return res.status(404).json({ success: false, error: "Draft not found" });
2557
+ }
2558
+ res.json({ success: true, data: { draft } });
2559
+ } catch (error) {
2560
+ const message = error instanceof Error ? error.message : "Unknown error";
2561
+ res.status(500).json({ success: false, error: message });
2562
+ }
2563
+ },
2564
+ async countByStatus(_req, res) {
2565
+ try {
2566
+ const counts = await approvalService.countByStatus();
2567
+ res.json({ success: true, data: counts });
2568
+ } catch (error) {
2569
+ const message = error instanceof Error ? error.message : "Unknown error";
2570
+ res.status(500).json({ success: false, error: message });
2571
+ }
2572
+ },
2573
+ async approve(req, res) {
2574
+ try {
2575
+ await approvalService.approve(req.params.id);
2576
+ res.json({ success: true });
2577
+ } catch (error) {
2578
+ const message = error instanceof Error ? error.message : "Unknown error";
2579
+ res.status(500).json({ success: false, error: message });
2580
+ }
2581
+ },
2582
+ async reject(req, res) {
2583
+ try {
2584
+ const { reason } = req.body;
2585
+ await approvalService.reject(req.params.id, reason);
2586
+ res.json({ success: true });
2587
+ } catch (error) {
2588
+ const message = error instanceof Error ? error.message : "Unknown error";
2589
+ res.status(500).json({ success: false, error: message });
2590
+ }
2591
+ },
2592
+ async sendNow(req, res) {
2593
+ try {
2594
+ await approvalService.sendNow(req.params.id);
2595
+ res.json({ success: true });
2596
+ } catch (error) {
2597
+ const message = error instanceof Error ? error.message : "Unknown error";
2598
+ res.status(500).json({ success: false, error: message });
2599
+ }
2600
+ },
2601
+ async updateContent(req, res) {
2602
+ try {
2603
+ const draft = await approvalService.updateContent(req.params.id, req.body);
2604
+ res.json({ success: true, data: { draft } });
2605
+ } catch (error) {
2606
+ const message = error instanceof Error ? error.message : "Unknown error";
2607
+ res.status(500).json({ success: false, error: message });
2608
+ }
2609
+ },
2610
+ async bulkApprove(req, res) {
2611
+ try {
2612
+ const { draftIds } = req.body;
2613
+ if (!Array.isArray(draftIds) || draftIds.length === 0) {
2614
+ return res.status(400).json({ success: false, error: "draftIds array is required" });
2615
+ }
2616
+ await approvalService.bulkApprove(draftIds);
2617
+ res.json({ success: true, data: { approved: draftIds.length } });
2618
+ } catch (error) {
2619
+ const message = error instanceof Error ? error.message : "Unknown error";
2620
+ res.status(500).json({ success: false, error: message });
2621
+ }
2622
+ },
2623
+ async bulkReject(req, res) {
2624
+ try {
2625
+ const { draftIds, reason } = req.body;
2626
+ if (!Array.isArray(draftIds) || draftIds.length === 0) {
2627
+ return res.status(400).json({ success: false, error: "draftIds array is required" });
2628
+ }
2629
+ await approvalService.bulkReject(draftIds, reason);
2630
+ res.json({ success: true, data: { rejected: draftIds.length } });
2631
+ } catch (error) {
2632
+ const message = error instanceof Error ? error.message : "Unknown error";
2633
+ res.status(500).json({ success: false, error: message });
2634
+ }
2635
+ }
2636
+ };
2637
+ }
2638
+
2639
+ // src/controllers/settings.controller.ts
2640
+ function createSettingsController(settingsService) {
2641
+ return {
2642
+ async getSettings(_req, res) {
2643
+ try {
2644
+ const settings = await settingsService.get();
2645
+ res.json({ success: true, data: { settings } });
2646
+ } catch (error) {
2647
+ const message = error instanceof Error ? error.message : "Unknown error";
2648
+ res.status(500).json({ success: false, error: message });
2649
+ }
2650
+ },
2651
+ async updateSettings(req, res) {
2652
+ try {
2653
+ const settings = await settingsService.update(req.body);
2654
+ res.json({ success: true, data: { settings } });
2655
+ } catch (error) {
2656
+ const message = error instanceof Error ? error.message : "Unknown error";
2657
+ res.status(500).json({ success: false, error: message });
2658
+ }
2659
+ },
2660
+ async updateTimezone(req, res) {
2661
+ try {
2662
+ const settings = await settingsService.updateSection("timezone", req.body.timezone);
2663
+ res.json({ success: true, data: { settings } });
2664
+ } catch (error) {
2665
+ const message = error instanceof Error ? error.message : "Unknown error";
2666
+ res.status(500).json({ success: false, error: message });
2667
+ }
2668
+ },
2669
+ async updateDevMode(req, res) {
2670
+ try {
2671
+ const settings = await settingsService.updateSection("devMode", req.body);
2672
+ res.json({ success: true, data: { settings } });
2673
+ } catch (error) {
2674
+ const message = error instanceof Error ? error.message : "Unknown error";
2675
+ res.status(500).json({ success: false, error: message });
2676
+ }
2677
+ },
2678
+ async updateImap(req, res) {
2679
+ try {
2680
+ const settings = await settingsService.updateSection("imap", req.body);
2681
+ res.json({ success: true, data: { settings } });
2682
+ } catch (error) {
2683
+ const message = error instanceof Error ? error.message : "Unknown error";
2684
+ res.status(500).json({ success: false, error: message });
2685
+ }
2686
+ },
2687
+ async updateApproval(req, res) {
2688
+ try {
2689
+ const settings = await settingsService.updateSection("approval", req.body);
2690
+ res.json({ success: true, data: { settings } });
2691
+ } catch (error) {
2692
+ const message = error instanceof Error ? error.message : "Unknown error";
2693
+ res.status(500).json({ success: false, error: message });
2694
+ }
2695
+ },
2696
+ async updateQueues(req, res) {
2697
+ try {
2698
+ const settings = await settingsService.updateSection("queues", req.body);
2699
+ res.json({ success: true, data: { settings } });
2700
+ } catch (error) {
2701
+ const message = error instanceof Error ? error.message : "Unknown error";
2702
+ res.status(500).json({ success: false, error: message });
2703
+ }
2704
+ },
2705
+ async updateSes(req, res) {
2706
+ try {
2707
+ const settings = await settingsService.updateSection("ses", req.body);
2708
+ res.json({ success: true, data: { settings } });
2709
+ } catch (error) {
2710
+ const message = error instanceof Error ? error.message : "Unknown error";
2711
+ res.status(500).json({ success: false, error: message });
2712
+ }
2713
+ }
2714
+ };
2715
+ }
2716
+
2717
+ // src/controllers/unsubscribe.controller.ts
2718
+ function createUnsubscribeController(unsubscribeService) {
2719
+ return {
2720
+ async handleGet(req, res) {
2721
+ try {
2722
+ const token = req.query.token;
2723
+ if (!token) {
2724
+ const html2 = unsubscribeService.getConfirmationHtml("", false);
2725
+ return res.status(400).type("html").send(html2);
2726
+ }
2727
+ const result = await unsubscribeService.handleUnsubscribe("", token);
2728
+ const html = unsubscribeService.getConfirmationHtml(result.email || "", result.success);
2729
+ res.status(200).type("html").send(html);
2730
+ } catch {
2731
+ const html = unsubscribeService.getConfirmationHtml("", false);
2732
+ res.status(500).type("html").send(html);
2733
+ }
2734
+ },
2735
+ async handlePost(req, res) {
2736
+ try {
2737
+ const token = req.query.token || req.body?.token;
2738
+ if (!token) {
2739
+ return res.status(400).json({ success: false, error: "Missing token" });
2740
+ }
2741
+ const result = await unsubscribeService.handleUnsubscribe("", token);
2742
+ res.json({ success: result.success, error: result.error });
2743
+ } catch (error) {
2744
+ const message = error instanceof Error ? error.message : "Unknown error";
2745
+ res.status(500).json({ success: false, error: message });
2746
+ }
2747
+ }
2748
+ };
2749
+ }
2750
+ function createAdminRoutes(deps) {
2751
+ const router = Router();
2752
+ const {
2753
+ accountController,
2754
+ identifierController,
2755
+ approvalController,
2756
+ settingsController,
2757
+ queueService
2758
+ } = deps;
2759
+ router.get("/accounts", accountController.list);
2760
+ router.post("/accounts", accountController.create);
2761
+ router.get("/accounts/capacity", accountController.getCapacity);
2762
+ router.get("/accounts/health", accountController.getHealth);
2763
+ router.get("/accounts/warmup", accountController.getWarmupStatus);
2764
+ router.patch("/accounts/bulk-update", accountController.bulkUpdate);
2765
+ router.get("/accounts/:id", accountController.getById);
2766
+ router.put("/accounts/:id", accountController.update);
2767
+ router.delete("/accounts/:id", accountController.remove);
2768
+ router.post("/accounts/:id/test", accountController.testConnection);
2769
+ router.post("/accounts/:id/check-bounces", accountController.checkBounces);
2770
+ router.get("/accounts/:id/warmup", accountController.getWarmup);
2771
+ router.put("/accounts/:id/warmup/schedule", accountController.updateWarmupSchedule);
2772
+ router.post("/accounts/:id/warmup/start", accountController.startWarmup);
2773
+ router.post("/accounts/:id/warmup/complete", accountController.completeWarmup);
2774
+ router.post("/accounts/:id/warmup/reset", accountController.resetWarmup);
2775
+ router.put("/accounts/:id/health/thresholds", accountController.updateHealthThresholds);
2776
+ router.get("/identifiers", identifierController.list);
2777
+ router.get("/identifiers/:email", identifierController.getByEmail);
2778
+ router.patch("/identifiers/:email/status", identifierController.updateStatus);
2779
+ router.post("/identifiers/merge", identifierController.merge);
2780
+ router.get("/drafts", approvalController.getDrafts);
2781
+ router.get("/drafts/count", approvalController.countByStatus);
2782
+ router.post("/drafts/bulk-approve", approvalController.bulkApprove);
2783
+ router.post("/drafts/bulk-reject", approvalController.bulkReject);
2784
+ router.get("/drafts/:id", approvalController.getDraftById);
2785
+ router.patch("/drafts/:id/approve", approvalController.approve);
2786
+ router.patch("/drafts/:id/reject", approvalController.reject);
2787
+ router.post("/drafts/:id/send-now", approvalController.sendNow);
2788
+ router.patch("/drafts/:id/content", approvalController.updateContent);
2789
+ router.get("/settings", settingsController.getSettings);
2790
+ router.put("/settings", settingsController.updateSettings);
2791
+ router.patch("/settings/timezone", settingsController.updateTimezone);
2792
+ router.patch("/settings/dev-mode", settingsController.updateDevMode);
2793
+ router.patch("/settings/imap", settingsController.updateImap);
2794
+ router.patch("/settings/approval", settingsController.updateApproval);
2795
+ router.patch("/settings/queues", settingsController.updateQueues);
2796
+ router.patch("/settings/ses", settingsController.updateSes);
2797
+ router.get("/queues/stats", async (_req, res) => {
2798
+ try {
2799
+ const stats = await queueService.getStats();
2800
+ res.json({ success: true, data: stats });
2801
+ } catch (error) {
2802
+ const message = error instanceof Error ? error.message : "Unknown error";
2803
+ res.status(500).json({ success: false, error: message });
2804
+ }
2805
+ });
2806
+ return router;
2807
+ }
2808
+ function createSesWebhookRoutes(handler) {
2809
+ const router = Router();
2810
+ router.use(text({ type: "*/*" }));
2811
+ router.use(json({ type: "application/json" }));
2812
+ router.post("/", async (req, res) => {
2813
+ try {
2814
+ const body = typeof req.body === "string" ? JSON.parse(req.body) : req.body;
2815
+ const result = await handler.handleSnsMessage(body);
2816
+ res.json({ success: true, ...result });
2817
+ } catch (error) {
2818
+ const message = error instanceof Error ? error.message : "Unknown error";
2819
+ const status = message.includes("signature") ? 403 : 500;
2820
+ res.status(status).json({ success: false, error: message });
2821
+ }
2822
+ });
2823
+ return router;
2824
+ }
2825
+ function createUnsubscribeRoutes(controller) {
2826
+ const router = Router();
2827
+ router.get("/", controller.handleGet);
2828
+ router.post("/", controller.handlePost);
2829
+ return router;
2830
+ }
2831
+
2832
+ // src/index.ts
2833
+ var noopLogger = {
2834
+ info: () => {
2835
+ },
2836
+ warn: () => {
2837
+ },
2838
+ error: () => {
2839
+ }
2840
+ };
2841
+ function createEmailAccountManager(config) {
2842
+ validateConfig(config);
2843
+ const conn = config.db.connection;
2844
+ const prefix = config.db.collectionPrefix || "";
2845
+ const logger = config.logger || noopLogger;
2846
+ const hooks = config.hooks;
2847
+ const EmailAccount = conn.model(
2848
+ `${prefix}EmailAccount`,
2849
+ createEmailAccountSchema()
2850
+ );
2851
+ const EmailDailyStats = conn.model(
2852
+ `${prefix}EmailDailyStats`,
2853
+ createEmailDailyStatsSchema()
2854
+ );
2855
+ const EmailIdentifier = conn.model(
2856
+ `${prefix}EmailIdentifier`,
2857
+ createEmailIdentifierSchema()
2858
+ );
2859
+ const EmailDraft = conn.model(
2860
+ `${prefix}EmailDraft`,
2861
+ createEmailDraftSchema()
2862
+ );
2863
+ const GlobalSettings = conn.model(
2864
+ `${prefix}GlobalSettings`,
2865
+ createGlobalSettingsSchema()
2866
+ );
2867
+ const settingsService = new SettingsService(GlobalSettings, logger);
2868
+ const identifierService = new IdentifierService(EmailIdentifier, logger, hooks);
2869
+ const healthTracker = new HealthTracker(
2870
+ EmailAccount,
2871
+ EmailDailyStats,
2872
+ settingsService,
2873
+ logger,
2874
+ hooks
2875
+ );
2876
+ const warmupManager = new WarmupManager(EmailAccount, config, logger, hooks);
2877
+ const capacityManager = new CapacityManager(
2878
+ EmailAccount,
2879
+ EmailDailyStats,
2880
+ warmupManager,
2881
+ settingsService,
2882
+ logger
2883
+ );
2884
+ const unsubscribeService = new UnsubscribeService(EmailIdentifier, config, logger, hooks);
2885
+ const queueService = new QueueService(
2886
+ config.redis.connection,
2887
+ config,
2888
+ settingsService,
2889
+ logger
2890
+ );
2891
+ const smtpService = new SmtpService(
2892
+ EmailAccount,
2893
+ capacityManager,
2894
+ healthTracker,
2895
+ identifierService,
2896
+ unsubscribeService,
2897
+ queueService,
2898
+ settingsService,
2899
+ config,
2900
+ logger,
2901
+ hooks
2902
+ );
2903
+ const approvalService = new ApprovalService(
2904
+ EmailDraft,
2905
+ queueService,
2906
+ settingsService,
2907
+ logger,
2908
+ hooks
2909
+ );
2910
+ const imapBounceChecker = new ImapBounceChecker(
2911
+ EmailAccount,
2912
+ healthTracker,
2913
+ identifierService,
2914
+ settingsService,
2915
+ logger,
2916
+ hooks
2917
+ );
2918
+ const sesWebhookHandler = new SesWebhookHandler(
2919
+ healthTracker,
2920
+ identifierService,
2921
+ EmailAccount,
2922
+ config,
2923
+ logger,
2924
+ hooks
2925
+ );
2926
+ const sendProcessor = createSendProcessor(smtpService, logger);
2927
+ const approvalProcessor = createApprovalProcessor(
2928
+ EmailDraft,
2929
+ smtpService,
2930
+ queueService,
2931
+ logger
2932
+ );
2933
+ queueService.init({ sendProcessor, approvalProcessor }).catch((err) => {
2934
+ logger.error("Failed to initialize queues", {
2935
+ error: err instanceof Error ? err.message : "Unknown error"
2936
+ });
2937
+ });
2938
+ imapBounceChecker.start().catch((err) => {
2939
+ logger.error("Failed to start IMAP bounce checker", {
2940
+ error: err instanceof Error ? err.message : "Unknown error"
2941
+ });
2942
+ });
2943
+ const accountController = createAccountController(
2944
+ EmailAccount,
2945
+ capacityManager,
2946
+ healthTracker,
2947
+ warmupManager,
2948
+ smtpService,
2949
+ imapBounceChecker,
2950
+ config
2951
+ );
2952
+ const identifierController = createIdentifierController(identifierService);
2953
+ const approvalController = createApprovalController(approvalService);
2954
+ const settingsController = createSettingsController(settingsService);
2955
+ const unsubscribeController = createUnsubscribeController(unsubscribeService);
2956
+ const routes = createAdminRoutes({
2957
+ accountController,
2958
+ identifierController,
2959
+ approvalController,
2960
+ settingsController,
2961
+ queueService
2962
+ });
2963
+ const sesWebhookRouter = createSesWebhookRoutes(sesWebhookHandler);
2964
+ const unsubscribeRouter = createUnsubscribeRoutes(unsubscribeController);
2965
+ const accounts = {
2966
+ model: EmailAccount,
2967
+ create: (data) => EmailAccount.create(data),
2968
+ findById: (id) => EmailAccount.findById(id),
2969
+ update: (id, data) => EmailAccount.findByIdAndUpdate(id, { $set: data }, { new: true }),
2970
+ remove: (id) => EmailAccount.findByIdAndDelete(id)
2971
+ };
2972
+ async function destroy() {
2973
+ imapBounceChecker.stop();
2974
+ smtpService.closeAll();
2975
+ await queueService.close();
2976
+ logger.info("EmailAccountManager destroyed");
2977
+ }
2978
+ return {
2979
+ routes,
2980
+ webhookRoutes: { ses: sesWebhookRouter },
2981
+ unsubscribeRoutes: unsubscribeRouter,
2982
+ accounts,
2983
+ capacity: capacityManager,
2984
+ health: healthTracker,
2985
+ warmup: warmupManager,
2986
+ smtp: smtpService,
2987
+ imap: imapBounceChecker,
2988
+ approval: approvalService,
2989
+ unsubscribe: unsubscribeService,
2990
+ identifiers: identifierService,
2991
+ queues: queueService,
2992
+ settings: settingsService,
2993
+ destroy
2994
+ };
2995
+ }
2996
+
2997
+ export { ACCOUNT_PROVIDER, ACCOUNT_STATUS, AccountDisabledError, AccountNotFoundError, AlxAccountError, ApprovalService, BOUNCE_TYPE, CapacityManager, ConfigValidationError, DRAFT_STATUS, DraftNotFoundError, EMAIL_EVENT_TYPE, HealthTracker, IDENTIFIER_STATUS, IdentifierService, ImapBounceChecker, InvalidTokenError, NoAvailableAccountError, QueueService, QuotaExceededError, SES_BOUNCE_TYPE, SES_COMPLAINT_TYPE, SES_NOTIFICATION_TYPE, SNS_MESSAGE_TYPE, SesWebhookHandler, SettingsService, SmtpConnectionError, SmtpService, SnsSignatureError, UnsubscribeService, WarmupManager, createEmailAccountManager, createEmailAccountSchema, createEmailDailyStatsSchema, createEmailDraftSchema, createEmailIdentifierSchema, createGlobalSettingsSchema, validateConfig };
2998
+ //# sourceMappingURL=index.mjs.map
2999
+ //# sourceMappingURL=index.mjs.map