DEV Community

QuentinBarrett5281
QuentinBarrett5281

Posted on

Marketplace Transactional Email Deliverability: Node.js Domain Verification, DMARC Evidence and Bounce Polling

Short answer: treat transactional email as an evidence pipeline: verify the sending domain, publish SPF, DKIM, and DMARC, record every bounce, and suppress a recipient only after a repeatable rule has been evaluated. The useful unit at 3am is not a delivery percentage; it is a trace showing which message, policy, and recipient decision produced the page.

The incident starts before the bounce

In a marketplace, a seller order receipt and a buyer payment notice can be operationally important even when neither is legally classified as a statement. A misconfigured domain makes those messages look like spoofing. A permissive suppression rule then turns one malformed address into a silent loss of future receipts.

I start an incident review with one unfashionable question: what page fired? If the answer is “mail is down,” the alert is too broad to act on. Separate authentication failures, provider acceptance failures, mailbox rejection, and application-side queue delay. Each has a different owner and a different piece of evidence. I've been woken by alerts that meant nothing, and the fix is usually to name the missing signal, not to add another dashboard.

Name the signal.

The DNS side is a chain, not a checkbox. SPF authorizes envelope senders. DKIM signs the message and lets a receiver verify that selected headers and the body survived transit. DMARC evaluates alignment between the visible From domain and either SPF or DKIM, then publishes a reporting policy. RFC 7489 describes the policy and aggregate/forensic reporting model; it does not promise inbox placement.

How should Node.js teams verify a domain, SPF, DKIM, and DMARC before sending?

Keep domain verification outside the send path. A deployment job creates a random token, the operator publishes the requested TXT or CNAME record, and a verifier polls authoritative DNS until the exact value is visible. Cache a successful result with an expiry, but retain the raw lookup and timestamp as compliance evidence.

The same record should be checked from at least two resolvers during a change. Resolver disagreement is normal during propagation; accepting the first positive response hides that fact. Do not “fix” a missing record by lowering a threshold in application code. Mark the domain pending and keep the send queue bounded.

The sending service can be written in Node.js, Go, or another runtime. The protocol matters more than the SDK. Here is a deliberately small Go shape for a verifier result that can be stored beside a message audit record:

package main

import (
    "fmt"
    "net"
    "strings"
)

func txtContains(name, want string) (bool, error) {
    records, err := net.LookupTXT(name)
    if err != nil {
        return false, err
    }
    for _, record := range records {
        if strings.Contains(record, want) {
            return true, nil
        }
    }
    return false, nil
}

func main() {
    ok, err := txtContains("example-market.test", "v=spf1")
    fmt.Println(ok, err)
}
Enter fullscreen mode Exit fullscreen mode

That snippet is only a shape: production code must query the exact selector for DKIM, parse multiple TXT strings correctly, enforce timeouts, and persist the resolver identity. The evidence record should include domain, selector, expected value hash, observed value, resolver, and verification time. I am not sure any single mailbox provider exposes enough data to prove a message reached a human, so keep this boundary explicit.

What does a safe bounce suppression loop actually decide?

A bounce event is an input, not a verdict. Normalize the address, provider event ID, SMTP status, diagnostic text, message ID, and event time. Classify a permanent rejection (for example, a syntactically valid recipient that the destination says does not exist) separately from a transient deferral. Suppress permanent failures promptly; retry transient failures with bounded backoff and a maximum age.

Polling is useful when the event source is pull-based, but it must be idempotent. Store the provider event ID before applying the suppression mutation, or use a transaction that makes replay harmless. Keep the original payload for the retention period required by your compliance policy, with access controls and a deletion job that is itself auditable. Consider a worker that retries the same event four times after a timeout: if the event key is not unique, the suppression table records four decisions and the audit export becomes ambiguous. The durable fix is to make the event ID a uniqueness constraint, record the policy version with the decision, and expose the duplicate count as a metric. That gives an operator a bounded, explainable action when the next poll is late.

Small details matter.

One bad address should not suppress an entire household domain. Conversely, repeated hard bounces for the same normalized recipient should not remain eligible because a dashboard looks green. A simple decision table keeps the policy reviewable:

Signal Action Evidence retained
Permanent recipient rejection Suppress immediately Event ID, status, message ID
Temporary 4xx-style deferral Retry with cap Attempt count and next time
Authentication failure Pause sending for domain DNS snapshot and deployment ID
Complaint or abuse report Suppress and escalate Report ID and policy version

Verification, rollback, and the limits of dashboards

Before enabling a new domain, send a controlled message to test mailboxes and inspect Authentication-Results, From alignment, DKIM selector, and the receiving system's disposition. Confirm that a deliberate invalid recipient produces one suppression record, not a retry storm. Test the poller twice against the same event; the second run should change nothing.

Rollback means restoring the last known-good DNS records or disabling the affected sending identity, then draining or quarantining queued messages. Do not bulk-unsuppress recipients during an incident. Reversal should require a reason, an operator, and a link to the evidence that justified it.

The catch is that this runbook is not suitable when you need real-time per-message feedback and your event source only offers delayed polling; choose a webhook-capable integration or accept slower suppression. Stick with a simple DNS-and-poller design when auditability and portability matter more than sub-minute reaction time. Apple Mail Privacy Protection also makes open-rate signals noisy, so opens are a weak recovery metric; delivery and bounce evidence should drive the decision.

References

Top comments (0)