<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Paulo C</title>
    <description>The latest articles on DEV Community by Paulo C (@cannypigeons-pc).</description>
    <link>https://dev.to/cannypigeons-pc</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4046517%2F80dea233-d97a-4854-b949-4ca8c4a5ab48.png</url>
      <title>DEV Community: Paulo C</title>
      <link>https://dev.to/cannypigeons-pc</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/cannypigeons-pc"/>
    <language>en</language>
    <item>
      <title>"Our emails are going to spam" — a sysadmin's triage order</title>
      <dc:creator>Paulo C</dc:creator>
      <pubDate>Sun, 26 Jul 2026 05:39:17 +0000</pubDate>
      <link>https://dev.to/cannypigeons-pc/our-emails-are-going-to-spam-a-sysadmins-triage-order-55ij</link>
      <guid>https://dev.to/cannypigeons-pc/our-emails-are-going-to-spam-a-sysadmins-triage-order-55ij</guid>
      <description>&lt;p&gt;The ticket says "our emails are going to spam." It is one of the least actionable reports you will ever receive, because "spam" is the observable symptom of at least three unrelated failures, and the fix for one does nothing for the other two.&lt;/p&gt;

&lt;p&gt;The useful thing is a triage order. Rule out the cheap, binary, provable cause first, and only descend into the vague ones when you have eliminated the specific one. Most teams do this backwards — they start rewriting subject lines while their DKIM key has been broken for six weeks.&lt;/p&gt;

&lt;p&gt;Here is the order.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three causes
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Authentication&lt;/strong&gt; — SPF, DKIM and DMARC. Binary, free to check, fixable in an afternoon. Either the receiver can verify you sent the message or it can't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reputation&lt;/strong&gt; — what your sending IP and domain have done lately. Slow to fix, measurable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Content and engagement&lt;/strong&gt; — what's in the message and whether anyone wants it. The vaguest, the most over-blamed, and the last thing you should look at.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Cause 1 is the one you can settle in ten minutes with &lt;code&gt;dig&lt;/code&gt;. Start there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 0: get an actual failing message
&lt;/h2&gt;

&lt;p&gt;Before touching DNS, get the &lt;strong&gt;full headers&lt;/strong&gt; of one message that landed in spam. Not a screenshot, not a forward — a forward re-signs and rewrites everything you need to read. Ask for "show original" (Gmail) or "view message source" (Outlook) and have them paste the text.&lt;/p&gt;

&lt;p&gt;You want this header:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight email"&gt;&lt;code&gt;&lt;span class="nt"&gt;Authentication-Results&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="na"&gt; mx.google.com;
       dkim=pass header.i=@example.com header.s=selector1;
       spf=pass (google.com: domain of bounce@mail.example.com designates 1.2.3.4
            as permitted sender) smtp.mailfrom=bounce@mail.example.com;
       dmarc=fail (p=NONE sp=NONE dis=NONE) header.from=example.com&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read it carefully, because this exact shape is the single most common deliverability bug and it looks like success at a glance. &lt;strong&gt;SPF passed. DKIM passed. DMARC failed.&lt;/strong&gt; That's not a contradiction — it's an alignment failure, and I'll come back to it.&lt;/p&gt;

&lt;p&gt;If you can't get a real message, the &lt;a href="https://cannypigeons.com/tools/dmarc-checker" rel="noopener noreferrer"&gt;DMARC checker&lt;/a&gt; will read all three records for a domain from DNS and tell you what's published, which is enough to start.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cause 1: authentication
&lt;/h2&gt;

&lt;h3&gt;
  
  
  SPF
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;dig +short TXT example.com | &lt;span class="nb"&gt;grep &lt;/span&gt;spf1
&lt;span class="go"&gt;"v=spf1 include:_spf.google.com include:sendgrid.net ~all"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two failure modes matter, and neither is visible by eyeballing the record.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;More than ten DNS lookups.&lt;/strong&gt; RFC 7208 §4.6.4 caps evaluation at 10 DNS-querying mechanisms — &lt;code&gt;include&lt;/code&gt;, &lt;code&gt;a&lt;/code&gt;, &lt;code&gt;mx&lt;/code&gt;, &lt;code&gt;ptr&lt;/code&gt;, &lt;code&gt;exists&lt;/code&gt;, &lt;code&gt;redirect&lt;/code&gt; — counted &lt;em&gt;recursively&lt;/em&gt; through every include. Your record can be two lines long and still blow the limit, because a single vendor's include can spend five on its own. Over the limit, receivers return &lt;code&gt;permerror&lt;/code&gt;, and most treat that as an outright failure.&lt;/p&gt;

&lt;p&gt;This is the classic slow-motion outage: someone adds one more SaaS vendor, the record tips from 10 to 11, and mail that authenticated for years quietly stops. Nothing in your monitoring fires.&lt;/p&gt;

&lt;p&gt;I cleaned up a domain last week that was at 7 of 10 lookups, of which &lt;strong&gt;5 were spent on a vendor nobody had used in a year&lt;/strong&gt;. Removing one dead &lt;code&gt;include:&lt;/code&gt; took it to 2. Count yours: the &lt;a href="https://cannypigeons.com/tools/spf-checker" rel="noopener noreferrer"&gt;SPF checker&lt;/a&gt; resolves the whole tree, or write the recursion yourself, it's twenty lines.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two SPF records.&lt;/strong&gt; Publishing two &lt;code&gt;v=spf1&lt;/code&gt; TXT records is a permanent error, not a union. It happens when a second team adds a vendor without checking. Merge them.&lt;/p&gt;

&lt;h3&gt;
  
  
  DKIM
&lt;/h3&gt;

&lt;p&gt;You can't enumerate selectors — DNS has no "list the names under this label" operation — so you have to know the selector before you can look up the key. Read it from the &lt;code&gt;s=&lt;/code&gt; tag of the &lt;code&gt;DKIM-Signature&lt;/code&gt; header on that failing message:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;dig +short TXT selector1._domainkey.example.com
&lt;span class="gp"&gt;"v=DKIM1;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;rsa&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;p&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A...&lt;span class="s2"&gt;"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you don't have a message to read, a &lt;a href="https://cannypigeons.com/tools/dkim-checker" rel="noopener noreferrer"&gt;DKIM checker&lt;/a&gt; probes the well-known names — but understand what "not found" means there. It means &lt;em&gt;no key at a name we guessed&lt;/em&gt;, not &lt;em&gt;no DKIM&lt;/em&gt;. That distinction matters more than tool authors usually admit: I recently ran a 65-name probe against a domain I administer and got nothing, then found three perfectly valid keys under selectors that weren't on the list.&lt;/p&gt;

&lt;p&gt;Use 2048-bit keys. 1024 still validates but is deprecated in practice, and the large mailbox providers have signalled they intend to stop accepting it.&lt;/p&gt;

&lt;h3&gt;
  
  
  DMARC, and the alignment trap
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;dig +short TXT _dmarc.example.com
&lt;span class="gp"&gt;"v=DMARC1;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;p&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;none&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;rua&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;mailto:reports@example.com&lt;span class="s2"&gt;"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now back to that header. DMARC does not ask "did SPF pass?" It asks "did SPF pass &lt;strong&gt;for a domain that aligns with the From header&lt;/strong&gt;?" Same for DKIM.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SPF alignment&lt;/strong&gt; compares the &lt;code&gt;From:&lt;/code&gt; domain against the &lt;strong&gt;envelope sender&lt;/strong&gt; (&lt;code&gt;smtp.mailfrom&lt;/code&gt; / Return-Path) — a domain your recipients never see and your ESP usually controls.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DKIM alignment&lt;/strong&gt; compares the &lt;code&gt;From:&lt;/code&gt; domain against the signature's &lt;code&gt;d=&lt;/code&gt; tag.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;DMARC passes if &lt;strong&gt;either&lt;/strong&gt; aligns. Which is why the header above fails: SendGrid authenticated &lt;code&gt;bounce@mail.example.com&lt;/code&gt; for SPF, the DKIM signature carried &lt;code&gt;d=sendgrid.net&lt;/code&gt;, and the From header said &lt;code&gt;example.com&lt;/code&gt;. Both mechanisms passed for domains that aren't yours.&lt;/p&gt;

&lt;p&gt;Two dials control strictness, and both &lt;strong&gt;default to relaxed&lt;/strong&gt; (&lt;code&gt;r&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;v&lt;/span&gt;=&lt;span class="n"&gt;DMARC1&lt;/span&gt;; &lt;span class="n"&gt;p&lt;/span&gt;=&lt;span class="n"&gt;reject&lt;/span&gt;; &lt;span class="n"&gt;adkim&lt;/span&gt;=&lt;span class="n"&gt;s&lt;/span&gt;; &lt;span class="n"&gt;aspf&lt;/span&gt;=&lt;span class="n"&gt;s&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Relaxed alignment accepts a subdomain of your organizational domain — &lt;code&gt;mail.example.com&lt;/code&gt; aligns with &lt;code&gt;example.com&lt;/code&gt;. Strict demands an exact match. Almost nobody needs strict, and turning it on is a reliable way to break your own mail. If you're setting &lt;code&gt;adkim=s&lt;/code&gt; because it sounds more secure, don't.&lt;/p&gt;

&lt;p&gt;The fix for a misaligned ESP is nearly always: configure a &lt;strong&gt;custom return-path / sending subdomain&lt;/strong&gt; with the vendor, and have them sign with &lt;code&gt;d=&lt;/code&gt; on your domain. Every serious platform supports this; it's usually one page in their settings called "domain authentication".&lt;/p&gt;

&lt;h3&gt;
  
  
  Forwarding breaks SPF, permanently
&lt;/h3&gt;

&lt;p&gt;If the complaints come from one department or one partner, suspect forwarding before anything else. When a message is forwarded, the relaying server isn't in your SPF record, so SPF fails — through no fault of yours and with nothing you can publish to prevent it.&lt;/p&gt;

&lt;p&gt;DKIM usually survives, which is the real argument for having both. Mailing lists that append footers or rewrite subjects break DKIM too, and &lt;a href="https://cannypigeons.com/learn/arc" rel="noopener noreferrer"&gt;ARC&lt;/a&gt; exists to carry the original verdict across those hops — though it only helps if the receiver honours it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Then read the reports
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;p=none&lt;/code&gt; with a &lt;code&gt;rua=&lt;/code&gt; address costs nothing and turns speculation into data. &lt;a href="https://cannypigeons.com/learn/dmarc-reports" rel="noopener noreferrer"&gt;Aggregate reports&lt;/a&gt; arrive as daily XML from every major receiver, listing each sending IP with its SPF and DKIM results. It's how you discover the marketing platform someone set up in 2023 that nobody remembers.&lt;/p&gt;

&lt;p&gt;Deploy in the obvious order: &lt;code&gt;p=none&lt;/code&gt; until the reports are boring, then &lt;code&gt;quarantine&lt;/code&gt; with a &lt;code&gt;pct=&lt;/code&gt;, then &lt;code&gt;reject&lt;/code&gt;. Jumping straight to &lt;code&gt;reject&lt;/code&gt; on a domain you don't have reporting for is how you find out what else was sending as you — by breaking it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cause 2: reputation
&lt;/h2&gt;

&lt;p&gt;Only once authentication is clean. Fixing reputation while DMARC fails is pouring water into a bucket with no bottom.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check the actual signals.&lt;/strong&gt; Google Postmaster Tools and Microsoft SNDS both give you domain and IP reputation for free, and they are the only sources that reflect what those receivers actually think. Everything else is inference.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Watch the spam complaint rate.&lt;/strong&gt; Google's threshold is &lt;strong&gt;0.3%&lt;/strong&gt;, and you want to be under 0.1%. This is the metric with the sharpest cliff — crossing it degrades delivery for everything you send, transactional included.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check blocklists&lt;/strong&gt;, but keep them in proportion. Spamhaus matters. Most of the eighty-entry blocklist tables you'll find online are dead or irrelevant, and being on some hobbyist list nobody queries is not your problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check the boring infrastructure.&lt;/strong&gt; Forward-confirmed reverse DNS on your sending IP — PTR resolves to a hostname that resolves back to the same IP. TLS on transport. These are table stakes and they're occasionally just missing after a migration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bulk senders have hard requirements now.&lt;/strong&gt; Since February 2024, Google and Yahoo require senders of roughly 5,000+ messages a day to a given provider to have SPF and DKIM, publish a DMARC record, keep spam rates under that 0.3%, and support one-click unsubscribe per RFC 8058 — that's &lt;code&gt;List-Unsubscribe-Post: List-Unsubscribe=One-Click&lt;/code&gt;, honoured within two days. Microsoft has since introduced equivalent requirements for Outlook.com. If you're above that volume and missing any of it, you have found your answer and it isn't your subject lines.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cause 3: content and engagement
&lt;/h2&gt;

&lt;p&gt;Last, because it's where everyone starts and where the least leverage is.&lt;/p&gt;

&lt;p&gt;Engagement is what actually moves modern filters: opens, replies, and — most of all — people not marking you as spam. A clean list of people who asked to hear from you outperforms every subject-line trick. If you're mailing addresses harvested two years ago, no amount of DNS work saves you.&lt;/p&gt;

&lt;p&gt;The specific things worth checking: you're not sending bulk marketing from the same domain as your transactional mail (separate subdomains, separate reputations), you have working unsubscribe, and you're removing hard bounces promptly. Spam-trap hits are disproportionately damaging.&lt;/p&gt;

&lt;h2&gt;
  
  
  The triage order, condensed
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Get full headers of a real failing message
2. Read Authentication-Results
   ├─ dmarc=fail, spf=pass, dkim=pass  → alignment. Fix the ESP's return-path / d=
   ├─ spf=permerror                    → count lookups; you're over 10
   ├─ dkim=fail                        → key rotated, or a relay modified the message
   └─ all pass                         → authentication is fine, go to step 3
3. Postmaster Tools + SNDS: reputation and complaint rate vs 0.3%
4. rDNS, TLS, List-Unsubscribe, bulk-sender requirements
5. Only now: list hygiene, engagement, content
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Most "going to spam" tickets die at step 2, and a large share of those are the alignment case — the one where everything says &lt;code&gt;pass&lt;/code&gt; and the mail still fails.&lt;/p&gt;

&lt;p&gt;If you want the whole picture for a domain without assembling it by hand, the &lt;a href="https://cannypigeons.com/tools/dmarc-checker" rel="noopener noreferrer"&gt;DMARC checker&lt;/a&gt; reads all three records, counts the SPF lookups properly, and tells you what's actually wrong. It's free and there's no signup.&lt;/p&gt;

</description>
      <category>sysadmin</category>
      <category>email</category>
      <category>devops</category>
      <category>security</category>
    </item>
  </channel>
</rss>
