@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 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
- "7days": 0.25,
225
- "30days": 1,
226
- "90days": 3,
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
- indefinite: 24
229
- // Assume 2 years for cost estimation
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 * numEventTypes * retentionMonths * avgRecordSizeKB;
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 || "90days";
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
- "90-day email history storage",
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 === "indefinite") {
792
+ if (config.eventTracking?.archiveRetention === "permanent") {
570
793
  warnings.push(
571
- "\u26A0\uFE0F Indefinite retention can become expensive. Consider 90-day or 1-year retention."
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: "90days"
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 = "90days";
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: "90days",
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 : "90days"
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
- httpsPolicy: "REQUIRE"
1768
- // Always require HTTPS for security
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) dnsLines.push("");
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 SESv2Client({ region, credentials });
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 SESv2Client2
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 SESv2Client2({ region, credentials });
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 SESv2Client2({ region, credentials });
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) acc[tag.Key] = tag.Value || "";
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: SESv2Client4, PutConfigurationSetSendingOptionsCommand } = await import("@aws-sdk/client-sesv2");
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 SESv2Client4({ region: config.region, credentials });
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: SESv2Client4, PutConfigurationSetReputationOptionsCommand } = await import("@aws-sdk/client-sesv2");
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 SESv2Client4({ region: config.region, credentials });
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: SESv2Client4, PutConfigurationSetTrackingOptionsCommand } = await import("@aws-sdk/client-sesv2");
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 SESv2Client4({
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("\n" + pc7.bold("Cost Estimate:"));
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("\n" + pc7.yellow(pc7.bold("Configuration Warnings:")));
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: SESv2Client4, GetEmailIdentityCommand: GetEmailIdentityCommand4 } = await import("@aws-sdk/client-sesv2");
4406
- const sesv2Client = new SESv2Client4({ region });
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 || "90days")}`
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, indefinite"
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: "90days",
4853
- label: "90 days (recommended)",
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: "indefinite",
4859
- label: "Indefinite",
4860
- hint: "Higher storage cost"
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 || "90days"
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) + "/mo")})
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) + "/mo")})
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 SESv2Client3 } from "@aws-sdk/client-sesv2";
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 SESv2Client3({ region });
6088
+ const sesClient = new SESv2Client4({ region });
5121
6089
  let identity;
5122
6090
  let dkimTokens = [];
5123
6091
  let mailFromDomain;