Go Email Integration: How to Diagnose DKIM Verification Failures in 2026
For a developer tools service, resolve DKIM verification failures at the sending-domain boundary, and suppress a recipient only when the delivery evidence supports a recipient-level decision. Mixing those two decisions can turn a signing configuration mistake into an expanding list of incorrectly blocked developers.
TL;DR: inspect the actual message signature and the receiving system's authentication result, reconcile the selector with DNS, and then investigate signed-content changes. Keep this investigation separate from bounce classification. The decision below favors a small normalized evidence model because integration effort increases whenever provider-specific authentication labels leak into recipient state.
How should an email service handle DKIM verification failures?
The invariant is simple: a failed authentication check must not, by itself, mark an address invalid. RFC 6376 defines DKIM as a mechanism through which a signing domain claims responsibility for a message; it does not establish that the recipient exists, that the visible author is trustworthy, or that delivery will succeed. SPF evaluates a different identity and authorization relationship under RFC 7208. A passing SPF result therefore does not repair a failing DKIM signature.
Keep those boundaries.
Consider a proposed deployment-notification service that sends build failure alerts to developers. Its evidence record should distinguish a message identifier, recipient, signing domain, selector, authentication result, delivery status, and ingestion identifier. These are separate fields because an investigation may discover a common selector error across many messages while finding an invalid recipient in only one delivery attempt. Storing a single “email failed” flag discards the information needed to reconcile those cases.
An exactly-once mindset belongs at the state-transition boundary: one authenticated event identifier should cause at most one committed transition, with an audit record explaining the decision. It does not justify claiming exactly-once transport. Duplicate delivery, delayed feedback, and a failure between persistence and acknowledgment must remain explicit failure boundaries in the design.
Step 1: collect the signature before changing DNS
Start with a received message's original headers and body, rather than a screenshot of a domain setup page. Under RFC 6376, the signature's d= identifies the signing domain and s= identifies the selector; the public-key lookup uses the selector followed by ._domainkey. and that domain. For the illustrative values d=notify.example.org and s=release, investigate release._domainkey.notify.example.org. Do not substitute the visible From domain merely because it looks related.
Record the receiving system's result alongside that signature. RFC 8601 defines Authentication-Results, including its trust-boundary considerations: an arbitrary header supplied by an untrusted sender is not trustworthy evidence of verification. For this workflow, accept authentication results only from the receiving infrastructure whose assessment you intend to rely on.
The order of investigation matters. First establish whether the receiver found a usable key at the correct owner name. Then compare the published key with the key associated with the sender's active selector. Finally inspect canonicalization and whether signed headers or the body changed after signing. RFC 6376 specifies both body-hash verification and signature verification, so a body-hash mismatch is a reason to investigate message transformation; it is not sufficient evidence that the DNS record is wrong.
A selector is evidence, not decoration.
DNS TXT records also require care: RFC 6376 specifies concatenating character strings within a TXT resource record before use. Treating those strings as separate keys can produce a misleading diagnosis. When a record changes, retain the queried name, observation time, and resolver context in the investigation record; a subsequent observation should establish whether the receiver can see the intended record before the domain is declared healthy. Avoid promising a universal propagation interval.
Step 2: choose where evidence becomes recipient state
The operational decision is where to normalize feedback. This comparison concerns implementation choices for the notification service, rather than commercial products.
| Approach | Integration work | Failure boundary | Suitable use |
|---|---|---|---|
| Translate authenticated delivery events into a local evidence model | Write and test one adapter per event source; keep application decisions stable | Incorrect translation can misclassify a delivery result | A service that needs recipient suppression and an explanation for each change |
| Parse delivery status notifications directly | Handle message structure, per-recipient results, and enhanced status codes | A message-level outcome can obscure different recipient outcomes | Teams that need access to standards-based delivery reports |
| Maintain suppression manually | Build little initial ingestion code; require an operator for each decision | Slow or inconsistent updates as event volume grows | Small investigations and exceptional corrections |
RFC 3464 describes delivery status notifications, and RFC 3463 defines enhanced status codes. The latter distinguishes address status from security or policy status: 5.1.1 means a bad destination mailbox address, while 5.7.1 means delivery is not authorized and the message was refused. Both are permanent-failure codes, but they do not support the same recipient-state transition.
That distinction is the critical integration contract. An adapter should retain the reported status and supporting context instead of converting every permanent failure into “invalid address.” A temporary failure should remain outside permanent suppression, and an unclassified rejection should enter investigation rather than silently acquiring a recipient-level meaning. The narrow rule used below intentionally sacrifices classification coverage for an auditable reason to suppress.
Step 3: implement the critical transition in Go
This complete Go program is a deterministic rehearsal of the decision boundary. Run it as a standalone program: it checks that a DKIM-related policy rejection leaves the recipient eligible, that an explicit bad-mailbox result suppresses it, and that replaying the same event adds no audit entry.
The input is already normalized evidence from a trusted adapter.
The example does not authenticate incoming webhooks or infer a delivery status from a free-text error. Those responsibilities precede this function. The trade-off is reduced classification coverage in exchange for a smaller decision surface: this implementation suppresses only the explicit mailbox-invalid status because treating every permanent failure as address evidence would conflate authentication policy with recipient validity. This approach is not suitable for deployments that require automatic handling of every address-related status; those deployments need additional reviewed classification rules and corresponding evidence fixtures before enabling suppression.
package main import "fmt" type Event struct { Source string ID string Recipient string DKIM string Status string } type EventKey struct { Source string ID string } type Decision struct { Event Event Action string Reason string } type Store struct { Seen map[EventKey]bool Suppressed map[string]bool Audit []Decision } func (s *Store) Apply(e Event) (bool, error) { if e.Source == "" || e.ID == "" || e.Recipient == "" { return false, fmt.Errorf("missing evidence identity") } key := EventKey{Source: e.Source, ID: e.ID} if s.Seen[key] { return false, nil } action := "retain" reason := "no recipient-invalidity evidence" if e.Status == "5.1.1" { action = "suppress" reason = "bad destination mailbox address" s.Suppressed[e.Recipient] = true } s.Audit = append(s.Audit, Decision{ Event: e, Action: action, Reason: reason, }) s.Seen[key] = true return true, nil } func main() { s := Store{ Seen: make(map[EventKey]bool), Suppressed: make(map[string]bool), } recipient := "build-owner@example.net" policy := Event{ Source: "delivery-adapter", ID: "notification-policy", Recipient: recipient, DKIM: "fail", Status: "5.7.1", } if _, err := s.Apply(policy); err != nil { panic(err) } if s.Suppressed[recipient] { panic("policy rejection suppressed a recipient") } invalid := Event{ Source: "delivery-adapter", ID: "notification-invalid-mailbox", Recipient: recipient, DKIM: "pass", Status: "5.1.1", } if _, err := s.Apply(invalid); err != nil { panic(err) } if !s.Suppressed[recipient] { panic("invalid mailbox was not suppressed") } before := len(s.Audit) changed, err := s.Apply(invalid) if err != nil { panic(err) } if changed || len(s.Audit) != before { panic("replayed event changed state") } fmt.Println("policy retained; mailbox suppressed; replay ignored") } The two different event identifiers matter. Deduplicating on recipient alone would discard later evidence about the same address. The source is part of the key because independent integrations need not share an event-ID namespace. The adapter contract must additionally require that a replay preserves the event's meaning; a reused identifier with a conflicting payload warrants investigation rather than an unrecorded overwrite.
The durability limitation is deliberate.
This in-memory implementation has no restart durability or concurrency protection. Before deployment, persist the deduplication key, recipient mutation, and audit entry in one transaction, enforce uniqueness on the key, and acknowledge ingestion only after commit. A queue acknowledgment cannot substitute for that transaction. If two workers can accept the same event concurrently, the database constraint must arbitrate their writes.
Use a recipient identifier scoped to the application's tenant and sending policy in that persistent design. The one-recipient example deliberately omits those dimensions; a shared deployment must define them before deciding whether one tenant's delivery result can affect another tenant's notifications. Likewise, checking suppression before enqueueing leaves a race with already queued messages, so the sending worker needs a current suppression check under the application's stated consistency policy.
Step 4: verify recovery and record the rejected shortcut
Deploy the adapter in observation mode first: record proposed transitions, compare them with the underlying delivery evidence, and enable writes only after policy rejections and invalid-mailbox outcomes remain distinguishable. Track authentication failures by signing domain and selector, while tracking suppression decisions by their evidence and reason. A sudden concentration under one selector calls for a signing investigation; it should not automatically change the recipient-validity rule.
Recovery requires fresh evidence.
Send a fresh message signed through the production path and obtain a trusted receiver's verification result. Rechecking DNS alone does not establish that the sender used the intended key or that a later processing stage preserved the signed content. For a build alert, verify both authentication evidence and the delivery outcome.
Neither proves that the developer read the notification.
The rejected shortcut is automatic suppression after every permanent rejection. Its valid use case is narrower: temporarily holding outbound work during investigation, with an explicit release policy and without labeling those addresses invalid. A hold limits sending while evidence is incomplete; permanent recipient suppression asserts something more specific.
Finally, keep the audit trail proportionate. RFC 6376 does not establish regulatory compliance, and authentication evidence alone cannot establish consent, authorization to retain recipient data, or a lawful retention period. Define access, retention, and deletion rules for identifiers and message evidence under the obligations applicable to the service. Preserve the decision reason without collecting complete message bodies by default.
The acceptance criterion is a reconciled transition: a known event, a defensible classification, a durable decision, and enough evidence to explain it later. That criterion keeps DKIM recovery and invalid-recipient suppression independently testable while containing the integration work.
References
- RFC 6376, DomainKeys Identified Mail Signatures: https://datatracker.ietf.org/doc/html/rfc6376
- RFC 7208, Sender Policy Framework: https://datatracker.ietf.org/doc/html/rfc7208
- RFC 8601, Message Header Field for Indicating Message Authentication Status: https://datatracker.ietf.org/doc/html/rfc8601
- RFC 3463, Enhanced Mail System Status Codes: https://datatracker.ietf.org/doc/html/rfc3463
- RFC 3464, An Extensible Message Format for Delivery Status Notifications: https://datatracker.ietf.org/doc/html/rfc3464