@wraps.dev/cli 0.2.0 → 0.3.2

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,24 @@ 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
+ async function createMailManagerArchive(config) {
27
+ void config;
28
+ throw new Error(
29
+ "Mail Manager Archive is not yet supported in Pulumi AWS provider. Email archiving with Mail Manager is coming soon."
30
+ );
31
+ }
32
+ var init_mail_manager = __esm({
33
+ "src/infrastructure/resources/mail-manager.ts"() {
34
+ "use strict";
35
+ init_esm_shims();
36
+ }
37
+ });
38
+
21
39
  // src/utils/errors.ts
22
40
  import * as clack from "@clack/prompts";
23
41
  import pc from "picocolors";
@@ -224,11 +242,28 @@ function estimateStorageSize(emailsPerMonth, retention, numEventTypes = 8) {
224
242
  "7days": 0.25,
225
243
  "30days": 1,
226
244
  "90days": 3,
245
+ "6months": 6,
246
+ "1year": 12,
247
+ "18months": 18,
248
+ indefinite: 120
249
+ // Assume 10 years for cost estimation
250
+ }[retention];
251
+ const totalKB = emailsPerMonth * numEventTypes * (retentionMonths ?? 12) * avgRecordSizeKB;
252
+ return totalKB / 1024 / 1024;
253
+ }
254
+ function estimateArchiveStorageSize(emailsPerMonth, retention) {
255
+ const avgEmailSizeKB = 50;
256
+ const retentionMonths = {
257
+ "7days": 0.25,
258
+ "30days": 1,
259
+ "90days": 3,
260
+ "6months": 6,
227
261
  "1year": 12,
228
- indefinite: 24
229
- // Assume 2 years for cost estimation
262
+ "18months": 18,
263
+ indefinite: 120
264
+ // Assume 10 years for cost estimation
230
265
  }[retention];
231
- const totalKB = emailsPerMonth * numEventTypes * retentionMonths * avgRecordSizeKB;
266
+ const totalKB = emailsPerMonth * (retentionMonths ?? 12) * avgEmailSizeKB;
232
267
  return totalKB / 1024 / 1024;
233
268
  }
234
269
  function calculateEventTrackingCost(config, emailsPerMonth) {
@@ -307,19 +342,35 @@ function calculateDedicatedIpCost(config) {
307
342
  description: "Dedicated IP address (requires 100k+ emails/day for warmup)"
308
343
  };
309
344
  }
345
+ function calculateEmailArchivingCost(config, emailsPerMonth) {
346
+ if (!config.emailArchiving?.enabled) {
347
+ return;
348
+ }
349
+ const retention = config.emailArchiving.retention;
350
+ const storageGB = estimateArchiveStorageSize(emailsPerMonth, retention);
351
+ const monthlyDataGB = emailsPerMonth * 50 / 1024 / 1024;
352
+ const ingestionCost = monthlyDataGB * AWS_PRICING.MAIL_MANAGER_INGESTION_PER_GB;
353
+ const storageCost = storageGB * AWS_PRICING.MAIL_MANAGER_STORAGE_PER_GB;
354
+ return {
355
+ monthly: ingestionCost + storageCost,
356
+ description: `Email archiving (${retention}, ~${storageGB.toFixed(2)} GB at steady-state)`
357
+ };
358
+ }
310
359
  function calculateCosts(config, emailsPerMonth = 1e4) {
311
360
  const tracking = calculateTrackingCost(config);
312
361
  const reputationMetrics = calculateReputationMetricsCost(config);
313
362
  const eventTracking = calculateEventTrackingCost(config, emailsPerMonth);
314
363
  const dynamoDBHistory = calculateDynamoDBCost(config, emailsPerMonth);
364
+ const emailArchiving = calculateEmailArchivingCost(config, emailsPerMonth);
315
365
  const dedicatedIp = calculateDedicatedIpCost(config);
316
366
  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);
367
+ const totalMonthlyCost = sesEmailCost + (tracking?.monthly || 0) + (reputationMetrics?.monthly || 0) + (eventTracking?.monthly || 0) + (dynamoDBHistory?.monthly || 0) + (emailArchiving?.monthly || 0) + (dedicatedIp?.monthly || 0);
318
368
  return {
319
369
  tracking,
320
370
  reputationMetrics,
321
371
  eventTracking,
322
372
  dynamoDBHistory,
373
+ emailArchiving,
323
374
  dedicatedIp,
324
375
  total: {
325
376
  monthly: totalMonthlyCost,
@@ -366,6 +417,11 @@ function getCostSummary(config, emailsPerMonth = 1e4) {
366
417
  ` - ${costs.dynamoDBHistory.description}: ${formatCost(costs.dynamoDBHistory.monthly)}`
367
418
  );
368
419
  }
420
+ if (costs.emailArchiving) {
421
+ lines.push(
422
+ ` - ${costs.emailArchiving.description}: ${formatCost(costs.emailArchiving.monthly)}`
423
+ );
424
+ }
369
425
  if (costs.dedicatedIp) {
370
426
  lines.push(
371
427
  ` - ${costs.dedicatedIp.description}: ${formatCost(costs.dedicatedIp.monthly)}`
@@ -408,8 +464,13 @@ var init_costs = __esm({
408
464
  // CloudWatch pricing
409
465
  CLOUDWATCH_LOGS_PER_GB: 0.5,
410
466
  // $0.50 per GB ingested
411
- CLOUDWATCH_LOGS_STORAGE_PER_GB: 0.03
467
+ CLOUDWATCH_LOGS_STORAGE_PER_GB: 0.03,
412
468
  // $0.03 per GB-month
469
+ // SES Mail Manager Archiving
470
+ MAIL_MANAGER_INGESTION_PER_GB: 2,
471
+ // $2.00 per GB ingested
472
+ MAIL_MANAGER_STORAGE_PER_GB: 0.19
473
+ // $0.19 per GB-month
413
474
  };
414
475
  FREE_TIER = {
415
476
  // SES: 3,000 emails/month for first 12 months (new AWS accounts only)
@@ -487,7 +548,8 @@ function getPresetInfo(preset) {
487
548
  features: [
488
549
  "Open & click tracking",
489
550
  "TLS encryption required",
490
- "Automatic bounce/complaint suppression"
551
+ "Automatic bounce/complaint suppression",
552
+ "Optional: Email archiving (full content storage)"
491
553
  ]
492
554
  },
493
555
  production: {
@@ -500,6 +562,7 @@ function getPresetInfo(preset) {
500
562
  "Reputation metrics dashboard",
501
563
  "Real-time event tracking (EventBridge)",
502
564
  "90-day email history storage",
565
+ "Optional: Email archiving with rendered viewer",
503
566
  "Complete event visibility"
504
567
  ]
505
568
  },
@@ -512,6 +575,7 @@ function getPresetInfo(preset) {
512
575
  "Everything in Production",
513
576
  "Dedicated IP address",
514
577
  "1-year email history",
578
+ "Optional: 1-year+ email archiving",
515
579
  "All event types tracked",
516
580
  "Priority support eligibility"
517
581
  ]
@@ -594,6 +658,11 @@ var init_presets = __esm({
594
658
  eventTracking: {
595
659
  enabled: false
596
660
  },
661
+ // Email archiving disabled by default
662
+ emailArchiving: {
663
+ enabled: false,
664
+ retention: "30days"
665
+ },
597
666
  sendingEnabled: true
598
667
  };
599
668
  PRODUCTION_PRESET = {
@@ -624,6 +693,12 @@ var init_presets = __esm({
624
693
  dynamoDBHistory: true,
625
694
  archiveRetention: "90days"
626
695
  },
696
+ // Email archiving with 90-day retention
697
+ emailArchiving: {
698
+ enabled: false,
699
+ // User can opt-in
700
+ retention: "90days"
701
+ },
627
702
  sendingEnabled: true
628
703
  };
629
704
  ENTERPRISE_PRESET = {
@@ -656,6 +731,12 @@ var init_presets = __esm({
656
731
  dynamoDBHistory: true,
657
732
  archiveRetention: "1year"
658
733
  },
734
+ // Email archiving with 1-year retention
735
+ emailArchiving: {
736
+ enabled: false,
737
+ // User can opt-in
738
+ retention: "1year"
739
+ },
659
740
  dedicatedIp: true,
660
741
  sendingEnabled: true
661
742
  };
@@ -672,6 +753,7 @@ __export(prompts_exports, {
672
753
  promptConflictResolution: () => promptConflictResolution,
673
754
  promptCustomConfig: () => promptCustomConfig,
674
755
  promptDomain: () => promptDomain,
756
+ promptEmailArchiving: () => promptEmailArchiving,
675
757
  promptEstimatedVolume: () => promptEstimatedVolume,
676
758
  promptFeatureSelection: () => promptFeatureSelection,
677
759
  promptIntegrationLevel: () => promptIntegrationLevel,
@@ -1033,6 +1115,59 @@ async function promptEstimatedVolume() {
1033
1115
  }
1034
1116
  return volume;
1035
1117
  }
1118
+ async function promptEmailArchiving() {
1119
+ const enabled = await clack3.confirm({
1120
+ message: "Enable email archiving? (Store full email content with HTML for viewing in dashboard)",
1121
+ initialValue: false
1122
+ });
1123
+ if (clack3.isCancel(enabled)) {
1124
+ clack3.cancel("Operation cancelled.");
1125
+ process.exit(0);
1126
+ }
1127
+ if (!enabled) {
1128
+ return { enabled: false, retention: "90days" };
1129
+ }
1130
+ const retention = await clack3.select({
1131
+ message: "Email archive retention period:",
1132
+ options: [
1133
+ { value: "7days", label: "7 days", hint: "~$1-2/mo for 10k emails" },
1134
+ { value: "30days", label: "30 days", hint: "~$2-4/mo for 10k emails" },
1135
+ {
1136
+ value: "90days",
1137
+ label: "90 days (recommended)",
1138
+ hint: "~$5-10/mo for 10k emails"
1139
+ },
1140
+ {
1141
+ value: "6months",
1142
+ label: "6 months",
1143
+ hint: "~$15-25/mo for 10k emails"
1144
+ },
1145
+ { value: "1year", label: "1 year", hint: "~$25-40/mo for 10k emails" },
1146
+ {
1147
+ value: "18months",
1148
+ label: "18 months",
1149
+ hint: "~$35-60/mo for 10k emails"
1150
+ }
1151
+ ],
1152
+ initialValue: "90days"
1153
+ });
1154
+ if (clack3.isCancel(retention)) {
1155
+ clack3.cancel("Operation cancelled.");
1156
+ process.exit(0);
1157
+ }
1158
+ clack3.log.info(
1159
+ pc3.dim(
1160
+ "Archiving stores full RFC 822 emails with HTML, attachments, and headers"
1161
+ )
1162
+ );
1163
+ clack3.log.info(
1164
+ pc3.dim("Cost: $2/GB ingestion + $0.19/GB/month storage (~50KB per email)")
1165
+ );
1166
+ return {
1167
+ enabled: true,
1168
+ retention
1169
+ };
1170
+ }
1036
1171
  async function promptCustomConfig() {
1037
1172
  clack3.log.info("Custom configuration builder");
1038
1173
  clack3.log.info("Configure each feature individually");
@@ -1112,6 +1247,53 @@ async function promptCustomConfig() {
1112
1247
  clack3.cancel("Operation cancelled.");
1113
1248
  process.exit(0);
1114
1249
  }
1250
+ const emailArchivingEnabled = await clack3.confirm({
1251
+ message: "Enable email archiving? (Store full email content with HTML for viewing)",
1252
+ initialValue: false
1253
+ });
1254
+ if (clack3.isCancel(emailArchivingEnabled)) {
1255
+ clack3.cancel("Operation cancelled.");
1256
+ process.exit(0);
1257
+ }
1258
+ let emailArchiveRetention = "90days";
1259
+ if (emailArchivingEnabled) {
1260
+ emailArchiveRetention = await clack3.select({
1261
+ message: "Email archive retention period:",
1262
+ options: [
1263
+ { value: "7days", label: "7 days", hint: "~$1-2/mo for 10k emails" },
1264
+ { value: "30days", label: "30 days", hint: "~$2-4/mo for 10k emails" },
1265
+ {
1266
+ value: "90days",
1267
+ label: "90 days (recommended)",
1268
+ hint: "~$5-10/mo for 10k emails"
1269
+ },
1270
+ {
1271
+ value: "6months",
1272
+ label: "6 months",
1273
+ hint: "~$15-25/mo for 10k emails"
1274
+ },
1275
+ { value: "1year", label: "1 year", hint: "~$25-40/mo for 10k emails" },
1276
+ {
1277
+ value: "18months",
1278
+ label: "18 months",
1279
+ hint: "~$35-60/mo for 10k emails"
1280
+ }
1281
+ ],
1282
+ initialValue: "90days"
1283
+ });
1284
+ if (clack3.isCancel(emailArchiveRetention)) {
1285
+ clack3.cancel("Operation cancelled.");
1286
+ process.exit(0);
1287
+ }
1288
+ clack3.log.info(
1289
+ pc3.dim(
1290
+ "Note: Archiving stores full RFC 822 emails with HTML, attachments, and headers"
1291
+ )
1292
+ );
1293
+ clack3.log.info(
1294
+ pc3.dim("Cost: $2/GB ingestion + $0.19/GB/month storage (~50KB per email)")
1295
+ );
1296
+ }
1115
1297
  return {
1116
1298
  tracking: trackingEnabled ? {
1117
1299
  enabled: true,
@@ -1140,6 +1322,10 @@ async function promptCustomConfig() {
1140
1322
  dynamoDBHistory: Boolean(dynamoDBHistory),
1141
1323
  archiveRetention: typeof archiveRetention === "string" ? archiveRetention : "90days"
1142
1324
  } : { enabled: false },
1325
+ emailArchiving: emailArchivingEnabled ? {
1326
+ enabled: true,
1327
+ retention: typeof emailArchiveRetention === "string" ? emailArchiveRetention : "90days"
1328
+ } : { enabled: false, retention: "90days" },
1143
1329
  dedicatedIp,
1144
1330
  sendingEnabled: true
1145
1331
  };
@@ -1184,6 +1370,114 @@ var init_assume_role = __esm({
1184
1370
  }
1185
1371
  });
1186
1372
 
1373
+ // src/utils/archive.ts
1374
+ import {
1375
+ GetArchiveMessageCommand,
1376
+ MailManagerClient
1377
+ } from "@aws-sdk/client-mailmanager";
1378
+ import { simpleParser } from "mailparser";
1379
+ async function getArchivedEmail(_archiveId, messageId, region) {
1380
+ const client = new MailManagerClient({ region });
1381
+ const command2 = new GetArchiveMessageCommand({
1382
+ ArchivedMessageId: messageId
1383
+ });
1384
+ const response = await client.send(command2);
1385
+ if (!response.MessageDownloadLink) {
1386
+ throw new Error("No download link available for archived message");
1387
+ }
1388
+ const emailResponse = await fetch(response.MessageDownloadLink);
1389
+ if (!emailResponse.ok) {
1390
+ throw new Error(`Failed to download email: ${emailResponse.statusText}`);
1391
+ }
1392
+ const emailRaw = await emailResponse.text();
1393
+ const parsed = await simpleParser(emailRaw);
1394
+ const attachments = parsed.attachments?.map((att) => ({
1395
+ filename: att.filename,
1396
+ contentType: att.contentType,
1397
+ size: att.size
1398
+ })) || [];
1399
+ const headers = {};
1400
+ if (parsed.headers) {
1401
+ for (const [key, value] of parsed.headers) {
1402
+ if (value instanceof Date) {
1403
+ headers[key] = value.toISOString();
1404
+ } else if (typeof value === "string") {
1405
+ headers[key] = value;
1406
+ } else if (Array.isArray(value) && value.every((v) => typeof v === "string")) {
1407
+ headers[key] = value;
1408
+ } else {
1409
+ headers[key] = JSON.stringify(value);
1410
+ }
1411
+ }
1412
+ }
1413
+ const getAddressText = (addr) => {
1414
+ if (!addr) return "";
1415
+ if (Array.isArray(addr)) {
1416
+ return addr.map((a) => a.text).join(", ");
1417
+ }
1418
+ return addr.text || "";
1419
+ };
1420
+ return {
1421
+ messageId,
1422
+ // Use the input messageId since response may not have MessageMetadata
1423
+ from: getAddressText(parsed.from),
1424
+ to: getAddressText(parsed.to),
1425
+ subject: parsed.subject || "",
1426
+ html: parsed.html || void 0,
1427
+ text: parsed.text || void 0,
1428
+ attachments,
1429
+ headers,
1430
+ timestamp: parsed.date || /* @__PURE__ */ new Date(),
1431
+ // Note: MessageMetadata is not available in GetArchiveMessageCommandOutput
1432
+ // These fields would need to be retrieved separately if needed
1433
+ metadata: {}
1434
+ };
1435
+ }
1436
+ var init_archive = __esm({
1437
+ "src/utils/archive.ts"() {
1438
+ "use strict";
1439
+ init_esm_shims();
1440
+ }
1441
+ });
1442
+
1443
+ // src/console/services/email-archive.ts
1444
+ var email_archive_exports = {};
1445
+ __export(email_archive_exports, {
1446
+ fetchArchivedEmail: () => fetchArchivedEmail
1447
+ });
1448
+ async function fetchArchivedEmail(messageId, options) {
1449
+ const { region, archiveArn } = options;
1450
+ try {
1451
+ console.log("Fetching archived email:", {
1452
+ messageId,
1453
+ archiveArn,
1454
+ region
1455
+ });
1456
+ const email = await getArchivedEmail(archiveArn, messageId, region);
1457
+ console.log("Archived email fetched successfully:", {
1458
+ messageId: email.messageId,
1459
+ hasHtml: !!email.html,
1460
+ hasText: !!email.text,
1461
+ attachmentCount: email.attachments.length
1462
+ });
1463
+ return email;
1464
+ } catch (error) {
1465
+ if (error instanceof Error && (error.message.includes("not found") || error.message.includes("ResourceNotFoundException"))) {
1466
+ console.log("Archived email not found:", messageId);
1467
+ return null;
1468
+ }
1469
+ console.error("Error fetching archived email:", error);
1470
+ throw error;
1471
+ }
1472
+ }
1473
+ var init_email_archive = __esm({
1474
+ "src/console/services/email-archive.ts"() {
1475
+ "use strict";
1476
+ init_esm_shims();
1477
+ init_archive();
1478
+ }
1479
+ });
1480
+
1187
1481
  // src/console/services/dynamodb-metrics.ts
1188
1482
  var dynamodb_metrics_exports = {};
1189
1483
  __export(dynamodb_metrics_exports, {
@@ -1582,6 +1876,19 @@ async function createIAMRole(config) {
1582
1876
  Resource: "arn:aws:sqs:*:*:wraps-email-*"
1583
1877
  });
1584
1878
  }
1879
+ if (config.emailConfig.emailArchiving?.enabled) {
1880
+ statements.push({
1881
+ Effect: "Allow",
1882
+ Action: [
1883
+ "ses:GetArchive",
1884
+ "ses:GetArchiveMessage",
1885
+ "ses:GetArchiveMessageContent",
1886
+ "ses:SearchArchive",
1887
+ "ses:StartArchiveExport"
1888
+ ],
1889
+ Resource: "arn:aws:ses:*:*:mailmanager-archive/*"
1890
+ });
1891
+ }
1585
1892
  new aws3.iam.RolePolicy("wraps-email-policy", {
1586
1893
  role: role.name,
1587
1894
  policy: JSON.stringify({
@@ -1764,8 +2071,9 @@ async function createSESResources(config) {
1764
2071
  if (config.trackingConfig?.customRedirectDomain) {
1765
2072
  configSetOptions.trackingOptions = {
1766
2073
  customRedirectDomain: config.trackingConfig.customRedirectDomain,
1767
- httpsPolicy: "REQUIRE"
1768
- // Always require HTTPS for security
2074
+ // Use OPTIONAL because custom domains don't have SSL certificates by default
2075
+ // AWS's tracking domain (r.{region}.awstrack.me) doesn't have certs for custom domains
2076
+ httpsPolicy: "OPTIONAL"
1769
2077
  };
1770
2078
  }
1771
2079
  const configSet = new aws5.sesv2.ConfigurationSet(
@@ -1957,6 +2265,15 @@ async function deployEmailStack(config) {
1957
2265
  accountId
1958
2266
  });
1959
2267
  }
2268
+ let archiveResources;
2269
+ if (emailConfig.emailArchiving?.enabled && sesResources) {
2270
+ const { createMailManagerArchive: createMailManagerArchive2 } = await Promise.resolve().then(() => (init_mail_manager(), mail_manager_exports));
2271
+ archiveResources = await createMailManagerArchive2({
2272
+ name: "email",
2273
+ retention: emailConfig.emailArchiving.retention,
2274
+ configSetName: sesResources.configSet.configurationSetName
2275
+ });
2276
+ }
1960
2277
  return {
1961
2278
  roleArn: role.arn,
1962
2279
  configSetName: sesResources?.configSet.configurationSetName,
@@ -1970,7 +2287,10 @@ async function deployEmailStack(config) {
1970
2287
  queueUrl: sqsResources?.queue.url,
1971
2288
  dlqUrl: sqsResources?.dlq.url,
1972
2289
  customTrackingDomain: sesResources?.customTrackingDomain,
1973
- mailFromDomain: sesResources?.mailFromDomain
2290
+ mailFromDomain: sesResources?.mailFromDomain,
2291
+ archiveArn: archiveResources?.archive.arn,
2292
+ archivingEnabled: emailConfig.emailArchiving?.enabled,
2293
+ archiveRetention: emailConfig.emailArchiving?.enabled ? emailConfig.emailArchiving.retention : void 0
1974
2294
  };
1975
2295
  }
1976
2296
 
@@ -2282,6 +2602,23 @@ ${domainStrings.join("\n")}`);
2282
2602
  ` ${pc2.dim("\u25CB")} Bounce/Complaint Handling ${pc2.dim("(run 'wraps upgrade' to enable)")}`
2283
2603
  );
2284
2604
  }
2605
+ if (status2.resources.archivingEnabled) {
2606
+ const retentionLabel = {
2607
+ "7days": "7 days",
2608
+ "30days": "30 days",
2609
+ "90days": "90 days",
2610
+ "6months": "6 months",
2611
+ "1year": "1 year",
2612
+ "18months": "18 months"
2613
+ }[status2.resources.archiveRetention || "90days"] || "90 days";
2614
+ featureLines.push(
2615
+ ` ${pc2.green("\u2713")} Email Archiving ${pc2.dim(`(${retentionLabel} retention)`)}`
2616
+ );
2617
+ } else {
2618
+ featureLines.push(
2619
+ ` ${pc2.dim("\u25CB")} Email Archiving ${pc2.dim("(run 'wraps upgrade' to enable)")}`
2620
+ );
2621
+ }
2285
2622
  featureLines.push(
2286
2623
  ` ${pc2.green("\u2713")} Console Dashboard ${pc2.dim("(run 'wraps console')")}`
2287
2624
  );
@@ -2314,6 +2651,11 @@ ${domainStrings.join("\n")}`);
2314
2651
  ` ${pc2.green("\u2713")} SNS Topics: ${pc2.cyan(`${status2.resources.snsTopics} configured`)}`
2315
2652
  );
2316
2653
  }
2654
+ if (status2.resources.archiveArn) {
2655
+ resourceLines.push(
2656
+ ` ${pc2.green("\u2713")} Mail Manager Archive: ${pc2.cyan(status2.resources.archiveArn)}`
2657
+ );
2658
+ }
2317
2659
  clack2.note(resourceLines.join("\n"), "Resources");
2318
2660
  const domainsNeedingDNS = status2.domains.filter(
2319
2661
  (d) => d.status === "pending" && d.dkimTokens || d.mailFromDomain && d.mailFromStatus !== "SUCCESS"
@@ -2336,7 +2678,9 @@ ${domainStrings.join("\n")}`);
2336
2678
  );
2337
2679
  }
2338
2680
  if (domain.mailFromDomain && domain.mailFromStatus !== "SUCCESS") {
2339
- if (dnsLines.length > 0) dnsLines.push("");
2681
+ if (dnsLines.length > 0) {
2682
+ dnsLines.push("");
2683
+ }
2340
2684
  dnsLines.push(
2341
2685
  pc2.bold("MAIL FROM Domain Records (for DMARC alignment):"),
2342
2686
  ` ${pc2.cyan(domain.mailFromDomain)} ${pc2.dim("MX")} "10 feedback-smtp.${status2.region}.amazonses.com"`,
@@ -3271,6 +3615,46 @@ function createEmailsRouter(config) {
3271
3615
  res.status(500).json({ error: errorMessage });
3272
3616
  }
3273
3617
  });
3618
+ router.get("/:id/archive", async (req, res) => {
3619
+ try {
3620
+ const { id } = req.params;
3621
+ console.log("Archived email request received for message ID:", id);
3622
+ if (!config.archivingEnabled) {
3623
+ console.log("Email archiving not enabled");
3624
+ return res.status(400).json({
3625
+ error: "Email archiving not enabled for this deployment."
3626
+ });
3627
+ }
3628
+ if (!config.archiveArn) {
3629
+ console.log("No archive ARN configured");
3630
+ return res.status(400).json({
3631
+ error: "Archive ARN not configured."
3632
+ });
3633
+ }
3634
+ console.log("Fetching archived email from Mail Manager...");
3635
+ const { fetchArchivedEmail: fetchArchivedEmail2 } = await Promise.resolve().then(() => (init_email_archive(), email_archive_exports));
3636
+ const archivedEmail = await fetchArchivedEmail2(id, {
3637
+ region: config.region,
3638
+ archiveArn: config.archiveArn
3639
+ });
3640
+ if (!archivedEmail) {
3641
+ console.log("Archived email not found for message ID:", id);
3642
+ return res.status(404).json({
3643
+ error: "Archived email not found. It may have been sent before archiving was enabled."
3644
+ });
3645
+ }
3646
+ console.log("Archived email found:", archivedEmail.messageId);
3647
+ res.json(archivedEmail);
3648
+ } catch (error) {
3649
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
3650
+ console.error("Error fetching archived email:", error);
3651
+ console.error(
3652
+ "Stack trace:",
3653
+ error instanceof Error ? error.stack : "N/A"
3654
+ );
3655
+ res.status(500).json({ error: errorMessage });
3656
+ }
3657
+ });
3274
3658
  return router;
3275
3659
  }
3276
3660
 
@@ -3557,7 +3941,9 @@ async function fetchEmailIdentity(roleArn, region, identityName) {
3557
3941
  verifiedForSendingStatus: response.VerifiedForSendingStatus ?? false,
3558
3942
  tags: response.Tags?.reduce(
3559
3943
  (acc, tag) => {
3560
- if (tag.Key) acc[tag.Key] = tag.Value || "";
3944
+ if (tag.Key) {
3945
+ acc[tag.Key] = tag.Value || "";
3946
+ }
3561
3947
  return acc;
3562
3948
  },
3563
3949
  {}
@@ -3590,6 +3976,20 @@ async function fetchEmailSettings(roleArn, region, configSetName, domain) {
3590
3976
  // src/console/routes/settings.ts
3591
3977
  function createSettingsRouter(config) {
3592
3978
  const router = createRouter4();
3979
+ router.get("/deployment", async (_req, res) => {
3980
+ try {
3981
+ res.json({
3982
+ archivingEnabled: config.archivingEnabled ?? false,
3983
+ archiveArn: config.archiveArn,
3984
+ tableName: config.tableName,
3985
+ region: config.region
3986
+ });
3987
+ } catch (error) {
3988
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
3989
+ console.error("Error fetching deployment config:", error);
3990
+ res.status(500).json({ error: errorMessage });
3991
+ }
3992
+ });
3593
3993
  router.get("/", async (_req, res) => {
3594
3994
  try {
3595
3995
  const metadata = await loadConnectionMetadata(
@@ -3965,6 +4365,8 @@ async function runConsole(options) {
3965
4365
  process.exit(1);
3966
4366
  }
3967
4367
  const tableName = stackOutputs.tableName?.value;
4368
+ const archiveArn = stackOutputs.archiveArn?.value;
4369
+ const archivingEnabled = stackOutputs.archivingEnabled?.value ?? false;
3968
4370
  const port = options.port || await getPort({ port: [5555, 5556, 5557, 5558, 5559] });
3969
4371
  progress.stop();
3970
4372
  clack5.log.success("Starting console server...");
@@ -3978,7 +4380,9 @@ async function runConsole(options) {
3978
4380
  region,
3979
4381
  tableName,
3980
4382
  accountId: identity.accountId,
3981
- noOpen: options.noOpen ?? false
4383
+ noOpen: options.noOpen ?? false,
4384
+ archiveArn,
4385
+ archivingEnabled
3982
4386
  });
3983
4387
  console.log(`\\n${pc5.bold("Console:")} ${pc5.cyan(url)}`);
3984
4388
  console.log(`${pc5.dim("Press Ctrl+C to stop")}\\n`);
@@ -4118,17 +4522,22 @@ async function init(options) {
4118
4522
  emailConfig = await promptCustomConfig();
4119
4523
  } else {
4120
4524
  emailConfig = getPreset(preset);
4525
+ const { promptEmailArchiving: promptEmailArchiving2 } = await Promise.resolve().then(() => (init_prompts(), prompts_exports));
4526
+ const archivingConfig = await promptEmailArchiving2();
4527
+ emailConfig.emailArchiving = archivingConfig;
4121
4528
  }
4122
4529
  if (domain) {
4123
4530
  emailConfig.domain = domain;
4124
4531
  }
4125
4532
  const estimatedVolume = await promptEstimatedVolume();
4126
- progress.info("\n" + pc7.bold("Cost Estimate:"));
4533
+ progress.info(`
4534
+ ${pc7.bold("Cost Estimate:")}`);
4127
4535
  const costSummary = getCostSummary(emailConfig, estimatedVolume);
4128
4536
  clack7.log.info(costSummary);
4129
4537
  const warnings = validateConfig(emailConfig);
4130
4538
  if (warnings.length > 0) {
4131
- progress.info("\n" + pc7.yellow(pc7.bold("Configuration Warnings:")));
4539
+ progress.info(`
4540
+ ${pc7.yellow(pc7.bold("Configuration Warnings:"))}`);
4132
4541
  for (const warning of warnings) {
4133
4542
  clack7.log.warn(warning);
4134
4543
  }
@@ -4176,7 +4585,11 @@ async function init(options) {
4176
4585
  lambdaFunctions: result.lambdaFunctions,
4177
4586
  domain: result.domain,
4178
4587
  dkimTokens: result.dkimTokens,
4179
- customTrackingDomain: result.customTrackingDomain
4588
+ customTrackingDomain: result.customTrackingDomain,
4589
+ mailFromDomain: result.mailFromDomain,
4590
+ archiveArn: result.archiveArn,
4591
+ archivingEnabled: result.archivingEnabled,
4592
+ archiveRetention: result.archiveRetention
4180
4593
  };
4181
4594
  }
4182
4595
  },
@@ -4205,7 +4618,11 @@ async function init(options) {
4205
4618
  lambdaFunctions: pulumiOutputs.lambdaFunctions?.value,
4206
4619
  domain: pulumiOutputs.domain?.value,
4207
4620
  dkimTokens: pulumiOutputs.dkimTokens?.value,
4208
- customTrackingDomain: pulumiOutputs.customTrackingDomain?.value
4621
+ customTrackingDomain: pulumiOutputs.customTrackingDomain?.value,
4622
+ mailFromDomain: pulumiOutputs.mailFromDomain?.value,
4623
+ archiveArn: pulumiOutputs.archiveArn?.value,
4624
+ archivingEnabled: pulumiOutputs.archivingEnabled?.value,
4625
+ archiveRetention: pulumiOutputs.archiveRetention?.value
4209
4626
  };
4210
4627
  }
4211
4628
  );
@@ -4439,7 +4856,10 @@ Run ${pc9.cyan("wraps init")} to deploy infrastructure.
4439
4856
  configSetName: stackOutputs.configSetName?.value,
4440
4857
  tableName: stackOutputs.tableName?.value,
4441
4858
  lambdaFunctions: stackOutputs.lambdaFunctions?.value?.length || 0,
4442
- snsTopics: integrationLevel === "enhanced" ? 1 : 0
4859
+ snsTopics: integrationLevel === "enhanced" ? 1 : 0,
4860
+ archiveArn: stackOutputs.archiveArn?.value,
4861
+ archivingEnabled: stackOutputs.archivingEnabled?.value,
4862
+ archiveRetention: stackOutputs.archiveRetention?.value
4443
4863
  }
4444
4864
  });
4445
4865
  }
@@ -4706,6 +5126,18 @@ ${pc11.bold("Current Configuration:")}
4706
5126
  if (config.dedicatedIp) {
4707
5127
  console.log(` ${pc11.green("\u2713")} Dedicated IP Address`);
4708
5128
  }
5129
+ if (config.emailArchiving?.enabled) {
5130
+ const retentionLabel = {
5131
+ "7days": "7 days",
5132
+ "30days": "30 days",
5133
+ "90days": "90 days",
5134
+ "6months": "6 months",
5135
+ "1year": "1 year",
5136
+ "18months": "18 months",
5137
+ indefinite: "indefinite"
5138
+ }[config.emailArchiving.retention] || "90 days";
5139
+ console.log(` ${pc11.green("\u2713")} Email Archiving (${retentionLabel})`);
5140
+ }
4709
5141
  const currentCostData = calculateCosts(config, 5e4);
4710
5142
  console.log(
4711
5143
  `
@@ -4720,6 +5152,11 @@ ${pc11.bold("Current Configuration:")}
4720
5152
  label: "Upgrade to a different preset",
4721
5153
  hint: "Starter \u2192 Production \u2192 Enterprise"
4722
5154
  },
5155
+ {
5156
+ value: "archiving",
5157
+ label: config.emailArchiving?.enabled ? "Change email archiving settings" : "Enable email archiving",
5158
+ hint: config.emailArchiving?.enabled ? "Update retention or disable" : "Store full email content with HTML"
5159
+ },
4723
5160
  {
4724
5161
  value: "tracking-domain",
4725
5162
  label: "Add/change custom tracking domain",
@@ -4728,7 +5165,7 @@ ${pc11.bold("Current Configuration:")}
4728
5165
  {
4729
5166
  value: "retention",
4730
5167
  label: "Change email history retention",
4731
- hint: "7 days, 30 days, 90 days, 1 year, indefinite"
5168
+ hint: "7 days, 30 days, 90 days, 6 months, 1 year, 18 months"
4732
5169
  },
4733
5170
  {
4734
5171
  value: "events",
@@ -4786,6 +5223,166 @@ ${pc11.bold("Current Configuration:")}
4786
5223
  newPreset = selectedPreset;
4787
5224
  break;
4788
5225
  }
5226
+ case "archiving": {
5227
+ if (config.emailArchiving?.enabled) {
5228
+ const archivingAction = await clack11.select({
5229
+ message: "What would you like to do with email archiving?",
5230
+ options: [
5231
+ {
5232
+ value: "change-retention",
5233
+ label: "Change retention period",
5234
+ hint: `Current: ${config.emailArchiving.retention}`
5235
+ },
5236
+ {
5237
+ value: "disable",
5238
+ label: "Disable email archiving",
5239
+ hint: "Stop storing full email content"
5240
+ }
5241
+ ]
5242
+ });
5243
+ if (clack11.isCancel(archivingAction)) {
5244
+ clack11.cancel("Upgrade cancelled.");
5245
+ process.exit(0);
5246
+ }
5247
+ if (archivingAction === "disable") {
5248
+ const confirmDisable = await clack11.confirm({
5249
+ message: "Are you sure? Existing archived emails will remain, but new emails won't be archived.",
5250
+ initialValue: false
5251
+ });
5252
+ if (clack11.isCancel(confirmDisable) || !confirmDisable) {
5253
+ clack11.cancel("Archiving not disabled.");
5254
+ process.exit(0);
5255
+ }
5256
+ updatedConfig = {
5257
+ ...config,
5258
+ emailArchiving: {
5259
+ enabled: false,
5260
+ retention: config.emailArchiving.retention
5261
+ }
5262
+ };
5263
+ } else {
5264
+ const retention = await clack11.select({
5265
+ message: "Email archive retention period:",
5266
+ options: [
5267
+ {
5268
+ value: "7days",
5269
+ label: "7 days",
5270
+ hint: "~$1-2/mo for 10k emails"
5271
+ },
5272
+ {
5273
+ value: "30days",
5274
+ label: "30 days",
5275
+ hint: "~$2-4/mo for 10k emails"
5276
+ },
5277
+ {
5278
+ value: "90days",
5279
+ label: "90 days (recommended)",
5280
+ hint: "~$5-10/mo for 10k emails"
5281
+ },
5282
+ {
5283
+ value: "6months",
5284
+ label: "6 months",
5285
+ hint: "~$15-25/mo for 10k emails"
5286
+ },
5287
+ {
5288
+ value: "1year",
5289
+ label: "1 year",
5290
+ hint: "~$25-40/mo for 10k emails"
5291
+ },
5292
+ {
5293
+ value: "18months",
5294
+ label: "18 months",
5295
+ hint: "~$35-60/mo for 10k emails"
5296
+ }
5297
+ ],
5298
+ initialValue: config.emailArchiving.retention
5299
+ });
5300
+ if (clack11.isCancel(retention)) {
5301
+ clack11.cancel("Upgrade cancelled.");
5302
+ process.exit(0);
5303
+ }
5304
+ updatedConfig = {
5305
+ ...config,
5306
+ emailArchiving: {
5307
+ enabled: true,
5308
+ retention
5309
+ }
5310
+ };
5311
+ }
5312
+ } else {
5313
+ const enableArchiving = await clack11.confirm({
5314
+ message: "Enable email archiving? (Store full email content with HTML for viewing)",
5315
+ initialValue: true
5316
+ });
5317
+ if (clack11.isCancel(enableArchiving)) {
5318
+ clack11.cancel("Upgrade cancelled.");
5319
+ process.exit(0);
5320
+ }
5321
+ if (!enableArchiving) {
5322
+ clack11.log.info("Email archiving not enabled.");
5323
+ process.exit(0);
5324
+ }
5325
+ const retention = await clack11.select({
5326
+ message: "Email archive retention period:",
5327
+ options: [
5328
+ {
5329
+ value: "7days",
5330
+ label: "7 days",
5331
+ hint: "~$1-2/mo for 10k emails"
5332
+ },
5333
+ {
5334
+ value: "30days",
5335
+ label: "30 days",
5336
+ hint: "~$2-4/mo for 10k emails"
5337
+ },
5338
+ {
5339
+ value: "90days",
5340
+ label: "90 days (recommended)",
5341
+ hint: "~$5-10/mo for 10k emails"
5342
+ },
5343
+ {
5344
+ value: "6months",
5345
+ label: "6 months",
5346
+ hint: "~$15-25/mo for 10k emails"
5347
+ },
5348
+ {
5349
+ value: "1year",
5350
+ label: "1 year",
5351
+ hint: "~$25-40/mo for 10k emails"
5352
+ },
5353
+ {
5354
+ value: "18months",
5355
+ label: "18 months",
5356
+ hint: "~$35-60/mo for 10k emails"
5357
+ }
5358
+ ],
5359
+ initialValue: "90days"
5360
+ });
5361
+ if (clack11.isCancel(retention)) {
5362
+ clack11.cancel("Upgrade cancelled.");
5363
+ process.exit(0);
5364
+ }
5365
+ clack11.log.info(
5366
+ pc11.dim(
5367
+ "Archiving stores full RFC 822 emails with HTML, attachments, and headers"
5368
+ )
5369
+ );
5370
+ clack11.log.info(
5371
+ pc11.dim(
5372
+ "Cost: $2/GB ingestion + $0.19/GB/month storage (~50KB per email)"
5373
+ )
5374
+ );
5375
+ updatedConfig = {
5376
+ ...config,
5377
+ emailArchiving: {
5378
+ enabled: true,
5379
+ retention
5380
+ }
5381
+ };
5382
+ }
5383
+ newPreset = void 0;
5384
+ break;
5385
+ }
4789
5386
  case "tracking-domain": {
4790
5387
  if (!config.domain) {
4791
5388
  clack11.log.error(
@@ -4844,7 +5441,7 @@ ${pc11.bold("Current Configuration:")}
4844
5441
  }
4845
5442
  case "retention": {
4846
5443
  const retention = await clack11.select({
4847
- message: "Email history retention period:",
5444
+ message: "Email history retention period (event data in DynamoDB):",
4848
5445
  options: [
4849
5446
  { value: "7days", label: "7 days", hint: "Minimal storage cost" },
4850
5447
  { value: "30days", label: "30 days", hint: "Development/testing" },
@@ -4853,11 +5450,16 @@ ${pc11.bold("Current Configuration:")}
4853
5450
  label: "90 days (recommended)",
4854
5451
  hint: "Standard retention"
4855
5452
  },
5453
+ {
5454
+ value: "6months",
5455
+ label: "6 months",
5456
+ hint: "Extended retention"
5457
+ },
4856
5458
  { value: "1year", label: "1 year", hint: "Compliance requirements" },
4857
5459
  {
4858
- value: "indefinite",
4859
- label: "Indefinite",
4860
- hint: "Higher storage cost"
5460
+ value: "18months",
5461
+ label: "18 months",
5462
+ hint: "Long-term retention"
4861
5463
  }
4862
5464
  ],
4863
5465
  initialValue: config.eventTracking?.archiveRetention || "90days"
@@ -4866,6 +5468,16 @@ ${pc11.bold("Current Configuration:")}
4866
5468
  clack11.cancel("Upgrade cancelled.");
4867
5469
  process.exit(0);
4868
5470
  }
5471
+ clack11.log.info(
5472
+ pc11.dim(
5473
+ "Note: This is for event data (sent, delivered, opened, etc.) stored in DynamoDB."
5474
+ )
5475
+ );
5476
+ clack11.log.info(
5477
+ pc11.dim(
5478
+ "For full email content storage, use 'Enable email archiving' option."
5479
+ )
5480
+ );
4869
5481
  updatedConfig = {
4870
5482
  ...config,
4871
5483
  eventTracking: {
@@ -5029,7 +5641,10 @@ ${pc11.bold("Cost Impact:")}`);
5029
5641
  lambdaFunctions: result.lambdaFunctions,
5030
5642
  domain: result.domain,
5031
5643
  dkimTokens: result.dkimTokens,
5032
- customTrackingDomain: result.customTrackingDomain
5644
+ customTrackingDomain: result.customTrackingDomain,
5645
+ archiveArn: result.archiveArn,
5646
+ archivingEnabled: result.archivingEnabled,
5647
+ archiveRetention: result.archiveRetention
5033
5648
  };
5034
5649
  }
5035
5650
  },
@@ -5056,7 +5671,10 @@ ${pc11.bold("Cost Impact:")}`);
5056
5671
  lambdaFunctions: pulumiOutputs.lambdaFunctions?.value,
5057
5672
  domain: pulumiOutputs.domain?.value,
5058
5673
  dkimTokens: pulumiOutputs.dkimTokens?.value,
5059
- customTrackingDomain: pulumiOutputs.customTrackingDomain?.value
5674
+ customTrackingDomain: pulumiOutputs.customTrackingDomain?.value,
5675
+ archiveArn: pulumiOutputs.archiveArn?.value,
5676
+ archivingEnabled: pulumiOutputs.archivingEnabled?.value,
5677
+ archiveRetention: pulumiOutputs.archiveRetention?.value
5060
5678
  };
5061
5679
  }
5062
5680
  );
@@ -5095,12 +5713,12 @@ ${pc11.green("\u2713")} ${pc11.bold("Upgrade complete!")}
5095
5713
  `);
5096
5714
  if (upgradeAction === "preset" && newPreset) {
5097
5715
  console.log(
5098
- `Upgraded to ${pc11.cyan(newPreset)} preset (${pc11.green(formatCost(newCostData.total.monthly) + "/mo")})
5716
+ `Upgraded to ${pc11.cyan(newPreset)} preset (${pc11.green(`${formatCost(newCostData.total.monthly)}/mo`)})
5099
5717
  `
5100
5718
  );
5101
5719
  } else {
5102
5720
  console.log(
5103
- `Updated configuration (${pc11.green(formatCost(newCostData.total.monthly) + "/mo")})
5721
+ `Updated configuration (${pc11.green(`${formatCost(newCostData.total.monthly)}/mo`)})
5104
5722
  `
5105
5723
  );
5106
5724
  }