@wraps.dev/cli 1.6.1 → 1.6.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
@@ -147,7 +147,7 @@ var require_package = __commonJS({
147
147
  "package.json"(exports, module) {
148
148
  module.exports = {
149
149
  name: "@wraps.dev/cli",
150
- version: "1.6.1",
150
+ version: "1.6.3",
151
151
  description: "CLI for deploying Wraps email infrastructure to your AWS account",
152
152
  type: "module",
153
153
  main: "./dist/cli.js",
@@ -194,7 +194,6 @@ var require_package = __commonJS({
194
194
  author: "Wraps",
195
195
  license: "MIT",
196
196
  dependencies: {
197
- "@wraps/email-check": "workspace:*",
198
197
  "@aws-sdk/client-acm": "3.933.0",
199
198
  "@aws-sdk/client-cloudformation": "^3.490.0",
200
199
  "@aws-sdk/client-cloudfront": "3.933.0",
@@ -229,6 +228,7 @@ var require_package = __commonJS({
229
228
  uuid: "^11.0.3"
230
229
  },
231
230
  devDependencies: {
231
+ "@wraps/email-check": "workspace:*",
232
232
  "@types/args": "5.0.4",
233
233
  "@types/express": "^5.0.0",
234
234
  "@types/mailparser": "3.4.6",
@@ -4601,221 +4601,2638 @@ function buildConsolePolicyDocument(emailConfig, smsConfig) {
4601
4601
 
4602
4602
  // src/commands/email/check.ts
4603
4603
  init_esm_shims();
4604
- init_events();
4605
4604
  import { GetEmailIdentityCommand, SESv2Client } from "@aws-sdk/client-sesv2";
4606
4605
  import * as clack3 from "@clack/prompts";
4607
- import {
4608
- formatSpfLookupTree,
4609
- getExitCode,
4610
- runEmailCheck
4611
- } from "@wraps/email-check";
4612
- import pc4 from "picocolors";
4613
- async function check(options) {
4614
- const startTime = Date.now();
4615
- let domain = options.domain;
4616
- if (!domain) {
4617
- const input = await clack3.text({
4618
- message: "Enter domain to check:",
4619
- placeholder: "example.com",
4620
- validate: (value) => {
4621
- if (!value) return "Domain is required";
4622
- if (!/^[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/i.test(value)) {
4623
- return "Invalid domain format";
4624
- }
4625
- }
4606
+
4607
+ // ../email-check/dist/index.js
4608
+ init_esm_shims();
4609
+ import { promises as dns, Resolver } from "dns";
4610
+ import { URL } from "url";
4611
+ import * as net from "net";
4612
+ import * as tls from "tls";
4613
+ var QUICK_DKIM_SELECTORS = [
4614
+ // Top providers (most likely to find)
4615
+ "google",
4616
+ "selector1",
4617
+ "selector2",
4618
+ "default",
4619
+ "dkim",
4620
+ "mail",
4621
+ "email",
4622
+ "s1",
4623
+ "s2",
4624
+ "k1",
4625
+ // ESPs
4626
+ "sendgrid",
4627
+ "amazonses",
4628
+ "mandrill",
4629
+ "mailgun",
4630
+ "postmark",
4631
+ "resend",
4632
+ "ses",
4633
+ "sg",
4634
+ "mg",
4635
+ "pm",
4636
+ // Fallbacks
4637
+ "smtp",
4638
+ "mta",
4639
+ "mx",
4640
+ "primary",
4641
+ "main"
4642
+ ];
4643
+ var QUICK_BLACKLISTS = [
4644
+ { name: "Spamhaus ZEN", zone: "zen.spamhaus.org", priority: "critical" },
4645
+ { name: "Spamhaus DBL", zone: "dbl.spamhaus.org", priority: "critical" },
4646
+ { name: "Barracuda", zone: "b.barracudacentral.org", priority: "high" },
4647
+ { name: "SpamCop", zone: "bl.spamcop.net", priority: "high" },
4648
+ { name: "CBL", zone: "cbl.abuseat.org", priority: "high" },
4649
+ { name: "SORBS", zone: "dnsbl.sorbs.net", priority: "medium" },
4650
+ { name: "URIBL", zone: "multi.uribl.com", priority: "medium" },
4651
+ { name: "SURBL", zone: "multi.surbl.org", priority: "medium" },
4652
+ { name: "Mailspike", zone: "bl.mailspike.net", priority: "medium" },
4653
+ { name: "Invaluement", zone: "dnsbl.invaluement.com", priority: "medium" }
4654
+ ];
4655
+ var DEFAULT_DKIM_SELECTORS = [
4656
+ // Generic / common patterns
4657
+ "default",
4658
+ "dkim",
4659
+ "mail",
4660
+ "email",
4661
+ "smtp",
4662
+ "mta",
4663
+ "mx",
4664
+ "selector",
4665
+ "selector1",
4666
+ "selector2",
4667
+ "s",
4668
+ "s1",
4669
+ "s2",
4670
+ "s3",
4671
+ "k",
4672
+ "k1",
4673
+ "k2",
4674
+ "k3",
4675
+ "d",
4676
+ "d1",
4677
+ "d2",
4678
+ "m",
4679
+ "m1",
4680
+ "m2",
4681
+ "e",
4682
+ "e1",
4683
+ "e2",
4684
+ // Google Workspace
4685
+ "google",
4686
+ "g",
4687
+ "gm",
4688
+ // Microsoft 365 (selector1, selector2 already covered)
4689
+ // Amazon SES (note: often random, may not find)
4690
+ "amazonses",
4691
+ "ses",
4692
+ // Mailgun
4693
+ "mailo",
4694
+ "pic",
4695
+ "mg",
4696
+ "mailgun",
4697
+ // SendGrid
4698
+ "sendgrid",
4699
+ "sg",
4700
+ "smtpapi",
4701
+ // Mailchimp / Mandrill
4702
+ "mandrill",
4703
+ "mailchimp",
4704
+ "mc",
4705
+ "mte1",
4706
+ "mte2",
4707
+ // HubSpot
4708
+ "hs1",
4709
+ "hs2",
4710
+ "hubspot",
4711
+ "hubspotemail",
4712
+ // Klaviyo
4713
+ "kl",
4714
+ "kl2",
4715
+ "klaviyo",
4716
+ // Intercom
4717
+ "intercom",
4718
+ "ic",
4719
+ // Zendesk
4720
+ "zendesk",
4721
+ "zendesk1",
4722
+ "zendesk2",
4723
+ "zd",
4724
+ // Salesforce
4725
+ "sf",
4726
+ "sf1",
4727
+ "sf2",
4728
+ "salesforce",
4729
+ // Zoho
4730
+ "zoho",
4731
+ "zmail",
4732
+ "zm",
4733
+ // Fastmail
4734
+ "fm1",
4735
+ "fm2",
4736
+ "fm3",
4737
+ "mesmtp",
4738
+ // Proton
4739
+ "protonmail",
4740
+ "protonmail2",
4741
+ "protonmail3",
4742
+ // Mailerlite
4743
+ "ml",
4744
+ "litesrv",
4745
+ "mailerlite",
4746
+ // ConvertKit
4747
+ "ck",
4748
+ "ck1",
4749
+ "ck2",
4750
+ "convertkit",
4751
+ // ActiveCampaign
4752
+ "ac",
4753
+ "ac1",
4754
+ "ac2",
4755
+ "dk",
4756
+ "activecampaign",
4757
+ // Customer.io
4758
+ "cio",
4759
+ "customerio",
4760
+ // Postmark
4761
+ "pm",
4762
+ "postmark",
4763
+ // SparkPost
4764
+ "sparkpost",
4765
+ "sp",
4766
+ "sp1",
4767
+ "sp2",
4768
+ // SendPulse
4769
+ "sendpulse",
4770
+ // MailerSend
4771
+ "ms",
4772
+ "mailersend",
4773
+ // Loops
4774
+ "loops",
4775
+ // Resend
4776
+ "resend",
4777
+ // Campaign Monitor
4778
+ "cm",
4779
+ "cmail",
4780
+ // Drip
4781
+ "drip",
4782
+ // Brevo (Sendinblue)
4783
+ "sibmail",
4784
+ "brevo",
4785
+ // Mailjet
4786
+ "mailjet",
4787
+ "mj",
4788
+ // Constant Contact
4789
+ "cc",
4790
+ "ctct",
4791
+ "ctct1",
4792
+ "ctct2",
4793
+ // AWeber
4794
+ "aweber",
4795
+ "aw",
4796
+ // GetResponse
4797
+ "getresponse",
4798
+ "gr",
4799
+ // Moosend
4800
+ "moosend",
4801
+ // Omnisend
4802
+ "omnisend",
4803
+ // Keap / Infusionsoft
4804
+ "infusionsoft",
4805
+ "keap",
4806
+ // Twilio SendGrid
4807
+ "twilio",
4808
+ // Elastic Email
4809
+ "elastic",
4810
+ "ee",
4811
+ // Pepipost
4812
+ "pepipost",
4813
+ // SMTP.com
4814
+ "smtpcom",
4815
+ // Generic fallbacks
4816
+ "primary",
4817
+ "main",
4818
+ "prod",
4819
+ "live",
4820
+ "api",
4821
+ "bulk",
4822
+ "transactional",
4823
+ "marketing"
4824
+ ];
4825
+ var DOMAIN_BLACKLISTS = [
4826
+ { name: "Spamhaus DBL", zone: "dbl.spamhaus.org", priority: "critical" },
4827
+ { name: "SURBL Multi", zone: "multi.surbl.org", priority: "high" },
4828
+ { name: "URIBL Black", zone: "black.uribl.com", priority: "high" },
4829
+ { name: "URIBL Grey", zone: "grey.uribl.com", priority: "medium" },
4830
+ { name: "URIBL Red", zone: "red.uribl.com", priority: "high" },
4831
+ { name: "URIBL Multi", zone: "multi.uribl.com", priority: "high" },
4832
+ { name: "Spamcop URI", zone: "bl.spamcop.net", priority: "high" },
4833
+ { name: "DBL Abuse.ch", zone: "dbl.abuse.ch", priority: "medium" },
4834
+ { name: "FRESH URI", zone: "fresh.spameatingmonkey.net", priority: "low" },
4835
+ { name: "Mailspike Z", zone: "z.mailspike.net", priority: "medium" },
4836
+ { name: "SEM Fresh", zone: "fresh.spameatingmonkey.net", priority: "low" },
4837
+ { name: "SEM URI", zone: "uribl.spameatingmonkey.net", priority: "medium" }
4838
+ ];
4839
+ var IP_BLACKLISTS = [
4840
+ // Spamhaus (most important)
4841
+ { name: "Spamhaus ZEN", zone: "zen.spamhaus.org", priority: "critical" },
4842
+ { name: "Spamhaus SBL", zone: "sbl.spamhaus.org", priority: "critical" },
4843
+ { name: "Spamhaus XBL", zone: "xbl.spamhaus.org", priority: "critical" },
4844
+ { name: "Spamhaus PBL", zone: "pbl.spamhaus.org", priority: "high" },
4845
+ { name: "Spamhaus CSS", zone: "sbl-xbl.spamhaus.org", priority: "critical" },
4846
+ // Major lists
4847
+ { name: "Barracuda", zone: "b.barracudacentral.org", priority: "high" },
4848
+ { name: "SpamCop", zone: "bl.spamcop.net", priority: "high" },
4849
+ { name: "CBL", zone: "cbl.abuseat.org", priority: "high" },
4850
+ // Secondary lists
4851
+ { name: "SORBS", zone: "dnsbl.sorbs.net", priority: "medium" },
4852
+ { name: "SORBS Spam", zone: "spam.dnsbl.sorbs.net", priority: "medium" },
4853
+ {
4854
+ name: "SORBS Recent",
4855
+ zone: "recent.spam.dnsbl.sorbs.net",
4856
+ priority: "medium"
4857
+ },
4858
+ { name: "SORBS Web", zone: "web.dnsbl.sorbs.net", priority: "medium" },
4859
+ { name: "SORBS New", zone: "new.spam.dnsbl.sorbs.net", priority: "low" },
4860
+ { name: "Passive Spam Block", zone: "psbl.surriel.com", priority: "medium" },
4861
+ { name: "UCEPROTECT 1", zone: "dnsbl-1.uceprotect.net", priority: "medium" },
4862
+ { name: "UCEPROTECT 2", zone: "dnsbl-2.uceprotect.net", priority: "low" },
4863
+ { name: "UCEPROTECT 3", zone: "dnsbl-3.uceprotect.net", priority: "low" },
4864
+ { name: "WPBL", zone: "db.wpbl.info", priority: "medium" },
4865
+ { name: "Mailspike BL", zone: "bl.mailspike.net", priority: "medium" },
4866
+ { name: "Mailspike Z", zone: "z.mailspike.net", priority: "medium" },
4867
+ // Additional lists
4868
+ { name: "JustSpam", zone: "dnsbl.justspam.org", priority: "low" },
4869
+ {
4870
+ name: "Hostkarma Black",
4871
+ zone: "hostkarma.junkemailfilter.com",
4872
+ priority: "low"
4873
+ },
4874
+ { name: "Invaluement", zone: "dnsbl.invaluement.com", priority: "medium" },
4875
+ { name: "Truncate", zone: "truncate.gbudb.net", priority: "low" },
4876
+ { name: "SpamRATS NoPtr", zone: "noptr.spamrats.com", priority: "low" },
4877
+ { name: "SpamRATS Dyna", zone: "dyna.spamrats.com", priority: "low" },
4878
+ { name: "SpamRATS Auth", zone: "auth.spamrats.com", priority: "low" },
4879
+ { name: "SpamRATS Spam", zone: "spam.spamrats.com", priority: "low" },
4880
+ { name: "BlockList.de", zone: "bl.blocklist.de", priority: "medium" },
4881
+ { name: "DroneBL", zone: "dnsbl.dronebl.org", priority: "low" },
4882
+ { name: "InterServer", zone: "rbl.interserver.net", priority: "low" },
4883
+ // Reputation lists
4884
+ { name: "NiX Spam", zone: "ix.dnsbl.manitu.net", priority: "low" },
4885
+ { name: "Composite BL", zone: "cbl.anti-spam.org.cn", priority: "low" }
4886
+ ];
4887
+ var DEFAULT_TIMEOUT2 = 5e3;
4888
+ var SPF_LOOKUP_LIMIT = 10;
4889
+ var DKIM_BATCH_SIZE = 10;
4890
+ var BLACKLIST_BATCH_SIZE = 20;
4891
+ var SPAMHAUS_RETURN_CODES = {
4892
+ "127.0.0.2": "SBL - Spamhaus Block List (known spam source)",
4893
+ "127.0.0.3": "SBL - Spamhaus Block List CSS (snowshoe spam)",
4894
+ "127.0.0.4": "XBL - CBL (hijacked machine, compromised)",
4895
+ "127.0.0.5": "XBL - Njabl (open relay)",
4896
+ "127.0.0.6": "XBL - Njabl (spam source)",
4897
+ "127.0.0.9": "SBL - Spamhaus DROP (hijacked space)",
4898
+ "127.0.0.10": "PBL - Spamhaus Policy Block List (dynamic IP)",
4899
+ "127.0.0.11": "PBL - Spamhaus Policy Block List (ISP maintained)"
4900
+ };
4901
+ function createNodeDnsProvider(options = {}) {
4902
+ const timeout = options.timeout ?? DEFAULT_TIMEOUT2;
4903
+ const resolver = new Resolver();
4904
+ if (options.servers && options.servers.length > 0) {
4905
+ resolver.setServers(options.servers);
4906
+ }
4907
+ async function withTimeout(promise, operation) {
4908
+ const timeoutPromise = new Promise((_, reject) => {
4909
+ setTimeout(() => {
4910
+ reject(new Error(`DNS ${operation} timed out after ${timeout}ms`));
4911
+ }, timeout);
4626
4912
  });
4627
- if (clack3.isCancel(input)) {
4628
- clack3.cancel("Operation cancelled");
4629
- process.exit(0);
4630
- }
4631
- domain = input;
4632
- }
4633
- if (!options.json) {
4634
- clack3.intro(pc4.bold("Wraps Email Check"));
4635
- console.log();
4636
- }
4637
- const spinner4 = options.json ? null : clack3.spinner();
4638
- spinner4?.start(`Checking ${pc4.cyan(domain)}...`);
4639
- let dkimSelectors;
4640
- if (!options.dkimSelector) {
4641
- const sesTokens = await tryGetSesDkimTokens(domain);
4642
- if (sesTokens.length > 0) {
4643
- dkimSelectors = sesTokens;
4644
- }
4913
+ return Promise.race([promise, timeoutPromise]);
4645
4914
  }
4646
- try {
4647
- const result = await runEmailCheck(domain, {
4648
- quick: options.quick,
4649
- verbose: options.verbose,
4650
- dkimSelector: options.dkimSelector,
4651
- dkimSelectors,
4652
- // Use SES tokens if found
4653
- skipBlacklists: options.skipBlacklists,
4654
- skipTls: options.skipTls,
4655
- timeout: options.timeout
4656
- });
4657
- spinner4?.stop(`Check complete in ${result.duration}ms`);
4658
- if (options.json) {
4659
- console.log(JSON.stringify(result, null, 2));
4660
- } else {
4661
- displayResults(result, options);
4662
- }
4663
- const duration = Date.now() - startTime;
4664
- trackCommand("email:check", {
4665
- success: true,
4666
- duration_ms: duration,
4667
- grade: result.score.grade
4668
- });
4669
- process.exit(getExitCode(result.score.grade));
4670
- } catch (error) {
4671
- spinner4?.stop("Check failed");
4672
- if (options.json) {
4673
- console.log(JSON.stringify({ error: error.message }));
4674
- } else {
4675
- clack3.log.error(error.message);
4915
+ async function resolveTxt(domain) {
4916
+ try {
4917
+ const records = await withTimeout(
4918
+ dns.resolveTxt(domain),
4919
+ `TXT lookup for ${domain}`
4920
+ );
4921
+ return records;
4922
+ } catch (error) {
4923
+ if (error.code === "ENODATA" || error.code === "ENOTFOUND") {
4924
+ return [];
4925
+ }
4926
+ throw error;
4676
4927
  }
4677
- const duration = Date.now() - startTime;
4678
- trackCommand("email:check", {
4679
- success: false,
4680
- duration_ms: duration,
4681
- error: error.message
4682
- });
4683
- process.exit(4);
4684
4928
  }
4685
- }
4686
- function displayResults(result, options) {
4687
- const { score, spf, dkim, dmarc, mx, blacklist } = result;
4688
- console.log();
4689
- displayScoreBox(result.domain, score.finalScore, score.grade, options.quick);
4690
- console.log();
4691
- console.log(pc4.bold("AUTHENTICATION"));
4692
- console.log();
4693
- displaySpfResult(spf, options.verbose);
4694
- displayDkimResult(dkim, options.verbose);
4695
- displayDmarcResult(dmarc);
4696
- console.log();
4697
- console.log(pc4.bold("INFRASTRUCTURE"));
4698
- console.log();
4699
- displayMxResult(mx);
4700
- displayMxTlsResult(result);
4701
- displayReverseDnsResult(result);
4702
- displayIpv6Result(result);
4703
- console.log();
4704
- console.log(pc4.bold("REPUTATION"));
4705
- console.log();
4706
- displayBlacklistResult(blacklist, options.quick);
4707
- displayDomainAgeResult(result);
4708
- if (result.dnssec.enabled || result.caa.configured || result.mtaSts.configured || result.tlsRpt.configured) {
4709
- console.log();
4710
- console.log(pc4.bold("SECURITY"));
4711
- console.log();
4712
- if (result.dnssec.enabled) {
4713
- const status2 = result.dnssec.valid ? pc4.green("\u2713") : pc4.red("\u2717");
4714
- console.log(
4715
- ` ${status2} ${pc4.dim("DNSSEC")} ${result.dnssec.valid ? "Enabled and validated" : "Broken"}`
4716
- );
4717
- } else {
4718
- console.log(
4719
- ` ${pc4.dim("\u25CB")} ${pc4.dim("DNSSEC")} Not configured`
4929
+ async function resolveMx(domain) {
4930
+ try {
4931
+ const records = await withTimeout(
4932
+ dns.resolveMx(domain),
4933
+ `MX lookup for ${domain}`
4720
4934
  );
4935
+ return records.sort((a, b) => a.priority - b.priority);
4936
+ } catch (error) {
4937
+ if (error.code === "ENODATA" || error.code === "ENOTFOUND") {
4938
+ return [];
4939
+ }
4940
+ throw error;
4721
4941
  }
4722
- if (result.caa.configured) {
4723
- console.log(
4724
- ` ${pc4.green("\u2713")} ${pc4.dim("CAA")} Configured (${result.caa.allowedIssuers.join(", ")})`
4725
- );
4726
- } else {
4727
- console.log(
4728
- ` ${pc4.dim("\u25CB")} ${pc4.dim("CAA")} Not configured`
4942
+ }
4943
+ async function resolveA(domain) {
4944
+ try {
4945
+ const records = await withTimeout(
4946
+ dns.resolve4(domain),
4947
+ `A lookup for ${domain}`
4729
4948
  );
4949
+ return records;
4950
+ } catch (error) {
4951
+ if (error.code === "ENODATA" || error.code === "ENOTFOUND") {
4952
+ return [];
4953
+ }
4954
+ throw error;
4730
4955
  }
4731
- if (result.mtaSts.configured) {
4732
- const mode = result.mtaSts.policy?.mode || "unknown";
4733
- console.log(
4734
- ` ${pc4.green("\u2713")} ${pc4.dim("MTA-STS")} ${mode === "enforce" ? "Enforcing mode" : `${mode} mode`}`
4735
- );
4736
- } else {
4737
- console.log(
4738
- ` ${pc4.dim("\u25CB")} ${pc4.dim("MTA-STS")} Not configured`
4956
+ }
4957
+ async function resolveAaaa(domain) {
4958
+ try {
4959
+ const records = await withTimeout(
4960
+ dns.resolve6(domain),
4961
+ `AAAA lookup for ${domain}`
4739
4962
  );
4963
+ return records;
4964
+ } catch (error) {
4965
+ if (error.code === "ENODATA" || error.code === "ENOTFOUND") {
4966
+ return [];
4967
+ }
4968
+ throw error;
4740
4969
  }
4741
- if (result.tlsRpt.configured) {
4742
- console.log(
4743
- ` ${pc4.green("\u2713")} ${pc4.dim("TLS-RPT")} Configured`
4744
- );
4745
- } else {
4746
- console.log(
4747
- ` ${pc4.dim("\u25CB")} ${pc4.dim("TLS-RPT")} Not configured`
4970
+ }
4971
+ async function resolvePtr(ip) {
4972
+ try {
4973
+ const records = await withTimeout(
4974
+ dns.reverse(ip),
4975
+ `PTR lookup for ${ip}`
4748
4976
  );
4977
+ return records;
4978
+ } catch (error) {
4979
+ if (error.code === "ENODATA" || error.code === "ENOTFOUND" || error.code === "ENOENT") {
4980
+ return [];
4981
+ }
4982
+ throw error;
4749
4983
  }
4750
4984
  }
4751
- const criticalDeductions = score.deductions.filter((d) => d.points >= 20);
4752
- const warningDeductions = score.deductions.filter(
4753
- (d) => d.points >= 5 && d.points < 20
4754
- );
4755
- if (criticalDeductions.length > 0 || warningDeductions.length > 0) {
4756
- console.log();
4757
- console.log(pc4.dim("\u2500".repeat(78)));
4758
- console.log();
4759
- console.log(
4760
- pc4.bold(
4761
- `ISSUES (${criticalDeductions.length} critical, ${warningDeductions.length} warnings)`
4762
- )
4763
- );
4764
- console.log();
4765
- if (criticalDeductions.length > 0) {
4766
- console.log(pc4.red("\u274C CRITICAL"));
4767
- console.log();
4768
- for (let i = 0; i < criticalDeductions.length; i++) {
4769
- const d = criticalDeductions[i];
4770
- console.log(
4771
- ` ${i + 1}. ${d.reason} (${pc4.red(`-${d.points} points`)})`
4772
- );
4773
- console.log();
4774
- console.log(` ${getFixSuggestion(d.check, d.reason)}`);
4775
- console.log();
4985
+ async function resolveCaa(domain) {
4986
+ try {
4987
+ const records = await withTimeout(
4988
+ dns.resolveCaa(domain),
4989
+ `CAA lookup for ${domain}`
4990
+ );
4991
+ return records.map((r) => {
4992
+ let tag = "issue";
4993
+ let value = "";
4994
+ if (r.issue !== void 0) {
4995
+ tag = "issue";
4996
+ value = r.issue;
4997
+ } else if (r.issuewild !== void 0) {
4998
+ tag = "issuewild";
4999
+ value = r.issuewild;
5000
+ } else if (r.iodef !== void 0) {
5001
+ tag = "iodef";
5002
+ value = r.iodef;
5003
+ }
5004
+ return {
5005
+ flags: r.critical,
5006
+ tag,
5007
+ value
5008
+ };
5009
+ });
5010
+ } catch (error) {
5011
+ if (error.code === "ENODATA" || error.code === "ENOTFOUND") {
5012
+ return [];
4776
5013
  }
5014
+ throw error;
4777
5015
  }
4778
- if (warningDeductions.length > 0) {
4779
- console.log(pc4.yellow("\u26A0\uFE0F WARNINGS"));
4780
- console.log();
4781
- for (let i = 0; i < warningDeductions.length; i++) {
4782
- const d = warningDeductions[i];
4783
- console.log(` ${criticalDeductions.length + i + 1}. ${d.reason}`);
4784
- console.log();
5016
+ }
5017
+ async function resolveCname(domain) {
5018
+ try {
5019
+ const records = await withTimeout(
5020
+ dns.resolveCname(domain),
5021
+ `CNAME lookup for ${domain}`
5022
+ );
5023
+ return records;
5024
+ } catch (error) {
5025
+ if (error.code === "ENODATA" || error.code === "ENOTFOUND") {
5026
+ return [];
4785
5027
  }
5028
+ throw error;
4786
5029
  }
4787
5030
  }
4788
- console.log();
4789
- console.log(pc4.dim("\u2500".repeat(78)));
4790
- console.log();
4791
- if (score.grade === "A" || score.grade === "B") {
4792
- console.log(
4793
- pc4.green(
4794
- "\u2705 " + (score.grade === "A" ? "Excellent! Your email configuration follows all best practices." : "Good! Your email configuration is solid with minor improvements possible.")
4795
- )
4796
- );
4797
- } else {
4798
- console.log(
4799
- pc4.yellow(
4800
- "Need help fixing these? Deploy Wraps to manage your email infrastructure:"
4801
- )
4802
- );
4803
- console.log();
4804
- console.log(` ${pc4.cyan("npx @wraps.dev/cli email init")}`);
4805
- }
4806
- console.log();
4807
- console.log(`Share: ${pc4.cyan(`https://wraps.dev/check/${result.domain}`)}`);
4808
- console.log();
5031
+ return {
5032
+ resolveTxt,
5033
+ resolveMx,
5034
+ resolveA,
5035
+ resolveAaaa,
5036
+ resolvePtr,
5037
+ resolveCaa,
5038
+ resolveCname
5039
+ };
4809
5040
  }
4810
- function displayScoreBox(domain, score, grade, quick) {
4811
- const width = 78;
4812
- const bar = "\u2588".repeat(Math.round(score / 100 * 60));
4813
- const emptyBar = "\u2591".repeat(60 - bar.length);
4814
- const gradeColor = grade === "A" || grade === "B" ? pc4.green : grade === "C" ? pc4.yellow : grade === "D" ? pc4.magenta : pc4.red;
4815
- console.log(pc4.dim("\u256D" + "\u2500".repeat(width - 2) + "\u256E"));
4816
- console.log(pc4.dim("\u2502") + " ".repeat(width - 2) + pc4.dim("\u2502"));
4817
- console.log(
4818
- pc4.dim("\u2502") + " " + pc4.bold(`wraps email check${quick ? " --quick" : ""}`) + " ".repeat(width - 25 - (quick ? 8 : 0)) + pc4.dim("\u2502")
5041
+ var nodeDns = createNodeDnsProvider();
5042
+ var currentProvider = nodeDns;
5043
+ async function resolveTxtRecords(domain) {
5044
+ const records = await currentProvider.resolveTxt(domain);
5045
+ return records.map((parts) => parts.join(""));
5046
+ }
5047
+ async function resolveMxRecords(domain) {
5048
+ return currentProvider.resolveMx(domain);
5049
+ }
5050
+ async function resolveARecords(domain) {
5051
+ return currentProvider.resolveA(domain);
5052
+ }
5053
+ async function resolveAaaaRecords(domain) {
5054
+ return currentProvider.resolveAaaa(domain);
5055
+ }
5056
+ async function resolvePtrRecords(ip) {
5057
+ return currentProvider.resolvePtr(ip);
5058
+ }
5059
+ async function findSpfRecord(domain) {
5060
+ const txtRecords = await resolveTxtRecords(domain);
5061
+ return txtRecords.filter((r) => r.startsWith("v=spf1 ") || r === "v=spf1");
5062
+ }
5063
+ async function findDkimRecord(domain, selector) {
5064
+ const dkimDomain = `${selector}._domainkey.${domain}`;
5065
+ const txtRecords = await resolveTxtRecords(dkimDomain);
5066
+ const dkimRecord = txtRecords.find(
5067
+ (r) => r.startsWith("v=DKIM1") || r.includes("p=")
5068
+ );
5069
+ return dkimRecord || null;
5070
+ }
5071
+ async function findDmarcRecord(domain) {
5072
+ const dmarcDomain = `_dmarc.${domain}`;
5073
+ const txtRecords = await resolveTxtRecords(dmarcDomain);
5074
+ const dmarcRecord = txtRecords.find((r) => r.startsWith("v=DMARC1"));
5075
+ return dmarcRecord || null;
5076
+ }
5077
+ async function batchDnsQuery(items, queryFn, concurrency = 10) {
5078
+ const results = [];
5079
+ for (let i = 0; i < items.length; i += concurrency) {
5080
+ const batch = items.slice(i, i + concurrency);
5081
+ const batchResults = await Promise.all(batch.map(queryFn));
5082
+ results.push(...batchResults);
5083
+ }
5084
+ return results;
5085
+ }
5086
+ function toAsciiDomain(domain) {
5087
+ try {
5088
+ const url = new URL(`http://${domain.toLowerCase()}`);
5089
+ return url.hostname;
5090
+ } catch {
5091
+ return domain.toLowerCase();
5092
+ }
5093
+ }
5094
+ function isValidDomain(domain) {
5095
+ if (!domain || domain.length > 253) return false;
5096
+ const asciiDomain = toAsciiDomain(domain);
5097
+ const labels = asciiDomain.split(".");
5098
+ if (labels.length < 2) return false;
5099
+ for (const label of labels) {
5100
+ if (!label || label.length > 63) return false;
5101
+ if (!/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/i.test(label)) return false;
5102
+ }
5103
+ return true;
5104
+ }
5105
+ function isIpAddress(str) {
5106
+ return isIpv4(str) || isIpv6(str);
5107
+ }
5108
+ function isIpv4(str) {
5109
+ const parts = str.split(".");
5110
+ if (parts.length !== 4) return false;
5111
+ return parts.every((part) => {
5112
+ const num = Number.parseInt(part, 10);
5113
+ return !Number.isNaN(num) && num >= 0 && num <= 255 && part === String(num);
5114
+ });
5115
+ }
5116
+ function isIpv6(str) {
5117
+ const ipv6Pattern = /^(?:(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,7}:|(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,5}(?::[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){1,4}(?::[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){1,3}(?::[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){1,2}(?::[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:(?:(?::[0-9a-fA-F]{1,4}){1,6})|:(?:(?::[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(?::[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]+|::(?:ffff(?::0{1,4})?:)?(?:(?:25[0-5]|(?:2[0-4]|1?[0-9])?[0-9])\.){3}(?:25[0-5]|(?:2[0-4]|1?[0-9])?[0-9])|(?:[0-9a-fA-F]{1,4}:){1,4}:(?:(?:25[0-5]|(?:2[0-4]|1?[0-9])?[0-9])\.){3}(?:25[0-5]|(?:2[0-4]|1?[0-9])?[0-9]))$/;
5118
+ return ipv6Pattern.test(str);
5119
+ }
5120
+ function reverseIp(ip) {
5121
+ if (isIpv4(ip)) {
5122
+ return ip.split(".").reverse().join(".");
5123
+ }
5124
+ throw new Error("IPv6 reverse not yet implemented");
5125
+ }
5126
+ function isLocalhost(ip) {
5127
+ if (isIpv4(ip)) {
5128
+ return ip === "127.0.0.1" || ip.startsWith("127.");
5129
+ }
5130
+ return ip === "::1" || ip === "0:0:0:0:0:0:0:1";
5131
+ }
5132
+ var TEST_RETURN_CODES = /* @__PURE__ */ new Set([
5133
+ "127.0.0.1",
5134
+ // Generic test response / query confirmation
5135
+ "127.0.1.255",
5136
+ // Spamhaus DBL test/error response
5137
+ "127.255.255.252",
5138
+ // DNS resolver test
5139
+ "127.255.255.254",
5140
+ // URIBL test response
5141
+ "127.255.255.255"
5142
+ // Reserved/test
5143
+ ]);
5144
+ function isTestResponse(returnCode) {
5145
+ return TEST_RETURN_CODES.has(returnCode);
5146
+ }
5147
+ async function checkBlacklist(options) {
5148
+ const { quick = false, skip = false, domain, ips = [] } = options;
5149
+ const result = {
5150
+ domainChecks: {
5151
+ checked: 0,
5152
+ listed: [],
5153
+ clean: [],
5154
+ errors: [],
5155
+ timeouts: []
5156
+ },
5157
+ ipChecks: {
5158
+ checked: 0,
5159
+ listed: [],
5160
+ clean: [],
5161
+ errors: [],
5162
+ timeouts: []
5163
+ },
5164
+ overallClean: true,
5165
+ quickMode: quick
5166
+ };
5167
+ if (skip) {
5168
+ return result;
5169
+ }
5170
+ const domainBlacklists = quick ? QUICK_BLACKLISTS.slice(0, 5) : DOMAIN_BLACKLISTS;
5171
+ const ipBlacklists = quick ? QUICK_BLACKLISTS : IP_BLACKLISTS;
5172
+ await checkDomainBlacklists(domain, domainBlacklists, result);
5173
+ if (ips.length > 0) {
5174
+ await checkIpBlacklists(ips, ipBlacklists, result);
5175
+ }
5176
+ result.overallClean = result.domainChecks.listed.length === 0 && result.ipChecks.listed.length === 0;
5177
+ return result;
5178
+ }
5179
+ async function checkDomainBlacklists(domain, blacklists, result) {
5180
+ const checks = blacklists.map((bl) => ({
5181
+ blacklist: bl,
5182
+ query: `${domain}.${bl.zone}`
5183
+ }));
5184
+ await batchDnsQuery(
5185
+ checks,
5186
+ async (check2) => {
5187
+ result.domainChecks.checked++;
5188
+ try {
5189
+ const records = await resolveARecords(check2.query);
5190
+ const firstRecord = records[0];
5191
+ if (records.length > 0 && firstRecord && !isTestResponse(firstRecord)) {
5192
+ const listing = createListing(
5193
+ check2.blacklist,
5194
+ "domain",
5195
+ domain,
5196
+ firstRecord
5197
+ );
5198
+ result.domainChecks.listed.push(listing);
5199
+ result.overallClean = false;
5200
+ } else {
5201
+ result.domainChecks.clean.push(check2.blacklist.name);
5202
+ }
5203
+ } catch (error) {
5204
+ if (error.message?.includes("timed out")) {
5205
+ result.domainChecks.timeouts.push(check2.blacklist.name);
5206
+ } else if (error.code !== "ENOTFOUND" && error.code !== "ENODATA") {
5207
+ result.domainChecks.errors.push(
5208
+ `${check2.blacklist.name}: ${error.message}`
5209
+ );
5210
+ } else {
5211
+ result.domainChecks.clean.push(check2.blacklist.name);
5212
+ }
5213
+ }
5214
+ },
5215
+ BLACKLIST_BATCH_SIZE
5216
+ );
5217
+ }
5218
+ async function checkIpBlacklists(ips, blacklists, result) {
5219
+ const ipv4Ips = ips.filter(isIpv4);
5220
+ for (const ip of ipv4Ips) {
5221
+ const reversedIp = reverseIp(ip);
5222
+ const checks = blacklists.map((bl) => ({
5223
+ blacklist: bl,
5224
+ query: `${reversedIp}.${bl.zone}`,
5225
+ ip
5226
+ }));
5227
+ await batchDnsQuery(
5228
+ checks,
5229
+ async (check2) => {
5230
+ result.ipChecks.checked++;
5231
+ try {
5232
+ const records = await resolveARecords(check2.query);
5233
+ const firstRecord = records[0];
5234
+ if (records.length > 0 && firstRecord && !isTestResponse(firstRecord)) {
5235
+ const listing = createListing(
5236
+ check2.blacklist,
5237
+ "ip",
5238
+ check2.ip,
5239
+ firstRecord
5240
+ );
5241
+ result.ipChecks.listed.push(listing);
5242
+ result.overallClean = false;
5243
+ } else {
5244
+ result.ipChecks.clean.push(check2.blacklist.name);
5245
+ }
5246
+ } catch (error) {
5247
+ if (error.message?.includes("timed out")) {
5248
+ result.ipChecks.timeouts.push(check2.blacklist.name);
5249
+ } else if (error.code !== "ENOTFOUND" && error.code !== "ENODATA") {
5250
+ result.ipChecks.errors.push(
5251
+ `${check2.blacklist.name}: ${error.message}`
5252
+ );
5253
+ } else {
5254
+ result.ipChecks.clean.push(check2.blacklist.name);
5255
+ }
5256
+ }
5257
+ },
5258
+ BLACKLIST_BATCH_SIZE
5259
+ );
5260
+ }
5261
+ }
5262
+ function createListing(blacklist, type, target, returnCode) {
5263
+ return {
5264
+ blacklist: blacklist.name,
5265
+ zone: blacklist.zone,
5266
+ priority: blacklist.priority || "medium",
5267
+ type,
5268
+ target,
5269
+ returnCode,
5270
+ meaning: getMeaning(blacklist.zone, returnCode),
5271
+ delistUrl: getDelistUrl(blacklist.zone)
5272
+ };
5273
+ }
5274
+ function getMeaning(zone, returnCode) {
5275
+ if (zone.includes("spamhaus")) {
5276
+ return SPAMHAUS_RETURN_CODES[returnCode] || `Listed (${returnCode})`;
5277
+ }
5278
+ if (returnCode === "127.0.0.2") {
5279
+ return "Listed as spam source";
5280
+ }
5281
+ if (returnCode === "127.0.0.3") {
5282
+ return "Listed as spam source (elevated threat)";
5283
+ }
5284
+ return `Listed (${returnCode})`;
5285
+ }
5286
+ function getDelistUrl(zone) {
5287
+ const delistUrls = {
5288
+ "zen.spamhaus.org": "https://check.spamhaus.org/",
5289
+ "dbl.spamhaus.org": "https://check.spamhaus.org/",
5290
+ "sbl.spamhaus.org": "https://check.spamhaus.org/",
5291
+ "xbl.spamhaus.org": "https://check.spamhaus.org/",
5292
+ "pbl.spamhaus.org": "https://check.spamhaus.org/",
5293
+ "b.barracudacentral.org": "https://www.barracudacentral.org/lookups",
5294
+ "bl.spamcop.net": "https://www.spamcop.net/bl.shtml",
5295
+ "cbl.abuseat.org": "https://cbl.abuseat.org/lookup.cgi",
5296
+ "dnsbl.sorbs.net": "http://www.sorbs.net/lookup.shtml"
5297
+ };
5298
+ return delistUrls[zone] || null;
5299
+ }
5300
+ async function checkDkim(domain, options = {}) {
5301
+ const {
5302
+ quick = false,
5303
+ selector,
5304
+ selectors: customSelectors,
5305
+ verbose = false,
5306
+ detectedProvider
5307
+ } = options;
5308
+ let selectorsToCheck;
5309
+ if (selector) {
5310
+ selectorsToCheck = [selector];
5311
+ } else if (customSelectors && customSelectors.length > 0) {
5312
+ selectorsToCheck = customSelectors;
5313
+ } else if (quick) {
5314
+ selectorsToCheck = [...QUICK_DKIM_SELECTORS];
5315
+ } else {
5316
+ selectorsToCheck = [...DEFAULT_DKIM_SELECTORS];
5317
+ }
5318
+ const result = {
5319
+ found: false,
5320
+ selectors: [],
5321
+ selectorsChecked: 0,
5322
+ earlyExit: false,
5323
+ warnings: []
5324
+ };
5325
+ let foundValid = false;
5326
+ for (let i = 0; i < selectorsToCheck.length; i += DKIM_BATCH_SIZE) {
5327
+ if (foundValid && !verbose) {
5328
+ result.earlyExit = true;
5329
+ break;
5330
+ }
5331
+ const batch = selectorsToCheck.slice(i, i + DKIM_BATCH_SIZE);
5332
+ const batchResults = await batchDnsQuery(
5333
+ batch,
5334
+ async (sel) => checkDkimSelector(domain, sel),
5335
+ DKIM_BATCH_SIZE
5336
+ );
5337
+ for (const selectorResult of batchResults) {
5338
+ result.selectorsChecked++;
5339
+ if (selectorResult.exists) {
5340
+ result.selectors.push(selectorResult);
5341
+ result.found = true;
5342
+ if (selectorResult.valid && !selectorResult.revoked) {
5343
+ foundValid = true;
5344
+ }
5345
+ }
5346
+ }
5347
+ }
5348
+ result.selectors.sort((a, b) => {
5349
+ const aScore = (a.valid ? 2 : 0) + (a.revoked ? 0 : 1);
5350
+ const bScore = (b.valid ? 2 : 0) + (b.revoked ? 0 : 1);
5351
+ return bScore - aScore;
5352
+ });
5353
+ if (!result.found && detectedProvider) {
5354
+ const randomSelectorProviders = {
5355
+ ses: "AWS SES uses random DKIM selectors. Check your SES console or use --dkimSelector with your actual selector.",
5356
+ amazonses: "AWS SES uses random DKIM selectors. Check your SES console or use --dkimSelector with your actual selector.",
5357
+ sendgrid: "SendGrid may use custom selectors. Check your SendGrid dashboard or use --dkimSelector.",
5358
+ mailgun: "Mailgun may use custom selectors. Check your Mailgun dashboard or use --dkimSelector."
5359
+ };
5360
+ const providerLower = detectedProvider.toLowerCase();
5361
+ for (const [key, message] of Object.entries(randomSelectorProviders)) {
5362
+ if (providerLower.includes(key)) {
5363
+ result.warnings.push(message);
5364
+ break;
5365
+ }
5366
+ }
5367
+ }
5368
+ return result;
5369
+ }
5370
+ async function checkDkimSelector(domain, selector) {
5371
+ const result = {
5372
+ selector,
5373
+ exists: false,
5374
+ record: null,
5375
+ valid: false,
5376
+ keyType: null,
5377
+ keyBits: null,
5378
+ publicKey: null,
5379
+ testMode: false,
5380
+ revoked: false,
5381
+ expired: false,
5382
+ hashAlgorithms: [],
5383
+ serviceTypes: [],
5384
+ flags: [],
5385
+ errors: [],
5386
+ warnings: []
5387
+ };
5388
+ try {
5389
+ const record = await findDkimRecord(domain, selector);
5390
+ if (!record) {
5391
+ return result;
5392
+ }
5393
+ result.exists = true;
5394
+ result.record = record;
5395
+ parseDkimRecord(record, result);
5396
+ } catch (error) {
5397
+ result.errors.push(error.message);
5398
+ }
5399
+ return result;
5400
+ }
5401
+ function parseDkimRecord(record, result) {
5402
+ const tags = parseDkimTags(record);
5403
+ const version = tags.get("v");
5404
+ if (version && version !== "DKIM1") {
5405
+ result.errors.push(`Invalid DKIM version: ${version}`);
5406
+ }
5407
+ const keyType = tags.get("k") || "rsa";
5408
+ if (keyType === "rsa") {
5409
+ result.keyType = "rsa";
5410
+ } else if (keyType === "ed25519") {
5411
+ result.keyType = "ed25519";
5412
+ } else {
5413
+ result.keyType = "unknown";
5414
+ result.warnings.push(`Unknown key type: ${keyType}`);
5415
+ }
5416
+ const publicKey = tags.get("p");
5417
+ if (publicKey === void 0) {
5418
+ result.errors.push("Missing public key (p=)");
5419
+ return;
5420
+ }
5421
+ if (publicKey === "") {
5422
+ result.revoked = true;
5423
+ result.warnings.push("DKIM key is revoked (empty p= tag)");
5424
+ return;
5425
+ }
5426
+ result.publicKey = publicKey;
5427
+ if (result.keyType === "rsa") {
5428
+ try {
5429
+ const keyData = Buffer.from(publicKey, "base64");
5430
+ const estimatedBits = keyData.length * 8;
5431
+ result.keyBits = Math.round((estimatedBits - 256) * 0.95);
5432
+ if (result.keyBits < 512) result.keyBits = 512;
5433
+ if (result.keyBits > 4096) result.keyBits = 4096;
5434
+ if (result.keyBits <= 768) result.keyBits = 512;
5435
+ else if (result.keyBits <= 1200) result.keyBits = 1024;
5436
+ else if (result.keyBits <= 2500) result.keyBits = 2048;
5437
+ else if (result.keyBits <= 3500) result.keyBits = 3072;
5438
+ else result.keyBits = 4096;
5439
+ if (result.keyBits < 1024) {
5440
+ result.errors.push(`RSA key too weak: ${result.keyBits} bits`);
5441
+ } else if (result.keyBits < 2048) {
5442
+ result.warnings.push(
5443
+ `RSA key should be 2048+ bits (currently ${result.keyBits})`
5444
+ );
5445
+ }
5446
+ } catch {
5447
+ result.warnings.push("Could not determine RSA key size");
5448
+ }
5449
+ }
5450
+ const hashAlg = tags.get("h");
5451
+ if (hashAlg) {
5452
+ result.hashAlgorithms = hashAlg.split(":").map((h) => h.trim());
5453
+ if (result.hashAlgorithms.includes("sha1") && !result.hashAlgorithms.includes("sha256")) {
5454
+ result.warnings.push("Only sha1 allowed - sha256 recommended");
5455
+ }
5456
+ } else {
5457
+ result.hashAlgorithms = ["sha1", "sha256"];
5458
+ }
5459
+ const serviceType = tags.get("s");
5460
+ if (serviceType) {
5461
+ result.serviceTypes = serviceType.split(":").map((s) => s.trim());
5462
+ if (!(result.serviceTypes.includes("*") || result.serviceTypes.includes("email"))) {
5463
+ result.warnings.push(
5464
+ `Service type restricts to: ${result.serviceTypes.join(", ")}`
5465
+ );
5466
+ }
5467
+ } else {
5468
+ result.serviceTypes = ["*"];
5469
+ }
5470
+ const flagsTag = tags.get("t");
5471
+ if (flagsTag) {
5472
+ result.flags = flagsTag.split(":").map((f) => f.trim());
5473
+ if (result.flags.includes("y")) {
5474
+ result.testMode = true;
5475
+ result.warnings.push("DKIM in testing mode (t=y)");
5476
+ }
5477
+ }
5478
+ const notes = tags.get("n");
5479
+ if (notes) {
5480
+ result.warnings.push(`DKIM note: ${notes}`);
5481
+ }
5482
+ result.valid = result.errors.length === 0 && !result.revoked;
5483
+ }
5484
+ function parseDkimTags(record) {
5485
+ const tags = /* @__PURE__ */ new Map();
5486
+ const parts = record.split(";").map((p) => p.trim()).filter(Boolean);
5487
+ for (const part of parts) {
5488
+ const eqIndex = part.indexOf("=");
5489
+ if (eqIndex === -1) continue;
5490
+ const key = part.slice(0, eqIndex).trim().toLowerCase();
5491
+ const value = part.slice(eqIndex + 1).trim();
5492
+ tags.set(key, value);
5493
+ }
5494
+ return tags;
5495
+ }
5496
+ async function checkDmarc(domain) {
5497
+ const result = {
5498
+ exists: false,
5499
+ record: null,
5500
+ valid: false,
5501
+ policy: null,
5502
+ subdomainPolicy: null,
5503
+ percentage: 100,
5504
+ reportingEnabled: false,
5505
+ ruaAddresses: [],
5506
+ rufAddresses: [],
5507
+ alignmentSpf: "relaxed",
5508
+ alignmentDkim: "relaxed",
5509
+ failureOptions: "0",
5510
+ reportInterval: 86400,
5511
+ reportFormat: "afrf",
5512
+ errors: [],
5513
+ warnings: []
5514
+ };
5515
+ try {
5516
+ const record = await findDmarcRecord(domain);
5517
+ if (!record) {
5518
+ return result;
5519
+ }
5520
+ result.exists = true;
5521
+ result.record = record;
5522
+ parseDmarcRecord(record, result);
5523
+ } catch (error) {
5524
+ result.errors.push(error.message);
5525
+ }
5526
+ return result;
5527
+ }
5528
+ function parseDmarcRecord(record, result) {
5529
+ const tags = parseDmarcTags(record);
5530
+ const version = tags.get("v");
5531
+ if (version !== "DMARC1") {
5532
+ result.errors.push(`Invalid DMARC version: ${version || "missing"}`);
5533
+ return;
5534
+ }
5535
+ const policy = tags.get("p");
5536
+ if (!policy) {
5537
+ result.errors.push("Missing required policy (p=)");
5538
+ return;
5539
+ }
5540
+ if (!["none", "quarantine", "reject"].includes(policy)) {
5541
+ result.errors.push(`Invalid policy: ${policy}`);
5542
+ return;
5543
+ }
5544
+ result.policy = policy;
5545
+ const subdomainPolicy = tags.get("sp");
5546
+ if (subdomainPolicy) {
5547
+ if (["none", "quarantine", "reject"].includes(subdomainPolicy)) {
5548
+ result.subdomainPolicy = subdomainPolicy;
5549
+ } else {
5550
+ result.warnings.push(`Invalid subdomain policy: ${subdomainPolicy}`);
5551
+ }
5552
+ } else {
5553
+ result.subdomainPolicy = result.policy;
5554
+ }
5555
+ const pct = tags.get("pct");
5556
+ if (pct) {
5557
+ const pctNum = Number.parseInt(pct, 10);
5558
+ if (Number.isNaN(pctNum) || pctNum < 0 || pctNum > 100) {
5559
+ result.warnings.push(`Invalid percentage: ${pct}`);
5560
+ } else {
5561
+ result.percentage = pctNum;
5562
+ if (pctNum < 100) {
5563
+ result.warnings.push(`Only ${pctNum}% of messages subject to policy`);
5564
+ }
5565
+ }
5566
+ }
5567
+ const rua = tags.get("rua");
5568
+ if (rua) {
5569
+ result.ruaAddresses = parseReportAddresses(rua);
5570
+ result.reportingEnabled = result.ruaAddresses.length > 0;
5571
+ }
5572
+ const ruf = tags.get("ruf");
5573
+ if (ruf) {
5574
+ result.rufAddresses = parseReportAddresses(ruf);
5575
+ }
5576
+ const aspf = tags.get("aspf");
5577
+ if (aspf) {
5578
+ if (aspf === "s") {
5579
+ result.alignmentSpf = "strict";
5580
+ } else if (aspf === "r") {
5581
+ result.alignmentSpf = "relaxed";
5582
+ } else {
5583
+ result.warnings.push(`Invalid SPF alignment mode: ${aspf}`);
5584
+ }
5585
+ }
5586
+ const adkim = tags.get("adkim");
5587
+ if (adkim) {
5588
+ if (adkim === "s") {
5589
+ result.alignmentDkim = "strict";
5590
+ } else if (adkim === "r") {
5591
+ result.alignmentDkim = "relaxed";
5592
+ } else {
5593
+ result.warnings.push(`Invalid DKIM alignment mode: ${adkim}`);
5594
+ }
5595
+ }
5596
+ const fo = tags.get("fo");
5597
+ if (fo) {
5598
+ const validOptions = ["0", "1", "d", "s"];
5599
+ const options = fo.split(":").map((o) => o.trim());
5600
+ for (const opt of options) {
5601
+ if (!validOptions.includes(opt)) {
5602
+ result.warnings.push(`Unknown failure option: ${opt}`);
5603
+ }
5604
+ }
5605
+ result.failureOptions = fo;
5606
+ }
5607
+ const ri = tags.get("ri");
5608
+ if (ri) {
5609
+ const riNum = Number.parseInt(ri, 10);
5610
+ if (Number.isNaN(riNum) || riNum < 0) {
5611
+ result.warnings.push(`Invalid report interval: ${ri}`);
5612
+ } else {
5613
+ result.reportInterval = riNum;
5614
+ }
5615
+ }
5616
+ const rf = tags.get("rf");
5617
+ if (rf) {
5618
+ result.reportFormat = rf;
5619
+ }
5620
+ if (result.policy === "none") {
5621
+ result.warnings.push(
5622
+ 'DMARC policy is "none" - not enforcing authentication'
5623
+ );
5624
+ }
5625
+ if (result.subdomainPolicy === "none" && result.policy !== "none") {
5626
+ result.warnings.push("Subdomain policy is less strict than domain policy");
5627
+ }
5628
+ if (!result.reportingEnabled) {
5629
+ result.warnings.push("No aggregate reporting configured (rua=)");
5630
+ }
5631
+ for (const addr of [...result.ruaAddresses, ...result.rufAddresses]) {
5632
+ const match = addr.match(/^mailto:([^@]+)@(.+)$/i);
5633
+ if (match) {
5634
+ const domain = result.record?.includes("_dmarc.") ? result.record.split("_dmarc.")[1]?.split(" ")[0] : null;
5635
+ }
5636
+ }
5637
+ result.valid = result.errors.length === 0;
5638
+ }
5639
+ function parseDmarcTags(record) {
5640
+ const tags = /* @__PURE__ */ new Map();
5641
+ const parts = record.split(";").map((p) => p.trim()).filter(Boolean);
5642
+ for (const part of parts) {
5643
+ const eqIndex = part.indexOf("=");
5644
+ if (eqIndex === -1) continue;
5645
+ const key = part.slice(0, eqIndex).trim().toLowerCase();
5646
+ const value = part.slice(eqIndex + 1).trim();
5647
+ tags.set(key, value);
5648
+ }
5649
+ return tags;
5650
+ }
5651
+ function parseReportAddresses(value) {
5652
+ return value.split(",").map((addr) => addr.trim()).filter(Boolean);
5653
+ }
5654
+ async function checkMx(domain) {
5655
+ const result = {
5656
+ exists: false,
5657
+ records: [],
5658
+ hasRedundancy: false,
5659
+ warnings: []
5660
+ };
5661
+ try {
5662
+ const mxRecords = await resolveMxRecords(domain);
5663
+ if (mxRecords.length === 0) {
5664
+ result.warnings.push("No MX records found");
5665
+ return result;
5666
+ }
5667
+ result.exists = true;
5668
+ for (const mx of mxRecords) {
5669
+ const mxResult = await checkMxRecord(mx);
5670
+ result.records.push(mxResult);
5671
+ }
5672
+ const uniquePriorities = new Set(mxRecords.map((r) => r.priority));
5673
+ result.hasRedundancy = mxRecords.length > 1 && uniquePriorities.size > 1;
5674
+ const unresolving = result.records.filter((r) => !r.resolves);
5675
+ if (unresolving.length > 0) {
5676
+ result.warnings.push(`${unresolving.length} MX record(s) do not resolve`);
5677
+ }
5678
+ const ipMx = result.records.filter((r) => r.isIpAddress);
5679
+ if (ipMx.length > 0) {
5680
+ result.warnings.push(
5681
+ "MX records should point to hostnames, not IP addresses"
5682
+ );
5683
+ }
5684
+ const localhostMx = result.records.filter((r) => r.isLocalhost);
5685
+ if (localhostMx.length > 0) {
5686
+ result.warnings.push("MX record points to localhost");
5687
+ }
5688
+ } catch (error) {
5689
+ result.warnings.push(`Error checking MX: ${error.message}`);
5690
+ }
5691
+ return result;
5692
+ }
5693
+ async function checkMxRecord(mx) {
5694
+ const result = {
5695
+ priority: mx.priority,
5696
+ exchange: mx.exchange,
5697
+ resolves: false,
5698
+ ipv4Addresses: [],
5699
+ ipv6Addresses: [],
5700
+ isLocalhost: false,
5701
+ isIpAddress: isIpAddress(mx.exchange),
5702
+ reverseHostnames: []
5703
+ };
5704
+ if (result.isIpAddress) {
5705
+ result.isLocalhost = isLocalhost(mx.exchange);
5706
+ result.resolves = true;
5707
+ if (mx.exchange.includes(":")) {
5708
+ result.ipv6Addresses = [mx.exchange];
5709
+ } else {
5710
+ result.ipv4Addresses = [mx.exchange];
5711
+ }
5712
+ return result;
5713
+ }
5714
+ try {
5715
+ const ipv4 = await resolveARecords(mx.exchange);
5716
+ result.ipv4Addresses = ipv4;
5717
+ const ipv6 = await resolveAaaaRecords(mx.exchange);
5718
+ result.ipv6Addresses = ipv6;
5719
+ result.resolves = ipv4.length > 0 || ipv6.length > 0;
5720
+ for (const ip of [...ipv4, ...ipv6]) {
5721
+ if (isLocalhost(ip)) {
5722
+ result.isLocalhost = true;
5723
+ break;
5724
+ }
5725
+ }
5726
+ const allIps = [...ipv4, ...ipv6];
5727
+ for (const ip of allIps.slice(0, 3)) {
5728
+ try {
5729
+ const ptrs = await resolvePtrRecords(ip);
5730
+ result.reverseHostnames.push(...ptrs);
5731
+ } catch {
5732
+ }
5733
+ }
5734
+ } catch (error) {
5735
+ }
5736
+ return result;
5737
+ }
5738
+ var DEFAULT_TIMEOUT22 = 1e4;
5739
+ var SMTP_PORT = 25;
5740
+ async function checkMxTls(mxRecords, options = {}) {
5741
+ const { skip = false, timeout = DEFAULT_TIMEOUT22, quick = false } = options;
5742
+ const result = {
5743
+ checked: false,
5744
+ skipped: skip,
5745
+ skipReason: skip ? "--skip-tls flag" : null,
5746
+ servers: []
5747
+ };
5748
+ if (skip) {
5749
+ return result;
5750
+ }
5751
+ if (mxRecords.length === 0) {
5752
+ result.skipReason = "No MX records";
5753
+ result.skipped = true;
5754
+ return result;
5755
+ }
5756
+ const sortedMx = [...mxRecords].sort((a, b) => a.priority - b.priority);
5757
+ const mxToCheck = quick ? sortedMx.slice(0, 1) : sortedMx.slice(0, 5);
5758
+ const checkPromises = mxToCheck.map(
5759
+ (mx) => checkServer(mx.exchange, timeout)
5760
+ );
5761
+ try {
5762
+ result.servers = await Promise.all(checkPromises);
5763
+ result.checked = true;
5764
+ } catch (error) {
5765
+ result.skipReason = error.message || "TLS check failed";
5766
+ }
5767
+ return result;
5768
+ }
5769
+ async function checkServer(hostname, timeout) {
5770
+ const result = {
5771
+ server: hostname,
5772
+ port: SMTP_PORT,
5773
+ connected: false,
5774
+ connectionError: null,
5775
+ supportsStarttls: false,
5776
+ tlsVersions: [],
5777
+ preferredTlsVersion: null,
5778
+ cipherSuite: null,
5779
+ certificate: null,
5780
+ errors: []
5781
+ };
5782
+ try {
5783
+ const smtpSession = await connectSmtp(hostname, SMTP_PORT, timeout);
5784
+ result.connected = true;
5785
+ const capabilities = await sendEhlo(smtpSession, timeout);
5786
+ result.supportsStarttls = capabilities.includes("STARTTLS");
5787
+ if (!result.supportsStarttls) {
5788
+ smtpSession.socket.destroy();
5789
+ result.errors.push("Server does not advertise STARTTLS");
5790
+ return result;
5791
+ }
5792
+ const tlsResult = await upgradeToTls(smtpSession, hostname, timeout);
5793
+ if (tlsResult.success && tlsResult.socket) {
5794
+ result.preferredTlsVersion = tlsResult.tlsVersion || null;
5795
+ result.cipherSuite = tlsResult.cipher || null;
5796
+ if (tlsResult.tlsVersion) {
5797
+ result.tlsVersions.push(tlsResult.tlsVersion);
5798
+ }
5799
+ if (tlsResult.certificate) {
5800
+ const cert = tlsResult.certificate;
5801
+ const now = /* @__PURE__ */ new Date();
5802
+ const expiresAt = new Date(cert.valid_to);
5803
+ const daysUntilExpiry = Math.floor(
5804
+ (expiresAt.getTime() - now.getTime()) / (1e3 * 60 * 60 * 24)
5805
+ );
5806
+ result.certificate = {
5807
+ valid: tlsResult.authorized ?? false,
5808
+ issuer: formatX509Name(cert.issuer),
5809
+ subject: formatX509Name(cert.subject),
5810
+ altNames: parseAltNames(cert.subjectaltname),
5811
+ expiresAt: cert.valid_to,
5812
+ daysUntilExpiry,
5813
+ matchesHostname: tlsResult.authorized ?? false,
5814
+ selfSigned: isSelfSigned(cert),
5815
+ chainValid: tlsResult.authorized ?? false
5816
+ };
5817
+ }
5818
+ tlsResult.socket.destroy();
5819
+ } else {
5820
+ result.errors.push(tlsResult.error || "TLS upgrade failed");
5821
+ }
5822
+ } catch (error) {
5823
+ result.connectionError = error.message || "Connection failed";
5824
+ if (error.code === "ECONNREFUSED") {
5825
+ result.connectionError = "Connection refused (port 25 blocked?)";
5826
+ } else if (error.code === "ETIMEDOUT" || error.message?.includes("timeout")) {
5827
+ result.connectionError = "Connection timed out";
5828
+ } else if (error.code === "ENOTFOUND") {
5829
+ result.connectionError = "Host not found";
5830
+ }
5831
+ }
5832
+ return result;
5833
+ }
5834
+ function connectSmtp(hostname, port, timeout) {
5835
+ return new Promise((resolve, reject) => {
5836
+ const socket = net.createConnection({ host: hostname, port });
5837
+ let buffer = "";
5838
+ let resolved = false;
5839
+ const timer = setTimeout(() => {
5840
+ if (!resolved) {
5841
+ resolved = true;
5842
+ socket.destroy();
5843
+ reject(new Error("Connection timeout"));
5844
+ }
5845
+ }, timeout);
5846
+ socket.on("connect", () => {
5847
+ });
5848
+ socket.on("data", (data) => {
5849
+ buffer += data.toString();
5850
+ if (buffer.includes("\r\n") && buffer.startsWith("220") && !resolved) {
5851
+ resolved = true;
5852
+ clearTimeout(timer);
5853
+ resolve({ socket, buffer: "" });
5854
+ }
5855
+ });
5856
+ socket.on("error", (err) => {
5857
+ if (!resolved) {
5858
+ resolved = true;
5859
+ clearTimeout(timer);
5860
+ reject(err);
5861
+ }
5862
+ });
5863
+ socket.on("close", () => {
5864
+ if (!resolved) {
5865
+ resolved = true;
5866
+ clearTimeout(timer);
5867
+ reject(new Error("Connection closed"));
5868
+ }
5869
+ });
5870
+ });
5871
+ }
5872
+ function sendEhlo(session, timeout) {
5873
+ return new Promise((resolve, reject) => {
5874
+ let buffer = "";
5875
+ let resolved = false;
5876
+ const timer = setTimeout(() => {
5877
+ if (!resolved) {
5878
+ resolved = true;
5879
+ reject(new Error("EHLO timeout"));
5880
+ }
5881
+ }, timeout);
5882
+ session.socket.on("data", (data) => {
5883
+ buffer += data.toString();
5884
+ const lines = buffer.split("\r\n").filter(Boolean);
5885
+ const lastLine = lines[lines.length - 1];
5886
+ if (lastLine && /^250[ ]/.test(lastLine) && !resolved) {
5887
+ resolved = true;
5888
+ clearTimeout(timer);
5889
+ const capabilities = lines.filter((line) => line.startsWith("250")).map((line) => line.slice(4).trim().toUpperCase());
5890
+ resolve(capabilities);
5891
+ }
5892
+ });
5893
+ session.socket.on("error", (err) => {
5894
+ if (!resolved) {
5895
+ resolved = true;
5896
+ clearTimeout(timer);
5897
+ reject(err);
5898
+ }
5899
+ });
5900
+ session.socket.write("EHLO mail.check.wraps.dev\r\n");
5901
+ });
5902
+ }
5903
+ function upgradeToTls(session, hostname, timeout) {
5904
+ return new Promise((resolve) => {
5905
+ let buffer = "";
5906
+ let resolved = false;
5907
+ const timer = setTimeout(() => {
5908
+ if (!resolved) {
5909
+ resolved = true;
5910
+ resolve({ success: false, error: "STARTTLS timeout" });
5911
+ }
5912
+ }, timeout);
5913
+ const onData = (data) => {
5914
+ buffer += data.toString();
5915
+ if (buffer.includes("220")) {
5916
+ session.socket.removeListener("data", onData);
5917
+ const tlsSocket = tls.connect(
5918
+ {
5919
+ socket: session.socket,
5920
+ servername: hostname,
5921
+ rejectUnauthorized: false,
5922
+ // We'll check manually
5923
+ minVersion: "TLSv1.2"
5924
+ },
5925
+ () => {
5926
+ if (!resolved) {
5927
+ resolved = true;
5928
+ clearTimeout(timer);
5929
+ const cipher = tlsSocket.getCipher();
5930
+ const cert = tlsSocket.getPeerCertificate();
5931
+ resolve({
5932
+ success: true,
5933
+ socket: tlsSocket,
5934
+ tlsVersion: tlsSocket.getProtocol() || void 0,
5935
+ cipher: cipher?.name,
5936
+ certificate: cert,
5937
+ authorized: tlsSocket.authorized
5938
+ });
5939
+ }
5940
+ }
5941
+ );
5942
+ tlsSocket.on("error", (err) => {
5943
+ if (!resolved) {
5944
+ resolved = true;
5945
+ clearTimeout(timer);
5946
+ resolve({ success: false, error: err.message });
5947
+ }
5948
+ });
5949
+ } else if (buffer.includes("454") || buffer.includes("501")) {
5950
+ if (!resolved) {
5951
+ resolved = true;
5952
+ clearTimeout(timer);
5953
+ resolve({ success: false, error: "Server rejected STARTTLS" });
5954
+ }
5955
+ }
5956
+ };
5957
+ session.socket.on("data", onData);
5958
+ session.socket.on("error", (err) => {
5959
+ if (!resolved) {
5960
+ resolved = true;
5961
+ clearTimeout(timer);
5962
+ resolve({ success: false, error: err.message });
5963
+ }
5964
+ });
5965
+ session.socket.write("STARTTLS\r\n");
5966
+ });
5967
+ }
5968
+ function formatX509Name(name) {
5969
+ if (!name) return "";
5970
+ const parts = [];
5971
+ if (name.O) parts.push(name.O);
5972
+ if (name.CN) parts.push(name.CN);
5973
+ return parts.join(" - ") || JSON.stringify(name);
5974
+ }
5975
+ function parseAltNames(subjectaltname) {
5976
+ if (!subjectaltname) return [];
5977
+ return subjectaltname.split(", ").filter((san) => san.startsWith("DNS:")).map((san) => san.slice(4));
5978
+ }
5979
+ function isSelfSigned(cert) {
5980
+ if (!(cert.issuer && cert.subject)) return false;
5981
+ return cert.issuer.CN === cert.subject.CN && cert.issuer.O === cert.subject.O;
5982
+ }
5983
+ var RDAP_BOOTSTRAP_URL = "https://data.iana.org/rdap/dns.json";
5984
+ var rdapBootstrapCache = null;
5985
+ var bootstrapCacheTime = 0;
5986
+ var BOOTSTRAP_CACHE_TTL = 36e5;
5987
+ async function checkDomainAge(domain, options = {}) {
5988
+ const { quick = false, timeout = 1e4 } = options;
5989
+ const result = {
5990
+ createdAt: null,
5991
+ expiresAt: null,
5992
+ updatedAt: null,
5993
+ ageInDays: null,
5994
+ daysUntilExpiry: null,
5995
+ registrar: null,
5996
+ registrantOrganization: null,
5997
+ registrantCountry: null,
5998
+ nameservers: [],
5999
+ dnssecEnabled: false,
6000
+ source: "unavailable",
6001
+ privacyEnabled: false,
6002
+ errors: []
6003
+ };
6004
+ if (quick) {
6005
+ result.errors.push("Skipped in quick mode");
6006
+ return result;
6007
+ }
6008
+ try {
6009
+ const tld = getTld(domain);
6010
+ if (!tld) {
6011
+ result.errors.push("Could not determine TLD");
6012
+ return result;
6013
+ }
6014
+ const rdapServer = await findRdapServer(tld, timeout);
6015
+ if (!rdapServer) {
6016
+ result.errors.push(`No RDAP server found for .${tld}`);
6017
+ return result;
6018
+ }
6019
+ const rdapData = await queryRdap(rdapServer, domain, timeout);
6020
+ if (!rdapData) {
6021
+ result.errors.push("RDAP query returned no data");
6022
+ return result;
6023
+ }
6024
+ parseRdapResponse(rdapData, result);
6025
+ result.source = "rdap";
6026
+ } catch (error) {
6027
+ result.errors.push(error.message || "RDAP lookup failed");
6028
+ }
6029
+ return result;
6030
+ }
6031
+ function getTld(domain) {
6032
+ const parts = domain.toLowerCase().split(".");
6033
+ if (parts.length < 2) return null;
6034
+ const lastTwo = parts.slice(-2).join(".");
6035
+ const secondLevelTlds = [
6036
+ "co.uk",
6037
+ "org.uk",
6038
+ "me.uk",
6039
+ "ac.uk",
6040
+ "com.au",
6041
+ "net.au",
6042
+ "org.au",
6043
+ "co.nz",
6044
+ "org.nz",
6045
+ "co.jp",
6046
+ "or.jp",
6047
+ "com.br",
6048
+ "org.br"
6049
+ ];
6050
+ if (secondLevelTlds.includes(lastTwo) && parts.length > 2) {
6051
+ return lastTwo;
6052
+ }
6053
+ return parts[parts.length - 1] || null;
6054
+ }
6055
+ async function findRdapServer(tld, timeout) {
6056
+ const now = Date.now();
6057
+ if (rdapBootstrapCache && now - bootstrapCacheTime < BOOTSTRAP_CACHE_TTL) {
6058
+ return rdapBootstrapCache.get(tld.toLowerCase()) || null;
6059
+ }
6060
+ try {
6061
+ const controller = new AbortController();
6062
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
6063
+ const response = await fetch(RDAP_BOOTSTRAP_URL, {
6064
+ signal: controller.signal,
6065
+ headers: { Accept: "application/json" }
6066
+ });
6067
+ clearTimeout(timeoutId);
6068
+ if (!response.ok) {
6069
+ return null;
6070
+ }
6071
+ const bootstrap = await response.json();
6072
+ rdapBootstrapCache = /* @__PURE__ */ new Map();
6073
+ bootstrapCacheTime = now;
6074
+ for (const [tlds, urls] of bootstrap.services) {
6075
+ const serverUrl = urls[0];
6076
+ if (serverUrl) {
6077
+ for (const t of tlds) {
6078
+ rdapBootstrapCache.set(t.toLowerCase(), serverUrl);
6079
+ }
6080
+ }
6081
+ }
6082
+ return rdapBootstrapCache.get(tld.toLowerCase()) || null;
6083
+ } catch {
6084
+ return null;
6085
+ }
6086
+ }
6087
+ async function queryRdap(serverUrl, domain, timeout) {
6088
+ try {
6089
+ let url = serverUrl;
6090
+ if (!url.endsWith("/")) url += "/";
6091
+ url += `domain/${domain}`;
6092
+ const controller = new AbortController();
6093
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
6094
+ const response = await fetch(url, {
6095
+ signal: controller.signal,
6096
+ headers: { Accept: "application/rdap+json, application/json" }
6097
+ });
6098
+ clearTimeout(timeoutId);
6099
+ if (!response.ok) {
6100
+ return null;
6101
+ }
6102
+ return await response.json();
6103
+ } catch {
6104
+ return null;
6105
+ }
6106
+ }
6107
+ function parseRdapResponse(data, result) {
6108
+ const now = /* @__PURE__ */ new Date();
6109
+ if (data.events) {
6110
+ for (const event of data.events) {
6111
+ const date = event.eventDate;
6112
+ switch (event.eventAction) {
6113
+ case "registration":
6114
+ result.createdAt = date;
6115
+ break;
6116
+ case "expiration":
6117
+ result.expiresAt = date;
6118
+ break;
6119
+ case "last changed":
6120
+ case "last update of RDAP database":
6121
+ if (!result.updatedAt) {
6122
+ result.updatedAt = date;
6123
+ }
6124
+ break;
6125
+ }
6126
+ }
6127
+ }
6128
+ if (result.createdAt) {
6129
+ const created = new Date(result.createdAt);
6130
+ result.ageInDays = Math.floor(
6131
+ (now.getTime() - created.getTime()) / (1e3 * 60 * 60 * 24)
6132
+ );
6133
+ }
6134
+ if (result.expiresAt) {
6135
+ const expires = new Date(result.expiresAt);
6136
+ result.daysUntilExpiry = Math.floor(
6137
+ (expires.getTime() - now.getTime()) / (1e3 * 60 * 60 * 24)
6138
+ );
6139
+ }
6140
+ if (data.nameservers) {
6141
+ result.nameservers = data.nameservers.map((ns) => ns.ldhName?.toLowerCase()).filter((ns) => !!ns);
6142
+ }
6143
+ if (data.secureDNS?.delegationSigned) {
6144
+ result.dnssecEnabled = true;
6145
+ }
6146
+ if (data.entities) {
6147
+ for (const entity of data.entities) {
6148
+ const roles = entity.roles || [];
6149
+ if (roles.includes("registrar")) {
6150
+ const name = extractVcardName(entity.vcardArray);
6151
+ if (name) result.registrar = name;
6152
+ }
6153
+ if (roles.includes("registrant")) {
6154
+ const name = extractVcardName(entity.vcardArray);
6155
+ if (name) {
6156
+ result.registrantOrganization = name;
6157
+ const privacyIndicators = [
6158
+ "privacy",
6159
+ "proxy",
6160
+ "whoisguard",
6161
+ "domains by proxy",
6162
+ "contact privacy",
6163
+ "redacted",
6164
+ "withheld"
6165
+ ];
6166
+ const nameLower = name.toLowerCase();
6167
+ result.privacyEnabled = privacyIndicators.some(
6168
+ (p) => nameLower.includes(p)
6169
+ );
6170
+ }
6171
+ const country = extractVcardCountry(entity.vcardArray);
6172
+ if (country) result.registrantCountry = country;
6173
+ }
6174
+ }
6175
+ }
6176
+ }
6177
+ function extractVcardName(vcardArray) {
6178
+ if (!vcardArray || vcardArray[0] !== "vcard") return null;
6179
+ const properties = vcardArray[1];
6180
+ if (!Array.isArray(properties)) return null;
6181
+ for (const prop of properties) {
6182
+ if (!Array.isArray(prop)) continue;
6183
+ const [name, , , value] = prop;
6184
+ if (name === "fn" && typeof value === "string" && value.trim()) {
6185
+ return value.trim();
6186
+ }
6187
+ if (name === "org" && typeof value === "string" && value.trim()) {
6188
+ return value.trim();
6189
+ }
6190
+ }
6191
+ return null;
6192
+ }
6193
+ function extractVcardCountry(vcardArray) {
6194
+ if (!vcardArray || vcardArray[0] !== "vcard") return null;
6195
+ const properties = vcardArray[1];
6196
+ if (!Array.isArray(properties)) return null;
6197
+ for (const prop of properties) {
6198
+ if (!Array.isArray(prop)) continue;
6199
+ const [name, , , value] = prop;
6200
+ if (name === "adr" && Array.isArray(value)) {
6201
+ const country = value[6];
6202
+ if (typeof country === "string" && country.trim()) {
6203
+ return country.trim();
6204
+ }
6205
+ }
6206
+ }
6207
+ return null;
6208
+ }
6209
+ async function checkSpf(domain) {
6210
+ const spfRecords = await findSpfRecord(domain);
6211
+ const result = {
6212
+ exists: spfRecords.length > 0,
6213
+ record: spfRecords[0] || null,
6214
+ records: spfRecords,
6215
+ multipleRecords: spfRecords.length > 1,
6216
+ valid: false,
6217
+ syntaxErrors: [],
6218
+ warnings: [],
6219
+ lookupCount: 0,
6220
+ lookupLimit: SPF_LOOKUP_LIMIT,
6221
+ lookupTree: [],
6222
+ allMechanism: null,
6223
+ includes: [],
6224
+ hasPtr: false,
6225
+ hasDuplicates: false,
6226
+ hasCircularInclude: false,
6227
+ recordLength: spfRecords[0]?.length || 0,
6228
+ usesMacros: false,
6229
+ macros: []
6230
+ };
6231
+ if (spfRecords.length > 1) {
6232
+ result.syntaxErrors.push(
6233
+ `Multiple SPF records found (${spfRecords.length}). RFC 7208 allows only one.`
6234
+ );
6235
+ return result;
6236
+ }
6237
+ if (!(result.exists && result.record)) {
6238
+ return result;
6239
+ }
6240
+ try {
6241
+ await parseAndValidateSpf(domain, result.record, result);
6242
+ } catch (error) {
6243
+ result.syntaxErrors.push(error.message);
6244
+ }
6245
+ return result;
6246
+ }
6247
+ async function parseAndValidateSpf(domain, record, result) {
6248
+ if (!record.startsWith("v=spf1")) {
6249
+ result.syntaxErrors.push('SPF record must start with "v=spf1"');
6250
+ return;
6251
+ }
6252
+ const mechanisms = record.slice(6).trim().split(/\s+/).filter(Boolean);
6253
+ const context = {
6254
+ visited: /* @__PURE__ */ new Set([domain]),
6255
+ totalLookups: 0,
6256
+ hasCircular: false,
6257
+ errors: []
6258
+ };
6259
+ const includes = [];
6260
+ let allMechanism = null;
6261
+ let hasPtr = false;
6262
+ for (const mechanism of mechanisms) {
6263
+ const parsed = parseMechanism(mechanism);
6264
+ if (!parsed) {
6265
+ if (mechanism.includes("%{")) {
6266
+ result.usesMacros = true;
6267
+ result.macros.push(mechanism);
6268
+ result.warnings.push(
6269
+ `SPF uses macro "${mechanism}" - cannot fully validate without sending IP`
6270
+ );
6271
+ } else if (mechanism !== "") {
6272
+ result.syntaxErrors.push(`Invalid mechanism: ${mechanism}`);
6273
+ }
6274
+ continue;
6275
+ }
6276
+ switch (parsed.type) {
6277
+ case "all":
6278
+ allMechanism = parsed.qualifier;
6279
+ break;
6280
+ case "include":
6281
+ if (parsed.domain) {
6282
+ includes.push(parsed.domain);
6283
+ const includeNode = await checkSpfInclude(
6284
+ parsed.domain,
6285
+ context,
6286
+ mechanism
6287
+ );
6288
+ result.lookupTree.push(includeNode);
6289
+ }
6290
+ break;
6291
+ case "redirect":
6292
+ if (parsed.domain) {
6293
+ const redirectNode = await checkSpfInclude(
6294
+ parsed.domain,
6295
+ context,
6296
+ mechanism
6297
+ );
6298
+ result.lookupTree.push(redirectNode);
6299
+ }
6300
+ break;
6301
+ case "a":
6302
+ context.totalLookups++;
6303
+ if (parsed.domain) {
6304
+ const aNode = {
6305
+ mechanism,
6306
+ type: "a",
6307
+ domain: parsed.domain,
6308
+ lookups: 1,
6309
+ children: [],
6310
+ error: null
6311
+ };
6312
+ result.lookupTree.push(aNode);
6313
+ }
6314
+ break;
6315
+ case "mx":
6316
+ context.totalLookups++;
6317
+ if (parsed.domain) {
6318
+ const mxNode = {
6319
+ mechanism,
6320
+ type: "mx",
6321
+ domain: parsed.domain,
6322
+ lookups: 1,
6323
+ children: [],
6324
+ error: null
6325
+ };
6326
+ result.lookupTree.push(mxNode);
6327
+ }
6328
+ break;
6329
+ case "ptr":
6330
+ hasPtr = true;
6331
+ context.totalLookups++;
6332
+ result.warnings.push("SPF uses deprecated 'ptr' mechanism (RFC 7208)");
6333
+ break;
6334
+ case "exists":
6335
+ context.totalLookups++;
6336
+ break;
6337
+ case "ip4":
6338
+ case "ip6":
6339
+ break;
6340
+ default:
6341
+ result.warnings.push(`Unknown mechanism type: ${parsed.type}`);
6342
+ }
6343
+ }
6344
+ const seenIncludes = /* @__PURE__ */ new Set();
6345
+ for (const inc of includes) {
6346
+ if (seenIncludes.has(inc)) {
6347
+ result.hasDuplicates = true;
6348
+ result.warnings.push(`Duplicate include: ${inc}`);
6349
+ }
6350
+ seenIncludes.add(inc);
6351
+ }
6352
+ result.includes = includes;
6353
+ result.allMechanism = allMechanism;
6354
+ result.hasPtr = hasPtr;
6355
+ result.lookupCount = context.totalLookups;
6356
+ result.hasCircularInclude = context.hasCircular;
6357
+ result.valid = result.syntaxErrors.length === 0 && !result.multipleRecords && !result.hasCircularInclude;
6358
+ if (context.totalLookups > SPF_LOOKUP_LIMIT) {
6359
+ result.warnings.push(
6360
+ `SPF exceeds 10 DNS lookup limit (${context.totalLookups} lookups)`
6361
+ );
6362
+ } else if (context.totalLookups === SPF_LOOKUP_LIMIT) {
6363
+ result.warnings.push(
6364
+ "SPF at lookup limit (10). Adding more includes will fail."
6365
+ );
6366
+ }
6367
+ if (allMechanism === "+all") {
6368
+ result.syntaxErrors.push("SPF ends with +all which allows anyone to send");
6369
+ result.valid = false;
6370
+ } else if (allMechanism === "?all") {
6371
+ result.warnings.push(
6372
+ "SPF uses ?all (neutral) - consider using -all or ~all"
6373
+ );
6374
+ } else if (!allMechanism) {
6375
+ result.warnings.push(
6376
+ "SPF record has no 'all' mechanism - defaults to neutral"
6377
+ );
6378
+ }
6379
+ if (context.errors.length > 0) {
6380
+ result.warnings.push(...context.errors);
6381
+ }
6382
+ }
6383
+ async function checkSpfInclude(domain, context, mechanism) {
6384
+ const node = {
6385
+ mechanism,
6386
+ type: mechanism.startsWith("redirect=") ? "redirect" : "include",
6387
+ domain,
6388
+ lookups: 1,
6389
+ children: [],
6390
+ error: null
6391
+ };
6392
+ if (context.visited.has(domain)) {
6393
+ context.hasCircular = true;
6394
+ node.error = `Circular include detected: ${domain}`;
6395
+ return node;
6396
+ }
6397
+ context.visited.add(domain);
6398
+ context.totalLookups++;
6399
+ try {
6400
+ const spfRecords = await findSpfRecord(domain);
6401
+ if (spfRecords.length === 0) {
6402
+ node.error = `No SPF record found for ${domain}`;
6403
+ return node;
6404
+ }
6405
+ if (spfRecords.length > 1) {
6406
+ node.error = `Multiple SPF records found for ${domain}`;
6407
+ return node;
6408
+ }
6409
+ const record = spfRecords[0];
6410
+ if (!record.startsWith("v=spf1")) {
6411
+ node.error = `Invalid SPF record for ${domain}`;
6412
+ return node;
6413
+ }
6414
+ const mechanisms = record.slice(6).trim().split(/\s+/).filter(Boolean);
6415
+ for (const mech of mechanisms) {
6416
+ const parsed = parseMechanism(mech);
6417
+ if (!parsed) continue;
6418
+ switch (parsed.type) {
6419
+ case "include":
6420
+ case "redirect":
6421
+ if (parsed.domain) {
6422
+ const childNode = await checkSpfInclude(
6423
+ parsed.domain,
6424
+ context,
6425
+ mech
6426
+ );
6427
+ node.children.push(childNode);
6428
+ node.lookups += childNode.lookups;
6429
+ }
6430
+ break;
6431
+ case "a":
6432
+ case "mx":
6433
+ case "ptr":
6434
+ case "exists":
6435
+ context.totalLookups++;
6436
+ node.lookups++;
6437
+ break;
6438
+ }
6439
+ }
6440
+ } catch (error) {
6441
+ node.error = `Failed to resolve ${domain}: ${error.message}`;
6442
+ }
6443
+ return node;
6444
+ }
6445
+ function parseMechanism(mechanism) {
6446
+ if (!mechanism) return null;
6447
+ let qualifier = "+";
6448
+ let mech = mechanism;
6449
+ if (/^[+\-~?]/.test(mech)) {
6450
+ qualifier = mech.charAt(0);
6451
+ mech = mech.slice(1);
6452
+ }
6453
+ const match = mech.match(
6454
+ /^(all|include|a|mx|ptr|ip4|ip6|exists|redirect|exp)(?:[:=]([^\s/]+))?(?:\/(\d+))?$/i
6455
+ );
6456
+ if (!match) {
6457
+ return null;
6458
+ }
6459
+ const type = match[1];
6460
+ const value = match[2];
6461
+ const prefix = match[3];
6462
+ if (!type) {
6463
+ return null;
6464
+ }
6465
+ return {
6466
+ qualifier,
6467
+ type: type.toLowerCase(),
6468
+ domain: value,
6469
+ prefix
6470
+ };
6471
+ }
6472
+ function formatSpfLookupTree(nodes, indent = "") {
6473
+ const lines = [];
6474
+ for (const [i, node] of nodes.entries()) {
6475
+ const isLast = i === nodes.length - 1;
6476
+ const prefix = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
6477
+ const childIndent = isLast ? " " : "\u2502 ";
6478
+ let line = `${indent}${prefix}${node.mechanism}`;
6479
+ if (node.lookups > 1) {
6480
+ line += ` (${node.lookups} lookups)`;
6481
+ } else {
6482
+ line += " (1)";
6483
+ }
6484
+ if (node.error) {
6485
+ line += ` \u26A0 ${node.error}`;
6486
+ }
6487
+ lines.push(line);
6488
+ if (node.children.length > 0) {
6489
+ lines.push(formatSpfLookupTree(node.children, indent + childIndent));
6490
+ }
6491
+ }
6492
+ return lines.join("\n");
6493
+ }
6494
+ function calculateScore(checks) {
6495
+ let score = 100;
6496
+ const deductions = [];
6497
+ const bonuses = [];
6498
+ if (!checks.spf.exists) {
6499
+ score -= 30;
6500
+ deductions.push({ check: "spf", points: 30, reason: "No SPF record" });
6501
+ } else if (checks.spf.multipleRecords) {
6502
+ score -= 30;
6503
+ deductions.push({
6504
+ check: "spf",
6505
+ points: 30,
6506
+ reason: "Multiple SPF records (RFC violation)"
6507
+ });
6508
+ } else if (!checks.spf.valid) {
6509
+ score -= 30;
6510
+ deductions.push({ check: "spf", points: 30, reason: "Invalid SPF syntax" });
6511
+ } else if (checks.spf.allMechanism === "+all") {
6512
+ score -= 30;
6513
+ deductions.push({
6514
+ check: "spf",
6515
+ points: 30,
6516
+ reason: "SPF +all allows anyone"
6517
+ });
6518
+ } else if (checks.spf.hasCircularInclude) {
6519
+ score -= 30;
6520
+ deductions.push({
6521
+ check: "spf",
6522
+ points: 30,
6523
+ reason: "SPF has circular include (infinite loop)"
6524
+ });
6525
+ } else {
6526
+ if (checks.spf.allMechanism === "?all") {
6527
+ score -= 15;
6528
+ deductions.push({
6529
+ check: "spf",
6530
+ points: 15,
6531
+ reason: "SPF ?all is too permissive"
6532
+ });
6533
+ } else if (checks.spf.allMechanism === "~all") {
6534
+ score -= 5;
6535
+ deductions.push({
6536
+ check: "spf",
6537
+ points: 5,
6538
+ reason: "SPF ~all (softfail) instead of -all"
6539
+ });
6540
+ }
6541
+ if (checks.spf.lookupCount > 10) {
6542
+ score -= 10;
6543
+ deductions.push({
6544
+ check: "spf",
6545
+ points: 10,
6546
+ reason: `SPF exceeds 10 lookups (${checks.spf.lookupCount})`
6547
+ });
6548
+ }
6549
+ if (checks.spf.hasPtr) {
6550
+ score -= 2;
6551
+ deductions.push({
6552
+ check: "spf",
6553
+ points: 2,
6554
+ reason: "SPF uses deprecated ptr mechanism"
6555
+ });
6556
+ }
6557
+ }
6558
+ if (checks.dkim.found) {
6559
+ const validSelector = checks.dkim.selectors.find(
6560
+ (s) => s.valid && !s.revoked
6561
+ );
6562
+ if (validSelector) {
6563
+ if (validSelector.keyType === "rsa" && validSelector.keyBits && validSelector.keyBits < 1024) {
6564
+ score -= 20;
6565
+ deductions.push({
6566
+ check: "dkim",
6567
+ points: 20,
6568
+ reason: `DKIM key too weak (${validSelector.keyBits} bits)`
6569
+ });
6570
+ } else if (validSelector.keyType === "rsa" && validSelector.keyBits && validSelector.keyBits < 2048) {
6571
+ score -= 5;
6572
+ deductions.push({
6573
+ check: "dkim",
6574
+ points: 5,
6575
+ reason: `DKIM key should be 2048+ bits (${validSelector.keyBits})`
6576
+ });
6577
+ }
6578
+ if (validSelector.testMode) {
6579
+ score -= 10;
6580
+ deductions.push({
6581
+ check: "dkim",
6582
+ points: 10,
6583
+ reason: "DKIM in testing mode (t=y)"
6584
+ });
6585
+ }
6586
+ const validCount = checks.dkim.selectors.filter(
6587
+ (s) => s.valid && !s.revoked
6588
+ ).length;
6589
+ if (validCount > 1) {
6590
+ bonuses.push({
6591
+ check: "dkim",
6592
+ points: 2,
6593
+ reason: `${validCount} valid DKIM selectors`
6594
+ });
6595
+ }
6596
+ } else {
6597
+ score -= 25;
6598
+ deductions.push({
6599
+ check: "dkim",
6600
+ points: 25,
6601
+ reason: "No valid DKIM record"
6602
+ });
6603
+ }
6604
+ } else {
6605
+ score -= 25;
6606
+ deductions.push({
6607
+ check: "dkim",
6608
+ points: 25,
6609
+ reason: "No DKIM record found"
6610
+ });
6611
+ }
6612
+ if (!checks.dmarc.exists) {
6613
+ score -= 20;
6614
+ deductions.push({ check: "dmarc", points: 20, reason: "No DMARC record" });
6615
+ } else if (checks.dmarc.valid) {
6616
+ if (checks.dmarc.policy === "none") {
6617
+ score -= 10;
6618
+ deductions.push({
6619
+ check: "dmarc",
6620
+ points: 10,
6621
+ reason: "DMARC policy is none (not enforcing)"
6622
+ });
6623
+ }
6624
+ if (checks.dmarc.percentage < 100) {
6625
+ score -= 5;
6626
+ deductions.push({
6627
+ check: "dmarc",
6628
+ points: 5,
6629
+ reason: `DMARC pct=${checks.dmarc.percentage} (not 100%)`
6630
+ });
6631
+ }
6632
+ if (!checks.dmarc.reportingEnabled) {
6633
+ score -= 5;
6634
+ deductions.push({
6635
+ check: "dmarc",
6636
+ points: 5,
6637
+ reason: "DMARC reporting not configured (no rua=)"
6638
+ });
6639
+ }
6640
+ if (checks.dmarc.alignmentSpf === "strict" && checks.dmarc.alignmentDkim === "strict") {
6641
+ bonuses.push({
6642
+ check: "dmarc",
6643
+ points: 2,
6644
+ reason: "DMARC strict alignment configured"
6645
+ });
6646
+ }
6647
+ } else {
6648
+ score -= 20;
6649
+ deductions.push({
6650
+ check: "dmarc",
6651
+ points: 20,
6652
+ reason: "Invalid DMARC syntax"
6653
+ });
6654
+ }
6655
+ if (checks.mx.exists) {
6656
+ const unresolving = checks.mx.records.filter((r) => !r.resolves);
6657
+ if (unresolving.length > 0) {
6658
+ score -= 5;
6659
+ deductions.push({
6660
+ check: "mx",
6661
+ points: 5,
6662
+ reason: `${unresolving.length} MX records don't resolve`
6663
+ });
6664
+ }
6665
+ if (checks.mx.hasRedundancy) {
6666
+ bonuses.push({
6667
+ check: "mx",
6668
+ points: 1,
6669
+ reason: "Multiple MX records for redundancy"
6670
+ });
6671
+ }
6672
+ } else {
6673
+ score -= 5;
6674
+ deductions.push({ check: "mx", points: 5, reason: "No MX records" });
6675
+ }
6676
+ const domainListings = checks.blacklist.domainChecks.listed;
6677
+ const ipListings = checks.blacklist.ipChecks.listed;
6678
+ for (const listing of [...domainListings, ...ipListings]) {
6679
+ if (listing.zone.includes("spamhaus")) {
6680
+ score -= 30;
6681
+ deductions.push({
6682
+ check: "blacklist",
6683
+ points: 30,
6684
+ reason: `Listed on ${listing.blacklist}`
6685
+ });
6686
+ } else if (listing.priority === "critical" || listing.priority === "high") {
6687
+ const pts = listing.priority === "critical" ? 15 : 10;
6688
+ score -= pts;
6689
+ deductions.push({
6690
+ check: "blacklist",
6691
+ points: pts,
6692
+ reason: `Listed on ${listing.blacklist}`
6693
+ });
6694
+ } else if (listing.priority === "medium") {
6695
+ score -= 5;
6696
+ deductions.push({
6697
+ check: "blacklist",
6698
+ points: 5,
6699
+ reason: `Listed on ${listing.blacklist}`
6700
+ });
6701
+ } else {
6702
+ score -= 2;
6703
+ deductions.push({
6704
+ check: "blacklist",
6705
+ points: 2,
6706
+ reason: `Listed on ${listing.blacklist}`
6707
+ });
6708
+ }
6709
+ }
6710
+ if (checks.blacklist.overallClean) {
6711
+ bonuses.push({
6712
+ check: "blacklist",
6713
+ points: 5,
6714
+ reason: "Clean on all blacklists"
6715
+ });
6716
+ }
6717
+ if (checks.mtaSts.configured && checks.mtaSts.policy?.mode === "enforce") {
6718
+ bonuses.push({ check: "mta-sts", points: 5, reason: "MTA-STS enforcing" });
6719
+ } else if (checks.mtaSts.configured && checks.mtaSts.policy?.mode === "testing") {
6720
+ bonuses.push({
6721
+ check: "mta-sts",
6722
+ points: 2,
6723
+ reason: "MTA-STS testing mode"
6724
+ });
6725
+ }
6726
+ if (checks.tlsRpt.configured) {
6727
+ bonuses.push({ check: "tls-rpt", points: 2, reason: "TLS-RPT configured" });
6728
+ }
6729
+ if (checks.bimi.configured && checks.bimi.dmarcCompatible) {
6730
+ if (checks.bimi.vmcValid) {
6731
+ bonuses.push({ check: "bimi", points: 5, reason: "BIMI with VMC" });
6732
+ } else if (checks.bimi.logoAccessible) {
6733
+ bonuses.push({
6734
+ check: "bimi",
6735
+ points: 2,
6736
+ reason: "BIMI configured (no VMC)"
6737
+ });
6738
+ }
6739
+ }
6740
+ if (checks.mxTls.checked && !checks.mxTls.skipped) {
6741
+ const allMxTls13 = checks.mxTls.servers.every(
6742
+ (s) => s.tlsVersions?.includes("TLSv1.3")
6743
+ );
6744
+ if (allMxTls13) {
6745
+ bonuses.push({
6746
+ check: "mx-tls",
6747
+ points: 2,
6748
+ reason: "All MX servers support TLS 1.3"
6749
+ });
6750
+ }
6751
+ }
6752
+ if (checks.dnssec.enabled && checks.dnssec.valid) {
6753
+ bonuses.push({
6754
+ check: "dnssec",
6755
+ points: 3,
6756
+ reason: "DNSSEC enabled and valid"
6757
+ });
6758
+ } else if (checks.dnssec.enabled && !checks.dnssec.valid) {
6759
+ score -= 5;
6760
+ deductions.push({ check: "dnssec", points: 5, reason: "DNSSEC broken" });
6761
+ }
6762
+ if (checks.ipv6.mxHasIpv6 && checks.ipv6.spfIncludesIpv6) {
6763
+ bonuses.push({ check: "ipv6", points: 2, reason: "Full IPv6 support" });
6764
+ } else if (checks.ipv6.mxHasIpv6) {
6765
+ bonuses.push({ check: "ipv6", points: 1, reason: "Partial IPv6 support" });
6766
+ }
6767
+ if (checks.reverseDns.allHavePtr && checks.reverseDns.allConfirm) {
6768
+ bonuses.push({ check: "ptr", points: 2, reason: "All PTR records valid" });
6769
+ } else if (!checks.reverseDns.allHavePtr) {
6770
+ score -= 5;
6771
+ deductions.push({ check: "ptr", points: 5, reason: "Missing reverse DNS" });
6772
+ }
6773
+ if (checks.domainAge.ageInDays !== null) {
6774
+ if (checks.domainAge.ageInDays < 30) {
6775
+ score -= 10;
6776
+ deductions.push({
6777
+ check: "domain-age",
6778
+ points: 10,
6779
+ reason: "Domain less than 30 days old"
6780
+ });
6781
+ } else if (checks.domainAge.ageInDays < 90) {
6782
+ score -= 5;
6783
+ deductions.push({
6784
+ check: "domain-age",
6785
+ points: 5,
6786
+ reason: "Domain less than 90 days old"
6787
+ });
6788
+ } else if (checks.domainAge.ageInDays > 365) {
6789
+ bonuses.push({
6790
+ check: "domain-age",
6791
+ points: 2,
6792
+ reason: "Domain over 1 year old"
6793
+ });
6794
+ }
6795
+ }
6796
+ if (checks.caa.configured) {
6797
+ bonuses.push({ check: "caa", points: 1, reason: "CAA records configured" });
6798
+ }
6799
+ const totalBonus = bonuses.reduce((sum, b) => sum + b.points, 0);
6800
+ const rawScore = score + Math.min(totalBonus, 20);
6801
+ const finalScore = Math.min(100, Math.max(0, rawScore));
6802
+ return {
6803
+ rawScore,
6804
+ finalScore,
6805
+ grade: getGrade(finalScore),
6806
+ deductions,
6807
+ bonuses,
6808
+ breakdown: {
6809
+ spf: {
6810
+ max: 30,
6811
+ score: Math.max(0, 30 - sumDeductions(deductions, "spf"))
6812
+ },
6813
+ dkim: {
6814
+ max: 25,
6815
+ score: Math.max(0, 25 - sumDeductions(deductions, "dkim"))
6816
+ },
6817
+ dmarc: {
6818
+ max: 25,
6819
+ score: Math.max(0, 25 - sumDeductions(deductions, "dmarc"))
6820
+ },
6821
+ mx: { max: 10, score: Math.max(0, 10 - sumDeductions(deductions, "mx")) },
6822
+ blacklist: {
6823
+ max: 10,
6824
+ score: Math.max(0, 10 - sumDeductions(deductions, "blacklist"))
6825
+ },
6826
+ bonus: { earned: Math.min(totalBonus, 20), possible: 20 }
6827
+ }
6828
+ };
6829
+ }
6830
+ function getGrade(score) {
6831
+ if (score >= 90) return "A";
6832
+ if (score >= 80) return "B";
6833
+ if (score >= 70) return "C";
6834
+ if (score >= 50) return "D";
6835
+ return "F";
6836
+ }
6837
+ function sumDeductions(deductions, check2) {
6838
+ return deductions.filter((d) => d.check === check2).reduce((sum, d) => sum + d.points, 0);
6839
+ }
6840
+ async function runEmailCheck(domain, options = {}) {
6841
+ const startTime = Date.now();
6842
+ const normalizedDomain = toAsciiDomain(domain);
6843
+ if (!isValidDomain(normalizedDomain)) {
6844
+ throw new Error(`Invalid domain: ${domain}`);
6845
+ }
6846
+ const { quick = false, skipBlacklists = false, skipTls = false } = options;
6847
+ const [spfResult, dkimResult, dmarcResult, mxResult] = await Promise.all([
6848
+ checkSpf(normalizedDomain),
6849
+ checkDkim(normalizedDomain, {
6850
+ quick,
6851
+ selector: options.dkimSelector,
6852
+ selectors: options.dkimSelectors,
6853
+ verbose: options.verbose
6854
+ }),
6855
+ checkDmarc(normalizedDomain),
6856
+ checkMx(normalizedDomain)
6857
+ ]);
6858
+ if (!dkimResult.found && spfResult.record) {
6859
+ const spfLower = spfResult.record.toLowerCase();
6860
+ if (spfLower.includes("amazonses") || spfLower.includes("_spf.aws")) {
6861
+ dkimResult.warnings.push(
6862
+ "AWS SES uses random DKIM selectors. Run `wraps email domains get-dkim -d <domain>` to find yours."
6863
+ );
6864
+ } else if (spfLower.includes("sendgrid")) {
6865
+ dkimResult.warnings.push(
6866
+ "SendGrid uses custom DKIM selectors. Check your SendGrid dashboard or use --dkimSelector."
6867
+ );
6868
+ } else if (spfLower.includes("mailgun")) {
6869
+ dkimResult.warnings.push(
6870
+ "Mailgun uses custom DKIM selectors. Check your Mailgun dashboard or use --dkimSelector."
6871
+ );
6872
+ }
6873
+ }
6874
+ const mxIps = [];
6875
+ for (const mx of mxResult.records) {
6876
+ mxIps.push(...mx.ipv4Addresses);
6877
+ }
6878
+ const blacklistResult = await checkBlacklist({
6879
+ domain: normalizedDomain,
6880
+ ips: mxIps,
6881
+ quick,
6882
+ skip: skipBlacklists
6883
+ });
6884
+ const mxTlsResult = await checkMxTls(mxResult.records, {
6885
+ skip: skipTls,
6886
+ quick
6887
+ });
6888
+ const mtaStsResult = {
6889
+ configured: false,
6890
+ dnsRecord: null,
6891
+ dnsRecordId: null,
6892
+ policyFetched: false,
6893
+ policyUrl: `https://mta-sts.${normalizedDomain}/.well-known/mta-sts.txt`,
6894
+ policy: null,
6895
+ mxPatternsMatch: false,
6896
+ errors: [],
6897
+ warnings: []
6898
+ };
6899
+ const tlsRptResult = {
6900
+ configured: false,
6901
+ record: null,
6902
+ version: null,
6903
+ reportingUris: [],
6904
+ errors: []
6905
+ };
6906
+ const reverseDnsResult = {
6907
+ results: [],
6908
+ allHavePtr: false,
6909
+ allConfirm: false,
6910
+ warnings: []
6911
+ };
6912
+ for (const mx of mxResult.records) {
6913
+ for (const ip of mx.ipv4Addresses) {
6914
+ const hasPtr = mx.reverseHostnames.length > 0;
6915
+ reverseDnsResult.results.push({
6916
+ ip,
6917
+ ipVersion: 4,
6918
+ ptrHostname: mx.reverseHostnames[0] || null,
6919
+ forwardConfirms: hasPtr,
6920
+ // Simplified - would need actual check
6921
+ looksGeneric: false,
6922
+ matchesDomain: mx.reverseHostnames.some(
6923
+ (h) => h.includes(normalizedDomain)
6924
+ )
6925
+ });
6926
+ }
6927
+ }
6928
+ reverseDnsResult.allHavePtr = reverseDnsResult.results.every(
6929
+ (r) => r.ptrHostname !== null
6930
+ );
6931
+ reverseDnsResult.allConfirm = reverseDnsResult.results.every(
6932
+ (r) => r.forwardConfirms
6933
+ );
6934
+ const ipv6Result = {
6935
+ mxHasIpv6: mxResult.records.some((r) => r.ipv6Addresses.length > 0),
6936
+ mxIpv6Addresses: mxResult.records.filter((r) => r.ipv6Addresses.length > 0).map((r) => ({ mx: r.exchange, addresses: r.ipv6Addresses })),
6937
+ ipv6Connectable: false,
6938
+ // Would need actual check
6939
+ spfIncludesIpv6: spfResult.record?.includes("ip6:") ?? false,
6940
+ warnings: []
6941
+ };
6942
+ const domainAgeResult = await checkDomainAge(normalizedDomain, { quick });
6943
+ const dnssecResult = {
6944
+ enabled: false,
6945
+ valid: false,
6946
+ validationMethod: "system-resolver",
6947
+ chainOfTrust: [],
6948
+ algorithm: null,
6949
+ keyTag: null,
6950
+ errors: []
6951
+ };
6952
+ const caaResult = {
6953
+ configured: false,
6954
+ records: [],
6955
+ allowedIssuers: [],
6956
+ allowedWildcardIssuers: [],
6957
+ reportingConfigured: false,
6958
+ iodefUri: null
6959
+ };
6960
+ const bimiResult = {
6961
+ configured: false,
6962
+ record: null,
6963
+ logoUrl: null,
6964
+ vmcUrl: null,
6965
+ logoAccessible: false,
6966
+ logoValid: false,
6967
+ vmcAccessible: false,
6968
+ vmcValid: false,
6969
+ dmarcCompatible: dmarcResult.policy === "quarantine" || dmarcResult.policy === "reject",
6970
+ errors: [],
6971
+ warnings: []
6972
+ };
6973
+ const score = calculateScore({
6974
+ spf: spfResult,
6975
+ dkim: dkimResult,
6976
+ dmarc: dmarcResult,
6977
+ mx: mxResult,
6978
+ mxTls: mxTlsResult,
6979
+ mtaSts: mtaStsResult,
6980
+ tlsRpt: tlsRptResult,
6981
+ bimi: bimiResult,
6982
+ dnssec: dnssecResult,
6983
+ ipv6: ipv6Result,
6984
+ reverseDns: reverseDnsResult,
6985
+ blacklist: blacklistResult,
6986
+ domainAge: domainAgeResult,
6987
+ caa: caaResult
6988
+ });
6989
+ const duration = Date.now() - startTime;
6990
+ return {
6991
+ domain: normalizedDomain,
6992
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
6993
+ duration,
6994
+ options,
6995
+ spf: spfResult,
6996
+ dkim: dkimResult,
6997
+ dmarc: dmarcResult,
6998
+ mx: mxResult,
6999
+ mxTls: mxTlsResult,
7000
+ mtaSts: mtaStsResult,
7001
+ tlsRpt: tlsRptResult,
7002
+ reverseDns: reverseDnsResult,
7003
+ ipv6: ipv6Result,
7004
+ blacklist: blacklistResult,
7005
+ domainAge: domainAgeResult,
7006
+ dnssec: dnssecResult,
7007
+ caa: caaResult,
7008
+ bimi: bimiResult,
7009
+ score
7010
+ };
7011
+ }
7012
+ function getExitCode(grade) {
7013
+ switch (grade) {
7014
+ case "A":
7015
+ case "B":
7016
+ return 0;
7017
+ case "C":
7018
+ case "D":
7019
+ return 1;
7020
+ case "F":
7021
+ return 2;
7022
+ default:
7023
+ return 4;
7024
+ }
7025
+ }
7026
+
7027
+ // src/commands/email/check.ts
7028
+ init_events();
7029
+ import pc4 from "picocolors";
7030
+ async function check(options) {
7031
+ const startTime = Date.now();
7032
+ let domain = options.domain;
7033
+ if (!domain) {
7034
+ const input = await clack3.text({
7035
+ message: "Enter domain to check:",
7036
+ placeholder: "example.com",
7037
+ validate: (value) => {
7038
+ if (!value) return "Domain is required";
7039
+ if (!/^[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/i.test(value)) {
7040
+ return "Invalid domain format";
7041
+ }
7042
+ }
7043
+ });
7044
+ if (clack3.isCancel(input)) {
7045
+ clack3.cancel("Operation cancelled");
7046
+ process.exit(0);
7047
+ }
7048
+ domain = input;
7049
+ }
7050
+ if (!options.json) {
7051
+ clack3.intro(pc4.bold("Wraps Email Check"));
7052
+ console.log();
7053
+ }
7054
+ const spinner4 = options.json ? null : clack3.spinner();
7055
+ spinner4?.start(`Checking ${pc4.cyan(domain)}...`);
7056
+ let dkimSelectors;
7057
+ if (!options.dkimSelector) {
7058
+ const sesTokens = await tryGetSesDkimTokens(domain);
7059
+ if (sesTokens.length > 0) {
7060
+ dkimSelectors = sesTokens;
7061
+ }
7062
+ }
7063
+ try {
7064
+ const result = await runEmailCheck(domain, {
7065
+ quick: options.quick,
7066
+ verbose: options.verbose,
7067
+ dkimSelector: options.dkimSelector,
7068
+ dkimSelectors,
7069
+ // Use SES tokens if found
7070
+ skipBlacklists: options.skipBlacklists,
7071
+ skipTls: options.skipTls,
7072
+ timeout: options.timeout
7073
+ });
7074
+ spinner4?.stop(`Check complete in ${result.duration}ms`);
7075
+ if (options.json) {
7076
+ console.log(JSON.stringify(result, null, 2));
7077
+ } else {
7078
+ displayResults(result, options);
7079
+ }
7080
+ const duration = Date.now() - startTime;
7081
+ trackCommand("email:check", {
7082
+ success: true,
7083
+ duration_ms: duration,
7084
+ grade: result.score.grade
7085
+ });
7086
+ process.exit(getExitCode(result.score.grade));
7087
+ } catch (error) {
7088
+ spinner4?.stop("Check failed");
7089
+ if (options.json) {
7090
+ console.log(JSON.stringify({ error: error.message }));
7091
+ } else {
7092
+ clack3.log.error(error.message);
7093
+ }
7094
+ const duration = Date.now() - startTime;
7095
+ trackCommand("email:check", {
7096
+ success: false,
7097
+ duration_ms: duration,
7098
+ error: error.message
7099
+ });
7100
+ process.exit(4);
7101
+ }
7102
+ }
7103
+ function displayResults(result, options) {
7104
+ const { score, spf, dkim, dmarc, mx, blacklist } = result;
7105
+ console.log();
7106
+ displayScoreBox(result.domain, score.finalScore, score.grade, options.quick);
7107
+ console.log();
7108
+ console.log(pc4.bold("AUTHENTICATION"));
7109
+ console.log();
7110
+ displaySpfResult(spf, options.verbose);
7111
+ displayDkimResult(dkim, options.verbose);
7112
+ displayDmarcResult(dmarc);
7113
+ console.log();
7114
+ console.log(pc4.bold("INFRASTRUCTURE"));
7115
+ console.log();
7116
+ displayMxResult(mx);
7117
+ displayMxTlsResult(result);
7118
+ displayReverseDnsResult(result);
7119
+ displayIpv6Result(result);
7120
+ console.log();
7121
+ console.log(pc4.bold("REPUTATION"));
7122
+ console.log();
7123
+ displayBlacklistResult(blacklist, options.quick);
7124
+ displayDomainAgeResult(result);
7125
+ if (result.dnssec.enabled || result.caa.configured || result.mtaSts.configured || result.tlsRpt.configured) {
7126
+ console.log();
7127
+ console.log(pc4.bold("SECURITY"));
7128
+ console.log();
7129
+ if (result.dnssec.enabled) {
7130
+ const status2 = result.dnssec.valid ? pc4.green("\u2713") : pc4.red("\u2717");
7131
+ console.log(
7132
+ ` ${status2} ${pc4.dim("DNSSEC")} ${result.dnssec.valid ? "Enabled and validated" : "Broken"}`
7133
+ );
7134
+ } else {
7135
+ console.log(
7136
+ ` ${pc4.dim("\u25CB")} ${pc4.dim("DNSSEC")} Not configured`
7137
+ );
7138
+ }
7139
+ if (result.caa.configured) {
7140
+ console.log(
7141
+ ` ${pc4.green("\u2713")} ${pc4.dim("CAA")} Configured (${result.caa.allowedIssuers.join(", ")})`
7142
+ );
7143
+ } else {
7144
+ console.log(
7145
+ ` ${pc4.dim("\u25CB")} ${pc4.dim("CAA")} Not configured`
7146
+ );
7147
+ }
7148
+ if (result.mtaSts.configured) {
7149
+ const mode = result.mtaSts.policy?.mode || "unknown";
7150
+ console.log(
7151
+ ` ${pc4.green("\u2713")} ${pc4.dim("MTA-STS")} ${mode === "enforce" ? "Enforcing mode" : `${mode} mode`}`
7152
+ );
7153
+ } else {
7154
+ console.log(
7155
+ ` ${pc4.dim("\u25CB")} ${pc4.dim("MTA-STS")} Not configured`
7156
+ );
7157
+ }
7158
+ if (result.tlsRpt.configured) {
7159
+ console.log(
7160
+ ` ${pc4.green("\u2713")} ${pc4.dim("TLS-RPT")} Configured`
7161
+ );
7162
+ } else {
7163
+ console.log(
7164
+ ` ${pc4.dim("\u25CB")} ${pc4.dim("TLS-RPT")} Not configured`
7165
+ );
7166
+ }
7167
+ }
7168
+ const criticalDeductions = score.deductions.filter((d) => d.points >= 20);
7169
+ const warningDeductions = score.deductions.filter(
7170
+ (d) => d.points >= 5 && d.points < 20
7171
+ );
7172
+ if (criticalDeductions.length > 0 || warningDeductions.length > 0) {
7173
+ console.log();
7174
+ console.log(pc4.dim("\u2500".repeat(78)));
7175
+ console.log();
7176
+ console.log(
7177
+ pc4.bold(
7178
+ `ISSUES (${criticalDeductions.length} critical, ${warningDeductions.length} warnings)`
7179
+ )
7180
+ );
7181
+ console.log();
7182
+ if (criticalDeductions.length > 0) {
7183
+ console.log(pc4.red("\u274C CRITICAL"));
7184
+ console.log();
7185
+ for (let i = 0; i < criticalDeductions.length; i++) {
7186
+ const d = criticalDeductions[i];
7187
+ console.log(
7188
+ ` ${i + 1}. ${d.reason} (${pc4.red(`-${d.points} points`)})`
7189
+ );
7190
+ console.log();
7191
+ console.log(` ${getFixSuggestion(d.check, d.reason)}`);
7192
+ console.log();
7193
+ }
7194
+ }
7195
+ if (warningDeductions.length > 0) {
7196
+ console.log(pc4.yellow("\u26A0\uFE0F WARNINGS"));
7197
+ console.log();
7198
+ for (let i = 0; i < warningDeductions.length; i++) {
7199
+ const d = warningDeductions[i];
7200
+ console.log(` ${criticalDeductions.length + i + 1}. ${d.reason}`);
7201
+ console.log();
7202
+ }
7203
+ }
7204
+ }
7205
+ console.log();
7206
+ console.log(pc4.dim("\u2500".repeat(78)));
7207
+ console.log();
7208
+ if (score.grade === "A" || score.grade === "B") {
7209
+ console.log(
7210
+ pc4.green(
7211
+ "\u2705 " + (score.grade === "A" ? "Excellent! Your email configuration follows all best practices." : "Good! Your email configuration is solid with minor improvements possible.")
7212
+ )
7213
+ );
7214
+ } else {
7215
+ console.log(
7216
+ pc4.yellow(
7217
+ "Need help fixing these? Deploy Wraps to manage your email infrastructure:"
7218
+ )
7219
+ );
7220
+ console.log();
7221
+ console.log(` ${pc4.cyan("npx @wraps.dev/cli email init")}`);
7222
+ }
7223
+ console.log();
7224
+ console.log(`Share: ${pc4.cyan(`https://wraps.dev/check/${result.domain}`)}`);
7225
+ console.log();
7226
+ }
7227
+ function displayScoreBox(domain, score, grade, quick) {
7228
+ const width = 78;
7229
+ const bar = "\u2588".repeat(Math.round(score / 100 * 60));
7230
+ const emptyBar = "\u2591".repeat(60 - bar.length);
7231
+ const gradeColor = grade === "A" || grade === "B" ? pc4.green : grade === "C" ? pc4.yellow : grade === "D" ? pc4.magenta : pc4.red;
7232
+ console.log(pc4.dim("\u256D" + "\u2500".repeat(width - 2) + "\u256E"));
7233
+ console.log(pc4.dim("\u2502") + " ".repeat(width - 2) + pc4.dim("\u2502"));
7234
+ console.log(
7235
+ pc4.dim("\u2502") + " " + pc4.bold(`wraps email check${quick ? " --quick" : ""}`) + " ".repeat(width - 25 - (quick ? 8 : 0)) + pc4.dim("\u2502")
4819
7236
  );
4820
7237
  console.log(pc4.dim("\u2502") + " ".repeat(width - 2) + pc4.dim("\u2502"));
4821
7238
  console.log(
@@ -6498,7 +8915,7 @@ async function scanAWSResources(region) {
6498
8915
  }
6499
8916
 
6500
8917
  // src/commands/email/connect.ts
6501
- async function connect(options) {
8918
+ async function connect2(options) {
6502
8919
  const startTime = Date.now();
6503
8920
  clack6.intro(
6504
8921
  pc7.bold(
@@ -7056,7 +9473,7 @@ Run ${pc8.cyan("wraps email init")} to deploy infrastructure again.
7056
9473
  init_esm_shims();
7057
9474
  init_events();
7058
9475
  init_aws();
7059
- import { Resolver } from "dns/promises";
9476
+ import { Resolver as Resolver2 } from "dns/promises";
7060
9477
  import { GetEmailIdentityCommand as GetEmailIdentityCommand2, SESv2Client as SESv2Client3 } from "@aws-sdk/client-sesv2";
7061
9478
  import * as clack8 from "@clack/prompts";
7062
9479
  import pc9 from "picocolors";
@@ -7091,7 +9508,7 @@ Run ${pc9.cyan(`wraps email init --domain ${options.domain}`)} to add this domai
7091
9508
  process.exit(1);
7092
9509
  return;
7093
9510
  }
7094
- const resolver = new Resolver();
9511
+ const resolver = new Resolver2();
7095
9512
  resolver.setServers(["8.8.8.8", "1.1.1.1"]);
7096
9513
  const dnsResults = [];
7097
9514
  for (const token of dkimTokens) {
@@ -10009,7 +12426,7 @@ function createMetricsRouter(config2) {
10009
12426
 
10010
12427
  // src/console/routes/settings.ts
10011
12428
  init_esm_shims();
10012
- import dns from "dns/promises";
12429
+ import dns2 from "dns/promises";
10013
12430
  import { Router as createRouter4 } from "express";
10014
12431
 
10015
12432
  // src/console/services/settings-service.ts
@@ -10166,7 +12583,7 @@ function createSettingsRouter(config2) {
10166
12583
  }
10167
12584
  console.log(`[Verify] Checking CNAME for: ${domain}`);
10168
12585
  console.log(`[Verify] Expected target: ${expectedTarget}`);
10169
- const records = await dns.resolveCname(domain);
12586
+ const records = await dns2.resolveCname(domain);
10170
12587
  console.log("[Verify] CNAME records found:", records);
10171
12588
  const verified = records.some(
10172
12589
  (record) => record.toLowerCase().includes(expectedTarget.toLowerCase())
@@ -10199,7 +12616,7 @@ function createSettingsRouter(config2) {
10199
12616
  }
10200
12617
  const dmarcDomain = `_dmarc.${domain}`;
10201
12618
  console.log(`[Verify] Checking DMARC for: ${dmarcDomain}`);
10202
- const records = await dns.resolveTxt(dmarcDomain);
12619
+ const records = await dns2.resolveTxt(dmarcDomain);
10203
12620
  console.log("[Verify] TXT records found:", records);
10204
12621
  const hasDmarc = records.some((record) => {
10205
12622
  const value = record.join("");
@@ -15726,7 +18143,7 @@ if (!primaryCommand) {
15726
18143
  preview: flags.preview
15727
18144
  });
15728
18145
  } else {
15729
- await connect({
18146
+ await connect2({
15730
18147
  provider: flags.provider,
15731
18148
  region: flags.region,
15732
18149
  yes: flags.yes,
@@ -15785,7 +18202,7 @@ async function run() {
15785
18202
  });
15786
18203
  break;
15787
18204
  case "connect":
15788
- await connect({
18205
+ await connect2({
15789
18206
  provider: flags.provider,
15790
18207
  region: flags.region,
15791
18208
  yes: flags.yes,