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