@wraps.dev/cli 0.2.0 → 0.3.3
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/cli.js +1028 -60
- package/dist/cli.js.map +1 -1
- package/dist/console/assets/index-BXE8qB8y.css +1 -0
- package/dist/console/assets/index-BmOMPb6r.js +381 -0
- package/dist/console/index.html +2 -2
- package/dist/lambda/event-processor/.bundled +1 -1
- package/package.json +4 -1
- package/dist/console/assets/index-D7Es86Zn.css +0 -1
- package/dist/console/assets/index-VnWZyGbz.js +0 -366
package/dist/cli.js
CHANGED
|
@@ -18,6 +18,165 @@ var init_esm_shims = __esm({
|
|
|
18
18
|
}
|
|
19
19
|
});
|
|
20
20
|
|
|
21
|
+
// src/infrastructure/resources/mail-manager.ts
|
|
22
|
+
var mail_manager_exports = {};
|
|
23
|
+
__export(mail_manager_exports, {
|
|
24
|
+
createMailManagerArchive: () => createMailManagerArchive
|
|
25
|
+
});
|
|
26
|
+
import {
|
|
27
|
+
CreateArchiveCommand,
|
|
28
|
+
GetArchiveCommand,
|
|
29
|
+
ListArchivesCommand,
|
|
30
|
+
MailManagerClient
|
|
31
|
+
} from "@aws-sdk/client-mailmanager";
|
|
32
|
+
import {
|
|
33
|
+
PutConfigurationSetArchivingOptionsCommand,
|
|
34
|
+
SESv2Client
|
|
35
|
+
} from "@aws-sdk/client-sesv2";
|
|
36
|
+
function retentionToAWSPeriod(retention) {
|
|
37
|
+
switch (retention) {
|
|
38
|
+
case "3months":
|
|
39
|
+
return "THREE_MONTHS";
|
|
40
|
+
case "6months":
|
|
41
|
+
return "SIX_MONTHS";
|
|
42
|
+
case "9months":
|
|
43
|
+
return "NINE_MONTHS";
|
|
44
|
+
case "1year":
|
|
45
|
+
return "ONE_YEAR";
|
|
46
|
+
case "18months":
|
|
47
|
+
return "EIGHTEEN_MONTHS";
|
|
48
|
+
case "2years":
|
|
49
|
+
return "TWO_YEARS";
|
|
50
|
+
case "30months":
|
|
51
|
+
return "THIRTY_MONTHS";
|
|
52
|
+
case "3years":
|
|
53
|
+
return "THREE_YEARS";
|
|
54
|
+
case "4years":
|
|
55
|
+
return "FOUR_YEARS";
|
|
56
|
+
case "5years":
|
|
57
|
+
return "FIVE_YEARS";
|
|
58
|
+
case "6years":
|
|
59
|
+
return "SIX_YEARS";
|
|
60
|
+
case "7years":
|
|
61
|
+
return "SEVEN_YEARS";
|
|
62
|
+
case "8years":
|
|
63
|
+
return "EIGHT_YEARS";
|
|
64
|
+
case "9years":
|
|
65
|
+
return "NINE_YEARS";
|
|
66
|
+
case "10years":
|
|
67
|
+
return "TEN_YEARS";
|
|
68
|
+
case "permanent":
|
|
69
|
+
return "PERMANENT";
|
|
70
|
+
default:
|
|
71
|
+
return "THREE_MONTHS";
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
async function createMailManagerArchive(config) {
|
|
75
|
+
const region = config.region || process.env.AWS_REGION || "us-east-1";
|
|
76
|
+
const archiveName = `wraps-${config.name}-archive`;
|
|
77
|
+
const mailManagerClient = new MailManagerClient({ region });
|
|
78
|
+
const sesClient = new SESv2Client({ region });
|
|
79
|
+
const kmsKeyArn = config.kmsKeyArn;
|
|
80
|
+
if (!kmsKeyArn) {
|
|
81
|
+
}
|
|
82
|
+
const awsRetention = retentionToAWSPeriod(config.retention);
|
|
83
|
+
let archiveId;
|
|
84
|
+
let archiveArn;
|
|
85
|
+
try {
|
|
86
|
+
const listCommand = new ListArchivesCommand({});
|
|
87
|
+
const listResult = await mailManagerClient.send(listCommand);
|
|
88
|
+
const existingArchive = listResult.Archives?.find(
|
|
89
|
+
(archive) => archive.ArchiveName === archiveName
|
|
90
|
+
);
|
|
91
|
+
if (existingArchive?.ArchiveId) {
|
|
92
|
+
console.log(`Using existing Mail Manager archive: ${archiveName}`);
|
|
93
|
+
archiveId = existingArchive.ArchiveId;
|
|
94
|
+
const getCommand = new GetArchiveCommand({ ArchiveId: archiveId });
|
|
95
|
+
const getResult = await mailManagerClient.send(getCommand);
|
|
96
|
+
archiveArn = getResult.ArchiveArn;
|
|
97
|
+
}
|
|
98
|
+
} catch (error) {
|
|
99
|
+
console.log("Error checking for existing archive:", error);
|
|
100
|
+
}
|
|
101
|
+
if (!archiveId) {
|
|
102
|
+
try {
|
|
103
|
+
const createArchiveCommand = new CreateArchiveCommand({
|
|
104
|
+
ArchiveName: archiveName,
|
|
105
|
+
Retention: {
|
|
106
|
+
RetentionPeriod: awsRetention
|
|
107
|
+
},
|
|
108
|
+
...kmsKeyArn && { KmsKeyArn: kmsKeyArn },
|
|
109
|
+
Tags: [
|
|
110
|
+
{ Key: "ManagedBy", Value: "wraps-cli" },
|
|
111
|
+
{ Key: "Name", Value: archiveName },
|
|
112
|
+
{ Key: "Retention", Value: config.retention }
|
|
113
|
+
]
|
|
114
|
+
});
|
|
115
|
+
const archiveResult = await mailManagerClient.send(createArchiveCommand);
|
|
116
|
+
archiveId = archiveResult.ArchiveId;
|
|
117
|
+
if (!archiveId) {
|
|
118
|
+
throw new Error(
|
|
119
|
+
"Failed to create Mail Manager Archive: No ArchiveId returned"
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
console.log(`Created new Mail Manager archive: ${archiveName}`);
|
|
123
|
+
} catch (error) {
|
|
124
|
+
if (error instanceof Error && error.name === "ConflictException" && error.message.includes("Archive already exists")) {
|
|
125
|
+
console.log(
|
|
126
|
+
"Archive was created concurrently, fetching existing archive..."
|
|
127
|
+
);
|
|
128
|
+
const listCommand = new ListArchivesCommand({});
|
|
129
|
+
const listResult = await mailManagerClient.send(listCommand);
|
|
130
|
+
const existingArchive = listResult.Archives?.find(
|
|
131
|
+
(archive) => archive.ArchiveName === archiveName
|
|
132
|
+
);
|
|
133
|
+
if (!existingArchive?.ArchiveId) {
|
|
134
|
+
throw new Error(
|
|
135
|
+
`Archive exists but couldn't find it: ${archiveName}`
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
archiveId = existingArchive.ArchiveId;
|
|
139
|
+
const getCommand = new GetArchiveCommand({ ArchiveId: archiveId });
|
|
140
|
+
const getResult = await mailManagerClient.send(getCommand);
|
|
141
|
+
archiveArn = getResult.ArchiveArn;
|
|
142
|
+
} else {
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (!archiveArn) {
|
|
148
|
+
const identity = await import("@aws-sdk/client-sts").then(
|
|
149
|
+
(m) => new m.STSClient({ region }).send(new m.GetCallerIdentityCommand({}))
|
|
150
|
+
);
|
|
151
|
+
const accountId = identity.Account;
|
|
152
|
+
archiveArn = `arn:aws:ses:${region}:${accountId}:mailmanager-archive/${archiveId}`;
|
|
153
|
+
}
|
|
154
|
+
const configSetName = await new Promise((resolve) => {
|
|
155
|
+
config.configSetName.apply((name) => {
|
|
156
|
+
resolve(name);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
const putArchivingOptionsCommand = new PutConfigurationSetArchivingOptionsCommand({
|
|
160
|
+
ConfigurationSetName: configSetName,
|
|
161
|
+
ArchiveArn: archiveArn
|
|
162
|
+
});
|
|
163
|
+
await sesClient.send(putArchivingOptionsCommand);
|
|
164
|
+
if (!(archiveId && archiveArn)) {
|
|
165
|
+
throw new Error("Failed to get archive ID or ARN");
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
archiveId,
|
|
169
|
+
archiveArn,
|
|
170
|
+
kmsKeyArn
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
var init_mail_manager = __esm({
|
|
174
|
+
"src/infrastructure/resources/mail-manager.ts"() {
|
|
175
|
+
"use strict";
|
|
176
|
+
init_esm_shims();
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
21
180
|
// src/utils/errors.ts
|
|
22
181
|
import * as clack from "@clack/prompts";
|
|
23
182
|
import pc from "picocolors";
|
|
@@ -221,14 +380,49 @@ var init_aws = __esm({
|
|
|
221
380
|
function estimateStorageSize(emailsPerMonth, retention, numEventTypes = 8) {
|
|
222
381
|
const avgRecordSizeKB = 2;
|
|
223
382
|
const retentionMonths = {
|
|
224
|
-
"
|
|
225
|
-
"
|
|
226
|
-
"
|
|
383
|
+
"3months": 3,
|
|
384
|
+
"6months": 6,
|
|
385
|
+
"9months": 9,
|
|
386
|
+
"1year": 12,
|
|
387
|
+
"18months": 18,
|
|
388
|
+
"2years": 24,
|
|
389
|
+
"30months": 30,
|
|
390
|
+
"3years": 36,
|
|
391
|
+
"4years": 48,
|
|
392
|
+
"5years": 60,
|
|
393
|
+
"6years": 72,
|
|
394
|
+
"7years": 84,
|
|
395
|
+
"8years": 96,
|
|
396
|
+
"9years": 108,
|
|
397
|
+
"10years": 120,
|
|
398
|
+
permanent: 120
|
|
399
|
+
// Assume 10 years for cost estimation
|
|
400
|
+
}[retention];
|
|
401
|
+
const totalKB = emailsPerMonth * numEventTypes * (retentionMonths ?? 12) * avgRecordSizeKB;
|
|
402
|
+
return totalKB / 1024 / 1024;
|
|
403
|
+
}
|
|
404
|
+
function estimateArchiveStorageSize(emailsPerMonth, retention) {
|
|
405
|
+
const avgEmailSizeKB = 50;
|
|
406
|
+
const retentionMonths = {
|
|
407
|
+
"3months": 3,
|
|
408
|
+
"6months": 6,
|
|
409
|
+
"9months": 9,
|
|
227
410
|
"1year": 12,
|
|
228
|
-
|
|
229
|
-
|
|
411
|
+
"18months": 18,
|
|
412
|
+
"2years": 24,
|
|
413
|
+
"30months": 30,
|
|
414
|
+
"3years": 36,
|
|
415
|
+
"4years": 48,
|
|
416
|
+
"5years": 60,
|
|
417
|
+
"6years": 72,
|
|
418
|
+
"7years": 84,
|
|
419
|
+
"8years": 96,
|
|
420
|
+
"9years": 108,
|
|
421
|
+
"10years": 120,
|
|
422
|
+
permanent: 120
|
|
423
|
+
// Assume 10 years for cost estimation
|
|
230
424
|
}[retention];
|
|
231
|
-
const totalKB = emailsPerMonth *
|
|
425
|
+
const totalKB = emailsPerMonth * (retentionMonths ?? 12) * avgEmailSizeKB;
|
|
232
426
|
return totalKB / 1024 / 1024;
|
|
233
427
|
}
|
|
234
428
|
function calculateEventTrackingCost(config, emailsPerMonth) {
|
|
@@ -265,7 +459,7 @@ function calculateDynamoDBCost(config, emailsPerMonth) {
|
|
|
265
459
|
if (!config.eventTracking?.dynamoDBHistory) {
|
|
266
460
|
return;
|
|
267
461
|
}
|
|
268
|
-
const retention = config.eventTracking.archiveRetention || "
|
|
462
|
+
const retention = config.eventTracking.archiveRetention || "3months";
|
|
269
463
|
const numEventTypes = config.eventTracking.events?.length || 8;
|
|
270
464
|
const totalEvents = emailsPerMonth * numEventTypes;
|
|
271
465
|
const writeCost = Math.max(0, totalEvents - FREE_TIER.DYNAMODB_WRITES) / 1e6 * AWS_PRICING.DYNAMODB_WRITE_PER_MILLION;
|
|
@@ -307,19 +501,35 @@ function calculateDedicatedIpCost(config) {
|
|
|
307
501
|
description: "Dedicated IP address (requires 100k+ emails/day for warmup)"
|
|
308
502
|
};
|
|
309
503
|
}
|
|
504
|
+
function calculateEmailArchivingCost(config, emailsPerMonth) {
|
|
505
|
+
if (!config.emailArchiving?.enabled) {
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
const retention = config.emailArchiving.retention;
|
|
509
|
+
const storageGB = estimateArchiveStorageSize(emailsPerMonth, retention);
|
|
510
|
+
const monthlyDataGB = emailsPerMonth * 50 / 1024 / 1024;
|
|
511
|
+
const ingestionCost = monthlyDataGB * AWS_PRICING.MAIL_MANAGER_INGESTION_PER_GB;
|
|
512
|
+
const storageCost = storageGB * AWS_PRICING.MAIL_MANAGER_STORAGE_PER_GB;
|
|
513
|
+
return {
|
|
514
|
+
monthly: ingestionCost + storageCost,
|
|
515
|
+
description: `Email archiving (${retention}, ~${storageGB.toFixed(2)} GB at steady-state)`
|
|
516
|
+
};
|
|
517
|
+
}
|
|
310
518
|
function calculateCosts(config, emailsPerMonth = 1e4) {
|
|
311
519
|
const tracking = calculateTrackingCost(config);
|
|
312
520
|
const reputationMetrics = calculateReputationMetricsCost(config);
|
|
313
521
|
const eventTracking = calculateEventTrackingCost(config, emailsPerMonth);
|
|
314
522
|
const dynamoDBHistory = calculateDynamoDBCost(config, emailsPerMonth);
|
|
523
|
+
const emailArchiving = calculateEmailArchivingCost(config, emailsPerMonth);
|
|
315
524
|
const dedicatedIp = calculateDedicatedIpCost(config);
|
|
316
525
|
const sesEmailCost = Math.max(0, emailsPerMonth - FREE_TIER.SES_EMAILS) * AWS_PRICING.SES_PER_EMAIL;
|
|
317
|
-
const totalMonthlyCost = sesEmailCost + (tracking?.monthly || 0) + (reputationMetrics?.monthly || 0) + (eventTracking?.monthly || 0) + (dynamoDBHistory?.monthly || 0) + (dedicatedIp?.monthly || 0);
|
|
526
|
+
const totalMonthlyCost = sesEmailCost + (tracking?.monthly || 0) + (reputationMetrics?.monthly || 0) + (eventTracking?.monthly || 0) + (dynamoDBHistory?.monthly || 0) + (emailArchiving?.monthly || 0) + (dedicatedIp?.monthly || 0);
|
|
318
527
|
return {
|
|
319
528
|
tracking,
|
|
320
529
|
reputationMetrics,
|
|
321
530
|
eventTracking,
|
|
322
531
|
dynamoDBHistory,
|
|
532
|
+
emailArchiving,
|
|
323
533
|
dedicatedIp,
|
|
324
534
|
total: {
|
|
325
535
|
monthly: totalMonthlyCost,
|
|
@@ -366,6 +576,11 @@ function getCostSummary(config, emailsPerMonth = 1e4) {
|
|
|
366
576
|
` - ${costs.dynamoDBHistory.description}: ${formatCost(costs.dynamoDBHistory.monthly)}`
|
|
367
577
|
);
|
|
368
578
|
}
|
|
579
|
+
if (costs.emailArchiving) {
|
|
580
|
+
lines.push(
|
|
581
|
+
` - ${costs.emailArchiving.description}: ${formatCost(costs.emailArchiving.monthly)}`
|
|
582
|
+
);
|
|
583
|
+
}
|
|
369
584
|
if (costs.dedicatedIp) {
|
|
370
585
|
lines.push(
|
|
371
586
|
` - ${costs.dedicatedIp.description}: ${formatCost(costs.dedicatedIp.monthly)}`
|
|
@@ -408,8 +623,13 @@ var init_costs = __esm({
|
|
|
408
623
|
// CloudWatch pricing
|
|
409
624
|
CLOUDWATCH_LOGS_PER_GB: 0.5,
|
|
410
625
|
// $0.50 per GB ingested
|
|
411
|
-
CLOUDWATCH_LOGS_STORAGE_PER_GB: 0.03
|
|
626
|
+
CLOUDWATCH_LOGS_STORAGE_PER_GB: 0.03,
|
|
412
627
|
// $0.03 per GB-month
|
|
628
|
+
// SES Mail Manager Archiving
|
|
629
|
+
MAIL_MANAGER_INGESTION_PER_GB: 2,
|
|
630
|
+
// $2.00 per GB ingested
|
|
631
|
+
MAIL_MANAGER_STORAGE_PER_GB: 0.19
|
|
632
|
+
// $0.19 per GB-month
|
|
413
633
|
};
|
|
414
634
|
FREE_TIER = {
|
|
415
635
|
// SES: 3,000 emails/month for first 12 months (new AWS accounts only)
|
|
@@ -487,7 +707,8 @@ function getPresetInfo(preset) {
|
|
|
487
707
|
features: [
|
|
488
708
|
"Open & click tracking",
|
|
489
709
|
"TLS encryption required",
|
|
490
|
-
"Automatic bounce/complaint suppression"
|
|
710
|
+
"Automatic bounce/complaint suppression",
|
|
711
|
+
"Optional: Email archiving (full content storage)"
|
|
491
712
|
]
|
|
492
713
|
},
|
|
493
714
|
production: {
|
|
@@ -499,7 +720,8 @@ function getPresetInfo(preset) {
|
|
|
499
720
|
"Everything in Starter",
|
|
500
721
|
"Reputation metrics dashboard",
|
|
501
722
|
"Real-time event tracking (EventBridge)",
|
|
502
|
-
"
|
|
723
|
+
"3-month email history storage",
|
|
724
|
+
"Optional: Email archiving with rendered viewer",
|
|
503
725
|
"Complete event visibility"
|
|
504
726
|
]
|
|
505
727
|
},
|
|
@@ -512,6 +734,7 @@ function getPresetInfo(preset) {
|
|
|
512
734
|
"Everything in Production",
|
|
513
735
|
"Dedicated IP address",
|
|
514
736
|
"1-year email history",
|
|
737
|
+
"Optional: 1-year+ email archiving",
|
|
515
738
|
"All event types tracked",
|
|
516
739
|
"Priority support eligibility"
|
|
517
740
|
]
|
|
@@ -566,9 +789,9 @@ function validateConfig(config) {
|
|
|
566
789
|
"\u{1F4A1} Event tracking is enabled but history storage is disabled. Events will only be available in real-time."
|
|
567
790
|
);
|
|
568
791
|
}
|
|
569
|
-
if (config.eventTracking?.archiveRetention === "
|
|
792
|
+
if (config.eventTracking?.archiveRetention === "permanent") {
|
|
570
793
|
warnings.push(
|
|
571
|
-
"\u26A0\uFE0F
|
|
794
|
+
"\u26A0\uFE0F Permanent retention can become expensive. Consider 3-month or 1-year retention."
|
|
572
795
|
);
|
|
573
796
|
}
|
|
574
797
|
return warnings;
|
|
@@ -594,6 +817,11 @@ var init_presets = __esm({
|
|
|
594
817
|
eventTracking: {
|
|
595
818
|
enabled: false
|
|
596
819
|
},
|
|
820
|
+
// Email archiving disabled by default
|
|
821
|
+
emailArchiving: {
|
|
822
|
+
enabled: false,
|
|
823
|
+
retention: "3months"
|
|
824
|
+
},
|
|
597
825
|
sendingEnabled: true
|
|
598
826
|
};
|
|
599
827
|
PRODUCTION_PRESET = {
|
|
@@ -622,7 +850,13 @@ var init_presets = __esm({
|
|
|
622
850
|
"RENDERING_FAILURE"
|
|
623
851
|
],
|
|
624
852
|
dynamoDBHistory: true,
|
|
625
|
-
archiveRetention: "
|
|
853
|
+
archiveRetention: "3months"
|
|
854
|
+
},
|
|
855
|
+
// Email archiving with 3-month retention
|
|
856
|
+
emailArchiving: {
|
|
857
|
+
enabled: false,
|
|
858
|
+
// User can opt-in
|
|
859
|
+
retention: "3months"
|
|
626
860
|
},
|
|
627
861
|
sendingEnabled: true
|
|
628
862
|
};
|
|
@@ -656,6 +890,12 @@ var init_presets = __esm({
|
|
|
656
890
|
dynamoDBHistory: true,
|
|
657
891
|
archiveRetention: "1year"
|
|
658
892
|
},
|
|
893
|
+
// Email archiving with 1-year retention
|
|
894
|
+
emailArchiving: {
|
|
895
|
+
enabled: false,
|
|
896
|
+
// User can opt-in
|
|
897
|
+
retention: "1year"
|
|
898
|
+
},
|
|
659
899
|
dedicatedIp: true,
|
|
660
900
|
sendingEnabled: true
|
|
661
901
|
};
|
|
@@ -672,6 +912,7 @@ __export(prompts_exports, {
|
|
|
672
912
|
promptConflictResolution: () => promptConflictResolution,
|
|
673
913
|
promptCustomConfig: () => promptCustomConfig,
|
|
674
914
|
promptDomain: () => promptDomain,
|
|
915
|
+
promptEmailArchiving: () => promptEmailArchiving,
|
|
675
916
|
promptEstimatedVolume: () => promptEstimatedVolume,
|
|
676
917
|
promptFeatureSelection: () => promptFeatureSelection,
|
|
677
918
|
promptIntegrationLevel: () => promptIntegrationLevel,
|
|
@@ -1033,6 +1274,59 @@ async function promptEstimatedVolume() {
|
|
|
1033
1274
|
}
|
|
1034
1275
|
return volume;
|
|
1035
1276
|
}
|
|
1277
|
+
async function promptEmailArchiving() {
|
|
1278
|
+
const enabled = await clack3.confirm({
|
|
1279
|
+
message: "Enable email archiving? (Store full email content with HTML for viewing in dashboard)",
|
|
1280
|
+
initialValue: false
|
|
1281
|
+
});
|
|
1282
|
+
if (clack3.isCancel(enabled)) {
|
|
1283
|
+
clack3.cancel("Operation cancelled.");
|
|
1284
|
+
process.exit(0);
|
|
1285
|
+
}
|
|
1286
|
+
if (!enabled) {
|
|
1287
|
+
return { enabled: false, retention: "3months" };
|
|
1288
|
+
}
|
|
1289
|
+
const retention = await clack3.select({
|
|
1290
|
+
message: "Email archive retention period:",
|
|
1291
|
+
options: [
|
|
1292
|
+
{ value: "7days", label: "7 days", hint: "~$1-2/mo for 10k emails" },
|
|
1293
|
+
{ value: "30days", label: "30 days", hint: "~$2-4/mo for 10k emails" },
|
|
1294
|
+
{
|
|
1295
|
+
value: "3months",
|
|
1296
|
+
label: "90 days (recommended)",
|
|
1297
|
+
hint: "~$5-10/mo for 10k emails"
|
|
1298
|
+
},
|
|
1299
|
+
{
|
|
1300
|
+
value: "6months",
|
|
1301
|
+
label: "6 months",
|
|
1302
|
+
hint: "~$15-25/mo for 10k emails"
|
|
1303
|
+
},
|
|
1304
|
+
{ value: "1year", label: "1 year", hint: "~$25-40/mo for 10k emails" },
|
|
1305
|
+
{
|
|
1306
|
+
value: "18months",
|
|
1307
|
+
label: "18 months",
|
|
1308
|
+
hint: "~$35-60/mo for 10k emails"
|
|
1309
|
+
}
|
|
1310
|
+
],
|
|
1311
|
+
initialValue: "3months"
|
|
1312
|
+
});
|
|
1313
|
+
if (clack3.isCancel(retention)) {
|
|
1314
|
+
clack3.cancel("Operation cancelled.");
|
|
1315
|
+
process.exit(0);
|
|
1316
|
+
}
|
|
1317
|
+
clack3.log.info(
|
|
1318
|
+
pc3.dim(
|
|
1319
|
+
"Archiving stores full RFC 822 emails with HTML, attachments, and headers"
|
|
1320
|
+
)
|
|
1321
|
+
);
|
|
1322
|
+
clack3.log.info(
|
|
1323
|
+
pc3.dim("Cost: $2/GB ingestion + $0.19/GB/month storage (~50KB per email)")
|
|
1324
|
+
);
|
|
1325
|
+
return {
|
|
1326
|
+
enabled: true,
|
|
1327
|
+
retention
|
|
1328
|
+
};
|
|
1329
|
+
}
|
|
1036
1330
|
async function promptCustomConfig() {
|
|
1037
1331
|
clack3.log.info("Custom configuration builder");
|
|
1038
1332
|
clack3.log.info("Configure each feature individually");
|
|
@@ -1053,7 +1347,7 @@ async function promptCustomConfig() {
|
|
|
1053
1347
|
process.exit(0);
|
|
1054
1348
|
}
|
|
1055
1349
|
let dynamoDBHistory = false;
|
|
1056
|
-
let archiveRetention = "
|
|
1350
|
+
let archiveRetention = "3months";
|
|
1057
1351
|
if (eventTrackingEnabled) {
|
|
1058
1352
|
dynamoDBHistory = await clack3.confirm({
|
|
1059
1353
|
message: "Store email history in DynamoDB?",
|
|
@@ -1070,7 +1364,7 @@ async function promptCustomConfig() {
|
|
|
1070
1364
|
{ value: "7days", label: "7 days", hint: "Minimal storage cost" },
|
|
1071
1365
|
{ value: "30days", label: "30 days", hint: "Development/testing" },
|
|
1072
1366
|
{
|
|
1073
|
-
value: "
|
|
1367
|
+
value: "3months",
|
|
1074
1368
|
label: "90 days (recommended)",
|
|
1075
1369
|
hint: "Standard retention"
|
|
1076
1370
|
},
|
|
@@ -1112,6 +1406,53 @@ async function promptCustomConfig() {
|
|
|
1112
1406
|
clack3.cancel("Operation cancelled.");
|
|
1113
1407
|
process.exit(0);
|
|
1114
1408
|
}
|
|
1409
|
+
const emailArchivingEnabled = await clack3.confirm({
|
|
1410
|
+
message: "Enable email archiving? (Store full email content with HTML for viewing)",
|
|
1411
|
+
initialValue: false
|
|
1412
|
+
});
|
|
1413
|
+
if (clack3.isCancel(emailArchivingEnabled)) {
|
|
1414
|
+
clack3.cancel("Operation cancelled.");
|
|
1415
|
+
process.exit(0);
|
|
1416
|
+
}
|
|
1417
|
+
let emailArchiveRetention = "3months";
|
|
1418
|
+
if (emailArchivingEnabled) {
|
|
1419
|
+
emailArchiveRetention = await clack3.select({
|
|
1420
|
+
message: "Email archive retention period:",
|
|
1421
|
+
options: [
|
|
1422
|
+
{ value: "7days", label: "7 days", hint: "~$1-2/mo for 10k emails" },
|
|
1423
|
+
{ value: "30days", label: "30 days", hint: "~$2-4/mo for 10k emails" },
|
|
1424
|
+
{
|
|
1425
|
+
value: "3months",
|
|
1426
|
+
label: "90 days (recommended)",
|
|
1427
|
+
hint: "~$5-10/mo for 10k emails"
|
|
1428
|
+
},
|
|
1429
|
+
{
|
|
1430
|
+
value: "6months",
|
|
1431
|
+
label: "6 months",
|
|
1432
|
+
hint: "~$15-25/mo for 10k emails"
|
|
1433
|
+
},
|
|
1434
|
+
{ value: "1year", label: "1 year", hint: "~$25-40/mo for 10k emails" },
|
|
1435
|
+
{
|
|
1436
|
+
value: "18months",
|
|
1437
|
+
label: "18 months",
|
|
1438
|
+
hint: "~$35-60/mo for 10k emails"
|
|
1439
|
+
}
|
|
1440
|
+
],
|
|
1441
|
+
initialValue: "3months"
|
|
1442
|
+
});
|
|
1443
|
+
if (clack3.isCancel(emailArchiveRetention)) {
|
|
1444
|
+
clack3.cancel("Operation cancelled.");
|
|
1445
|
+
process.exit(0);
|
|
1446
|
+
}
|
|
1447
|
+
clack3.log.info(
|
|
1448
|
+
pc3.dim(
|
|
1449
|
+
"Note: Archiving stores full RFC 822 emails with HTML, attachments, and headers"
|
|
1450
|
+
)
|
|
1451
|
+
);
|
|
1452
|
+
clack3.log.info(
|
|
1453
|
+
pc3.dim("Cost: $2/GB ingestion + $0.19/GB/month storage (~50KB per email)")
|
|
1454
|
+
);
|
|
1455
|
+
}
|
|
1115
1456
|
return {
|
|
1116
1457
|
tracking: trackingEnabled ? {
|
|
1117
1458
|
enabled: true,
|
|
@@ -1138,8 +1479,12 @@ async function promptCustomConfig() {
|
|
|
1138
1479
|
"RENDERING_FAILURE"
|
|
1139
1480
|
],
|
|
1140
1481
|
dynamoDBHistory: Boolean(dynamoDBHistory),
|
|
1141
|
-
archiveRetention: typeof archiveRetention === "string" ? archiveRetention : "
|
|
1482
|
+
archiveRetention: typeof archiveRetention === "string" ? archiveRetention : "3months"
|
|
1142
1483
|
} : { enabled: false },
|
|
1484
|
+
emailArchiving: emailArchivingEnabled ? {
|
|
1485
|
+
enabled: true,
|
|
1486
|
+
retention: typeof emailArchiveRetention === "string" ? emailArchiveRetention : "3months"
|
|
1487
|
+
} : { enabled: false, retention: "3months" },
|
|
1143
1488
|
dedicatedIp,
|
|
1144
1489
|
sendingEnabled: true
|
|
1145
1490
|
};
|
|
@@ -1184,6 +1529,219 @@ var init_assume_role = __esm({
|
|
|
1184
1529
|
}
|
|
1185
1530
|
});
|
|
1186
1531
|
|
|
1532
|
+
// src/utils/archive.ts
|
|
1533
|
+
import {
|
|
1534
|
+
GetArchiveMessageCommand,
|
|
1535
|
+
GetArchiveSearchResultsCommand,
|
|
1536
|
+
MailManagerClient as MailManagerClient2,
|
|
1537
|
+
StartArchiveSearchCommand
|
|
1538
|
+
} from "@aws-sdk/client-mailmanager";
|
|
1539
|
+
import { simpleParser } from "mailparser";
|
|
1540
|
+
function extractArchiveId(archiveArnOrId) {
|
|
1541
|
+
if (archiveArnOrId.startsWith("arn:")) {
|
|
1542
|
+
const parts = archiveArnOrId.split("/");
|
|
1543
|
+
return parts.at(-1);
|
|
1544
|
+
}
|
|
1545
|
+
return archiveArnOrId;
|
|
1546
|
+
}
|
|
1547
|
+
async function getArchivedEmail(archiveArnOrId, searchCriteria, region) {
|
|
1548
|
+
const client = new MailManagerClient2({ region });
|
|
1549
|
+
const archiveId = extractArchiveId(archiveArnOrId);
|
|
1550
|
+
const searchTime = searchCriteria.timestamp || /* @__PURE__ */ new Date();
|
|
1551
|
+
const dayBefore = new Date(searchTime.getTime() - 24 * 60 * 60 * 1e3);
|
|
1552
|
+
const dayAfter = new Date(searchTime.getTime() + 24 * 60 * 60 * 1e3);
|
|
1553
|
+
const filters = [];
|
|
1554
|
+
if (searchCriteria.from) {
|
|
1555
|
+
filters.push({
|
|
1556
|
+
StringExpression: {
|
|
1557
|
+
Evaluate: {
|
|
1558
|
+
Attribute: "FROM"
|
|
1559
|
+
},
|
|
1560
|
+
Operator: "CONTAINS",
|
|
1561
|
+
Values: [searchCriteria.from]
|
|
1562
|
+
}
|
|
1563
|
+
});
|
|
1564
|
+
}
|
|
1565
|
+
if (searchCriteria.to) {
|
|
1566
|
+
filters.push({
|
|
1567
|
+
StringExpression: {
|
|
1568
|
+
Evaluate: {
|
|
1569
|
+
Attribute: "TO"
|
|
1570
|
+
},
|
|
1571
|
+
Operator: "CONTAINS",
|
|
1572
|
+
Values: [searchCriteria.to]
|
|
1573
|
+
}
|
|
1574
|
+
});
|
|
1575
|
+
}
|
|
1576
|
+
if (searchCriteria.subject) {
|
|
1577
|
+
filters.push({
|
|
1578
|
+
StringExpression: {
|
|
1579
|
+
Evaluate: {
|
|
1580
|
+
Attribute: "SUBJECT"
|
|
1581
|
+
},
|
|
1582
|
+
Operator: "CONTAINS",
|
|
1583
|
+
Values: [searchCriteria.subject]
|
|
1584
|
+
}
|
|
1585
|
+
});
|
|
1586
|
+
}
|
|
1587
|
+
if (filters.length === 0) {
|
|
1588
|
+
throw new Error(
|
|
1589
|
+
"At least one search criterion (from, to, or subject) is required"
|
|
1590
|
+
);
|
|
1591
|
+
}
|
|
1592
|
+
const searchCommand = new StartArchiveSearchCommand({
|
|
1593
|
+
ArchiveId: archiveId,
|
|
1594
|
+
FromTimestamp: dayBefore,
|
|
1595
|
+
ToTimestamp: dayAfter,
|
|
1596
|
+
Filters: {
|
|
1597
|
+
Include: filters
|
|
1598
|
+
},
|
|
1599
|
+
MaxResults: 10
|
|
1600
|
+
// Get a few results in case there are multiple matches
|
|
1601
|
+
});
|
|
1602
|
+
const searchResponse = await client.send(searchCommand);
|
|
1603
|
+
const searchId = searchResponse.SearchId;
|
|
1604
|
+
if (!searchId) {
|
|
1605
|
+
throw new Error("Failed to start archive search");
|
|
1606
|
+
}
|
|
1607
|
+
let archivedMessageId;
|
|
1608
|
+
let attempts = 0;
|
|
1609
|
+
const maxAttempts = 20;
|
|
1610
|
+
const pollInterval = 1e3;
|
|
1611
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
1612
|
+
while (attempts < maxAttempts) {
|
|
1613
|
+
try {
|
|
1614
|
+
const resultsCommand = new GetArchiveSearchResultsCommand({
|
|
1615
|
+
SearchId: searchId
|
|
1616
|
+
});
|
|
1617
|
+
const resultsResponse = await client.send(resultsCommand);
|
|
1618
|
+
if (resultsResponse.Rows && resultsResponse.Rows.length > 0) {
|
|
1619
|
+
archivedMessageId = resultsResponse.Rows[0].ArchivedMessageId;
|
|
1620
|
+
break;
|
|
1621
|
+
}
|
|
1622
|
+
if (resultsResponse.Rows && resultsResponse.Rows.length === 0) {
|
|
1623
|
+
break;
|
|
1624
|
+
}
|
|
1625
|
+
} catch (error) {
|
|
1626
|
+
if (error instanceof Error && error.name === "ConflictException" && error.message.includes("still in progress")) {
|
|
1627
|
+
console.log(`Search still in progress, attempt ${attempts + 1}...`);
|
|
1628
|
+
} else {
|
|
1629
|
+
throw error;
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
|
1633
|
+
attempts++;
|
|
1634
|
+
}
|
|
1635
|
+
if (!archivedMessageId) {
|
|
1636
|
+
throw new Error(
|
|
1637
|
+
"Email not found in archive with the provided search criteria. It may have been sent before archiving was enabled."
|
|
1638
|
+
);
|
|
1639
|
+
}
|
|
1640
|
+
const command2 = new GetArchiveMessageCommand({
|
|
1641
|
+
ArchivedMessageId: archivedMessageId
|
|
1642
|
+
});
|
|
1643
|
+
const response = await client.send(command2);
|
|
1644
|
+
if (!response.MessageDownloadLink) {
|
|
1645
|
+
throw new Error("No download link available for archived message");
|
|
1646
|
+
}
|
|
1647
|
+
const emailResponse = await fetch(response.MessageDownloadLink);
|
|
1648
|
+
if (!emailResponse.ok) {
|
|
1649
|
+
throw new Error(`Failed to download email: ${emailResponse.statusText}`);
|
|
1650
|
+
}
|
|
1651
|
+
const emailRaw = await emailResponse.text();
|
|
1652
|
+
const parsed = await simpleParser(emailRaw);
|
|
1653
|
+
const attachments = parsed.attachments?.map((att) => ({
|
|
1654
|
+
filename: att.filename,
|
|
1655
|
+
contentType: att.contentType,
|
|
1656
|
+
size: att.size
|
|
1657
|
+
})) || [];
|
|
1658
|
+
const headers = {};
|
|
1659
|
+
if (parsed.headers) {
|
|
1660
|
+
for (const [key, value] of parsed.headers) {
|
|
1661
|
+
if (value instanceof Date) {
|
|
1662
|
+
headers[key] = value.toISOString();
|
|
1663
|
+
} else if (typeof value === "string") {
|
|
1664
|
+
headers[key] = value;
|
|
1665
|
+
} else if (Array.isArray(value) && value.every((v) => typeof v === "string")) {
|
|
1666
|
+
headers[key] = value;
|
|
1667
|
+
} else {
|
|
1668
|
+
headers[key] = JSON.stringify(value);
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
const getAddressText = (addr) => {
|
|
1673
|
+
if (!addr) return "";
|
|
1674
|
+
if (Array.isArray(addr)) {
|
|
1675
|
+
return addr.map((a) => a.text).join(", ");
|
|
1676
|
+
}
|
|
1677
|
+
return addr.text || "";
|
|
1678
|
+
};
|
|
1679
|
+
return {
|
|
1680
|
+
messageId: parsed.messageId || headers["message-id"]?.toString() || "",
|
|
1681
|
+
from: getAddressText(parsed.from),
|
|
1682
|
+
to: getAddressText(parsed.to),
|
|
1683
|
+
subject: parsed.subject || "",
|
|
1684
|
+
html: parsed.html || void 0,
|
|
1685
|
+
text: parsed.text || void 0,
|
|
1686
|
+
attachments,
|
|
1687
|
+
headers,
|
|
1688
|
+
timestamp: parsed.date || /* @__PURE__ */ new Date(),
|
|
1689
|
+
// Note: MessageMetadata is not available in GetArchiveMessageCommandOutput
|
|
1690
|
+
// These fields would need to be retrieved separately if needed
|
|
1691
|
+
metadata: {}
|
|
1692
|
+
};
|
|
1693
|
+
}
|
|
1694
|
+
var init_archive = __esm({
|
|
1695
|
+
"src/utils/archive.ts"() {
|
|
1696
|
+
"use strict";
|
|
1697
|
+
init_esm_shims();
|
|
1698
|
+
}
|
|
1699
|
+
});
|
|
1700
|
+
|
|
1701
|
+
// src/console/services/email-archive.ts
|
|
1702
|
+
var email_archive_exports = {};
|
|
1703
|
+
__export(email_archive_exports, {
|
|
1704
|
+
fetchArchivedEmail: () => fetchArchivedEmail
|
|
1705
|
+
});
|
|
1706
|
+
async function fetchArchivedEmail(messageId, options) {
|
|
1707
|
+
const { region, archiveArn, from, to, subject, timestamp } = options;
|
|
1708
|
+
try {
|
|
1709
|
+
console.log("Fetching archived email:", {
|
|
1710
|
+
messageId,
|
|
1711
|
+
archiveArn,
|
|
1712
|
+
region
|
|
1713
|
+
});
|
|
1714
|
+
const searchCriteria = {
|
|
1715
|
+
from,
|
|
1716
|
+
to,
|
|
1717
|
+
subject,
|
|
1718
|
+
timestamp
|
|
1719
|
+
};
|
|
1720
|
+
const email = await getArchivedEmail(archiveArn, searchCriteria, region);
|
|
1721
|
+
console.log("Archived email fetched successfully:", {
|
|
1722
|
+
messageId: email.messageId,
|
|
1723
|
+
hasHtml: !!email.html,
|
|
1724
|
+
hasText: !!email.text,
|
|
1725
|
+
attachmentCount: email.attachments.length
|
|
1726
|
+
});
|
|
1727
|
+
return email;
|
|
1728
|
+
} catch (error) {
|
|
1729
|
+
if (error instanceof Error && (error.message.includes("not found") || error.message.includes("ResourceNotFoundException"))) {
|
|
1730
|
+
console.log("Archived email not found:", messageId);
|
|
1731
|
+
return null;
|
|
1732
|
+
}
|
|
1733
|
+
console.error("Error fetching archived email:", error);
|
|
1734
|
+
throw error;
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
var init_email_archive = __esm({
|
|
1738
|
+
"src/console/services/email-archive.ts"() {
|
|
1739
|
+
"use strict";
|
|
1740
|
+
init_esm_shims();
|
|
1741
|
+
init_archive();
|
|
1742
|
+
}
|
|
1743
|
+
});
|
|
1744
|
+
|
|
1187
1745
|
// src/console/services/dynamodb-metrics.ts
|
|
1188
1746
|
var dynamodb_metrics_exports = {};
|
|
1189
1747
|
__export(dynamodb_metrics_exports, {
|
|
@@ -1582,6 +2140,26 @@ async function createIAMRole(config) {
|
|
|
1582
2140
|
Resource: "arn:aws:sqs:*:*:wraps-email-*"
|
|
1583
2141
|
});
|
|
1584
2142
|
}
|
|
2143
|
+
if (config.emailConfig.emailArchiving?.enabled) {
|
|
2144
|
+
statements.push({
|
|
2145
|
+
Effect: "Allow",
|
|
2146
|
+
Action: [
|
|
2147
|
+
// Archive search operations
|
|
2148
|
+
"ses:StartArchiveSearch",
|
|
2149
|
+
"ses:GetArchiveSearchResults",
|
|
2150
|
+
// Archive message retrieval
|
|
2151
|
+
"ses:GetArchiveMessage",
|
|
2152
|
+
"ses:GetArchiveMessageContent",
|
|
2153
|
+
// Archive metadata
|
|
2154
|
+
"ses:GetArchive",
|
|
2155
|
+
"ses:ListArchives",
|
|
2156
|
+
// Archive export (for future use)
|
|
2157
|
+
"ses:StartArchiveExport",
|
|
2158
|
+
"ses:GetArchiveExport"
|
|
2159
|
+
],
|
|
2160
|
+
Resource: "arn:aws:ses:*:*:mailmanager-archive/*"
|
|
2161
|
+
});
|
|
2162
|
+
}
|
|
1585
2163
|
new aws3.iam.RolePolicy("wraps-email-policy", {
|
|
1586
2164
|
role: role.name,
|
|
1587
2165
|
policy: JSON.stringify({
|
|
@@ -1764,8 +2342,9 @@ async function createSESResources(config) {
|
|
|
1764
2342
|
if (config.trackingConfig?.customRedirectDomain) {
|
|
1765
2343
|
configSetOptions.trackingOptions = {
|
|
1766
2344
|
customRedirectDomain: config.trackingConfig.customRedirectDomain,
|
|
1767
|
-
|
|
1768
|
-
//
|
|
2345
|
+
// Use OPTIONAL because custom domains don't have SSL certificates by default
|
|
2346
|
+
// AWS's tracking domain (r.{region}.awstrack.me) doesn't have certs for custom domains
|
|
2347
|
+
httpsPolicy: "OPTIONAL"
|
|
1769
2348
|
};
|
|
1770
2349
|
}
|
|
1771
2350
|
const configSet = new aws5.sesv2.ConfigurationSet(
|
|
@@ -1957,6 +2536,16 @@ async function deployEmailStack(config) {
|
|
|
1957
2536
|
accountId
|
|
1958
2537
|
});
|
|
1959
2538
|
}
|
|
2539
|
+
let archiveResources;
|
|
2540
|
+
if (emailConfig.emailArchiving?.enabled && sesResources) {
|
|
2541
|
+
const { createMailManagerArchive: createMailManagerArchive2 } = await Promise.resolve().then(() => (init_mail_manager(), mail_manager_exports));
|
|
2542
|
+
archiveResources = await createMailManagerArchive2({
|
|
2543
|
+
name: "email",
|
|
2544
|
+
retention: emailConfig.emailArchiving.retention,
|
|
2545
|
+
configSetName: sesResources.configSet.configurationSetName,
|
|
2546
|
+
region: config.region
|
|
2547
|
+
});
|
|
2548
|
+
}
|
|
1960
2549
|
return {
|
|
1961
2550
|
roleArn: role.arn,
|
|
1962
2551
|
configSetName: sesResources?.configSet.configurationSetName,
|
|
@@ -1970,7 +2559,11 @@ async function deployEmailStack(config) {
|
|
|
1970
2559
|
queueUrl: sqsResources?.queue.url,
|
|
1971
2560
|
dlqUrl: sqsResources?.dlq.url,
|
|
1972
2561
|
customTrackingDomain: sesResources?.customTrackingDomain,
|
|
1973
|
-
mailFromDomain: sesResources?.mailFromDomain
|
|
2562
|
+
mailFromDomain: sesResources?.mailFromDomain,
|
|
2563
|
+
archiveId: archiveResources?.archiveId,
|
|
2564
|
+
archiveArn: archiveResources?.archiveArn,
|
|
2565
|
+
archivingEnabled: emailConfig.emailArchiving?.enabled,
|
|
2566
|
+
archiveRetention: emailConfig.emailArchiving?.enabled ? emailConfig.emailArchiving.retention : void 0
|
|
1974
2567
|
};
|
|
1975
2568
|
}
|
|
1976
2569
|
|
|
@@ -2282,6 +2875,23 @@ ${domainStrings.join("\n")}`);
|
|
|
2282
2875
|
` ${pc2.dim("\u25CB")} Bounce/Complaint Handling ${pc2.dim("(run 'wraps upgrade' to enable)")}`
|
|
2283
2876
|
);
|
|
2284
2877
|
}
|
|
2878
|
+
if (status2.resources.archivingEnabled) {
|
|
2879
|
+
const retentionLabel = {
|
|
2880
|
+
"7days": "7 days",
|
|
2881
|
+
"30days": "30 days",
|
|
2882
|
+
"3months": "90 days",
|
|
2883
|
+
"6months": "6 months",
|
|
2884
|
+
"1year": "1 year",
|
|
2885
|
+
"18months": "18 months"
|
|
2886
|
+
}[status2.resources.archiveRetention || "3months"] || "90 days";
|
|
2887
|
+
featureLines.push(
|
|
2888
|
+
` ${pc2.green("\u2713")} Email Archiving ${pc2.dim(`(${retentionLabel} retention)`)}`
|
|
2889
|
+
);
|
|
2890
|
+
} else {
|
|
2891
|
+
featureLines.push(
|
|
2892
|
+
` ${pc2.dim("\u25CB")} Email Archiving ${pc2.dim("(run 'wraps upgrade' to enable)")}`
|
|
2893
|
+
);
|
|
2894
|
+
}
|
|
2285
2895
|
featureLines.push(
|
|
2286
2896
|
` ${pc2.green("\u2713")} Console Dashboard ${pc2.dim("(run 'wraps console')")}`
|
|
2287
2897
|
);
|
|
@@ -2314,6 +2924,11 @@ ${domainStrings.join("\n")}`);
|
|
|
2314
2924
|
` ${pc2.green("\u2713")} SNS Topics: ${pc2.cyan(`${status2.resources.snsTopics} configured`)}`
|
|
2315
2925
|
);
|
|
2316
2926
|
}
|
|
2927
|
+
if (status2.resources.archiveArn) {
|
|
2928
|
+
resourceLines.push(
|
|
2929
|
+
` ${pc2.green("\u2713")} Mail Manager Archive: ${pc2.cyan(status2.resources.archiveArn)}`
|
|
2930
|
+
);
|
|
2931
|
+
}
|
|
2317
2932
|
clack2.note(resourceLines.join("\n"), "Resources");
|
|
2318
2933
|
const domainsNeedingDNS = status2.domains.filter(
|
|
2319
2934
|
(d) => d.status === "pending" && d.dkimTokens || d.mailFromDomain && d.mailFromStatus !== "SUCCESS"
|
|
@@ -2336,7 +2951,9 @@ ${domainStrings.join("\n")}`);
|
|
|
2336
2951
|
);
|
|
2337
2952
|
}
|
|
2338
2953
|
if (domain.mailFromDomain && domain.mailFromStatus !== "SUCCESS") {
|
|
2339
|
-
if (dnsLines.length > 0)
|
|
2954
|
+
if (dnsLines.length > 0) {
|
|
2955
|
+
dnsLines.push("");
|
|
2956
|
+
}
|
|
2340
2957
|
dnsLines.push(
|
|
2341
2958
|
pc2.bold("MAIL FROM Domain Records (for DMARC alignment):"),
|
|
2342
2959
|
` ${pc2.cyan(domain.mailFromDomain)} ${pc2.dim("MX")} "10 feedback-smtp.${status2.region}.amazonses.com"`,
|
|
@@ -2855,7 +3472,7 @@ import { Router as createRouter } from "express";
|
|
|
2855
3472
|
init_esm_shims();
|
|
2856
3473
|
init_assume_role();
|
|
2857
3474
|
import { GetSendQuotaCommand, SESClient as SESClient3 } from "@aws-sdk/client-ses";
|
|
2858
|
-
import { GetEmailIdentityCommand, SESv2Client } from "@aws-sdk/client-sesv2";
|
|
3475
|
+
import { GetEmailIdentityCommand, SESv2Client as SESv2Client2 } from "@aws-sdk/client-sesv2";
|
|
2859
3476
|
async function fetchSendQuota(roleArn, region) {
|
|
2860
3477
|
const credentials = roleArn ? await assumeRole(roleArn, region) : void 0;
|
|
2861
3478
|
const ses = new SESClient3({ region, credentials });
|
|
@@ -2868,7 +3485,7 @@ async function fetchSendQuota(roleArn, region) {
|
|
|
2868
3485
|
}
|
|
2869
3486
|
async function fetchDomainInfo(roleArn, region, domain) {
|
|
2870
3487
|
const credentials = roleArn ? await assumeRole(roleArn, region) : void 0;
|
|
2871
|
-
const sesv22 = new
|
|
3488
|
+
const sesv22 = new SESv2Client2({ region, credentials });
|
|
2872
3489
|
const response = await sesv22.send(
|
|
2873
3490
|
new GetEmailIdentityCommand({
|
|
2874
3491
|
EmailIdentity: domain
|
|
@@ -3271,6 +3888,68 @@ function createEmailsRouter(config) {
|
|
|
3271
3888
|
res.status(500).json({ error: errorMessage });
|
|
3272
3889
|
}
|
|
3273
3890
|
});
|
|
3891
|
+
router.get("/:id/archive", async (req, res) => {
|
|
3892
|
+
try {
|
|
3893
|
+
const { id } = req.params;
|
|
3894
|
+
console.log("Archived email request received for message ID:", id);
|
|
3895
|
+
if (!config.archivingEnabled) {
|
|
3896
|
+
console.log("Email archiving not enabled");
|
|
3897
|
+
return res.status(400).json({
|
|
3898
|
+
error: "Email archiving not enabled for this deployment."
|
|
3899
|
+
});
|
|
3900
|
+
}
|
|
3901
|
+
if (!config.archiveArn) {
|
|
3902
|
+
console.log("No archive ARN configured");
|
|
3903
|
+
return res.status(400).json({
|
|
3904
|
+
error: "Archive ARN not configured."
|
|
3905
|
+
});
|
|
3906
|
+
}
|
|
3907
|
+
if (!config.tableName) {
|
|
3908
|
+
console.log("No table name configured");
|
|
3909
|
+
return res.status(400).json({
|
|
3910
|
+
error: "Email tracking not enabled. Need email metadata to search archive."
|
|
3911
|
+
});
|
|
3912
|
+
}
|
|
3913
|
+
console.log("Fetching email metadata from DynamoDB...");
|
|
3914
|
+
const emailDetails = await fetchEmailById(id, {
|
|
3915
|
+
region: config.region,
|
|
3916
|
+
tableName: config.tableName
|
|
3917
|
+
});
|
|
3918
|
+
if (!emailDetails) {
|
|
3919
|
+
console.log("Email metadata not found in DynamoDB for ID:", id);
|
|
3920
|
+
return res.status(404).json({
|
|
3921
|
+
error: "Email metadata not found. Cannot search archive."
|
|
3922
|
+
});
|
|
3923
|
+
}
|
|
3924
|
+
console.log("Fetching archived email from Mail Manager...");
|
|
3925
|
+
const { fetchArchivedEmail: fetchArchivedEmail2 } = await Promise.resolve().then(() => (init_email_archive(), email_archive_exports));
|
|
3926
|
+
const archivedEmail = await fetchArchivedEmail2(id, {
|
|
3927
|
+
region: config.region,
|
|
3928
|
+
archiveArn: config.archiveArn,
|
|
3929
|
+
from: emailDetails.from,
|
|
3930
|
+
to: emailDetails.to[0],
|
|
3931
|
+
// Use first recipient for search
|
|
3932
|
+
subject: emailDetails.subject,
|
|
3933
|
+
timestamp: new Date(emailDetails.sentAt)
|
|
3934
|
+
});
|
|
3935
|
+
if (!archivedEmail) {
|
|
3936
|
+
console.log("Archived email not found for message ID:", id);
|
|
3937
|
+
return res.status(404).json({
|
|
3938
|
+
error: "Archived email not found. It may have been sent before archiving was enabled."
|
|
3939
|
+
});
|
|
3940
|
+
}
|
|
3941
|
+
console.log("Archived email found:", archivedEmail.messageId);
|
|
3942
|
+
res.json(archivedEmail);
|
|
3943
|
+
} catch (error) {
|
|
3944
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
3945
|
+
console.error("Error fetching archived email:", error);
|
|
3946
|
+
console.error(
|
|
3947
|
+
"Stack trace:",
|
|
3948
|
+
error instanceof Error ? error.stack : "N/A"
|
|
3949
|
+
);
|
|
3950
|
+
res.status(500).json({ error: errorMessage });
|
|
3951
|
+
}
|
|
3952
|
+
});
|
|
3274
3953
|
return router;
|
|
3275
3954
|
}
|
|
3276
3955
|
|
|
@@ -3498,11 +4177,11 @@ init_assume_role();
|
|
|
3498
4177
|
import {
|
|
3499
4178
|
GetConfigurationSetCommand,
|
|
3500
4179
|
GetEmailIdentityCommand as GetEmailIdentityCommand2,
|
|
3501
|
-
SESv2Client as
|
|
4180
|
+
SESv2Client as SESv2Client3
|
|
3502
4181
|
} from "@aws-sdk/client-sesv2";
|
|
3503
4182
|
async function fetchConfigurationSet(roleArn, region, configSetName) {
|
|
3504
4183
|
const credentials = roleArn ? await assumeRole(roleArn, region) : void 0;
|
|
3505
|
-
const sesv22 = new
|
|
4184
|
+
const sesv22 = new SESv2Client3({ region, credentials });
|
|
3506
4185
|
const response = await sesv22.send(
|
|
3507
4186
|
new GetConfigurationSetCommand({
|
|
3508
4187
|
ConfigurationSetName: configSetName
|
|
@@ -3532,7 +4211,7 @@ async function fetchConfigurationSet(roleArn, region, configSetName) {
|
|
|
3532
4211
|
}
|
|
3533
4212
|
async function fetchEmailIdentity(roleArn, region, identityName) {
|
|
3534
4213
|
const credentials = roleArn ? await assumeRole(roleArn, region) : void 0;
|
|
3535
|
-
const sesv22 = new
|
|
4214
|
+
const sesv22 = new SESv2Client3({ region, credentials });
|
|
3536
4215
|
const response = await sesv22.send(
|
|
3537
4216
|
new GetEmailIdentityCommand2({
|
|
3538
4217
|
EmailIdentity: identityName
|
|
@@ -3557,7 +4236,9 @@ async function fetchEmailIdentity(roleArn, region, identityName) {
|
|
|
3557
4236
|
verifiedForSendingStatus: response.VerifiedForSendingStatus ?? false,
|
|
3558
4237
|
tags: response.Tags?.reduce(
|
|
3559
4238
|
(acc, tag) => {
|
|
3560
|
-
if (tag.Key)
|
|
4239
|
+
if (tag.Key) {
|
|
4240
|
+
acc[tag.Key] = tag.Value || "";
|
|
4241
|
+
}
|
|
3561
4242
|
return acc;
|
|
3562
4243
|
},
|
|
3563
4244
|
{}
|
|
@@ -3590,6 +4271,20 @@ async function fetchEmailSettings(roleArn, region, configSetName, domain) {
|
|
|
3590
4271
|
// src/console/routes/settings.ts
|
|
3591
4272
|
function createSettingsRouter(config) {
|
|
3592
4273
|
const router = createRouter4();
|
|
4274
|
+
router.get("/deployment", async (_req, res) => {
|
|
4275
|
+
try {
|
|
4276
|
+
res.json({
|
|
4277
|
+
archivingEnabled: config.archivingEnabled ?? false,
|
|
4278
|
+
archiveArn: config.archiveArn,
|
|
4279
|
+
tableName: config.tableName,
|
|
4280
|
+
region: config.region
|
|
4281
|
+
});
|
|
4282
|
+
} catch (error) {
|
|
4283
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
4284
|
+
console.error("Error fetching deployment config:", error);
|
|
4285
|
+
res.status(500).json({ error: errorMessage });
|
|
4286
|
+
}
|
|
4287
|
+
});
|
|
3593
4288
|
router.get("/", async (_req, res) => {
|
|
3594
4289
|
try {
|
|
3595
4290
|
const metadata = await loadConnectionMetadata(
|
|
@@ -3708,10 +4403,10 @@ function createSettingsRouter(config) {
|
|
|
3708
4403
|
console.log(
|
|
3709
4404
|
`[Settings] Updating sending options for ${configSetName}: ${enabled}`
|
|
3710
4405
|
);
|
|
3711
|
-
const { SESv2Client:
|
|
4406
|
+
const { SESv2Client: SESv2Client5, PutConfigurationSetSendingOptionsCommand } = await import("@aws-sdk/client-sesv2");
|
|
3712
4407
|
const { assumeRole: assumeRole2 } = await Promise.resolve().then(() => (init_assume_role(), assume_role_exports));
|
|
3713
4408
|
const credentials = config.roleArn ? await assumeRole2(config.roleArn, config.region) : void 0;
|
|
3714
|
-
const sesClient = new
|
|
4409
|
+
const sesClient = new SESv2Client5({ region: config.region, credentials });
|
|
3715
4410
|
await sesClient.send(
|
|
3716
4411
|
new PutConfigurationSetSendingOptionsCommand({
|
|
3717
4412
|
ConfigurationSetName: configSetName,
|
|
@@ -3745,10 +4440,10 @@ function createSettingsRouter(config) {
|
|
|
3745
4440
|
console.log(
|
|
3746
4441
|
`[Settings] Updating reputation options for ${configSetName}: ${enabled}`
|
|
3747
4442
|
);
|
|
3748
|
-
const { SESv2Client:
|
|
4443
|
+
const { SESv2Client: SESv2Client5, PutConfigurationSetReputationOptionsCommand } = await import("@aws-sdk/client-sesv2");
|
|
3749
4444
|
const { assumeRole: assumeRole2 } = await Promise.resolve().then(() => (init_assume_role(), assume_role_exports));
|
|
3750
4445
|
const credentials = config.roleArn ? await assumeRole2(config.roleArn, config.region) : void 0;
|
|
3751
|
-
const sesClient = new
|
|
4446
|
+
const sesClient = new SESv2Client5({ region: config.region, credentials });
|
|
3752
4447
|
await sesClient.send(
|
|
3753
4448
|
new PutConfigurationSetReputationOptionsCommand({
|
|
3754
4449
|
ConfigurationSetName: configSetName,
|
|
@@ -3788,10 +4483,10 @@ function createSettingsRouter(config) {
|
|
|
3788
4483
|
console.log(
|
|
3789
4484
|
`[Settings] Updating tracking domain for ${configSetName}: ${domain}`
|
|
3790
4485
|
);
|
|
3791
|
-
const { SESv2Client:
|
|
4486
|
+
const { SESv2Client: SESv2Client5, PutConfigurationSetTrackingOptionsCommand } = await import("@aws-sdk/client-sesv2");
|
|
3792
4487
|
const { assumeRole: assumeRole2 } = await Promise.resolve().then(() => (init_assume_role(), assume_role_exports));
|
|
3793
4488
|
const credentials = config.roleArn ? await assumeRole2(config.roleArn, config.region) : void 0;
|
|
3794
|
-
const sesClient = new
|
|
4489
|
+
const sesClient = new SESv2Client5({
|
|
3795
4490
|
region: config.region,
|
|
3796
4491
|
credentials
|
|
3797
4492
|
});
|
|
@@ -3965,6 +4660,8 @@ async function runConsole(options) {
|
|
|
3965
4660
|
process.exit(1);
|
|
3966
4661
|
}
|
|
3967
4662
|
const tableName = stackOutputs.tableName?.value;
|
|
4663
|
+
const archiveArn = stackOutputs.archiveArn?.value;
|
|
4664
|
+
const archivingEnabled = stackOutputs.archivingEnabled?.value ?? false;
|
|
3968
4665
|
const port = options.port || await getPort({ port: [5555, 5556, 5557, 5558, 5559] });
|
|
3969
4666
|
progress.stop();
|
|
3970
4667
|
clack5.log.success("Starting console server...");
|
|
@@ -3978,7 +4675,9 @@ async function runConsole(options) {
|
|
|
3978
4675
|
region,
|
|
3979
4676
|
tableName,
|
|
3980
4677
|
accountId: identity.accountId,
|
|
3981
|
-
noOpen: options.noOpen ?? false
|
|
4678
|
+
noOpen: options.noOpen ?? false,
|
|
4679
|
+
archiveArn,
|
|
4680
|
+
archivingEnabled
|
|
3982
4681
|
});
|
|
3983
4682
|
console.log(`\\n${pc5.bold("Console:")} ${pc5.cyan(url)}`);
|
|
3984
4683
|
console.log(`${pc5.dim("Press Ctrl+C to stop")}\\n`);
|
|
@@ -4118,17 +4817,22 @@ async function init(options) {
|
|
|
4118
4817
|
emailConfig = await promptCustomConfig();
|
|
4119
4818
|
} else {
|
|
4120
4819
|
emailConfig = getPreset(preset);
|
|
4820
|
+
const { promptEmailArchiving: promptEmailArchiving2 } = await Promise.resolve().then(() => (init_prompts(), prompts_exports));
|
|
4821
|
+
const archivingConfig = await promptEmailArchiving2();
|
|
4822
|
+
emailConfig.emailArchiving = archivingConfig;
|
|
4121
4823
|
}
|
|
4122
4824
|
if (domain) {
|
|
4123
4825
|
emailConfig.domain = domain;
|
|
4124
4826
|
}
|
|
4125
4827
|
const estimatedVolume = await promptEstimatedVolume();
|
|
4126
|
-
progress.info(
|
|
4828
|
+
progress.info(`
|
|
4829
|
+
${pc7.bold("Cost Estimate:")}`);
|
|
4127
4830
|
const costSummary = getCostSummary(emailConfig, estimatedVolume);
|
|
4128
4831
|
clack7.log.info(costSummary);
|
|
4129
4832
|
const warnings = validateConfig(emailConfig);
|
|
4130
4833
|
if (warnings.length > 0) {
|
|
4131
|
-
progress.info(
|
|
4834
|
+
progress.info(`
|
|
4835
|
+
${pc7.yellow(pc7.bold("Configuration Warnings:"))}`);
|
|
4132
4836
|
for (const warning of warnings) {
|
|
4133
4837
|
clack7.log.warn(warning);
|
|
4134
4838
|
}
|
|
@@ -4176,7 +4880,11 @@ async function init(options) {
|
|
|
4176
4880
|
lambdaFunctions: result.lambdaFunctions,
|
|
4177
4881
|
domain: result.domain,
|
|
4178
4882
|
dkimTokens: result.dkimTokens,
|
|
4179
|
-
customTrackingDomain: result.customTrackingDomain
|
|
4883
|
+
customTrackingDomain: result.customTrackingDomain,
|
|
4884
|
+
mailFromDomain: result.mailFromDomain,
|
|
4885
|
+
archiveArn: result.archiveArn,
|
|
4886
|
+
archivingEnabled: result.archivingEnabled,
|
|
4887
|
+
archiveRetention: result.archiveRetention
|
|
4180
4888
|
};
|
|
4181
4889
|
}
|
|
4182
4890
|
},
|
|
@@ -4205,7 +4913,11 @@ async function init(options) {
|
|
|
4205
4913
|
lambdaFunctions: pulumiOutputs.lambdaFunctions?.value,
|
|
4206
4914
|
domain: pulumiOutputs.domain?.value,
|
|
4207
4915
|
dkimTokens: pulumiOutputs.dkimTokens?.value,
|
|
4208
|
-
customTrackingDomain: pulumiOutputs.customTrackingDomain?.value
|
|
4916
|
+
customTrackingDomain: pulumiOutputs.customTrackingDomain?.value,
|
|
4917
|
+
mailFromDomain: pulumiOutputs.mailFromDomain?.value,
|
|
4918
|
+
archiveArn: pulumiOutputs.archiveArn?.value,
|
|
4919
|
+
archivingEnabled: pulumiOutputs.archivingEnabled?.value,
|
|
4920
|
+
archiveRetention: pulumiOutputs.archiveRetention?.value
|
|
4209
4921
|
};
|
|
4210
4922
|
}
|
|
4211
4923
|
);
|
|
@@ -4402,8 +5114,8 @@ Run ${pc9.cyan("wraps init")} to deploy infrastructure.
|
|
|
4402
5114
|
process.exit(1);
|
|
4403
5115
|
}
|
|
4404
5116
|
const domains = await listSESDomains(region);
|
|
4405
|
-
const { SESv2Client:
|
|
4406
|
-
const sesv2Client = new
|
|
5117
|
+
const { SESv2Client: SESv2Client5, GetEmailIdentityCommand: GetEmailIdentityCommand4 } = await import("@aws-sdk/client-sesv2");
|
|
5118
|
+
const sesv2Client = new SESv2Client5({ region });
|
|
4407
5119
|
const domainsWithTokens = await Promise.all(
|
|
4408
5120
|
domains.map(async (d) => {
|
|
4409
5121
|
try {
|
|
@@ -4439,7 +5151,10 @@ Run ${pc9.cyan("wraps init")} to deploy infrastructure.
|
|
|
4439
5151
|
configSetName: stackOutputs.configSetName?.value,
|
|
4440
5152
|
tableName: stackOutputs.tableName?.value,
|
|
4441
5153
|
lambdaFunctions: stackOutputs.lambdaFunctions?.value?.length || 0,
|
|
4442
|
-
snsTopics: integrationLevel === "enhanced" ? 1 : 0
|
|
5154
|
+
snsTopics: integrationLevel === "enhanced" ? 1 : 0,
|
|
5155
|
+
archiveArn: stackOutputs.archiveArn?.value,
|
|
5156
|
+
archivingEnabled: stackOutputs.archivingEnabled?.value,
|
|
5157
|
+
archiveRetention: stackOutputs.archiveRetention?.value
|
|
4443
5158
|
}
|
|
4444
5159
|
});
|
|
4445
5160
|
}
|
|
@@ -4561,7 +5276,11 @@ ${pc10.bold("Current Configuration:")}
|
|
|
4561
5276
|
lambdaFunctions: result.lambdaFunctions,
|
|
4562
5277
|
domain: result.domain,
|
|
4563
5278
|
dkimTokens: result.dkimTokens,
|
|
4564
|
-
customTrackingDomain: result.customTrackingDomain
|
|
5279
|
+
customTrackingDomain: result.customTrackingDomain,
|
|
5280
|
+
mailFromDomain: result.mailFromDomain,
|
|
5281
|
+
archiveArn: result.archiveArn,
|
|
5282
|
+
archivingEnabled: result.archivingEnabled,
|
|
5283
|
+
archiveRetention: result.archiveRetention
|
|
4565
5284
|
};
|
|
4566
5285
|
}
|
|
4567
5286
|
},
|
|
@@ -4588,7 +5307,11 @@ ${pc10.bold("Current Configuration:")}
|
|
|
4588
5307
|
lambdaFunctions: pulumiOutputs.lambdaFunctions?.value,
|
|
4589
5308
|
domain: pulumiOutputs.domain?.value,
|
|
4590
5309
|
dkimTokens: pulumiOutputs.dkimTokens?.value,
|
|
4591
|
-
customTrackingDomain: pulumiOutputs.customTrackingDomain?.value
|
|
5310
|
+
customTrackingDomain: pulumiOutputs.customTrackingDomain?.value,
|
|
5311
|
+
mailFromDomain: pulumiOutputs.mailFromDomain?.value,
|
|
5312
|
+
archiveArn: pulumiOutputs.archiveArn?.value,
|
|
5313
|
+
archivingEnabled: pulumiOutputs.archivingEnabled?.value,
|
|
5314
|
+
archiveRetention: pulumiOutputs.archiveRetention?.value
|
|
4592
5315
|
};
|
|
4593
5316
|
}
|
|
4594
5317
|
);
|
|
@@ -4699,13 +5422,34 @@ ${pc11.bold("Current Configuration:")}
|
|
|
4699
5422
|
console.log(` ${pc11.green("\u2713")} Event Tracking (EventBridge)`);
|
|
4700
5423
|
if (config.eventTracking.dynamoDBHistory) {
|
|
4701
5424
|
console.log(
|
|
4702
|
-
` ${pc11.dim("\u2514\u2500")} Email History: ${pc11.cyan(config.eventTracking.archiveRetention || "
|
|
5425
|
+
` ${pc11.dim("\u2514\u2500")} Email History: ${pc11.cyan(config.eventTracking.archiveRetention || "3months")}`
|
|
4703
5426
|
);
|
|
4704
5427
|
}
|
|
4705
5428
|
}
|
|
4706
5429
|
if (config.dedicatedIp) {
|
|
4707
5430
|
console.log(` ${pc11.green("\u2713")} Dedicated IP Address`);
|
|
4708
5431
|
}
|
|
5432
|
+
if (config.emailArchiving?.enabled) {
|
|
5433
|
+
const retentionLabel = {
|
|
5434
|
+
"3months": "3 months",
|
|
5435
|
+
"6months": "6 months",
|
|
5436
|
+
"9months": "9 months",
|
|
5437
|
+
"1year": "1 year",
|
|
5438
|
+
"18months": "18 months",
|
|
5439
|
+
"2years": "2 years",
|
|
5440
|
+
"30months": "30 months",
|
|
5441
|
+
"3years": "3 years",
|
|
5442
|
+
"4years": "4 years",
|
|
5443
|
+
"5years": "5 years",
|
|
5444
|
+
"6years": "6 years",
|
|
5445
|
+
"7years": "7 years",
|
|
5446
|
+
"8years": "8 years",
|
|
5447
|
+
"9years": "9 years",
|
|
5448
|
+
"10years": "10 years",
|
|
5449
|
+
permanent: "permanent"
|
|
5450
|
+
}[config.emailArchiving.retention] || "3 months";
|
|
5451
|
+
console.log(` ${pc11.green("\u2713")} Email Archiving (${retentionLabel})`);
|
|
5452
|
+
}
|
|
4709
5453
|
const currentCostData = calculateCosts(config, 5e4);
|
|
4710
5454
|
console.log(
|
|
4711
5455
|
`
|
|
@@ -4720,6 +5464,11 @@ ${pc11.bold("Current Configuration:")}
|
|
|
4720
5464
|
label: "Upgrade to a different preset",
|
|
4721
5465
|
hint: "Starter \u2192 Production \u2192 Enterprise"
|
|
4722
5466
|
},
|
|
5467
|
+
{
|
|
5468
|
+
value: "archiving",
|
|
5469
|
+
label: config.emailArchiving?.enabled ? "Change email archiving settings" : "Enable email archiving",
|
|
5470
|
+
hint: config.emailArchiving?.enabled ? "Update retention or disable" : "Store full email content with HTML"
|
|
5471
|
+
},
|
|
4723
5472
|
{
|
|
4724
5473
|
value: "tracking-domain",
|
|
4725
5474
|
label: "Add/change custom tracking domain",
|
|
@@ -4728,7 +5477,7 @@ ${pc11.bold("Current Configuration:")}
|
|
|
4728
5477
|
{
|
|
4729
5478
|
value: "retention",
|
|
4730
5479
|
label: "Change email history retention",
|
|
4731
|
-
hint: "7 days, 30 days, 90 days, 1 year,
|
|
5480
|
+
hint: "7 days, 30 days, 90 days, 6 months, 1 year, 18 months"
|
|
4732
5481
|
},
|
|
4733
5482
|
{
|
|
4734
5483
|
value: "events",
|
|
@@ -4786,6 +5535,196 @@ ${pc11.bold("Current Configuration:")}
|
|
|
4786
5535
|
newPreset = selectedPreset;
|
|
4787
5536
|
break;
|
|
4788
5537
|
}
|
|
5538
|
+
case "archiving": {
|
|
5539
|
+
if (config.emailArchiving?.enabled) {
|
|
5540
|
+
const archivingAction = await clack11.select({
|
|
5541
|
+
message: "What would you like to do with email archiving?",
|
|
5542
|
+
options: [
|
|
5543
|
+
{
|
|
5544
|
+
value: "change-retention",
|
|
5545
|
+
label: "Change retention period",
|
|
5546
|
+
hint: `Current: ${config.emailArchiving.retention}`
|
|
5547
|
+
},
|
|
5548
|
+
{
|
|
5549
|
+
value: "disable",
|
|
5550
|
+
label: "Disable email archiving",
|
|
5551
|
+
hint: "Stop storing full email content"
|
|
5552
|
+
}
|
|
5553
|
+
]
|
|
5554
|
+
});
|
|
5555
|
+
if (clack11.isCancel(archivingAction)) {
|
|
5556
|
+
clack11.cancel("Upgrade cancelled.");
|
|
5557
|
+
process.exit(0);
|
|
5558
|
+
}
|
|
5559
|
+
if (archivingAction === "disable") {
|
|
5560
|
+
const confirmDisable = await clack11.confirm({
|
|
5561
|
+
message: "Are you sure? Existing archived emails will remain, but new emails won't be archived.",
|
|
5562
|
+
initialValue: false
|
|
5563
|
+
});
|
|
5564
|
+
if (clack11.isCancel(confirmDisable) || !confirmDisable) {
|
|
5565
|
+
clack11.cancel("Archiving not disabled.");
|
|
5566
|
+
process.exit(0);
|
|
5567
|
+
}
|
|
5568
|
+
updatedConfig = {
|
|
5569
|
+
...config,
|
|
5570
|
+
emailArchiving: {
|
|
5571
|
+
enabled: false,
|
|
5572
|
+
retention: config.emailArchiving.retention
|
|
5573
|
+
}
|
|
5574
|
+
};
|
|
5575
|
+
} else {
|
|
5576
|
+
const retention = await clack11.select({
|
|
5577
|
+
message: "Email archive retention period:",
|
|
5578
|
+
options: [
|
|
5579
|
+
{
|
|
5580
|
+
value: "3months",
|
|
5581
|
+
label: "3 months (minimum)",
|
|
5582
|
+
hint: "~$5-10/mo for 10k emails"
|
|
5583
|
+
},
|
|
5584
|
+
{
|
|
5585
|
+
value: "6months",
|
|
5586
|
+
label: "6 months",
|
|
5587
|
+
hint: "~$15-25/mo for 10k emails"
|
|
5588
|
+
},
|
|
5589
|
+
{
|
|
5590
|
+
value: "1year",
|
|
5591
|
+
label: "1 year (recommended)",
|
|
5592
|
+
hint: "~$25-40/mo for 10k emails"
|
|
5593
|
+
},
|
|
5594
|
+
{
|
|
5595
|
+
value: "2years",
|
|
5596
|
+
label: "2 years",
|
|
5597
|
+
hint: "~$50-80/mo for 10k emails"
|
|
5598
|
+
},
|
|
5599
|
+
{
|
|
5600
|
+
value: "3years",
|
|
5601
|
+
label: "3 years",
|
|
5602
|
+
hint: "~$75-120/mo for 10k emails"
|
|
5603
|
+
},
|
|
5604
|
+
{
|
|
5605
|
+
value: "5years",
|
|
5606
|
+
label: "5 years",
|
|
5607
|
+
hint: "~$125-200/mo for 10k emails"
|
|
5608
|
+
},
|
|
5609
|
+
{
|
|
5610
|
+
value: "7years",
|
|
5611
|
+
label: "7 years",
|
|
5612
|
+
hint: "~$175-280/mo for 10k emails"
|
|
5613
|
+
},
|
|
5614
|
+
{
|
|
5615
|
+
value: "10years",
|
|
5616
|
+
label: "10 years",
|
|
5617
|
+
hint: "~$250-400/mo for 10k emails"
|
|
5618
|
+
},
|
|
5619
|
+
{
|
|
5620
|
+
value: "permanent",
|
|
5621
|
+
label: "Permanent",
|
|
5622
|
+
hint: "Expensive, not recommended"
|
|
5623
|
+
}
|
|
5624
|
+
],
|
|
5625
|
+
initialValue: config.emailArchiving.retention
|
|
5626
|
+
});
|
|
5627
|
+
if (clack11.isCancel(retention)) {
|
|
5628
|
+
clack11.cancel("Upgrade cancelled.");
|
|
5629
|
+
process.exit(0);
|
|
5630
|
+
}
|
|
5631
|
+
updatedConfig = {
|
|
5632
|
+
...config,
|
|
5633
|
+
emailArchiving: {
|
|
5634
|
+
enabled: true,
|
|
5635
|
+
retention
|
|
5636
|
+
}
|
|
5637
|
+
};
|
|
5638
|
+
}
|
|
5639
|
+
} else {
|
|
5640
|
+
const enableArchiving = await clack11.confirm({
|
|
5641
|
+
message: "Enable email archiving? (Store full email content with HTML for viewing)",
|
|
5642
|
+
initialValue: true
|
|
5643
|
+
});
|
|
5644
|
+
if (clack11.isCancel(enableArchiving)) {
|
|
5645
|
+
clack11.cancel("Upgrade cancelled.");
|
|
5646
|
+
process.exit(0);
|
|
5647
|
+
}
|
|
5648
|
+
if (!enableArchiving) {
|
|
5649
|
+
clack11.log.info("Email archiving not enabled.");
|
|
5650
|
+
process.exit(0);
|
|
5651
|
+
}
|
|
5652
|
+
const retention = await clack11.select({
|
|
5653
|
+
message: "Email archive retention period:",
|
|
5654
|
+
options: [
|
|
5655
|
+
{
|
|
5656
|
+
value: "3months",
|
|
5657
|
+
label: "3 months (minimum)",
|
|
5658
|
+
hint: "~$5-10/mo for 10k emails"
|
|
5659
|
+
},
|
|
5660
|
+
{
|
|
5661
|
+
value: "6months",
|
|
5662
|
+
label: "6 months",
|
|
5663
|
+
hint: "~$15-25/mo for 10k emails"
|
|
5664
|
+
},
|
|
5665
|
+
{
|
|
5666
|
+
value: "1year",
|
|
5667
|
+
label: "1 year (recommended)",
|
|
5668
|
+
hint: "~$25-40/mo for 10k emails"
|
|
5669
|
+
},
|
|
5670
|
+
{
|
|
5671
|
+
value: "2years",
|
|
5672
|
+
label: "2 years",
|
|
5673
|
+
hint: "~$50-80/mo for 10k emails"
|
|
5674
|
+
},
|
|
5675
|
+
{
|
|
5676
|
+
value: "3years",
|
|
5677
|
+
label: "3 years",
|
|
5678
|
+
hint: "~$75-120/mo for 10k emails"
|
|
5679
|
+
},
|
|
5680
|
+
{
|
|
5681
|
+
value: "5years",
|
|
5682
|
+
label: "5 years",
|
|
5683
|
+
hint: "~$125-200/mo for 10k emails"
|
|
5684
|
+
},
|
|
5685
|
+
{
|
|
5686
|
+
value: "7years",
|
|
5687
|
+
label: "7 years",
|
|
5688
|
+
hint: "~$175-280/mo for 10k emails"
|
|
5689
|
+
},
|
|
5690
|
+
{
|
|
5691
|
+
value: "10years",
|
|
5692
|
+
label: "10 years",
|
|
5693
|
+
hint: "~$250-400/mo for 10k emails"
|
|
5694
|
+
},
|
|
5695
|
+
{
|
|
5696
|
+
value: "permanent",
|
|
5697
|
+
label: "Permanent",
|
|
5698
|
+
hint: "Expensive, not recommended"
|
|
5699
|
+
}
|
|
5700
|
+
],
|
|
5701
|
+
initialValue: "3months"
|
|
5702
|
+
});
|
|
5703
|
+
if (clack11.isCancel(retention)) {
|
|
5704
|
+
clack11.cancel("Upgrade cancelled.");
|
|
5705
|
+
process.exit(0);
|
|
5706
|
+
}
|
|
5707
|
+
clack11.log.info(
|
|
5708
|
+
pc11.dim(
|
|
5709
|
+
"Archiving stores full RFC 822 emails with HTML, attachments, and headers"
|
|
5710
|
+
)
|
|
5711
|
+
);
|
|
5712
|
+
clack11.log.info(
|
|
5713
|
+
pc11.dim(
|
|
5714
|
+
"Cost: $2/GB ingestion + $0.19/GB/month storage (~50KB per email)"
|
|
5715
|
+
)
|
|
5716
|
+
);
|
|
5717
|
+
updatedConfig = {
|
|
5718
|
+
...config,
|
|
5719
|
+
emailArchiving: {
|
|
5720
|
+
enabled: true,
|
|
5721
|
+
retention
|
|
5722
|
+
}
|
|
5723
|
+
};
|
|
5724
|
+
}
|
|
5725
|
+
newPreset = void 0;
|
|
5726
|
+
break;
|
|
5727
|
+
}
|
|
4789
5728
|
case "tracking-domain": {
|
|
4790
5729
|
if (!config.domain) {
|
|
4791
5730
|
clack11.log.error(
|
|
@@ -4844,28 +5783,51 @@ ${pc11.bold("Current Configuration:")}
|
|
|
4844
5783
|
}
|
|
4845
5784
|
case "retention": {
|
|
4846
5785
|
const retention = await clack11.select({
|
|
4847
|
-
message: "Email history retention period:",
|
|
5786
|
+
message: "Email history retention period (event data in DynamoDB):",
|
|
4848
5787
|
options: [
|
|
4849
|
-
{ value: "7days", label: "7 days", hint: "Minimal storage cost" },
|
|
4850
|
-
{ value: "30days", label: "30 days", hint: "Development/testing" },
|
|
4851
5788
|
{
|
|
4852
|
-
value: "
|
|
4853
|
-
label: "
|
|
5789
|
+
value: "3months",
|
|
5790
|
+
label: "3 months (recommended)",
|
|
4854
5791
|
hint: "Standard retention"
|
|
4855
5792
|
},
|
|
5793
|
+
{
|
|
5794
|
+
value: "6months",
|
|
5795
|
+
label: "6 months",
|
|
5796
|
+
hint: "Extended retention"
|
|
5797
|
+
},
|
|
4856
5798
|
{ value: "1year", label: "1 year", hint: "Compliance requirements" },
|
|
4857
5799
|
{
|
|
4858
|
-
value: "
|
|
4859
|
-
label: "
|
|
4860
|
-
hint: "
|
|
5800
|
+
value: "18months",
|
|
5801
|
+
label: "18 months",
|
|
5802
|
+
hint: "Long-term retention"
|
|
5803
|
+
},
|
|
5804
|
+
{
|
|
5805
|
+
value: "2years",
|
|
5806
|
+
label: "2 years",
|
|
5807
|
+
hint: "Extended compliance"
|
|
5808
|
+
},
|
|
5809
|
+
{
|
|
5810
|
+
value: "permanent",
|
|
5811
|
+
label: "Permanent",
|
|
5812
|
+
hint: "Not recommended (expensive)"
|
|
4861
5813
|
}
|
|
4862
5814
|
],
|
|
4863
|
-
initialValue: config.eventTracking?.archiveRetention || "
|
|
5815
|
+
initialValue: config.eventTracking?.archiveRetention || "3months"
|
|
4864
5816
|
});
|
|
4865
5817
|
if (clack11.isCancel(retention)) {
|
|
4866
5818
|
clack11.cancel("Upgrade cancelled.");
|
|
4867
5819
|
process.exit(0);
|
|
4868
5820
|
}
|
|
5821
|
+
clack11.log.info(
|
|
5822
|
+
pc11.dim(
|
|
5823
|
+
"Note: This is for event data (sent, delivered, opened, etc.) stored in DynamoDB."
|
|
5824
|
+
)
|
|
5825
|
+
);
|
|
5826
|
+
clack11.log.info(
|
|
5827
|
+
pc11.dim(
|
|
5828
|
+
"For full email content storage, use 'Enable email archiving' option."
|
|
5829
|
+
)
|
|
5830
|
+
);
|
|
4869
5831
|
updatedConfig = {
|
|
4870
5832
|
...config,
|
|
4871
5833
|
eventTracking: {
|
|
@@ -5029,7 +5991,10 @@ ${pc11.bold("Cost Impact:")}`);
|
|
|
5029
5991
|
lambdaFunctions: result.lambdaFunctions,
|
|
5030
5992
|
domain: result.domain,
|
|
5031
5993
|
dkimTokens: result.dkimTokens,
|
|
5032
|
-
customTrackingDomain: result.customTrackingDomain
|
|
5994
|
+
customTrackingDomain: result.customTrackingDomain,
|
|
5995
|
+
archiveArn: result.archiveArn,
|
|
5996
|
+
archivingEnabled: result.archivingEnabled,
|
|
5997
|
+
archiveRetention: result.archiveRetention
|
|
5033
5998
|
};
|
|
5034
5999
|
}
|
|
5035
6000
|
},
|
|
@@ -5056,7 +6021,10 @@ ${pc11.bold("Cost Impact:")}`);
|
|
|
5056
6021
|
lambdaFunctions: pulumiOutputs.lambdaFunctions?.value,
|
|
5057
6022
|
domain: pulumiOutputs.domain?.value,
|
|
5058
6023
|
dkimTokens: pulumiOutputs.dkimTokens?.value,
|
|
5059
|
-
customTrackingDomain: pulumiOutputs.customTrackingDomain?.value
|
|
6024
|
+
customTrackingDomain: pulumiOutputs.customTrackingDomain?.value,
|
|
6025
|
+
archiveArn: pulumiOutputs.archiveArn?.value,
|
|
6026
|
+
archivingEnabled: pulumiOutputs.archivingEnabled?.value,
|
|
6027
|
+
archiveRetention: pulumiOutputs.archiveRetention?.value
|
|
5060
6028
|
};
|
|
5061
6029
|
}
|
|
5062
6030
|
);
|
|
@@ -5095,12 +6063,12 @@ ${pc11.green("\u2713")} ${pc11.bold("Upgrade complete!")}
|
|
|
5095
6063
|
`);
|
|
5096
6064
|
if (upgradeAction === "preset" && newPreset) {
|
|
5097
6065
|
console.log(
|
|
5098
|
-
`Upgraded to ${pc11.cyan(newPreset)} preset (${pc11.green(formatCost(newCostData.total.monthly)
|
|
6066
|
+
`Upgraded to ${pc11.cyan(newPreset)} preset (${pc11.green(`${formatCost(newCostData.total.monthly)}/mo`)})
|
|
5099
6067
|
`
|
|
5100
6068
|
);
|
|
5101
6069
|
} else {
|
|
5102
6070
|
console.log(
|
|
5103
|
-
`Updated configuration (${pc11.green(formatCost(newCostData.total.monthly)
|
|
6071
|
+
`Updated configuration (${pc11.green(`${formatCost(newCostData.total.monthly)}/mo`)})
|
|
5104
6072
|
`
|
|
5105
6073
|
);
|
|
5106
6074
|
}
|
|
@@ -5110,14 +6078,14 @@ ${pc11.green("\u2713")} ${pc11.bold("Upgrade complete!")}
|
|
|
5110
6078
|
init_esm_shims();
|
|
5111
6079
|
init_aws();
|
|
5112
6080
|
import { Resolver } from "dns/promises";
|
|
5113
|
-
import { GetEmailIdentityCommand as GetEmailIdentityCommand3, SESv2Client as
|
|
6081
|
+
import { GetEmailIdentityCommand as GetEmailIdentityCommand3, SESv2Client as SESv2Client4 } from "@aws-sdk/client-sesv2";
|
|
5114
6082
|
import * as clack12 from "@clack/prompts";
|
|
5115
6083
|
import pc12 from "picocolors";
|
|
5116
6084
|
async function verify(options) {
|
|
5117
6085
|
clack12.intro(pc12.bold(`Verifying ${options.domain}`));
|
|
5118
6086
|
const progress = new DeploymentProgress();
|
|
5119
6087
|
const region = await getAWSRegion();
|
|
5120
|
-
const sesClient = new
|
|
6088
|
+
const sesClient = new SESv2Client4({ region });
|
|
5121
6089
|
let identity;
|
|
5122
6090
|
let dkimTokens = [];
|
|
5123
6091
|
let mailFromDomain;
|