<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[StadiaSoft]]></title><description><![CDATA[StadiaSoft]]></description><link>https://stadiasoft.hashnode.dev</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1593680282896/kNC7E8IR4.png</url><title>StadiaSoft</title><link>https://stadiasoft.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Mon, 21 Sep 2026 06:57:30 GMT</lastBuildDate><atom:link href="https://stadiasoft.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Why SMTP Email Verification Needs ‘Unknown’ as a First-Class Result]]></title><description><![CDATA[Boolean email verification is attractive because it simplifies product decisions:
valid   -> accept
invalid -> reject

The real network is less cooperative.
A verifier can prove that an address is mal]]></description><link>https://stadiasoft.hashnode.dev/smtp-email-verification-unknown-result</link><guid isPermaLink="true">https://stadiasoft.hashnode.dev/smtp-email-verification-unknown-result</guid><category><![CDATA[smtp]]></category><category><![CDATA[email]]></category><category><![CDATA[Node.js]]></category><category><![CDATA[api]]></category><category><![CDATA[Backend Development]]></category><dc:creator><![CDATA[Faraz Ahmad]]></dc:creator><pubDate>Sun, 20 Sep 2026 17:49:18 GMT</pubDate><content:encoded><![CDATA[<p>Boolean email verification is attractive because it simplifies product decisions:</p>
<pre><code class="language-txt">valid   -&gt; accept
invalid -&gt; reject
</code></pre>
<p>The real network is less cooperative.</p>
<p>A verifier can prove that an address is malformed. It can observe that a domain has no usable mail infrastructure. It may receive a strong negative recipient response. But it cannot force every SMTP server to disclose whether a mailbox exists.</p>
<p>That is why <code>unknown</code> is not a weak implementation detail. It is a necessary part of an honest API contract.</p>
<h2>SMTP is a delivery protocol, not a mailbox directory</h2>
<p>During deep email verification, a service can connect to the destination mail system and observe parts of an SMTP conversation without sending a message. The response may provide evidence about connectivity, recipient acceptance or temporary failure.</p>
<p>However, providers have good reasons to limit what they reveal:</p>
<ul>
<li>address enumeration enables spam and phishing;</li>
<li>rate limits protect infrastructure;</li>
<li>greylisting intentionally defers unfamiliar clients;</li>
<li>overloaded servers can time out;</li>
<li>security gateways may accept recipients before later validation;</li>
<li>policy can vary by network, sender reputation or time.</li>
</ul>
<p>RFC 5321 explicitly allows operators to restrict address-verification behavior. A server’s refusal to answer a verification-style query is not evidence that the mailbox is nonexistent.</p>
<h2>Three results are the minimum</h2>
<p>At the application boundary, model at least three outcomes:</p>
<pre><code class="language-ts">type VerificationDecision = "allow" | "review" | "block";
</code></pre>
<p><code>allow</code> means the currently available evidence satisfies your policy. <code>block</code> means the evidence supports rejection. <code>review</code> preserves uncertainty or contextual risk.</p>
<p>This maps several SMTP and risk states without pretending they are equivalent:</p>
<table>
<thead>
<tr>
<th>Verifier state</th>
<th>Suggested application state</th>
<th>Why</th>
</tr>
</thead>
<tbody><tr>
<td><code>safe</code></td>
<td>allow</td>
<td>Positive mailbox and risk evidence, subject to policy</td>
</tr>
<tr>
<td><code>invalid</code> or <code>disabled</code></td>
<td>block</td>
<td>Strong negative evidence</td>
</tr>
<tr>
<td><code>disposable</code> or <code>spamtrap</code></td>
<td>block or review</td>
<td>Depends on collection policy</td>
</tr>
<tr>
<td><code>role_account</code></td>
<td>review</td>
<td>Often legitimate, but use-case dependent</td>
</tr>
<tr>
<td><code>catch_all</code></td>
<td>review</td>
<td>Domain may accept arbitrary recipients</td>
</tr>
<tr>
<td><code>inbox_full</code></td>
<td>review</td>
<td>Mailbox may exist but delivery can fail temporarily</td>
</tr>
<tr>
<td><code>unknown</code></td>
<td>review</td>
<td>Evidence is insufficient, not negative</td>
</tr>
</tbody></table>
<p>The table is a starting point. It is not universal business logic.</p>
<h2>Catch-all is a different kind of uncertainty</h2>
<p>A catch-all mail system accepts messages for recipients that may not exist. This configuration is useful for organizations that do not want to lose mail sent to mistyped aliases, but it frustrates mailbox-level verification.</p>
<p>Suppose a server accepts all of these:</p>
<pre><code class="language-txt">sales@example.com
alice@example.com
definitely-not-a-real-person-92831@example.com
</code></pre>
<p>The SMTP acceptance signal proves something about the domain’s behavior. It does not prove that <code>alice</code> has a real inbox.</p>
<p>Marking every catch-all result “valid” inflates confidence. Marking every one “invalid” throws away legitimate corporate contacts. Preserve the state and let the use case decide.</p>
<h2><code>unknown</code> should survive normalization</h2>
<p>Provider integrations often lose uncertainty in their adapter layer:</p>
<pre><code class="language-js">// Do not do this.
return Boolean(providerResult.is_deliverable);
</code></pre>
<p>If the provider omitted the field, returned <code>null</code>, or reported <code>unknown</code>, the Boolean conversion collapses all of that into <code>false</code>.</p>
<p>Use an explicit normalizer:</p>
<pre><code class="language-js">function normalizeStatus(result = {}) {
  const status = String(result.status ?? "unknown").toLowerCase();

  if (["safe"].includes(status) &amp;&amp; result.is_safe_to_send === true) {
    return "allow";
  }

  if (
    ["invalid", "disabled", "spamtrap"].includes(status) ||
    result.is_valid_syntax === false
  ) {
    return "block";
  }

  return "review";
}
</code></pre>
<p>This default is intentionally conservative: unrecognized future statuses also go to review instead of silently becoming allow or block.</p>
<h2>Quick validation and deep verification are not interchangeable</h2>
<p>Fast verification is valuable in interactive paths. It can evaluate syntax, disposable status, role accounts, MX records and whether a domain appears able to receive mail.</p>
<p>What it cannot do is infer an individual mailbox from domain health alone.</p>
<p>Deep or Power-mode verification adds SMTP and mailbox signals where possible. The receiving server controls part of the latency and observability, so a deep check can take longer and can still be inconclusive.</p>
<p>A practical architecture is:</p>
<pre><code class="language-txt">signup request
  -&gt; local syntax hygiene
  -&gt; quick API check
  -&gt; create account in pending state
  -&gt; send confirmation link
  -&gt; optional deep check asynchronously
</code></pre>
<p>For a CRM import, the sequence changes:</p>
<pre><code class="language-txt">import rows
  -&gt; deduplicate
  -&gt; create bulk verification task
  -&gt; poll in background
  -&gt; segment safe / review / block
  -&gt; preserve original evidence
</code></pre>
<p>The product decision stays separate from the transport check.</p>
<h2>Build retry semantics around uncertainty</h2>
<p>An <code>unknown</code> result can be temporary. That does not justify an immediate tight retry loop.</p>
<p>Use bounded exponential backoff:</p>
<pre><code class="language-js">const sleep = (ms) =&gt; new Promise((resolve) =&gt; setTimeout(resolve, ms));

async function retryUnknown(check, maxAttempts = 3) {
  let lastResult;

  for (let attempt = 0; attempt &lt; maxAttempts; attempt += 1) {
    lastResult = await check();
    if (lastResult.status !== "unknown") return lastResult;

    const delay = Math.min(15_000 * 2 ** attempt, 120_000);
    await sleep(delay);
  }

  return lastResult;
}
</code></pre>
<p>In a real service, move this work to a queue. Persist the attempt count and next-run timestamp so a process restart does not reset the loop. Honor API rate limits and do not use retries to probe a server aggressively.</p>
<h2>Verification errors are not address errors</h2>
<p>The same principle applies to your own integration. If the verification API returns a transient <code>5xx</code>, your application learned nothing about the mailbox.</p>
<p>Do not write this:</p>
<pre><code class="language-js">try {
  return await verify(email);
} catch {
  return { valid: false };
}
</code></pre>
<p>Return a separate operational state:</p>
<pre><code class="language-js">try {
  const evidence = await verify(email);
  return { checked: true, evidence, decision: normalizeStatus(evidence) };
} catch {
  return {
    checked: false,
    decision: "review",
    reason: "verification unavailable",
  };
}
</code></pre>
<p>Now your monitoring can distinguish “we observed a bad address” from “our dependency failed.”</p>
<h2>Measure decisions against later outcomes</h2>
<p>The most useful calibration data comes after verification.</p>
<p>Track whether each category later:</p>
<ul>
<li>confirms ownership;</li>
<li>hard-bounces or soft-bounces;</li>
<li>complains or unsubscribes;</li>
<li>becomes an active user;</li>
<li>remains unknown after a bounded retry.</li>
</ul>
<p>Do this with privacy controls and retention limits. You are not trying to build a permanent address dossier. You are testing whether your policy produces the intended product outcome.</p>
<p>If many legitimate users in <code>review</code> successfully confirm their inboxes, an automatic block would have been costly. If a segment marked <code>safe</code> still hard-bounces at an unusual rate, investigate data age, provider behavior and your sending system before increasing the threshold blindly.</p>
<h2>Verification cannot answer questions it was not designed to answer</h2>
<p>SMTP evidence does not establish consent. It does not override unsubscribe or suppression data. It does not guarantee inbox placement. It does not replace bounce processing, SPF, DKIM or DMARC.</p>
<p>Those boundaries are not reasons to avoid verification. They are the conditions for using it well.</p>
<h2>Try the model with real responses</h2>
<p>The <a href="https://rapidapi.com/farazahmad759/api/email-verify-api1?utm_source=hashnode&amp;utm_medium=article&amp;utm_campaign=article_email_verify">Email Verification API on RapidAPI</a> returns syntax, MX, SMTP and risk evidence through quick and power modes, with an asynchronous bulk workflow. The Basic plan includes 50 requests per month.</p>
<p>For the complete request code and bulk-polling example, see the <a href="https://stadiasoft.com/email-validation-api-nodejs/?utm_source=hashnode&amp;utm_medium=article&amp;utm_campaign=article_email_verify">Node.js email validation guide on StadiaSoft</a>.</p>
<p>Test with addresses you control. Preserve the raw evidence. Make <code>unknown</code> visible in logs and metrics. Then let your product—not an accidental Boolean conversion—decide what happens next.</p>
<blockquote>
<p>Disclosure: I publish the RapidAPI product referenced above. This article focuses on a protocol limitation that applies across providers and deliberately avoids delivery guarantees.</p>
</blockquote>
<h2>References</h2>
<ul>
<li><a href="https://www.rfc-editor.org/rfc/rfc5321.html">RFC 5321: Simple Mail Transfer Protocol</a></li>
<li><a href="https://nodejs.org/docs/latest/api/globals.html">Node.js global API documentation</a></li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Why Website Technology Detectors Disagree — and How Confidence Scoring Helps]]></title><description><![CDATA[Two technology detectors can inspect the same website and return different answers. One reports WordPress and Cloudflare. Another adds WooCommerce, Google Analytics, and jQuery. A third finds nothing.]]></description><link>https://stadiasoft.hashnode.dev/why-website-technology-detectors-disagree-and-how-confidence-scoring-helps</link><guid isPermaLink="true">https://stadiasoft.hashnode.dev/why-website-technology-detectors-disagree-and-how-confidence-scoring-helps</guid><category><![CDATA[Node.js]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[Security]]></category><category><![CDATA[Testing]]></category><dc:creator><![CDATA[Faraz Ahmad]]></dc:creator><pubDate>Fri, 18 Sep 2026 06:44:34 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6aab0d547506f7a9dcc4585b/12d5f9e3-b10d-4b1c-8a51-0241bf2b8e49.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Two technology detectors can inspect the same website and return different answers. One reports WordPress and Cloudflare. Another adds WooCommerce, Google Analytics, and jQuery. A third finds nothing.</p>
<p>That does not automatically mean two of them are broken. Website technology detection is an inference problem: a detector observes public clues, decides how specific they are, and combines them into a result. The hard part is not finding text in HTML. It is deciding what that text proves.</p>
<p>In this tutorial, we will build the small but important core of a detector in Node.js: an evidence-based rule engine with confidence scoring. The code works against already-collected response data, which keeps the detection logic testable and separates it from the security-sensitive job of fetching arbitrary URLs.</p>
<h2>The problem with Boolean fingerprints</h2>
<p>A naive rule usually looks like this:</p>
<pre><code class="language-js">if (html.includes("wp-content")) {
  technologies.push("WordPress");
}
</code></pre>
<p>This works on many WordPress sites, but it also creates awkward edge cases:</p>
<ul>
<li>A blog post can mention <code>/wp-content/</code> without running WordPress.</li>
<li>A reverse proxy can cache old asset URLs after a migration.</li>
<li>A site can hide generator tags while still serving WordPress assets.</li>
<li>One page can embed a widget from a technology that the rest of the site does not use.</li>
</ul>
<p>A Boolean result discards the reasoning. Downstream code sees only <code>WordPress: true</code>, so it cannot distinguish a strong combination of signals from one weak match.</p>
<p>Instead, we will represent every detection as evidence.</p>
<h2>Model fingerprints as weighted evidence</h2>
<p>Start with a small technology catalog. Each rule describes where to look, what to match, and how much confidence that signal deserves.</p>
<pre><code class="language-js">const fingerprints = [
  {
    name: "WordPress",
    category: "CMS",
    rules: [
      {
        source: "meta",
        key: "generator",
        pattern: /wordpress(?:\s+([\d.]+))?/i,
        weight: 75,
        description: "WordPress generator metadata",
      },
      {
        source: "html",
        pattern: /\/wp-content\//i,
        weight: 45,
        description: "WordPress content path",
      },
      {
        source: "html",
        pattern: /\/wp-includes\//i,
        weight: 45,
        description: "WordPress core asset path",
      },
    ],
  },
  {
    name: "Next.js",
    category: "JavaScript framework",
    rules: [
      {
        source: "html",
        pattern: /id=["']__next["']/i,
        weight: 70,
        description: "Next.js root element",
      },
      {
        source: "html",
        pattern: /\/_next\/static\//i,
        weight: 65,
        description: "Next.js static asset path",
      },
      {
        source: "header",
        key: "x-powered-by",
        pattern: /^next\.js$/i,
        weight: 85,
        description: "Next.js response header",
      },
    ],
  },
  {
    name: "Cloudflare",
    category: "CDN",
    rules: [
      {
        source: "header",
        key: "server",
        pattern: /^cloudflare$/i,
        weight: 90,
        description: "Cloudflare server header",
      },
      {
        source: "header",
        key: "cf-ray",
        pattern: /.+/,
        weight: 95,
        description: "Cloudflare request identifier",
      },
    ],
  },
];
</code></pre>
<p>The weights are not probabilities. They express the relative strength of each public clue. A vendor-specific response header is normally more persuasive than a generic string in the body.</p>
<h2>Normalize the response before matching</h2>
<p>The detector should not make every rule understand raw headers or HTML parsing. Convert the fetched response into one predictable shape first.</p>
<pre><code class="language-js">function normalizePage({ html = "", headers = {}, meta = {} }) {
  return {
    html: String(html),
    headers: Object.fromEntries(
      Object.entries(headers).map(([key, value]) =&gt; [
        key.toLowerCase(),
        String(value),
      ]),
    ),
    meta: Object.fromEntries(
      Object.entries(meta).map(([key, value]) =&gt; [
        key.toLowerCase(),
        String(value),
      ]),
    ),
  };
}

function valueForRule(page, rule) {
  if (rule.source === "html") return page.html;
  if (rule.source === "header") return page.headers[rule.key] ?? "";
  if (rule.source === "meta") return page.meta[rule.key] ?? "";
  return "";
}
</code></pre>
<p>In a production crawler, extract metadata with an HTML parser rather than regular expressions. The rule engine should receive clean inputs regardless of how they were collected.</p>
<h2>Return the match, not just the name</h2>
<p>Now evaluate every rule and preserve the evidence that matched.</p>
<pre><code class="language-js">function evaluateTechnology(page, technology) {
  const evidence = [];

  for (const rule of technology.rules) {
    const value = valueForRule(page, rule);
    const match = value.match(rule.pattern);

    if (!match) continue;

    evidence.push({
      source: rule.source,
      key: rule.key ?? null,
      description: rule.description,
      matched: match[0].slice(0, 160),
      weight: rule.weight,
      capturedVersion: match[1] ?? null,
    });
  }

  if (evidence.length === 0) return null;

  return {
    name: technology.name,
    category: technology.category,
    evidence,
  };
}
</code></pre>
<p>Truncating <code>matched</code> prevents a result from echoing a huge header or HTML fragment. It also makes logs easier to inspect.</p>
<h2>Combine independent signals without pretending certainty</h2>
<p>Adding weights directly creates inflated scores. Two 70-point rules should not produce 140% confidence. A useful alternative is diminishing returns: each new signal contributes only to the confidence that remains.</p>
<pre><code class="language-js">function combineWeights(weights) {
  let remainingUncertainty = 1;

  for (const weight of weights) {
    const bounded = Math.max(0, Math.min(99, weight)) / 100;
    remainingUncertainty *= 1 - bounded;
  }

  return Math.min(99, Math.round((1 - remainingUncertainty) * 100));
}

function detectTechnologies(rawPage) {
  const page = normalizePage(rawPage);

  return fingerprints
    .map((technology) =&gt; evaluateTechnology(page, technology))
    .filter(Boolean)
    .map((result) =&gt; ({
      ...result,
      confidence: combineWeights(result.evidence.map((item) =&gt; item.weight)),
      version:
        result.evidence.find((item) =&gt; item.capturedVersion)
          ?.capturedVersion ?? null,
    }))
    .sort((a, b) =&gt; b.confidence - a.confidence);
}
</code></pre>
<p>For example, independent signals weighted at 45 and 45 produce a combined score of 70, not 90. A 95-point vendor header remains very strong without being represented as absolute certainty.</p>
<p>This formula is still a heuristic. The important engineering property is that it is explicit, bounded, and testable.</p>
<h2>Try it with a fixture</h2>
<pre><code class="language-js">const fixture = {
  html: `
    &lt;!doctype html&gt;
    &lt;html&gt;
      &lt;head&gt;
        &lt;meta name="generator" content="WordPress 6.8.1"&gt;
        &lt;link rel="stylesheet" href="/wp-content/themes/example/style.css"&gt;
      &lt;/head&gt;
      &lt;body&gt;Example&lt;/body&gt;
    &lt;/html&gt;
  `,
  meta: {
    generator: "WordPress 6.8.1",
  },
  headers: {
    server: "cloudflare",
    "cf-ray": "example-request-id",
  },
};

console.dir(detectTechnologies(fixture), { depth: null });
</code></pre>
<p>The result contains both conclusions and their basis:</p>
<pre><code class="language-json">[
  {
    "name": "Cloudflare",
    "category": "CDN",
    "confidence": 99,
    "version": null,
    "evidence": [
      {
        "source": "header",
        "key": "server",
        "description": "Cloudflare server header",
        "matched": "cloudflare",
        "weight": 90,
        "capturedVersion": null
      },
      {
        "source": "header",
        "key": "cf-ray",
        "description": "Cloudflare request identifier",
        "matched": "example-request-id",
        "weight": 95,
        "capturedVersion": null
      }
    ]
  },
  {
    "name": "WordPress",
    "category": "CMS",
    "confidence": 86,
    "version": "6.8.1",
    "evidence": [
      {
        "source": "meta",
        "key": "generator",
        "description": "WordPress generator metadata",
        "matched": "WordPress 6.8.1",
        "weight": 75,
        "capturedVersion": "6.8.1"
      },
      {
        "source": "html",
        "key": null,
        "description": "WordPress content path",
        "matched": "/wp-content/",
        "weight": 45,
        "capturedVersion": null
      }
    ]
  }
]
</code></pre>
<p>A consumer can now choose a threshold appropriate to its use case. A developer tool might display every match above 50. An automated CRM routing rule might require 90. A security workflow should generally require independent verification rather than trusting fingerprinting alone.</p>
<h2>Test the uncertainty, not only the happy path</h2>
<p>Fingerprint tests should include negative and ambiguous cases.</p>
<pre><code class="language-js">import test from "node:test";
import assert from "node:assert/strict";

test("combines independent WordPress evidence", () =&gt; {
  const result = detectTechnologies({
    html: '&lt;link href="/wp-content/app.css"&gt;',
    meta: { generator: "WordPress 6.8.1" },
  });

  assert.equal(result[0].name, "WordPress");
  assert.equal(result[0].confidence, 86);
  assert.equal(result[0].version, "6.8.1");
});

test("does not infer WordPress from the word alone", () =&gt; {
  const result = detectTechnologies({
    html: "We help teams migrate away from WordPress.",
  });

  assert.deepEqual(result, []);
});

test("returns a strong result for a specific CDN header", () =&gt; {
  const result = detectTechnologies({
    headers: { "cf-ray": "example-request-id" },
  });

  assert.equal(result[0].name, "Cloudflare");
  assert.equal(result[0].confidence, 95);
});
</code></pre>
<p>Good fixtures include:</p>
<ul>
<li>a genuine match with several independent clues;</li>
<li>a single weak clue;</li>
<li>editorial text containing a product name;</li>
<li>stale or contradictory signals;</li>
<li>uppercase and lowercase header variations;</li>
<li>an empty body and missing headers;</li>
<li>markup intentionally trying to resemble another platform.</li>
</ul>
<p>When a false positive reaches production, turn it into a permanent regression fixture before adjusting the rule.</p>
<h2>Separate detection from fetching</h2>
<p>This tutorial begins with response data on purpose. Fetching arbitrary user-supplied URLs introduces server-side request forgery risk.</p>
<p>A production fetcher needs to do more than reject <code>localhost</code>. At minimum, it should:</p>
<ol>
<li>Accept only HTTP and HTTPS.</li>
<li>Restrict ports unless the product explicitly supports others.</li>
<li>Reject IP literals and local hostnames.</li>
<li>Resolve DNS and block private, loopback, link-local, multicast, and reserved addresses.</li>
<li>Validate every redirect destination again.</li>
<li>Defend against DNS rebinding between validation and connection.</li>
<li>Limit redirects, response size, and total request time.</li>
<li>Avoid returning raw page content or sensitive headers unnecessarily.</li>
</ol>
<p>Do not treat a URL regular expression as an SSRF defense. Fetching and fingerprinting should be separate modules with separate tests and threat models.</p>
<h2>Why detectors still disagree</h2>
<p>Even with good rules, results vary because tools make different choices:</p>
<ul>
<li><strong>Coverage:</strong> each catalog recognizes a different set of products.</li>
<li><strong>Crawl depth:</strong> one tool checks only the homepage while another follows internal pages.</li>
<li><strong>Execution:</strong> some tools render JavaScript; others inspect the initial response.</li>
<li><strong>Timing:</strong> consent banners and personalization can change which scripts load.</li>
<li><strong>Thresholds:</strong> one product exposes weak matches while another suppresses them.</li>
<li><strong>History:</strong> a database may retain technologies from an earlier crawl.</li>
<li><strong>Evidence policy:</strong> tools disagree about whether one third-party script proves adoption.</li>
</ul>
<p>The right question is not “Which detector returns the longest list?” It is “Which detector explains enough evidence for my decision?”</p>
<h2>What I would add next</h2>
<p>The rule engine above is intentionally small. A production implementation would also need:</p>
<ul>
<li>fingerprint versioning and change reviews;</li>
<li>rule-specific exclusions;</li>
<li>correlation rules for dependent signals;</li>
<li>a benchmark set of known sites and saved fixtures;</li>
<li>observed timestamps and request identifiers;</li>
<li>per-category precision and recall measurements;</li>
<li>caching and rate controls;</li>
<li>a clear distinction between “not detected” and “confirmed absent.”</li>
</ul>
<p>That last distinction matters. Public fingerprinting can observe clues; it cannot prove that a hidden backend technology is absent.</p>
<h2>Closing thought</h2>
<p>Confidence scoring does not turn fingerprinting into certainty. It makes uncertainty visible.</p>
<p>Once evidence is part of the response, developers can inspect false positives, choose thresholds, preserve an audit trail, and avoid treating a weak string match as a fact. That is a much stronger foundation than a growing list of regular expressions returning unexplained names.</p>
<hr />
<p><strong>Disclosure:</strong> I maintain a hosted website-technology detector that uses this evidence-first model. Developers who prefer a managed endpoint can evaluate it on <a href="https://rapidapi.com/farazahmad759/api/website-technology-stack-and-cms-detector?utm_source=hashnode&amp;utm_medium=article&amp;utm_campaign=evidence_scoring&amp;utm_content=disclosure">RapidAPI</a>. A longer comparison of real-time lookup APIs and historical technographic databases is available in my <a href="https://stadiasoft.com/builtwith-alternative-crm-enrichment/">StadiaSoft implementation guide</a>.</p>
<h3>References</h3>
<ul>
<li><a href="https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html">OWASP Server-Side Request Forgery Prevention Cheat Sheet</a></li>
<li><a href="https://nodejs.org/api/test.html">Node.js test runner documentation</a></li>
<li><a href="https://hashnode.com/code-of-conduct">Hashnode Code of Conduct</a></li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Build an SPF, DKIM & DMARC Checker API with Node.js]]></title><description><![CDATA[An SPF, DKIM and DMARC checker needs to do more than report whether three DNS records exist. A domain can publish SPF yet have no DMARC policy, sign mail with DKIM but use an unexpected selector, or h]]></description><link>https://stadiasoft.hashnode.dev/build-an-spf-dkim-dmarc-checker-api-with-node-js</link><guid isPermaLink="true">https://stadiasoft.hashnode.dev/build-an-spf-dkim-dmarc-checker-api-with-node-js</guid><category><![CDATA[Node.js]]></category><category><![CDATA[api]]></category><category><![CDATA[DMARC]]></category><category><![CDATA[Security]]></category><category><![CDATA[email]]></category><dc:creator><![CDATA[Faraz Ahmad]]></dc:creator><pubDate>Thu, 17 Sep 2026 21:15:10 GMT</pubDate><content:encoded><![CDATA[<p><img src="https://stadiasoft.com/wp-content/uploads/2026/09/stadiasoft-spf-dkim-dmarc-nodejs-featured.png" alt="Email passing through SPF, DKIM and DMARC security checks in a Node.js API workflow" /></p>
<p>An SPF, DKIM and DMARC checker needs to do more than report whether three DNS records exist. A domain can publish SPF yet have no DMARC policy, sign mail with DKIM but use an unexpected selector, or have strong authentication records while missing transport-security controls.</p>
<p>That makes a useful mail-security audit more than a set of three DNS lookups. It needs to collect evidence, distinguish a confirmed failure from an unknown result, and return fixes in a sensible order.</p>
<p>This guide shows how to build an API-based email security workflow that audits MX, SPF, DKIM, DMARC, MTA-STS, TLS-RPT and BIMI from Node.js. It also explains what the result can—and cannot—tell you about deliverability.</p>
<h2>What each mail-security check tells you</h2>
<table>
<thead>
<tr>
<th>Check</th>
<th>What it answers</th>
<th>Audit weight</th>
</tr>
</thead>
<tbody><tr>
<td>MX</td>
<td>Can other mail systems find this domain's mail exchangers?</td>
<td>20</td>
</tr>
<tr>
<td>SPF</td>
<td>Which servers are authorized to send for the domain?</td>
<td>20</td>
</tr>
<tr>
<td>DMARC</td>
<td>What should receivers do when alignment fails, and where should reports go?</td>
<td>25</td>
</tr>
<tr>
<td>DKIM</td>
<td>Can a receiver verify a message's cryptographic signature?</td>
<td>15</td>
</tr>
<tr>
<td>MTA-STS</td>
<td>Does the domain publish an HTTPS policy requiring secure SMTP transport?</td>
<td>10</td>
</tr>
<tr>
<td>TLS-RPT</td>
<td>Where should TLS delivery failures be reported?</td>
<td>5</td>
</tr>
<tr>
<td>BIMI</td>
<td>Does the domain publish a brand-indicator record?</td>
<td>5</td>
</tr>
</tbody></table>
<p>The first four controls are the core of an email-authentication review. The remaining controls improve transport visibility, policy enforcement and brand signaling.</p>
<p>One important distinction: SPF validates an envelope sender, DKIM validates a signed message, and DMARC evaluates alignment between an authenticated identity and the address visible to the recipient. A record merely existing does not guarantee that all legitimate mail passes.</p>
<h2>Audit a domain from Node.js</h2>
<p>The <a href="https://rapidapi.com/farazahmad759/api/domain-mail-security-and-deliverability-audit?utm_source=hashnode&amp;utm_medium=article&amp;utm_campaign=spf_dkim_dmarc_node">Domain Mail Security &amp; Deliverability Audit API</a> supports a fast GET request and a JSON POST request. In RapidAPI, copy your exact host value from the marketplace code snippet rather than hard-coding the placeholder below.</p>
<pre><code class="language-js">const domain = "example.com";
const selectors = ["google", "selector1"];

const params = new URLSearchParams({
  domain,
  dkim_selectors: selectors.join(","),
});

const response = await fetch(
  `https://${process.env.RAPIDAPI_HOST}/api/v1/mail-security/audit?${params}`,
  {
    headers: {
      "x-rapidapi-key": process.env.RAPIDAPI_KEY,
      "x-rapidapi-host": process.env.RAPIDAPI_HOST,
    },
  },
);

if (!response.ok) {
  const body = await response.text();
  throw new Error(`Mail-security audit failed (${response.status}): ${body}`);
}

const audit = await response.json();
console.log({
  domain: audit.domain,
  score: audit.score,
  summary: audit.summary,
  findings: audit.findings,
});
</code></pre>
<p>Keep the RapidAPI key in an environment variable or secret manager. Do not commit it to your repository or expose it in browser-side JavaScript.</p>
<p>The API returns the domain, audit timing, score and grade, individual checks, a summary, and prioritized findings. That structure makes the result suitable for dashboards, onboarding rules, scheduled monitoring and remediation tickets.</p>
<h2>Why DKIM needs special handling</h2>
<p>You can look up SPF at the root domain and DMARC at <code>_dmarc.example.com</code>. DKIM is different. Its DNS name includes a selector:</p>
<pre><code class="language-text">selector._domainkey.example.com
</code></pre>
<p>The selector is chosen by the sending provider or mail administrator. DNS does not publish a universal index of every selector a domain uses, so an auditor cannot reliably discover all of them.</p>
<p>If you know the provider, pass likely selectors with the request. For example, Google Workspace often uses <code>google</code>, while other providers or self-managed systems may use values such as <code>selector1</code>, <code>selector2</code>, or a date-based selector.</p>
<p>When no tested selector resolves, the responsible result is <code>unknown</code>, not <code>fail</code>. The score should also be treated as provisional. A definitive DKIM failure requires evidence from an actual signed message or the selector configured by the sender.</p>
<h2>Turn findings into a repair plan</h2>
<p>A raw DNS dump is not a remediation plan. Triage the findings in this order:</p>
<ol>
<li><strong>Restore mail routing first.</strong> Missing or broken MX records can prevent normal delivery.</li>
<li><strong>Fix SPF syntax and authorization.</strong> Keep one SPF record, remove obsolete senders, and stay within SPF's DNS-lookup constraints.</li>
<li><strong>Publish DMARC and improve it gradually.</strong> Start with reporting if necessary, study legitimate sources, then move toward quarantine or reject when alignment is stable.</li>
<li><strong>Verify DKIM with the correct selectors.</strong> Confirm every active sending platform, not just the primary provider.</li>
<li><strong>Add MTA-STS and TLS-RPT together.</strong> The policy can require secure transport; reporting provides visibility into failures.</li>
<li><strong>Treat BIMI as an optional final layer.</strong> It depends on strong email authentication and may involve additional brand or certificate requirements.</li>
</ol>
<p>This ordering reduces the chance of tightening policy before legitimate senders are accounted for.</p>
<h2>Use the score as a signal, not a verdict</h2>
<p>A mail-security score is useful for comparing domains, tracking configuration work and routing high-risk results for review. It is not proof that messages will reach the inbox.</p>
<p>Inbox placement also depends on factors that a DNS and HTTPS posture audit cannot observe, including sender reputation, complaint rates, bounce history, content, sending cadence, recipient engagement and per-message alignment.</p>
<p>The audit described here checks public DNS records and the standard MTA-STS HTTPS policy endpoint. It does not connect to mail exchangers, send test messages, inspect private provider settings or recursively expand every possible SPF include. For deliverability decisions, combine configuration evidence with message headers, provider telemetry and controlled sending tests.</p>
<h2>Practical ways to use the result</h2>
<ul>
<li><strong>Agency audits:</strong> generate a repeatable technical baseline before recommending email changes.</li>
<li><strong>SaaS onboarding:</strong> warn customers about missing authentication before they connect a sending domain.</li>
<li><strong>Vendor screening:</strong> identify domains with weak or incomplete mail-security posture.</li>
<li><strong>Scheduled monitoring:</strong> detect when records disappear or policies regress.</li>
<li><strong>Support triage:</strong> attach structured evidence and prioritized fixes to a ticket.</li>
</ul>
<p>For monitoring, save both the normalized finding codes and the underlying evidence. Alert on meaningful state changes, not small score movements alone.</p>
<h2>SPF, DKIM and DMARC checker FAQ</h2>
<h3>What does an SPF, DKIM and DMARC checker test?</h3>
<p>It looks up the public DNS records used for sender authorization, message signing and authentication policy. A deeper audit can also check MX routing, MTA-STS, TLS-RPT and BIMI, then return the evidence and recommended fixes.</p>
<h3>Can a checker find every DKIM record automatically?</h3>
<p>No. A DKIM lookup requires the selector used by the sending system, and DNS does not publish a complete list of selectors. Test known provider selectors or inspect the <code>DKIM-Signature</code> header of a real message.</p>
<h3>Does passing SPF, DKIM and DMARC guarantee email deliverability?</h3>
<p>No. These controls support authentication, but inbox placement also depends on sender reputation, complaints, bounces, content, volume patterns, engagement and per-message alignment.</p>
<h3>Can I automate checks with an API?</h3>
<p>Yes. A server-side checker API can run during customer onboarding, domain verification, vendor screening or scheduled monitoring. Store both the normalized status and the underlying evidence so changes can be reviewed.</p>
<h2>Start with a free audit</h2>
<p>The <a href="https://rapidapi.com/farazahmad759/api/domain-mail-security-and-deliverability-audit?utm_source=hashnode&amp;utm_medium=article&amp;utm_campaign=spf_dkim_dmarc_node">Domain Mail Security &amp; Deliverability Audit API</a> checks seven controls in one request. The Basic plan includes <strong>50 requests per month</strong>, so you can test the complete response before choosing a paid plan.</p>
<p>Run a domain in the RapidAPI playground, inspect the evidence behind each check, and use the findings—not just the score—to decide what to fix first.</p>
<hr />
<p><em>Originally published on <a href="https://stadiasoft.com/spf-dkim-dmarc-checker-api-nodejs/">StadiaSoft</a>.</em></p>
]]></content:encoded></item></channel></rss>