<?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: Jose Pollman</title>
    <description>The latest articles on DEV Community by Jose Pollman (@jose_pollman_fa7c6ec43cdd).</description>
    <link>https://dev.to/jose_pollman_fa7c6ec43cdd</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%2F4059554%2F63cedb60-cf00-4817-8cbd-9bc1eda1c48e.png</url>
      <title>DEV Community: Jose Pollman</title>
      <link>https://dev.to/jose_pollman_fa7c6ec43cdd</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jose_pollman_fa7c6ec43cdd"/>
    <language>en</language>
    <item>
      <title>Reading a vendor's email and HTTPS setup from DNS before you sign</title>
      <dc:creator>Jose Pollman</dc:creator>
      <pubDate>Thu, 08 Oct 2026 07:58:04 +0000</pubDate>
      <link>https://dev.to/jose_pollman_fa7c6ec43cdd/reading-a-vendors-email-and-https-setup-from-dns-before-you-sign-1e7e</link>
      <guid>https://dev.to/jose_pollman_fa7c6ec43cdd/reading-a-vendors-email-and-https-setup-from-dns-before-you-sign-1e7e</guid>
      <description>&lt;p&gt;Before a vendor gets to send mail as your domain, or hosts a login page your team will type passwords into, there are six things you can check yourself in about a minute: where their mail goes, who is allowed to send as them, what they ask receivers to do with forged mail, whether they require TLS for mail coming in, whether anyone reads the reports when that TLS fails, and whether their web hosts insist on HTTPS. All six are public, and none of them needs the vendor's cooperation.&lt;/p&gt;

&lt;p&gt;This is a capability check, not a verdict. A missing record tells you a protection is absent. It does not tell you why, and it says nothing about how a company handles your data internally. What it does give you is a short list of specific questions, which gets better answers than a generic security questionnaire.&lt;/p&gt;

&lt;h2&gt;
  
  
  No dig required
&lt;/h2&gt;

&lt;p&gt;The machine I wrote this on has no &lt;code&gt;dig&lt;/code&gt;, so everything below goes through DNS over HTTPS instead. Google Public DNS answers plain GET requests with JSON:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"https://dns.google/resolve?name=_dmarc.google.com&amp;amp;type=TXT"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"Status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"TC"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"RD"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"RA"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"AD"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"CD"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"Question"&lt;/span&gt;&lt;span class="p"&gt;:[{&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"_dmarc.google.com."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;&lt;span class="nl"&gt;"Answer"&lt;/span&gt;&lt;span class="p"&gt;:[{&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"_dmarc.google.com."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"TTL"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"v=DMARC1; p=reject; rua=mailto:mailauth-reports@google.com"&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;&lt;span class="nl"&gt;"Comment"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Response from 216.239.34.10."&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Status&lt;/code&gt; is the ordinary DNS response code (&lt;a href="https://developers.google.com/speed/public-dns/docs/doh/json" rel="noopener noreferrer"&gt;Google documents the format here&lt;/a&gt;): 0 is NOERROR, 3 is NXDOMAIN. A name with no record of the type you asked for comes back as 0 with no &lt;code&gt;Answer&lt;/code&gt; array. In principle that differs from a name that does not exist at all, but some zones answer 0 even for names nobody created (a made-up name under example.com got 0 that morning), so the script prints the status and treats both as "none".&lt;/p&gt;

&lt;h2&gt;
  
  
  The script
&lt;/h2&gt;

&lt;p&gt;Standard library only, Python 3, 68 lines. It looks up the five DNS records, fetches the MTA-STS policy file if the record says there is one, and walks the HTTPS redirect chain without following it automatically, so you see the HSTS header on every hop rather than only the last one.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;urllib.parse&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urljoin&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;doh&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rtype&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://dns.google/resolve?name=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;&amp;amp;type=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;rtype&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NXDOMAIN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="n"&gt;want&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TXT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MX&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;A&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AAAA&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="p"&gt;}[&lt;/span&gt;&lt;span class="n"&gt;rtype&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;answers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Answer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;want&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;rtype&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TXT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;answers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'"'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;answers&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NOERROR&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;answers&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;answers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;doh&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TXT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;found&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;answers&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;found&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;found&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;none (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;NoRedirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HTTPRedirectHandler&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;redirect_request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

&lt;span class="n"&gt;opener&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build_opener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NoRedirect&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;hsts_chain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hops&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hops&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HEAD&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;opener&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HTTPError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;      HSTS: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Strict-Transport-Security&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;none&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;location&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Location&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;301&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;302&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;303&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;307&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;308&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;
        &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;urljoin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;mta_sts_policy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mta-sts.&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;doh&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;A&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="nf"&gt;doh&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AAAA&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; has no A or AAAA record&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/.well-known/mta-sts.txt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fetch failed: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;modes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;splitlines&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mode:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;modes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;modes&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no mode line&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;== &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;doh&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MX&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MX      &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;none (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SPF     &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;v=spf1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DMARC   &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;_dmarc.&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;v=dmarc1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;sts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;_mta-sts.&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;v=stsv1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MTA-STS &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;sts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;none&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  policy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;mta_sts_policy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TLS-RPT &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;_smtp._tls.&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;v=tlsrptv1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HTTPS   &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;hsts_chain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:]:&lt;/span&gt;
    &lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is what it printed for google.com on the morning of 8 October 2026:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ python3 vendorcheck.py google.com
== google.com
MX       ['10 smtp.google.com.']
SPF      v=spf1 include:_spf.google.com ~all
DMARC    v=DMARC1; p=reject; rua=mailto:mailauth-reports@google.com
MTA-STS  v=STSv1; id=20210803T010101;
  policy mode: enforce
TLS-RPT  v=TLSRPTv1;rua=mailto:sts-reports@google.com
HTTPS
  301 https://google.com/
      HSTS: none
  200 https://www.google.com/
      HSTS: none
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every mail record is present and the MTA-STS policy is in enforce mode. Neither HTTPS response carries an HSTS header, which I come back to below.&lt;/p&gt;

&lt;h2&gt;
  
  
  What each line tells you, and what it does not
&lt;/h2&gt;

&lt;h3&gt;
  
  
  MX
&lt;/h3&gt;

&lt;p&gt;Who receives their mail. Usually this names the provider outright (&lt;code&gt;*.mail.protection.outlook.com&lt;/code&gt;, &lt;code&gt;smtp.google.com&lt;/code&gt;, and so on), which tells you whose security model their inbox inherits. A single MX of &lt;code&gt;0 .&lt;/code&gt; is a null MX, defined in &lt;a href="https://www.rfc-editor.org/rfc/rfc7505" rel="noopener noreferrer"&gt;RFC 7505&lt;/a&gt; as the way a domain says it accepts no mail at all.&lt;/p&gt;

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

&lt;p&gt;Which servers may send mail using the domain in the envelope sender. The &lt;code&gt;all&lt;/code&gt; term at the end matters less than it looks. &lt;code&gt;~all&lt;/code&gt; is softfail, which &lt;a href="https://www.rfc-editor.org/rfc/rfc7208" rel="noopener noreferrer"&gt;RFC 7208&lt;/a&gt; calls "a weak statement", and google.com uses it. That is not a weakness on its own, because DMARC sits on top: under &lt;a href="https://www.rfc-editor.org/rfc/rfc9989" rel="noopener noreferrer"&gt;RFC 9989&lt;/a&gt;, the current DMARC spec, a message only passes DMARC with an SPF or DKIM pass that is aligned with the From domain. A softfail is not a pass. With &lt;code&gt;p=reject&lt;/code&gt; behind it, &lt;code&gt;~all&lt;/code&gt; is fine.&lt;/p&gt;

&lt;h3&gt;
  
  
  DMARC
&lt;/h3&gt;

&lt;p&gt;What the domain asks receivers to do with mail that fails. RFC 9989 defines three values for &lt;code&gt;p=&lt;/code&gt;. &lt;code&gt;none&lt;/code&gt; means "the Domain Owner offers no expression of preference", &lt;code&gt;quarantine&lt;/code&gt; means the mail is suspicious, and &lt;code&gt;reject&lt;/code&gt; means a failure is a clear sign the domain is being misused. If a vendor will send invoices or password resets on your behalf, &lt;code&gt;p=none&lt;/code&gt; is the line worth asking about, because forged mail claiming to be them gets no instruction at all.&lt;/p&gt;

&lt;h3&gt;
  
  
  MTA-STS
&lt;/h3&gt;

&lt;p&gt;This one has two parts, and checking only the first gives a wrong answer. The TXT record at &lt;code&gt;_mta-sts.&amp;lt;domain&amp;gt;&lt;/code&gt; only announces that a policy exists. The policy itself is a text file at &lt;code&gt;https://mta-sts.&amp;lt;domain&amp;gt;/.well-known/mta-sts.txt&lt;/code&gt;, and its &lt;code&gt;mode&lt;/code&gt; line is what changes sender behaviour. &lt;a href="https://www.rfc-editor.org/rfc/rfc8461" rel="noopener noreferrer"&gt;RFC 8461&lt;/a&gt; defines three modes. In &lt;code&gt;enforce&lt;/code&gt;, sending servers must not deliver to an MX that fails certificate validation or does not offer STARTTLS. In &lt;code&gt;testing&lt;/code&gt;, they deliver anyway and report the failure. &lt;code&gt;none&lt;/code&gt; means no active policy.&lt;/p&gt;

&lt;p&gt;The reason the script fetches the file: one large domain I checked the same morning publishes a valid &lt;code&gt;v=STSv1&lt;/code&gt; TXT record, but its &lt;code&gt;mta-sts.&lt;/code&gt; host returns no A and no AAAA record at either Google or Cloudflare DNS. The output looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MTA-STS  v=STSv1;id=&amp;lt;redacted&amp;gt;;
  policy mta-sts.&amp;lt;redacted&amp;gt; has no A or AAAA record
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;RFC 8461 section 3.3 covers exactly this case: "If a valid TXT record is found but no policy can be fetched via HTTPS (for any reason), and there is no valid (non-expired) previously cached policy, senders MUST continue with delivery as though the domain has not implemented MTA-STS." So a checker that stops at the TXT record reports MTA-STS as deployed when a sender without a cached policy would treat it as absent. I am not naming the domain because I don't know whether this is a mistake, a policy that was retired with the TXT record left behind, or something mid-migration. It goes on the list of questions to ask.&lt;/p&gt;

&lt;h3&gt;
  
  
  TLS-RPT
&lt;/h3&gt;

&lt;p&gt;A TXT record at &lt;code&gt;_smtp._tls.&amp;lt;domain&amp;gt;&lt;/code&gt; (&lt;a href="https://www.rfc-editor.org/rfc/rfc8460" rel="noopener noreferrer"&gt;RFC 8460&lt;/a&gt;) tells other mail servers where to send reports about TLS failures when they deliver to this domain. Its presence means someone has at least set up a mailbox for those reports. It does not prove anyone reads them.&lt;/p&gt;

&lt;h3&gt;
  
  
  HSTS, which is per host
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Strict-Transport-Security&lt;/code&gt; tells a browser to refuse plain HTTP for that host for the next &lt;code&gt;max-age&lt;/code&gt; seconds. &lt;a href="https://www.rfc-editor.org/rfc/rfc6797" rel="noopener noreferrer"&gt;RFC 6797&lt;/a&gt; only lets a site send it over HTTPS, and it applies to the host that sent it plus subdomains if &lt;code&gt;includeSubDomains&lt;/code&gt; is set.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;google.com&lt;/code&gt; sends no HSTS header on the redirect or on &lt;code&gt;www.google.com&lt;/code&gt;. That looks bad until you check the host where the password actually goes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  302 https://accounts.google.com/
      HSTS: max-age=31536000; includeSubDomains
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is also a second route to HSTS that no header check can see: the preload list built into browsers (RFC 6797 section 12.3 describes the idea). Chromium's list is a JSON file in its source tree, &lt;a href="https://github.com/chromium/chromium/blob/main/net/http/transport_security_state_static.json" rel="noopener noreferrer"&gt;&lt;code&gt;net/http/transport_security_state_static.json&lt;/code&gt;&lt;/a&gt;. I downloaded it on the same morning and searched it. It has entries for &lt;code&gt;accounts.google.com&lt;/code&gt; and &lt;code&gt;mail.google.com&lt;/code&gt;, and none for &lt;code&gt;google.com&lt;/code&gt; or &lt;code&gt;www.google.com&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So for a vendor, run the HTTPS check against the login host and the API host you will actually use, not the marketing domain. A missing header on the home page and a preloaded login domain can both be true at the same company.&lt;/p&gt;

&lt;h2&gt;
  
  
  A reference point: a domain that sends no mail
&lt;/h2&gt;

&lt;p&gt;It helps to see what a domain that does no email at all should look like. example.com, reserved for documentation by &lt;a href="https://www.rfc-editor.org/rfc/rfc2606" rel="noopener noreferrer"&gt;RFC 2606&lt;/a&gt;, is a clean example. The mail lines from the same run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;== example.com
MX       ['0 .']
SPF      v=spf1 -all
DMARC    v=DMARC1;p=reject;sp=reject;adkim=s;aspf=s
MTA-STS  none (NOERROR)
TLS-RPT  none (NOERROR)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Null MX, an SPF record that authorises nobody, and DMARC set to reject with strict alignment. No MTA-STS or TLS-RPT, which is correct for a domain that accepts no mail. If a vendor has extra domains they use only for marketing pages, this is the setup you would hope to see on them, because without these records nothing tells a receiver that mail from that domain should not exist.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this does not check
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;DKIM: you cannot list a domain's DKIM keys from DNS. You need a selector, and the only reliable place to get one is the &lt;code&gt;s=&lt;/code&gt; tag in the &lt;code&gt;DKIM-Signature&lt;/code&gt; header of a real message from them. Ask them to send you one.&lt;/li&gt;
&lt;li&gt;Whether the SPF includes are current: an &lt;code&gt;include:&lt;/code&gt; for a service they dropped two years ago still authorises that service to send as them.&lt;/li&gt;
&lt;li&gt;DNSSEC, CAA and security.txt: each needs its own lookup, and they are left out to keep the script short.&lt;/li&gt;
&lt;li&gt;Anything internal: access control, logging, how they store your data. DNS cannot see any of it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Questions this gives you
&lt;/h2&gt;

&lt;p&gt;From one run you get specific questions instead of a questionnaire. Why is DMARC at &lt;code&gt;p=none&lt;/code&gt;? Is the MTA-STS policy meant to be live? Which host serves the login page, and does it send HSTS? Can you send us a message from your production mail system so we can check the DKIM signature?&lt;/p&gt;

&lt;p&gt;Written with AI assistance from my own notes and test results. I checked every fact and command before publishing.&lt;/p&gt;

</description>
      <category>security</category>
      <category>email</category>
      <category>dns</category>
      <category>python</category>
    </item>
    <item>
      <title>The IETF datatracker never sets rfc_number on a draft: fixing my status check and running it on DKIM2</title>
      <dc:creator>Jose Pollman</dc:creator>
      <pubDate>Tue, 06 Oct 2026 09:06:37 +0000</pubDate>
      <link>https://dev.to/jose_pollman_fa7c6ec43cdd/the-ietf-datatracker-never-sets-rfcnumber-on-a-draft-fixing-my-status-check-and-running-it-on-3ial</link>
      <guid>https://dev.to/jose_pollman_fa7c6ec43cdd/the-ietf-datatracker-never-sets-rfcnumber-on-a-draft-fixing-my-status-check-and-running-it-on-3ial</guid>
      <description>&lt;p&gt;In &lt;a href="https://dev.to/jose_pollman_fa7c6ec43cdd/checking-whether-an-ietf-draft-is-actually-a-standard-from-the-command-line-2647"&gt;an earlier post&lt;/a&gt; I wrote a short Python function that asks the IETF datatracker six questions about an Internet-Draft and counts the yes answers. I re-ran it three weeks later and two of its answers were wrong. One field does not mean what its name says. Another changes earlier than I said it does. This post fixes both, then runs the fixed version against every draft the DKIM working group has ever had.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug one: rfc_number is null on every draft
&lt;/h2&gt;

&lt;p&gt;The old function counted a draft as published when &lt;code&gt;rfc_number&lt;/code&gt; was not null. On 13 September it printed this for the DMARC working group's draft:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;draft-ietf-dmarc-dmarcbis-41  (83 pages, expires 2025-10-06)
  ...
  no   published as an RFC
  5/6 signals present
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That draft had been published as RFC 9989 on 20 May 2026. The &lt;a href="https://www.rfc-editor.org/info/rfc9989" rel="noopener noreferrer"&gt;RFC Editor's record&lt;/a&gt; lists &lt;code&gt;draft-ietf-dmarc-dmarcbis-41&lt;/code&gt; as its source and RFC 7489 and RFC 9091 as the documents it obsoletes.&lt;/p&gt;

&lt;p&gt;The datatracker stores an RFC as its own document record, named &lt;code&gt;rfc9989&lt;/code&gt;. That record has &lt;code&gt;rfc_number&lt;/code&gt; set. The draft that became it never does. You can see the scale of it with one request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urllib.request&lt;/span&gt;

&lt;span class="n"&gt;API&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://datatracker.ietf.org/api/v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;sep&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;amp;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;API&lt;/span&gt;&lt;span class="si"&gt;}{&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="si"&gt;}{&lt;/span&gt;&lt;span class="n"&gt;sep&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;format=json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;docs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/doc/document/?name__startswith=draft-ietf-dkim-&amp;amp;limit=100&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;objects&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;docs&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;drafts,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rfc_number&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;docs&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;with rfc_number set&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;16 drafts, 0 with rfc_number set
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nine of those sixteen are published RFCs. The link from a draft to its RFC lives in a separate table, under the relationship &lt;code&gt;became_rfc&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;rels&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/doc/relateddocument/?source__name=draft-ietf-dmarc-dmarcbis&amp;amp;relationship__slug=became_rfc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rels&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;objects&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;target&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/api/v1/doc/document/rfc9989/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is a cheaper tell on the draft itself. Its &lt;code&gt;states&lt;/code&gt; list includes one state of the type &lt;code&gt;draft&lt;/code&gt;, and that state's slug is &lt;code&gt;active&lt;/code&gt;, &lt;code&gt;expired&lt;/code&gt; or &lt;code&gt;rfc&lt;/code&gt;. So the fixed check reads the state first and only makes the second request when it says &lt;code&gt;rfc&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The earlier post also said that once &lt;code&gt;rfc_number&lt;/code&gt; is set, "the draft is history". On a draft record it is never set, so that sentence described something that does not happen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug two: stream is set at the call for adoption
&lt;/h2&gt;

&lt;p&gt;The earlier post said that when &lt;code&gt;stream&lt;/code&gt; stops being null, the document has been adopted. Its example of a draft with nothing going for it yet was &lt;code&gt;draft-brotman-aggregate-performance-reporting&lt;/code&gt;. Here are its latest three events:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/doc/docevent/?doc__name=draft-brotman-aggregate-performance-reporting&amp;amp;limit=3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;objects&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;time&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][:&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;desc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2026-09-24 IETF WG state changed to &amp;lt;b&amp;gt;Call For Adoption By WG Issued&amp;lt;/b&amp;gt;
2026-09-24 Changed group to &amp;lt;b&amp;gt;Mail Maintenance (MAILMAINT)&amp;lt;/b&amp;gt;
2026-09-24 Changed stream to &amp;lt;b&amp;gt;IETF&amp;lt;/b&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On 24 September a call for adoption went out in the mailmaint working group, and the stream, the group and the working group state all changed in the same second. The name still starts with &lt;code&gt;draft-brotman-&lt;/code&gt;. Nothing has been adopted. A call for adoption is a question to the group, and the answer can be no.&lt;/p&gt;

&lt;p&gt;So a non-null &lt;code&gt;stream&lt;/code&gt; means a working group is either considering the document or already owns it. The state of type &lt;code&gt;draft-stream-ietf&lt;/code&gt; says which. These are the slugs worth knowing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;c-adopt   Call For Adoption By WG Issued
wg-doc    WG Document
sub-pub   Submitted to IESG for Publication
dead      Dead WG Document
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fixed version drops the old "name contains ietf" signal, because the stream state answers the same question with more detail, and prints that state as its own column.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fixed check
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;_states&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;states&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Return {state type: slug}, e.g. {&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;draft&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;rfc&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;draft-stream-ietf&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;wg-doc&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;}.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;uri&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;states&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;uri&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;_states&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeprefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/api/v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="n"&gt;_states&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;rstrip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;slug&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;slug&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_states&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;slug&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;became_rfc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;rels&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/doc/relateddocument/?source__name=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;&amp;amp;relationship__slug=became_rfc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;objects&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;rels&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;target&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;rstrip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;rels&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;st&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;states&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;rfc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;became_rfc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;draft&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rfc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;signals&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stream&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;intended_std_level&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ad&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;shepherd&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;rfc&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;38&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;rev&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;time&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;draft&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;draft-stream-ietf&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;signals&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/5  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;rfc&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;''&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;group_report&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;docs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/doc/document/?name__startswith=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;&amp;amp;limit=100&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;objects&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;docs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;time&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;reverse&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="nf"&gt;group_report&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;draft-ietf-dkim-&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;State lookups are cached because the same dozen state URIs repeat across every document.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running it on a whole working group
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;name__startswith&lt;/code&gt; returns every draft under a prefix, so one request covers a working group's whole history. Output on 6 October 2026:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;draft-ietf-dkim-dkim2-bcp               01  2026-09-09  active   wg-doc   1/5
draft-ietf-dkim-dkim2-spec              06  2026-08-28  active   wg-doc   1/5
draft-ietf-dkim-dkim2-dns               00  2026-07-20  active   wg-doc   1/5
draft-ietf-dkim-dkim2-header            00  2026-05-07  expired  dead     1/5
draft-ietf-dkim-dkim2-motivation        02  2026-05-06  expired  wg-doc   1/5
draft-ietf-dkim-replay-problem          00  2024-01-29  expired  -        0/5
draft-ietf-dkim-mailinglists            12  2020-01-21  rfc      wg-doc   5/5  rfc6377
draft-ietf-dkim-rfc4871bis              15  2020-01-21  rfc      sub-pub  5/5  rfc6376
draft-ietf-dkim-base                    10  2020-01-21  rfc      wg-doc   4/5  rfc4871
draft-ietf-dkim-rfc4871-errata          07  2015-10-14  rfc      wg-doc   4/5  rfc5672
draft-ietf-dkim-deployment              11  2015-10-14  rfc      -        4/5  rfc5863
draft-ietf-dkim-overview                12  2015-10-14  rfc      -        4/5  rfc5585
draft-ietf-dkim-ssp-requirements        05  2015-10-14  rfc      -        4/5  rfc5016
draft-ietf-dkim-threats                 03  2015-10-14  rfc      -        4/5  rfc4686
draft-ietf-dkim-ssp                     10  2015-10-14  rfc      -        4/5  rfc5617
draft-ietf-dkim-implementation-report   06  2011-03-28  expired  dead     2/5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The columns are name, revision, the record's &lt;code&gt;time&lt;/code&gt;, draft state, working group state, signals out of five (stream, intended status, AD, shepherd, RFC) and the RFC it became.&lt;/p&gt;

&lt;p&gt;The prefix spans two eras. The bottom ten rows are the first DKIM working group, which produced RFC 4871 in May 2007 and its replacement, RFC 6376, in September 2011. The top five rows are DKIM2, and &lt;code&gt;replay-problem&lt;/code&gt; from January 2024 sits between the two. All of them share one name prefix and one group record.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;time&lt;/code&gt; column is not a history. &lt;code&gt;draft-ietf-dkim-base&lt;/code&gt; became RFC 4871 in 2007 and its record says 2020-01-21. Six records share 2015-10-14. Those are the dates the database rows were last touched. For when something happened, read the event log or the RFC.&lt;/p&gt;

&lt;p&gt;Most of the old RFCs score 4/5 because their shepherd field is empty, while two from the same group have one. Treat a missing field on an old published record as missing data, not as a signal.&lt;/p&gt;

&lt;p&gt;Every DKIM2 row scores 1/5: on the IETF stream, with no intended status, no AD and no shepherd yet. Two of the five have expired. The motivation document is still marked as a WG document, while the header document is marked dead.&lt;/p&gt;

&lt;h2&gt;
  
  
  The group record has its own history
&lt;/h2&gt;

&lt;p&gt;The group's state changes and closing notes come from the same API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/group/groupevent/?group__acronym=dkim&amp;amp;type=changed_state&amp;amp;limit=20&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;objects&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;time&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][:&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;desc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2025-02-20 Charter approved, group active
2024-01-18 State changed to &amp;lt;b&amp;gt;Concluded&amp;lt;/b&amp;gt; from Active
2023-02-21 Charter approved, group active
2011-09-26 Concluded group
2006-01-05 Started group
2005-10-01 Proposed group
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The closing note from January 2024, read with &lt;code&gt;type=closing_note&lt;/code&gt;, says: "WG closing without coming to consensus on even its first deliverable. Little to no activity in several months." The current charter came a year later. Its milestones are in &lt;code&gt;/group/groupmilestone/&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2025-04-30 not resolved | Adopt overview document
2025-04-30 not resolved | Adopt mechanism document
2025-11-30 not resolved | Adopt implementation guide
2025-12-31 not resolved | All documents submitted to the IESG
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first two look met: adopted &lt;code&gt;draft-ietf-dkim-&lt;/code&gt; drafts exist that fit both descriptions. Neither is marked resolved. Milestones are the weakest signal on the page. They slip routinely and the records often lag the work, so read them as the schedule the charter set, not as a status.&lt;/p&gt;

&lt;h2&gt;
  
  
  Following one reference
&lt;/h2&gt;

&lt;p&gt;A third trap shows up when you follow a document's references through the API.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;draft-ietf-dmarc-arc-to-historic-00&lt;/code&gt;, dated 22 April 2026, asks for RFC 8617 (ARC) to be reclassified as Historic and names DKIM2 as the direction the work has moved in. Its one reference to DKIM2 is informative, to &lt;code&gt;draft-ietf-dkim-dkim2-motivation-02&lt;/code&gt;. In the table above, that document has expired.&lt;/p&gt;

&lt;p&gt;Ask the API for the ARC draft's references and you get this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/doc/relateddocument/?source__name=draft-ietf-dmarc-arc-to-historic&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;objects&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;relationship&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;target&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;refnorm rfc7208
refnorm rfc6376
refnorm rfc8174
refnorm rfc8617
refnorm rfc2119
refnorm rfc7489
replaces draft-adams-arc-experiment-conclusion
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Six normative references and a &lt;code&gt;replaces&lt;/code&gt; link. Neither informative reference appears, so the DKIM2 pointer is invisible here. If you are tracing what a document depends on, read the references section of the &lt;a href="https://datatracker.ietf.org/doc/draft-ietf-dmarc-arc-to-historic/" rel="noopener noreferrer"&gt;document itself&lt;/a&gt; as well.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this does not say
&lt;/h2&gt;

&lt;p&gt;An expired motivation document does not mean DKIM2 has stalled. The spec draft reached revision 06 on 28 August and the BCP draft reached revision 01 on 9 September. Drafts lapse after six months unless revised, and a lapsed one stays in the record.&lt;/p&gt;

&lt;p&gt;ARC is not Historic. The request is a revision 00 working group document, and its IESG state is "I-D Exists", which means the IESG has not started on it. That draft expires on 24 October 2026 unless a new revision is posted.&lt;/p&gt;

&lt;p&gt;And citing the motivation draft was not a mistake. Revision 02 is dated 2 November 2025 and expired in early May 2026, so it was a live draft when the ARC document cited it in April.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lesson I am keeping
&lt;/h2&gt;

&lt;p&gt;The dmarcbis row was a known answer sitting in the output of the first post, and the function printed it as a no. A check that prints confident yes and no answers needs a case where you already know the answer, run every time the check changes. The fixed version now starts with two:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;became_rfc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;draft-ietf-dmarc-dmarcbis&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rfc9989&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;became_rfc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;draft-ietf-dkim-base&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rfc4871&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The API itself is documented at &lt;a href="https://datatracker.ietf.org/api/" rel="noopener noreferrer"&gt;datatracker.ietf.org/api&lt;/a&gt;, and none of it needs a key.&lt;/p&gt;

&lt;p&gt;Written with AI assistance from my own notes and test results. I checked every fact and command before publishing.&lt;/p&gt;

</description>
      <category>email</category>
      <category>python</category>
      <category>api</category>
      <category>programming</category>
    </item>
    <item>
      <title>Why INP comes back blank in lab speed tests, and what the Event Timing API actually reports</title>
      <dc:creator>Jose Pollman</dc:creator>
      <pubDate>Tue, 29 Sep 2026 09:59:26 +0000</pubDate>
      <link>https://dev.to/jose_pollman_fa7c6ec43cdd/why-inp-comes-back-blank-in-lab-speed-tests-and-what-the-event-timing-api-actually-reports-27j0</link>
      <guid>https://dev.to/jose_pollman_fa7c6ec43cdd/why-inp-comes-back-blank-in-lab-speed-tests-and-what-the-event-timing-api-actually-reports-27j0</guid>
      <description>&lt;h2&gt;
  
  
  Five numbers and a blank
&lt;/h2&gt;

&lt;p&gt;I run a Core Web Vitals checker. Pointed at its own URL this morning, on a simulated mid-range phone over throttled 4G, it returned this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LCP   Largest Contentful Paint     3.32 s    Needs work
CLS   Cumulative Layout Shift      0         Good
INP   Interaction to Next Paint    (blank)   Not measured
TTFB  Time to First Byte           3 ms      Good
FCP   First Contentful Paint       1.71 s    Good
TBT   Total Blocking Time          78 ms     Good

Tested whattofixfirst.com on mobile, 29 Sept 2026, 11:47.
Lighthouse performance score: 89/100.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Five numbers and a blank. The blank is one of the three Core Web Vitals, and no lab-only test can fill it, including the one I built.&lt;/p&gt;

&lt;p&gt;That is not a bug in any of them. It is what the metric is.&lt;/p&gt;

&lt;h2&gt;
  
  
  INP needs a person
&lt;/h2&gt;

&lt;p&gt;LCP and CLS can be observed by loading a page and watching it. INP cannot, because there is nothing to observe until somebody touches something.&lt;/p&gt;

&lt;p&gt;Google's own documentation for the metric is direct about the conditions that produce no value at all. A page returns no INP when the user never clicked, tapped or pressed a key; when they only scrolled or hovered, neither of which counts; or when "the page is being accessed by a bot such as a search crawler or headless browser that has not been scripted to interact with the page."&lt;/p&gt;

&lt;p&gt;That last line describes every lab run. Lighthouse loads the page in a headless browser and does not click anything, so there is no interaction, so there is no latency to report. The same documentation says so plainly: "some lab tools won't report a page's INP because they only observe the loading of a page without any interactions."&lt;/p&gt;

&lt;p&gt;The only three interaction types INP observes are a mouse click, a tap on a touchscreen, and a key press on a physical or onscreen keyboard. Scrolling and hovering are excluded outright.&lt;/p&gt;

&lt;p&gt;So a checker has two honest options. Report the blank and say why, or report something else and pretend it is the same thing. I went with the blank.&lt;/p&gt;

&lt;h2&gt;
  
  
  TBT is a stand-in, not a substitute
&lt;/h2&gt;

&lt;p&gt;The usual advice is to read Total Blocking Time instead, and that advice is reasonable as far as it goes. Google phrases it with a caveat worth copying verbatim: TBT "may be a reasonable proxy metric for INP, but it's not a substitute for INP in and of itself."&lt;/p&gt;

&lt;p&gt;The reason is in the thresholds. INP is assessed at the 75th percentile of page loads in the field, and the bands are 200 ms or less for good, above 200 ms up to 500 ms for needs improvement, and above 500 ms for poor. TBT measures main thread blocking during load only. A page can load with almost nothing blocking the main thread, score well on TBT, and then hang for 400 ms the first time someone opens a menu. My own check above reports TBT at 78 ms, which is good, and that tells you approximately nothing about what happens when a visitor types in the URL box.&lt;/p&gt;

&lt;p&gt;Field data closes the gap when it exists. On this run it did not: the checker reported that Google does not yet have enough real-visitor data for the origin, which is the normal state for a small site. Blank in the lab, blank in the field, and the output says so twice rather than filling the space.&lt;/p&gt;

&lt;h2&gt;
  
  
  Collecting it yourself
&lt;/h2&gt;

&lt;p&gt;If you want the number for your own page, you collect it from real visitors with the Event Timing API. Here is the version most people write first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PerformanceObserver&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getEntries&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;observe&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;event&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;buffered&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This runs, it throws no errors, and on a responsive page it logs nothing at all. There are three reasons, and all three are written into the spec rather than hidden in browser behaviour.&lt;/p&gt;

&lt;h3&gt;
  
  
  The default cutoff is 104 ms
&lt;/h3&gt;

&lt;p&gt;The Event Timing API does not surface every event. The &lt;a href="https://www.w3.org/TR/event-timing/" rel="noopener noreferrer"&gt;W3C Working Draft of 19 March 2026&lt;/a&gt; sets a default duration threshold of 104 ms, and explains the number: it "is just the first multiple of 8 greater than 100ms. An event whose rounded duration is greater than or equal to 104ms will have its pre-rounded duration greater than or equal to 100ms."&lt;/p&gt;

&lt;p&gt;So the observer above is, by default, a slow-interaction detector. A page where every interaction lands in 90 ms produces an empty log, and an empty log looks identical to a broken observer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Durations are rounded to 8 ms
&lt;/h3&gt;

&lt;p&gt;That "rounded duration" is not incidental. The spec picked 8 ms as the granularity because it "allows relatively precise timing even for 120Hz displays." Every duration you read has been rounded to a multiple of 8, which is why the default cutoff is 104 and not 100. If you are comparing two interactions that differ by 5 ms, you are comparing noise.&lt;/p&gt;

&lt;h3&gt;
  
  
  The floor on durationThreshold is 16 ms
&lt;/h3&gt;

&lt;p&gt;You can lower the threshold, but not to zero. The minimum allowed value is 16 ms, and the spec's reasoning is about frames rather than milliseconds: "In 120Hz displays, a response that skips more than a single frame will be at least 16ms, so the entry corresponding to this user input will be surfaced in the API under the minimum value."&lt;/p&gt;

&lt;p&gt;A response that skips no more than a single frame is, as far as this API is concerned, not worth a record.&lt;/p&gt;

&lt;h3&gt;
  
  
  first-input is the exception
&lt;/h3&gt;

&lt;p&gt;There is one entry type that ignores all of this. The spec says the first input entry "is reported even if it does not exceed a provided durationThreshold, and is buffered even if it does not exceed the default duration threshold of 104ms."&lt;/p&gt;

&lt;p&gt;That matters because it is the only way to guarantee a page with interactions reports something. &lt;a href="https://web.dev/articles/inp" rel="noopener noreferrer"&gt;Google's guidance&lt;/a&gt; says the same: observe &lt;code&gt;first-input&lt;/code&gt; alongside &lt;code&gt;event&lt;/code&gt; so that consistently fast pages still produce a value instead of looking like pages nobody touched.&lt;/p&gt;

&lt;p&gt;Put together, the version that actually collects data looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;interactions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PerformanceObserver&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getEntries&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;interactionId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;interactions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;observe&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;event&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;buffered&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;durationThreshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Guaranteed to fire even on a very fast page.&lt;/span&gt;
&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PerformanceObserver&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getEntries&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;interactions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;observe&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;first-input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;buffered&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Four things that still will not be right
&lt;/h2&gt;

&lt;p&gt;Even with that, a hand-rolled collector disagrees with what Google records, in four specific ways that Google documents.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The percentile.&lt;/strong&gt; INP is not the worst interaction. It is approximately the 98th percentile across all interactions on the page, computed when the page is unloaded. Keeping every sample in memory to compute that exactly is wasteful, and the documented shortcut is to keep only the worst N interactions, with 10 as the common choice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Back/forward cache.&lt;/strong&gt; If a page is restored from bfcache, INP resets to zero, because a user experiences that as a separate visit. A collector that keeps accumulating across a restore reports a number nobody experienced.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Backgrounded tabs.&lt;/strong&gt; People leave tabs open for weeks and mobile browsers often do not run unload callbacks for background tabs, so waiting for unload loses the data. The fix is to report on &lt;code&gt;visibilitychange&lt;/code&gt;, which covers both backgrounding and unload, and to compute the final value server side.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Iframes.&lt;/strong&gt; The API does not report event entries from inside iframes. The metric does count them, because a visitor clicking play on an embedded video has no idea they crossed a frame boundary. This is a documented source of disagreement between CrUX and your own monitoring, and closing it means having the subframe post its entries up to the parent.&lt;/p&gt;

&lt;p&gt;That list is why the practical answer for most people is &lt;code&gt;onINP&lt;/code&gt; from the &lt;code&gt;web-vitals&lt;/code&gt; library, which handles all of it except the iframe case. I am not arguing against using it. I am arguing that you should know what it is doing, because when your own dashboard and Search Console disagree, one of these four is usually the reason.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two lines you can run right now
&lt;/h2&gt;

&lt;p&gt;Open the console on any page you have interacted with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;PerformanceObserver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;supportedEntryTypes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="sr"&gt;/event|input/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="c1"&gt;// ["event", "first-input"] in Chrome, checked 29 Sept 2026&lt;/span&gt;

&lt;span class="nx"&gt;performance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;eventCounts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;click&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// how many click events this page has recorded&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second one is the cheapest sanity check available. If &lt;code&gt;eventCounts&lt;/code&gt; is counting clicks and your observer array is still empty, your observer is not broken. It is doing exactly what the default 104 ms threshold tells it to do, and your page is fast.&lt;/p&gt;

&lt;p&gt;Which brings it back to the blank cell. A lab tool reporting no INP is not missing a feature. It is reporting, correctly, that nobody was there.&lt;/p&gt;

&lt;p&gt;Written with AI assistance from my own notes and test results. I checked every fact and command before publishing. The cover image was generated with DEV's built-in image tool.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>performance</category>
      <category>javascript</category>
      <category>browser</category>
    </item>
    <item>
      <title>Checking whether an IETF draft is actually a standard, from the command line</title>
      <dc:creator>Jose Pollman</dc:creator>
      <pubDate>Sun, 13 Sep 2026 18:54:34 +0000</pubDate>
      <link>https://dev.to/jose_pollman_fa7c6ec43cdd/checking-whether-an-ietf-draft-is-actually-a-standard-from-the-command-line-2647</link>
      <guid>https://dev.to/jose_pollman_fa7c6ec43cdd/checking-whether-an-ietf-draft-is-actually-a-standard-from-the-command-line-2647</guid>
      <description>&lt;p&gt;Someone links an Internet-Draft, calls it a new standard, and now you have to decide whether to build against it. The draft looks official. It has a document number, section headings full of MUST and SHOULD, an IANA Considerations section and a Normative References list. None of that tells you what stage it is at.&lt;/p&gt;

&lt;p&gt;The IETF publishes the answer as structured data. You can get it in one HTTP request, and the fields you need are all null or not null, so the check fits in a few lines of code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The filename already tells you something
&lt;/h2&gt;

&lt;p&gt;Every Internet-Draft name follows a pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;draft-&amp;lt;who-submitted-it&amp;gt;-&amp;lt;topic&amp;gt;-&amp;lt;revision&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the second segment is &lt;code&gt;ietf&lt;/code&gt;, a working group adopted the document and owns it. &lt;code&gt;draft-ietf-dmarc-dmarcbis&lt;/code&gt; is the DMARC working group's revision of DMARC. Anything else is the submitter's own name or their employer's. &lt;code&gt;draft-brotman-aggregate-performance-reporting&lt;/code&gt; is an individual submission by Alex Brotman.&lt;/p&gt;

&lt;p&gt;That is a signal and it costs nothing to read. It is not the whole answer, because a document can be adopted later and keep circulating under the old name in people's bookmarks, so the rename matters and you want the current record.&lt;/p&gt;

&lt;h2&gt;
  
  
  One request to the datatracker
&lt;/h2&gt;

&lt;p&gt;The IETF datatracker has a read-only REST API at &lt;code&gt;/api/v1/&lt;/code&gt;. No key, no signup.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"https://datatracker.ietf.org/api/v1/doc/document/?name=draft-brotman-aggregate-performance-reporting&amp;amp;format=json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | jq &lt;span class="s1"&gt;'.objects[0] | {name, rev, stream, intended_std_level, ad, shepherd, rfc_number, pages, expires}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run on 13 September 2026, that returns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"draft-brotman-aggregate-performance-reporting"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rev"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"stream"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"intended_std_level"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ad"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"shepherd"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rfc_number"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"pages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expires"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2027-03-12T15:50:05Z"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Five nulls in a row. Now the same query against the DMARC working group document:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"draft-ietf-dmarc-dmarcbis"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rev"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"41"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"stream"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/api/v1/name/streamname/ietf/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"intended_std_level"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/api/v1/name/intendedstdlevelname/ps/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ad"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/api/v1/person/person/106842/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"shepherd"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/api/v1/person/email/tjw.ietf@gmail.com/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rfc_number"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"pages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;83&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expires"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2025-10-06T21:29:30Z"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same API, same shape, completely different picture. Revision 41 instead of 01. A stream. An intended status. A named Area Director and a named shepherd.&lt;/p&gt;

&lt;h2&gt;
  
  
  What each field is actually telling you
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;stream&lt;/code&gt;&lt;/strong&gt; is the publication path a document is on. &lt;code&gt;GET /api/v1/name/streamname/&lt;/code&gt; lists all six: IETF, ISE (Independent Submission), IRTF, IAB, Editorial, Legacy. &lt;code&gt;null&lt;/code&gt; means the document is not on any of them yet. This is the single most useful field, because a document with no stream has no group of people responsible for moving it forward.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;intended_std_level&lt;/code&gt;&lt;/strong&gt; is what the document is aiming to become. Dereference the URI to get the human name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"https://datatracker.ietf.org/api/v1/name/intendedstdlevelname/ps/?format=json"&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; .name
&lt;span class="c"&gt;# Proposed Standard&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Worth knowing: the text inside a draft can say &lt;code&gt;Intended status: Standards Track&lt;/code&gt; in its own header while this field is &lt;code&gt;null&lt;/code&gt;. The header line is typed by the author into the document template. The API field is set through the process. When they disagree, they are answering different questions, and the API field is the one that reflects where the document sits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;ad&lt;/code&gt;&lt;/strong&gt; and &lt;strong&gt;&lt;code&gt;shepherd&lt;/code&gt;&lt;/strong&gt; are the two humans assigned to shove a document through publication. Both null means nobody has been assigned, which means the publication process has not started.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;rfc_number&lt;/code&gt;&lt;/strong&gt; is null until the document is published as an RFC. Once it is set, the draft is history and you should be reading the RFC.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;group&lt;/code&gt;&lt;/strong&gt; is the working group. Individual submissions all point at the same placeholder group, which is worth resolving once so you recognise it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"https://datatracker.ietf.org/api/v1/group/group/1027/?format=json"&lt;/span&gt; | jq &lt;span class="s1"&gt;'{acronym, name, type}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"acronym"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"none"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Individual Submissions"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/api/v1/name/grouptypename/individ/"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;states&lt;/code&gt;&lt;/strong&gt; is a list of state URIs. The one from the &lt;code&gt;draft-iesg&lt;/code&gt; state type is the one people mean by "where is it":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"https://datatracker.ietf.org/api/v1/doc/state/150/?format=json"&lt;/span&gt; | jq &lt;span class="s1"&gt;'{name, desc}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"I-D Exists"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"desc"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The IESG has not started processing this draft, or has stopped processing it without publication."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That description is the clearest sentence on the whole site. It is the IESG saying, in its own data model, that nothing has happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  The whole check in one function
&lt;/h2&gt;

&lt;p&gt;This is small enough to keep in a scratch file and run whenever a draft turns up in a thread.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urllib.request&lt;/span&gt;

&lt;span class="n"&gt;API&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://datatracker.ietf.org/api/v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;draft_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;API&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/doc/document/?name=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;&amp;amp;format=json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;objects&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;objects&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: no such document&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;adopted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ietf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;signals&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;adopted by a working group&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;adopted&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;on a publication stream&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stream&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;has an intended status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;intended_std_level&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;has a responsible AD&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ad&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;has a shepherd&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;shepherd&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;published as an RFC&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rfc_number&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;rev&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;pages&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; pages, expires &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;expires&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;signals&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;no &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;signals&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/6 signals present&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;draft_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;draft-brotman-aggregate-performance-reporting&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;draft_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;draft-ietf-dmarc-dmarcbis&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output on 13 September 2026:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;draft-brotman-aggregate-performance-reporting-01  (14 pages, expires 2027-03-12)
  no   adopted by a working group
  no   on a publication stream
  no   has an intended status
  no   has a responsible AD
  no   has a shepherd
  no   published as an RFC
  0/6 signals present

draft-ietf-dmarc-dmarcbis-41  (83 pages, expires 2025-10-06)
  yes  adopted by a working group
  yes  on a publication stream
  yes  has an intended status
  yes  has a responsible AD
  yes  has a shepherd
  no   published as an RFC
  5/6 signals present
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the expiry dates. The individual submission expires in 2027 and the working group document expired in 2025. An expiry date in the past is not a signal of abandonment. Drafts lapse after six months and get replaced by an RFC or a newer revision, and the record stays. Use &lt;code&gt;rev&lt;/code&gt; and the &lt;code&gt;time&lt;/code&gt; field for freshness, not &lt;code&gt;expires&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Watching a draft instead of checking it once
&lt;/h2&gt;

&lt;p&gt;If you have decided a draft is worth tracking, the field to store is &lt;code&gt;time&lt;/code&gt;, which is when the record last changed. Poll it and compare.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"https://datatracker.ietf.org/api/v1/doc/document/?name=&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;format=json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.objects[0] | "\(.rev) \(.time)"'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the Brotman draft that prints &lt;code&gt;01 2026-09-08T15:50:05Z&lt;/code&gt;, so the revision that is live today went up on 8 September 2026. Store that string, run the same command on a schedule, and diff. When &lt;code&gt;rev&lt;/code&gt; moves you have a new version to read. When &lt;code&gt;stream&lt;/code&gt; stops being null, the document has been adopted and the questions above change.&lt;/p&gt;

&lt;p&gt;The datatracker also exposes a &lt;code&gt;/api/v1/submit/submission/&lt;/code&gt; endpoint, and the document record carries a list of submission URIs, one per revision. That list is the cheapest way to see how many times a document has actually been revised without scraping the history tab.&lt;/p&gt;

&lt;h2&gt;
  
  
  What none of this measures
&lt;/h2&gt;

&lt;p&gt;Zero out of six is not a criticism of a document. Individual submissions are the normal front door, and DMARC itself came in through it. Run the same query against &lt;code&gt;draft-kucherawy-dmarc-base&lt;/code&gt; and you get &lt;code&gt;group&lt;/code&gt; 1027, the same Individual Submissions placeholder, &lt;code&gt;stream&lt;/code&gt; set to &lt;code&gt;ise&lt;/code&gt; rather than &lt;code&gt;ietf&lt;/code&gt;, and &lt;code&gt;rev&lt;/code&gt; stopping at 13 with an expiry of 2015-08-10. That document became RFC 7489, on the Independent Submission stream, without a working group ever adopting it. The DMARC working group and &lt;code&gt;draft-ietf-dmarc-dmarcbis&lt;/code&gt; came afterwards.&lt;/p&gt;

&lt;p&gt;What the six signals tell you is narrower and more useful: how much process has run, and therefore how much the wire format is likely to move under you. A draft at 0/6 can change shape completely at the next revision, because nobody has agreed to anything yet. A document at 5/6 with an AD attached has had its incompatible changes argued out already.&lt;/p&gt;

&lt;p&gt;There is also a plain-language version of all of this on every datatracker page, in a yellow box, above the metadata. For an unadopted draft it reads: "This I-D is not endorsed by the IETF and has no formal standing in the IETF standards process." The API just lets you check a list of them without opening fifteen tabs.&lt;/p&gt;

&lt;p&gt;Most of what I build sits on email authentication records, which is an area with a lot of draft traffic, so this check runs before I read a new proposal rather than after. It takes about ten seconds. Implementing the wrong revision of a wire format takes a weekend.&lt;/p&gt;

&lt;p&gt;Written with AI assistance from my own notes and test results. I checked every fact and command before publishing. The cover image was generated with DEV's built-in image tool.&lt;/p&gt;

</description>
      <category>email</category>
      <category>programming</category>
      <category>api</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Which EU countries let you check a company for free: a status table</title>
      <dc:creator>Jose Pollman</dc:creator>
      <pubDate>Fri, 07 Aug 2026 15:24:10 +0000</pubDate>
      <link>https://dev.to/jose_pollman_fa7c6ec43cdd/which-eu-countries-let-you-check-a-company-for-free-a-status-table-2jfe</link>
      <guid>https://dev.to/jose_pollman_fa7c6ec43cdd/which-eu-countries-let-you-check-a-company-for-free-a-status-table-2jfe</guid>
      <description>&lt;p&gt;If you are building anything that touches European business data — onboarding, invoicing, KYB, fraud checks — you will eventually ask the same question I did: which countries can I actually get company data from, for free, without an account?&lt;/p&gt;

&lt;p&gt;I could not find this written down anywhere, so I worked it out the hard way while building a supplier checker. Here it is.&lt;/p&gt;

&lt;h2&gt;
  
  
  The baseline: VIES
&lt;/h2&gt;

&lt;p&gt;The European Commission runs &lt;a href="https://ec.europa.eu/taxation_customs/vies/" rel="noopener noreferrer"&gt;VIES&lt;/a&gt;, which validates VAT numbers across all 27 member states plus Northern Ireland (&lt;code&gt;XI&lt;/code&gt;). It is free, it needs no key, and it is the obvious starting point.&lt;/p&gt;

&lt;p&gt;Two things about it are worth knowing before you build on it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It answers one question: is this VAT number currently registered.&lt;/strong&gt; It does not tell you the company is solvent, trading, or that it has not been struck off. A company in liquidation keeps a cleanly resolving VAT number for months, because deregistration and insolvency are run by different authorities on different timetables.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Name and address are returned for 25 of the 28 jurisdictions, not all of them.&lt;/strong&gt; Germany and Spain confirm registration but publish no company name through VIES. I tested three valid numbers for each before accepting that. For those two, a yes/no is genuinely all you can honestly show.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where you can go further, free
&lt;/h2&gt;

&lt;p&gt;Ten countries publish enough through a national register to add something meaningful on top of VIES:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Country&lt;/th&gt;
&lt;th&gt;Free register&lt;/th&gt;
&lt;th&gt;Reports company state&lt;/th&gt;
&lt;th&gt;Reports VAT-active&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Romania&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Poland&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Slovenia&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Estonia&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;France&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Greece&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bulgaria&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Latvia&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Czechia&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Finland&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Company state&lt;/strong&gt; means the register tells you whether a business is inactive, in liquidation, bankrupt, insolvent, terminated or struck off. This is the valuable column, and only six countries have it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;VAT-active&lt;/strong&gt; matters more than it sounds. VIES cannot distinguish "this is a real company that is not VAT-registered" from "this number belongs to nobody". Three registers can.&lt;/p&gt;

&lt;p&gt;Note that Czechia and Finland are in the list but in neither of the other columns — they confirm a name and little else. Worth knowing before you design a feature around "the ten countries".&lt;/p&gt;

&lt;h2&gt;
  
  
  Where you cannot
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;No free company-data source I could find at all:&lt;/strong&gt; Germany, Austria, Italy, Spain, Portugal, Lithuania, Luxembourg, Cyprus, Malta.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Behind an account, an API key or a payment:&lt;/strong&gt; Hungary, Ireland, Belgium, Denmark, Croatia, Netherlands, Sweden.&lt;/p&gt;

&lt;p&gt;Hungary deserves its own paragraph. It &lt;em&gt;does&lt;/em&gt; return name and address through VIES, so it looks fine at first. What is missing is company state — and I found six separate official routes to it, every single one behind a CAPTCHA. That is not a technical problem you can engineer around. It is a policy decision, and the answer is no.&lt;/p&gt;

&lt;h2&gt;
  
  
  The UK, post-Brexit
&lt;/h2&gt;

&lt;p&gt;Great Britain left VIES on 1 January 2021. GB VAT numbers now return invalid there, and HMRC's own VAT checking API requires an approved application. If you do not hold one, what you &lt;em&gt;can&lt;/em&gt; do for free is verify the official check digit locally — which proves a number is impossible, never that it exists — and then link people to the free checker on GOV.UK.&lt;/p&gt;

&lt;p&gt;Northern Ireland is a separate case: &lt;code&gt;XI&lt;/code&gt; numbers go through VIES normally.&lt;/p&gt;

&lt;h2&gt;
  
  
  Poland is the outlier worth knowing about
&lt;/h2&gt;

&lt;p&gt;Poland is the only EU member state where you can check whether a bank account belongs to a company.&lt;/p&gt;

&lt;p&gt;The Ministry of Finance publishes the accounts that VAT-registered businesses have declared, on what is known as the white list. It exists because paying into an unlisted account has tax consequences for the payer, so there is a statutory reason for the data to be public and current.&lt;/p&gt;

&lt;p&gt;Nowhere else in the EU can you do this from public data. Everywhere else, an IBAN tells you the country and the bank and absolutely nothing about who owns it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical notes for anyone building this
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Greece files VAT under &lt;code&gt;EL&lt;/code&gt;, not &lt;code&gt;GR&lt;/code&gt;.&lt;/strong&gt; Every ISO country code list will tell you &lt;code&gt;GR&lt;/code&gt;. VIES wants &lt;code&gt;EL&lt;/code&gt;. This will cost you an afternoon if nobody warns you.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Northern Ireland is &lt;code&gt;XI&lt;/code&gt;.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Implement the check digits locally.&lt;/strong&gt; They are published arithmetic, they work offline, and they work when a registry is down. I did 19 countries and it is one of the highest value-per-line things in the project — it catches a transposed digit instantly with no network call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assume the registers will break.&lt;/strong&gt; They change their markup without warning, and that is the real maintenance cost. It is also why I have not shipped an API: I would rather a scraper break for me than inside somebody else's production pipeline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VIES has downtime&lt;/strong&gt;, disproportionately at weekends and quarter ends. National registers are a useful fallback for the ten countries above.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why the gaps matter more than the coverage
&lt;/h2&gt;

&lt;p&gt;The temptation when you build something like this is to present the ten countries as "European coverage" and let people assume the rest works too. It does not, and the honest version is more useful: sixteen of twenty-seven member states have no free route to company status at all.&lt;/p&gt;

&lt;p&gt;If you are relying on this data for anything that matters, know which column your country is in.&lt;/p&gt;

&lt;p&gt;I keep a running version of this against a free tool I maintain — &lt;a href="https://vetthisvendor.com/example?utm_source=devto" rel="noopener noreferrer"&gt;a worked example is here&lt;/a&gt; if you want to see what the combined output looks like. Happy to answer questions about any specific register in the comments.&lt;/p&gt;

</description>
      <category>api</category>
      <category>data</category>
      <category>software</category>
    </item>
    <item>
      <title>Four things get called "someone is spoofing my domain". DMARC fixes one.</title>
      <dc:creator>Jose Pollman</dc:creator>
      <pubDate>Wed, 05 Aug 2026 13:52:25 +0000</pubDate>
      <link>https://dev.to/jose_pollman_fa7c6ec43cdd/four-things-get-called-someone-is-spoofing-my-domain-dmarc-fixes-one-2ajb</link>
      <guid>https://dev.to/jose_pollman_fa7c6ec43cdd/four-things-get-called-someone-is-spoofing-my-domain-dmarc-fixes-one-2ajb</guid>
      <description>&lt;p&gt;Someone forwards you a message. It has your company's address on it and you did not send it. The obvious next move is to go and publish &lt;code&gt;p=reject&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Wait. Four quite different attacks get reported in exactly those words, and &lt;strong&gt;DMARC only fixes one of them.&lt;/strong&gt; Enforcing against the wrong one costs weeks and changes nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Triage first: look at the actual From address
&lt;/h2&gt;

&lt;p&gt;Not the display name your mail client shows you. The address.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What you see&lt;/th&gt;
&lt;th&gt;What it is&lt;/th&gt;
&lt;th&gt;What fixes it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;billing@example.com&lt;/code&gt; — your exact domain&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Domain spoofing.&lt;/strong&gt; Anyone can write any address into a &lt;code&gt;From:&lt;/code&gt; header.&lt;/td&gt;
&lt;td&gt;SPF + DKIM + DMARC at &lt;code&gt;p=reject&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Your Company &amp;lt;randomuser@gmail.com&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Display-name impersonation.&lt;/strong&gt; The address is theirs. Only the label is yours.&lt;/td&gt;
&lt;td&gt;Nothing in your DNS. Receiver-side rules and staff training.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;billing@examp1e.com&lt;/code&gt;, &lt;code&gt;example-inc.com&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Lookalike domain.&lt;/strong&gt; A different domain that reads like yours.&lt;/td&gt;
&lt;td&gt;Monitoring and takedowns. Your DMARC record has no authority over it.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Genuinely from your account, in your Sent folder&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Compromised account.&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Password, revoke sessions, and check for forwarding rules the attacker added.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Only the first row is domain spoofing.&lt;/strong&gt; It's also the only one where a stranger can put &lt;em&gt;your&lt;/em&gt; domain in front of your customers, which is why it's worth fixing first and why DMARC exists.&lt;/p&gt;

&lt;p&gt;If you can't tell which you're looking at, the raw headers will settle it — the &lt;code&gt;Authentication-Results&lt;/code&gt; header names the domain that actually authenticated.&lt;/p&gt;

&lt;h2&gt;
  
  
  "I'm getting bounces for mail I never sent"
&lt;/h2&gt;

&lt;p&gt;This is how most people discover the problem, and it's alarming in a misleading way.&lt;/p&gt;

&lt;p&gt;A spammer sent to a few thousand addresses with your address forged as the sender. Some of those didn't exist. Those servers bounced the messages back to the forged sender — you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It does not mean anything of yours was accessed.&lt;/strong&gt; No password is required to type your address into a header. It's evidence your domain is being forged, and a good reason to get to enforcement, but it is not a breach.&lt;/p&gt;

&lt;p&gt;One exception worth ruling out immediately: if the bounces correspond to messages that appear in your &lt;strong&gt;own Sent folder&lt;/strong&gt;, that's a compromised account, not spoofing. Different problem, much more urgent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why fixing the real thing takes weeks
&lt;/h2&gt;

&lt;p&gt;Worth understanding the mechanism, because it explains the timeline.&lt;/p&gt;

&lt;p&gt;Nothing in the original design of email verifies the &lt;code&gt;From:&lt;/code&gt; header. SMTP accepts whatever a sender writes. SPF, DKIM and DMARC are a layer bolted on afterwards that lets you publish a claim about who may send as you, and lets receivers act on it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Get SPF or DKIM passing for every legitimate sender.&lt;/strong&gt; Not just your mailbox provider — the CRM, the invoicing system, the ticketing tool, the booking form. And each must pass &lt;em&gt;aligned&lt;/em&gt; with your domain. A vendor sending from its own envelope domain can pass SPF perfectly and still fail DMARC.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Publish DMARC at &lt;code&gt;p=none&lt;/code&gt; with a reporting address.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;_dmarc.example.com.  TXT  "v=DMARC1; p=none; rua=mailto:dmarc@example.com"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This blocks nothing. It tells you who is sending as you — including the forger, and including the three internal systems you'd forgotten about.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Read the reports for four to six weeks, then enforce.&lt;/strong&gt; Long enough that monthly and quarterly senders show up. Then &lt;code&gt;p=quarantine&lt;/code&gt;, then &lt;code&gt;p=reject&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;There's no shortcut here that doesn't risk blocking your own mail.&lt;/p&gt;

&lt;h2&gt;
  
  
  The &lt;code&gt;sp=&lt;/code&gt; footgun
&lt;/h2&gt;

&lt;p&gt;Attackers move to subdomains once the parent is unusable, because &lt;code&gt;billing.example.com&lt;/code&gt; is just as convincing to a recipient and is usually left unprotected.&lt;/p&gt;

&lt;p&gt;Subdomains &lt;strong&gt;inherit &lt;code&gt;p=&lt;/code&gt;&lt;/strong&gt; unless you override it, so a bare &lt;code&gt;p=reject&lt;/code&gt; already covers them.&lt;/p&gt;

&lt;p&gt;The mistake is publishing &lt;code&gt;sp=none&lt;/code&gt; alongside it — almost always copied from an example record found somewhere — which reopens precisely the door you just shut. If you don't have a specific reason for an &lt;code&gt;sp=&lt;/code&gt; tag, don't publish one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What DMARC will not do, whatever the vendor said
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It does not stop lookalike domains.&lt;/strong&gt; &lt;code&gt;examp1e.com&lt;/code&gt; belongs to someone else. Your DNS says nothing about it, and &lt;code&gt;p=reject&lt;/code&gt; on your domain has no effect on theirs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It does not stop display-name impersonation.&lt;/strong&gt; That message is properly authenticated &lt;em&gt;for the sender's own domain&lt;/em&gt;. There is no authentication failure for DMARC to act on. This is now the more common attack against small organisations — precisely because DMARC adoption made the easy version harder.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;It does not stop your name being used in the message body.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It does not apply retroactively.&lt;/strong&gt; Mail already delivered stays delivered.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What it does is remove your domain from the attacker's toolkit. That's worth the few weeks. The rest is a filtering and training problem, not a DNS one.&lt;/p&gt;




&lt;p&gt;I maintain &lt;a href="https://notspoofed.com/?utm_source=devto" rel="noopener noreferrer"&gt;notspoofed&lt;/a&gt; — a free, no-signup SPF/DKIM/DMARC checker. It reports whether your policy is actually enforcing and flags an &lt;code&gt;sp=&lt;/code&gt; tag that's quietly weakening it, which is the failure above that people don't find on their own. There's also an in-browser header analyzer for the triage step; headers are parsed locally and never uploaded. Source on &lt;a href="https://github.com/josepollman-png/notspoofed" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; under MIT.&lt;/p&gt;

&lt;p&gt;If you've had the bounce-flood experience and it turned out to be something other than plain domain spoofing, I'd be interested to hear which of the four it was.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>security</category>
      <category>dns</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>What I learned reading ten EU company registers</title>
      <dc:creator>Jose Pollman</dc:creator>
      <pubDate>Wed, 05 Aug 2026 00:34:46 +0000</pubDate>
      <link>https://dev.to/jose_pollman_fa7c6ec43cdd/what-i-learned-reading-ten-eu-company-registers-8i2</link>
      <guid>https://dev.to/jose_pollman_fa7c6ec43cdd/what-i-learned-reading-ten-eu-company-registers-8i2</guid>
      <description>&lt;p&gt;I built a free tool that checks a supplier before you pay them. The part that took most of the work, and taught me most, was reading ten national company registers instead of relying on the EU's own VIES service.&lt;/p&gt;

&lt;p&gt;This is what I found out, mostly so the next person doesn't have to.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem with "the VAT number is valid"
&lt;/h2&gt;

&lt;p&gt;VIES — the European Commission's VAT Information Exchange System — answers one question: is this VAT number currently registered. That sounds like the question you want answered. It isn't.&lt;/p&gt;

&lt;p&gt;A company that has gone into liquidation keeps a cleanly resolving VAT number in VIES. So does one that has been struck off the register. Deregistration and insolvency are run by different authorities on different timetables, and the gap between "this company has stopped being a going concern" and "the VAT number stops validating" can be months.&lt;/p&gt;

&lt;p&gt;So you can check a supplier, get a green tick, and be looking at an insolvency estate.&lt;/p&gt;

&lt;p&gt;The national registers know. VIES doesn't ask them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ten registers, and what each actually gives you
&lt;/h2&gt;

&lt;p&gt;I found free, public, machine-readable-enough sources for ten countries: &lt;strong&gt;Bulgaria, Czechia, Estonia, Finland, France, Greece, Latvia, Poland, Romania and Slovenia.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;They are not equivalent, and this is the thing I'd have liked written down somewhere before I started:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Six of them report company *state&lt;/strong&gt;* — inactive, in liquidation, bankrupt, insolvent, terminated, ceased, struck off: Romania, Estonia, France, Greece, Bulgaria, Latvia. This is the valuable one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Three report whether the company is actually VAT-active&lt;/strong&gt; — Poland, Romania, Slovenia. That matters more than it sounds, because VIES does not distinguish "this is a real company that isn't VAT-registered" from "this number belongs to nobody".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The rest give you a name&lt;/strong&gt; and not much more.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Czechia, for instance, is in the ten but in neither of the other two groups. It confirms a name. That's it. Worth knowing before you build a feature around it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Poland is the interesting one
&lt;/h2&gt;

&lt;p&gt;Poland is the only EU member state where you can check whether a bank account belongs to the supplier.&lt;/p&gt;

&lt;p&gt;Not because of anything clever — because the Ministry of Finance publishes the accounts that VAT-registered businesses have declared, on what's known as the white list. It exists because paying an unlisted account has tax consequences for the &lt;em&gt;payer&lt;/em&gt;, so there's a statutory reason for the data to be public and current.&lt;/p&gt;

&lt;p&gt;Nowhere else in the EU can you do this from public data. In every other country, an IBAN tells you the country and the bank and nothing whatsoever about who owns it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it gets ugly
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Nine countries have no free company-data source I could find at all:&lt;/strong&gt; Germany, Austria, Italy, Spain, Portugal, Lithuania, Luxembourg, Cyprus, Malta.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Seven more are gated&lt;/strong&gt; behind an account, an API key or a payment: Hungary, Ireland, Belgium, Denmark, Croatia, Netherlands, Sweden.&lt;/p&gt;

&lt;p&gt;Hungary deserves a specific mention. It &lt;em&gt;does&lt;/em&gt; return a name and address through VIES. What's missing is company state — and I found six separate official routes to it, every one of them behind a CAPTCHA. That's not a technical problem, it's a policy one, and no amount of engineering fixes it.&lt;/p&gt;

&lt;p&gt;Germany and Spain are a different flavour of annoying: VIES confirms registration for both, but neither publishes a company name through it. I tested three valid numbers for each to be sure I wasn't holding it wrong. For those two, all you can honestly show is a yes/no.&lt;/p&gt;

&lt;h2&gt;
  
  
  Small things that cost me time
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Greece files VAT under &lt;code&gt;EL&lt;/code&gt;, not &lt;code&gt;GR&lt;/code&gt;.&lt;/strong&gt; Every list of ISO country codes will tell you &lt;code&gt;GR&lt;/code&gt;. VIES wants &lt;code&gt;EL&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Northern Ireland is &lt;code&gt;XI&lt;/code&gt;.&lt;/strong&gt; Post-Brexit, NI numbers go through VIES; Great Britain numbers don't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GB is not available&lt;/strong&gt; unless you hold an approved application for HMRC's VAT API. I don't. What you can do for free is verify the official check digit locally — which proves a number is impossible, never that it exists — and then link to the free checker on GOV.UK, which answers the user's actual question in ten seconds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check digits are arithmetic&lt;/strong&gt;, so they work when the registry is down. I implemented them for 19 countries. This turned out to be one of the highest-value-per-line things in the project: it catches a transposed digit immediately, offline, with no network call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Registers change their markup without warning.&lt;/strong&gt; This is the real maintenance cost, and the main reason I haven't shipped an API — I'd rather a scraper break for me than in someone else's production pipeline.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The design decision I'd defend hardest
&lt;/h2&gt;

&lt;p&gt;The tool shows five separate results and deliberately refuses to combine them into a score or a "safe to pay" verdict.&lt;/p&gt;

&lt;p&gt;That's not modesty, it's the whole point. The most common invoice fraud is bank-detail redirection: a real supplier's mailbox is compromised and only the account number on the invoice changes. The company is real. The VAT number is valid. The goods were delivered. The sanctions check is clean. Every individual check passes, because everything &lt;em&gt;is&lt;/em&gt; genuine except the destination of the money.&lt;/p&gt;

&lt;p&gt;A single green tick would be a confident lie about the one attack that actually happens. So there are five answers, and a standing instruction to confirm bank details by phone on a number you already had.&lt;/p&gt;

&lt;p&gt;If you're building anything in this space, that's the trap: the aggregate score is the most saleable feature and the most dishonest one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Boring stack, on purpose
&lt;/h2&gt;

&lt;p&gt;The whole thing runs in about 300 MB on a single VPS in Helsinki. No account, no cookies, no ads, no third-party analytics. The full IBAN is never stored or displayed — only country code, check digits and last four — and saved results are deleted after 90 days.&lt;/p&gt;

&lt;p&gt;There's no API and no bulk checking. For a developer audience that's probably the whole review, and it's a fair criticism. It's one person maintaining sixteen fragile integrations, and an API is the fastest way to turn that into an operations job I can't staff.&lt;/p&gt;

&lt;p&gt;A worked example, if you'd rather see the output than type a VAT number: &lt;a href="https://vetthisvendor.com/example?utm_source=devto" rel="noopener noreferrer"&gt;vetthisvendor.com/example&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Happy to go into any of the registry scraping in the comments — that was most of the work and most of the pain.&lt;/p&gt;

</description>
      <category>backend</category>
      <category>softwaredevelopment</category>
      <category>tooling</category>
    </item>
    <item>
      <title>Every DKIM checker is guessing — including mine</title>
      <dc:creator>Jose Pollman</dc:creator>
      <pubDate>Tue, 04 Aug 2026 12:53:34 +0000</pubDate>
      <link>https://dev.to/jose_pollman_fa7c6ec43cdd/every-dkim-checker-is-guessing-including-mine-1klb</link>
      <guid>https://dev.to/jose_pollman_fa7c6ec43cdd/every-dkim-checker-is-guessing-including-mine-1klb</guid>
      <description>&lt;p&gt;Almost every DNS record you care about can be looked up. Ask for the SPF record, you get it. Ask for &lt;code&gt;_dmarc&lt;/code&gt;, it's there.&lt;/p&gt;

&lt;p&gt;DKIM doesn't work that way, and almost nobody building tooling around it says so out loud.&lt;/p&gt;

&lt;h2&gt;
  
  
  You cannot list selectors. At all.
&lt;/h2&gt;

&lt;p&gt;A DKIM public key lives at:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;selector&amp;gt;._domainkey.&amp;lt;domain&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The selector is an arbitrary label chosen by whoever set up signing. Google Workspace uses &lt;code&gt;google&lt;/code&gt;. Microsoft 365 uses &lt;code&gt;selector1&lt;/code&gt; and &lt;code&gt;selector2&lt;/code&gt;. Mailchimp uses &lt;code&gt;k1&lt;/code&gt;. Amazon SES generates three random tokens per identity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;There is no DNS query that returns "every name under &lt;code&gt;_domainkey&lt;/code&gt;".&lt;/strong&gt; &lt;code&gt;ANY&lt;/code&gt; doesn't do it. Zone transfers are refused by every sane nameserver. To read a DKIM key, you must already know its name.&lt;/p&gt;

&lt;p&gt;Which means every DKIM checker in existence — mine included — is guessing from a list.&lt;/p&gt;

&lt;p&gt;So when a tool tells you &lt;strong&gt;"no DKIM found"&lt;/strong&gt;, the honest translation is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;We tried our list of forty or fifty common selectors and none of them matched.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is a genuinely different statement from "you have no DKIM", and the gap between the two is where people waste afternoons.&lt;/p&gt;

&lt;p&gt;Amazon SES makes it concrete. Its selectors are random tokens. &lt;strong&gt;No guess list will ever find them.&lt;/strong&gt; A domain signing perfectly through SES will read as "no DKIM" on every guessing tool, forever. If your checker doesn't tell you that, it's misleading you by omission.&lt;/p&gt;

&lt;h2&gt;
  
  
  The opposite failure: selectors that were never there
&lt;/h2&gt;

&lt;p&gt;Now the direction that's actually dangerous, because it produces confident output instead of a shrug.&lt;/p&gt;

&lt;p&gt;Some domains publish a &lt;strong&gt;wildcard DNS record&lt;/strong&gt;, which answers every name under the domain. On such a domain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dig +short TXT totally-made-up-selector._domainkey.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;...returns something. Not because that selector exists. Because the wildcard matches everything.&lt;/p&gt;

&lt;p&gt;A checker whose test is "did I get an answer?" will report a dozen imaginary selectors on these domains, and it will look authoritative doing it.&lt;/p&gt;

&lt;p&gt;The obvious defence is to require the answer to actually parse as DKIM — specifically to carry a &lt;code&gt;p=&lt;/code&gt; tag, which &lt;a href="https://www.rfc-editor.org/rfc/rfc6376" rel="noopener noreferrer"&gt;RFC 6376&lt;/a&gt; makes mandatory.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;That's necessary, and it isn't sufficient.&lt;/strong&gt; Here's the one that got me:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;*._domainkey.example.com.  TXT  "v=DKIM1; p="
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's a structurally valid DKIM record with an empty key. It passes the &lt;code&gt;p=&lt;/code&gt; test. So every guessed selector comes back looking like a &lt;strong&gt;revoked key&lt;/strong&gt; — and now your tool is telling someone they have thirty revoked DKIM selectors, which is both alarming and entirely fictional.&lt;/p&gt;

&lt;p&gt;The only defence I've found that holds: &lt;strong&gt;query a deliberately nonsensical selector first, then discard any result identical to what the wildcard returned.&lt;/strong&gt; Establish the baseline lie, then subtract it.&lt;/p&gt;

&lt;p&gt;If a checker has ever told you that you have dozens of revoked selectors, this is what happened. Ignore it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding yours for certain
&lt;/h2&gt;

&lt;p&gt;Two methods, neither of which involves guessing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read a message you sent.&lt;/strong&gt; Most reliable, and needs access to nothing. Send yourself a message, view the raw source (&lt;code&gt;⋮ → Show original&lt;/code&gt; in Gmail), and find:&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;DKIM-Signature&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="na"&gt; v=1; a=rsa-sha256; c=relaxed/relaxed;
 d=example.com; s=selector1; t=1785453729;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;s=&lt;/code&gt; is your selector. &lt;code&gt;d=&lt;/code&gt; is the signing domain — &lt;strong&gt;check that &lt;code&gt;d=&lt;/code&gt; matches your visible &lt;code&gt;From:&lt;/code&gt; domain&lt;/strong&gt;, because if it doesn't, DKIM is passing and not aligning, and DMARC fails anyway.&lt;/p&gt;

&lt;p&gt;Do this once per sending system. Each has its own selector and they all have to work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Or check the provider's console.&lt;/strong&gt; Every platform exposes this under "authentication", "verified domains" or "DKIM" — which is also where you switch signing on if it turns out it was never enabled.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading the record once you have the name
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dig +short TXT selector1._domainkey.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="s2"&gt;"v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA…"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four things worth noticing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;p=&lt;/code&gt; with nothing after it means revoked.&lt;/strong&gt; If anything is still signing with that selector, that mail is failing DKIM right now.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A missing &lt;code&gt;v=&lt;/code&gt; tag is fine.&lt;/strong&gt; RFC 6376 only recommends it. Plenty of production records start straight at &lt;code&gt;k=rsa&lt;/code&gt; and validate perfectly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Key length.&lt;/strong&gt; 1024-bit RSA is still accepted everywhere but is no longer considered strong. 2048 is the norm. Rotate through your provider, not by hand-editing DNS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A CNAME instead of a TXT is normal.&lt;/strong&gt; Most platforms have you delegate the selector to them so they can rotate keys without touching your zone. Follow it and you should land on a valid key.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Worth stealing: the revoke-everything wildcard
&lt;/h2&gt;

&lt;p&gt;The wildcard pattern that breaks checkers is genuinely useful if you own a domain that sends no mail:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;*._domainkey.example.com.  TXT  "v=DKIM1; p="
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That declares every selector on the domain revoked. Combined with &lt;code&gt;v=spf1 -all&lt;/code&gt; and &lt;code&gt;p=reject&lt;/code&gt;, it's an unambiguous statement that this domain sends nothing and any signature claiming otherwise is forged.&lt;/p&gt;

&lt;p&gt;And it doesn't paint you into a corner: &lt;strong&gt;DNS wildcards only apply where no exact match exists&lt;/strong&gt;, so publishing a real selector later takes precedence automatically. One working key coexists happily with a revoke-everything wildcard.&lt;/p&gt;

&lt;p&gt;Most parked and brand-protection domains have SPF and DMARC locked down and leave DKIM wide open. This closes it.&lt;/p&gt;




&lt;p&gt;I maintain &lt;a href="https://notspoofed.com/?utm_source=devto" rel="noopener noreferrer"&gt;notspoofed&lt;/a&gt; — a free, no-signup SPF/DKIM/DMARC checker. It guesses fifty selectors like everything else, but it says so, it verifies each hit is a real parseable signing key rather than a wildcard echo, and it runs the nonsense-selector baseline described above so it won't invent thirty revoked keys for you. I built that last part because I got it wrong first. Source is on &lt;a href="https://github.com/josepollman-png/notspoofed" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; under MIT.&lt;/p&gt;

&lt;p&gt;If you've seen a checker confidently report selectors that don't exist, I'd like to know which — that failure mode is more common than it should be.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>security</category>
      <category>dns</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>SPF passed. DKIM passed. DMARC failed. Nothing is broken.</title>
      <dc:creator>Jose Pollman</dc:creator>
      <pubDate>Mon, 03 Aug 2026 19:50:40 +0000</pubDate>
      <link>https://dev.to/jose_pollman_fa7c6ec43cdd/spf-passed-dkim-passed-dmarc-failed-nothing-is-broken-22kk</link>
      <guid>https://dev.to/jose_pollman_fa7c6ec43cdd/spf-passed-dkim-passed-dmarc-failed-nothing-is-broken-22kk</guid>
      <description>&lt;p&gt;This header block is where most people's understanding of email authentication falls apart:&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;
  spf=pass smtp.mailfrom=bounce@esp-vendor.net;
  dkim=pass header.d=esp-vendor.net;
  dmarc=fail (p=REJECT) header.from=yourcompany.com&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two passes and a fail. No malformed record anywhere. Nothing to fix in DNS. And the mail is being rejected.&lt;/p&gt;

&lt;p&gt;Every part of that is correct behaviour.&lt;/p&gt;

&lt;h2&gt;
  
  
  The question DMARC actually asks
&lt;/h2&gt;

&lt;p&gt;DMARC does not ask "did SPF pass?"&lt;/p&gt;

&lt;p&gt;It asks: &lt;strong&gt;did SPF or DKIM pass for the same domain that appears in the &lt;code&gt;From:&lt;/code&gt; header?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;There are three domains in play in any message, and only one of them is visible to a human:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Identifier&lt;/th&gt;
&lt;th&gt;Lives in&lt;/th&gt;
&lt;th&gt;Who sees it&lt;/th&gt;
&lt;th&gt;Checked by&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;From:&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The message headers&lt;/td&gt;
&lt;td&gt;Your recipient&lt;/td&gt;
&lt;td&gt;Nothing, on its own&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Envelope sender (&lt;code&gt;MAIL FROM&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;The SMTP conversation&lt;/td&gt;
&lt;td&gt;Nobody&lt;/td&gt;
&lt;td&gt;SPF&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DKIM &lt;code&gt;d=&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;The signature header&lt;/td&gt;
&lt;td&gt;Nobody&lt;/td&gt;
&lt;td&gt;DKIM&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;SPF authenticates the envelope sender. DKIM authenticates the signing domain. &lt;strong&gt;Neither of them is the &lt;code&gt;From:&lt;/code&gt; header.&lt;/strong&gt; DMARC exists entirely to insist that at least one of them matches it.&lt;/p&gt;

&lt;p&gt;In the header above, SPF authenticated &lt;code&gt;esp-vendor.net&lt;/code&gt; and DKIM signed as &lt;code&gt;esp-vendor.net&lt;/code&gt;. The recipient sees &lt;code&gt;yourcompany.com&lt;/code&gt;. Nothing authenticated &lt;code&gt;yourcompany.com&lt;/code&gt;, so DMARC fails.&lt;/p&gt;

&lt;p&gt;It has to work this way. If DMARC accepted "SPF passed" without the matching step, anyone with a mailbox at a provider with a valid SPF record could send as you and pass.&lt;/p&gt;

&lt;h2&gt;
  
  
  How you get here without doing anything wrong
&lt;/h2&gt;

&lt;p&gt;You sign up for a marketing platform, an invoicing tool, or a helpdesk. You set the From address to &lt;code&gt;hello@yourcompany.com&lt;/code&gt; because that is what your customers should see.&lt;/p&gt;

&lt;p&gt;The platform sends from its own infrastructure with its own envelope domain — it has to, because that is where bounces go. SPF passes, for them. Your &lt;code&gt;From:&lt;/code&gt; says you.&lt;/p&gt;

&lt;p&gt;Those do not align. If nothing is DKIM-signed with your domain, every message from that platform fails DMARC.&lt;/p&gt;

&lt;p&gt;This is the single most common cause of "we set up SPF and it still doesn't work".&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix that feels right and does nothing
&lt;/h2&gt;

&lt;p&gt;Adding &lt;code&gt;include:esp-vendor.net&lt;/code&gt; to your SPF record.&lt;/p&gt;

&lt;p&gt;It is the intuitive move. It changes nothing.&lt;/p&gt;

&lt;p&gt;That include authorises their servers to send &lt;strong&gt;for your envelope domain&lt;/strong&gt;. Their mail still uses &lt;strong&gt;their&lt;/strong&gt; envelope domain, so the alignment comparison is untouched. You have spent one of your &lt;a href="https://dev.to/jose_pollman_fa7c6ec43cdd/your-spf-record-can-be-valid-published-and-completely-ignored-bi9"&gt;ten SPF DNS lookups&lt;/a&gt; for no benefit at all.&lt;/p&gt;

&lt;p&gt;If your reaction to a &lt;code&gt;dmarc=fail&lt;/code&gt; is to edit your SPF record, stop and read the &lt;code&gt;Authentication-Results&lt;/code&gt; header first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Relaxed vs strict, and the trap in the middle
&lt;/h2&gt;

&lt;p&gt;Alignment has two modes, set per-mechanism with &lt;code&gt;aspf=&lt;/code&gt; and &lt;code&gt;adkim=&lt;/code&gt; in your DMARC record:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Relaxed&lt;/strong&gt; (&lt;code&gt;r&lt;/code&gt;, the default) — organisational domains must match. &lt;code&gt;mail.example.com&lt;/code&gt; aligns with &lt;code&gt;example.com&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Strict&lt;/strong&gt; (&lt;code&gt;s&lt;/code&gt;) — exact match only. &lt;code&gt;mail.example.com&lt;/code&gt; does &lt;em&gt;not&lt;/em&gt; align with &lt;code&gt;example.com&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Relaxed is right for almost everyone. Strict is worth it only if you are confident every sender uses the exact apex domain, and it breaks the day someone starts sending from a subdomain.&lt;/p&gt;

&lt;p&gt;Here is the part that matters if you are implementing this yourself: &lt;strong&gt;"organisational domain" is not "the last two labels".&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;attacker.co.uk&lt;/code&gt; and &lt;code&gt;victim.co.uk&lt;/code&gt; share their last two labels and have no relationship whatsoever. Getting this right means consulting the &lt;a href="https://publicsuffix.org/" rel="noopener noreferrer"&gt;Public Suffix List&lt;/a&gt;, not counting dots. A checker that counts dots will tell you two unrelated domains are aligned, which is the worst possible direction to be wrong in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading it from your own mail
&lt;/h2&gt;

&lt;p&gt;Send a message to a mailbox at a &lt;em&gt;different&lt;/em&gt; provider. Open the delivered copy and view the source — &lt;code&gt;⋮ → Show original&lt;/code&gt; in Gmail, &lt;code&gt;View message details&lt;/code&gt; in Outlook.&lt;/p&gt;

&lt;p&gt;Then compare exactly three lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;From: Your Company &amp;lt;hello@yourcompany.com&amp;gt;     &amp;lt;- the domain that must be matched
spf=pass smtp.mailfrom=bounce@esp.net          &amp;lt;- authenticated esp.net
dkim=pass header.d=esp.net                     &amp;lt;- signed by esp.net
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If neither the &lt;code&gt;smtp.mailfrom&lt;/code&gt; domain nor the DKIM &lt;code&gt;d=&lt;/code&gt; matches your From domain, DMARC fails regardless of how correct your SPF record is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use a message you received, not one from your Sent folder.&lt;/strong&gt; Sent copies have no &lt;code&gt;Authentication-Results&lt;/code&gt; header at all — that verdict is written by the &lt;em&gt;receiving&lt;/em&gt; server. Nearly everyone trips on this the first time and concludes their headers are broken.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fixing it properly
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Get DKIM signing with your own domain.&lt;/strong&gt; Every serious sending platform supports it. It is usually called "authenticate your domain", "domain authentication" or "custom DKIM", and it hands you a selector to publish in DNS. Once the platform signs with &lt;code&gt;d=yourcompany.com&lt;/code&gt;, DKIM aligns and DMARC passes no matter what the envelope sender says.&lt;/p&gt;

&lt;p&gt;There is a second reason to prefer DKIM, and it is the one that bites later: &lt;strong&gt;DKIM survives forwarding and SPF does not.&lt;/strong&gt; The moment a message is forwarded, the forwarding server is not in your SPF record. Every mailing list and every user-configured forward becomes a DMARC failure if SPF alignment is all you have.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Optionally, also set a custom return-path.&lt;/strong&gt; Some platforms let you point the envelope sender at a subdomain of yours — "custom return-path" or "custom bounce domain". You publish a CNAME, the envelope domain becomes &lt;code&gt;bounce.yourcompany.com&lt;/code&gt;, and that aligns under relaxed. This fixes only the SPF half and still dies on forwarding, so do it &lt;em&gt;as well as&lt;/em&gt; DKIM, not instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before you enforce
&lt;/h2&gt;

&lt;p&gt;This is the actual reason the &lt;code&gt;p=none&lt;/code&gt; → &lt;code&gt;quarantine&lt;/code&gt; → &lt;code&gt;reject&lt;/code&gt; order exists.&lt;/p&gt;

&lt;p&gt;Run at &lt;code&gt;p=none&lt;/code&gt; with a reporting address and read the aggregate reports. They tell you which sources pass SPF and DKIM &lt;strong&gt;with alignment&lt;/strong&gt; — a different and much smaller set than the sources that merely pass SPF. Move to &lt;code&gt;p=quarantine&lt;/code&gt; only when that list matches the systems you expect to be there.&lt;/p&gt;

&lt;p&gt;Skip it and you will find your misaligned senders the hard way: by having their mail rejected.&lt;/p&gt;




&lt;p&gt;I maintain &lt;a href="https://notspoofed.com/?utm_source=devto" rel="noopener noreferrer"&gt;notspoofed&lt;/a&gt; — a free, no-signup checker for SPF, DKIM and DMARC. It has an in-browser header analyzer that does the three-way alignment comparison above and shows relaxed vs strict as a table; headers are parsed locally and never uploaded. It uses the Public Suffix List for organisational domains, because counting labels is wrong. Source is on &lt;a href="https://github.com/josepollman-png/notspoofed" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; under MIT.&lt;/p&gt;

&lt;p&gt;If you have hit an alignment failure with a cause that is not on this list, I would like to hear it — those are the interesting ones.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>security</category>
      <category>dns</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Why your DMARC reports never arrive (and why Gmail can't be your rua address)</title>
      <dc:creator>Jose Pollman</dc:creator>
      <pubDate>Sun, 02 Aug 2026 21:02:50 +0000</pubDate>
      <link>https://dev.to/jose_pollman_fa7c6ec43cdd/why-your-dmarc-reports-never-arrive-and-why-gmail-cant-be-your-rua-address-4cj3</link>
      <guid>https://dev.to/jose_pollman_fa7c6ec43cdd/why-your-dmarc-reports-never-arrive-and-why-gmail-cant-be-your-rua-address-4cj3</guid>
      <description>&lt;p&gt;You published a DMARC record months ago. Every checker says it's valid. No reports have ever arrived, and at some point you stopped thinking about it and quietly concluded nobody's forging your domain.&lt;/p&gt;

&lt;p&gt;You don't know that. You just can't see.&lt;/p&gt;

&lt;p&gt;Two causes account for almost all of it, and both leave the DMARC record itself looking perfect.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Your reporting address never agreed to receive them
&lt;/h2&gt;

&lt;p&gt;This is in the spec and almost nobody knows about it.&lt;/p&gt;

&lt;p&gt;If your &lt;code&gt;rua=&lt;/code&gt; address is on a &lt;strong&gt;different organisational domain&lt;/strong&gt; from the domain publishing the record, &lt;a href="https://www.rfc-editor.org/rfc/rfc7489#section-7.1" rel="noopener noreferrer"&gt;RFC 7489 section 7.1&lt;/a&gt; requires the destination to explicitly consent. The reason is obvious once you see it: otherwise anyone could point &lt;code&gt;rua=&lt;/code&gt; at your mailbox and use the world's mail infrastructure as a free DDoS.&lt;/p&gt;

&lt;p&gt;Consent is a DNS record, published by the &lt;strong&gt;receiving&lt;/strong&gt; domain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;your-domain&amp;gt;._report._dmarc.&amp;lt;their-domain&amp;gt;.  TXT  "v=DMARC1"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So if &lt;code&gt;example.com&lt;/code&gt; sends reports to &lt;code&gt;dmarc@reports.vendor.com&lt;/code&gt;, then &lt;code&gt;vendor.com&lt;/code&gt; has to publish:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;example.com._report._dmarc.reports.vendor.com.  TXT  "v=DMARC1"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without it, conforming reporters send nothing at all. That includes Google, which is most of your report volume.&lt;/p&gt;

&lt;p&gt;You can watch this working in production. PayPal sends aggregate reports to a third party, and the authorisation record exists:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;paypal.com._report._dmarc.rua.agari.com.  TXT  "v=DMARC1;"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The practical consequence: not a Gmail address
&lt;/h3&gt;

&lt;p&gt;You cannot put a Gmail address in &lt;code&gt;rua=&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Google is not going to publish &lt;code&gt;yourdomain.com._report._dmarc.gmail.com&lt;/code&gt; on your behalf, and you can't create records in their zone. Same for Outlook, Yahoo, or any address at a domain you don't control.&lt;/p&gt;

&lt;p&gt;This is a common first move — publish DMARC, point reports at your personal inbox, wait. Nothing arrives, and nothing is wrong with the record.&lt;/p&gt;

&lt;p&gt;If you use a commercial DMARC service they normally publish these records during onboarding, often via a wildcard. If reports never start after you sign up, check this before anything else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When it doesn't apply:&lt;/strong&gt; reporting to your own organisational domain needs no authorisation. &lt;code&gt;example.com&lt;/code&gt; sending to &lt;code&gt;dmarc@mail.example.com&lt;/code&gt; is fine.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. There's no mail server behind the address
&lt;/h2&gt;

&lt;p&gt;Subtler, and easy to create by accident on a domain that only serves a website.&lt;/p&gt;

&lt;p&gt;You publish:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;_dmarc.example.com.  TXT  "v=DMARC1; p=reject; rua=mailto:dmarc@example.com"```

Self-referential, so no authorisation needed. Correct in every way.

But if `example.com` has **no MX record**, senders fall back to its A record — the implicit MX rule in [RFC 5321 section 5.1](https://www.rfc-editor.org/rfc/rfc5321#section-5.1). On a web-only domain that A record points at a web server, which doesn't speak SMTP. Every report bounces.

The DMARC record is flawless. You just have no mailbox.

This bites hardest on marketing sites and defensive brand registrations — exactly the domains where you published DMARC as a hardening measure and are least likely to notice the silence.

The fix doesn't require running a mail server. A forwarding service will publish MX records for you and forward `dmarc@` somewhere you actually read.

## Checking both in about thirty seconds



```bash
# 1. What does your record actually say?
dig +short TXT _dmarc.example.com

# 2. If rua points off-domain, is it authorised?
dig +short TXT example.com._report._dmarc.THEIR-DOMAIN.com
#    Expect "v=DMARC1". Anything else means reports are refused.

# 3. If rua points at your own domain, can it receive mail?
dig +short MX example.com
#    Empty means every report bounces.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  A third possibility, but rule the others out first
&lt;/h2&gt;

&lt;p&gt;Some providers only send aggregate reports once they see meaningful volume from a domain. A domain sending almost nothing may legitimately get few reports, particularly from smaller receivers.&lt;/p&gt;

&lt;p&gt;That's a real explanation, but it's also the comfortable one, so don't reach for it early. The two DNS causes above are deterministic and fixable. Google will typically report on any domain with real traffic within a day or two.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters more than it sounds
&lt;/h2&gt;

&lt;p&gt;The entire value of &lt;code&gt;p=none&lt;/code&gt; is the reporting. That's the whole point of the stage.&lt;/p&gt;

&lt;p&gt;If you're sitting at &lt;code&gt;p=none&lt;/code&gt; with broken reporting, you've done all the work of DMARC and got none of the benefit — no protection, because &lt;code&gt;p=none&lt;/code&gt; blocks nothing, and no visibility either. You're paying the setup cost and collecting nothing.&lt;/p&gt;

&lt;p&gt;Check it on the day you publish, not six months later when someone asks how the rollout is going.&lt;/p&gt;




&lt;p&gt;I maintain &lt;a href="https://notspoofed.com/?utm_source=devto" rel="noopener noreferrer"&gt;notspoofed&lt;/a&gt;, a free checker that tests both of these — whether an external &lt;code&gt;rua=&lt;/code&gt; is actually authorised, and whether the reporting address can receive mail at all. It's the failure mode I've seen most often and the one fewest tools look for. No signup, and checked domains aren't logged.&lt;/p&gt;

&lt;p&gt;If you've had reports silently missing for months, I'd be curious which of the two it turned out to be.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>security</category>
      <category>dns</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Your SPF record can be valid, published, and completely ignored</title>
      <dc:creator>Jose Pollman</dc:creator>
      <pubDate>Sun, 02 Aug 2026 20:55:09 +0000</pubDate>
      <link>https://dev.to/jose_pollman_fa7c6ec43cdd/your-spf-record-can-be-valid-published-and-completely-ignored-bi9</link>
      <guid>https://dev.to/jose_pollman_fa7c6ec43cdd/your-spf-record-can-be-valid-published-and-completely-ignored-bi9</guid>
      <description>&lt;p&gt;There's a failure mode in SPF that produces no error anywhere you'd think to look.&lt;/p&gt;

&lt;p&gt;Your record is syntactically valid. &lt;code&gt;dig&lt;/code&gt; returns it. Your DNS provider is happy. Your own test mail arrives fine. And yet some of your mail is failing authentication at the receiver, and nothing in your infrastructure will tell you.&lt;/p&gt;

&lt;p&gt;The cause is usually that you've gone past ten DNS lookups.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ten is a hard ceiling, not a guideline
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc7208#section-4.6.4" rel="noopener noreferrer"&gt;RFC 7208 section 4.6.4&lt;/a&gt; requires evaluators to cap DNS-querying terms at ten. Go past it and the result is &lt;code&gt;PermError&lt;/code&gt; — a permanent failure.&lt;/p&gt;

&lt;p&gt;The important part: receivers don't degrade gracefully. They don't evaluate the first ten and shrug at the rest. They treat the entire record as unusable. You go from "SPF configured" to "SPF absent" in one step, and the only place that's visible is in DMARC aggregate reports, which most people aren't reading yet.&lt;/p&gt;

&lt;p&gt;Six terms cost a lookup. Four are free.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Costs a lookup&lt;/th&gt;
&lt;th&gt;Free&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;include:&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ip4:&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;a&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ip6:&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mx&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;all&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ptr&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;exp=&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;exists:&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;redirect=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The count isn't yours to control
&lt;/h2&gt;

&lt;p&gt;This is what catches people out. The limit applies to the whole resolution tree, not just your record.&lt;/p&gt;

&lt;p&gt;You publish four includes. One of those vendors publishes six includes of their own. You're at eleven, and your record lists four things.&lt;/p&gt;

&lt;p&gt;Nothing changed on your side. A vendor updated their record and broke yours. There is no notification for this, from anyone, ever.&lt;/p&gt;

&lt;h2&gt;
  
  
  Counting it by hand
&lt;/h2&gt;

&lt;p&gt;You can walk the tree with &lt;code&gt;dig&lt;/code&gt; if you want to see it directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dig +short TXT example.com | &lt;span class="nb"&gt;grep &lt;/span&gt;spf1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then for every &lt;code&gt;include:&lt;/code&gt; you find, recurse:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dig +short TXT _spf.vendor.com | &lt;span class="nb"&gt;grep &lt;/span&gt;spf1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep going until you bottom out, counting every &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; and &lt;code&gt;redirect=&lt;/code&gt; along the way.&lt;/p&gt;

&lt;p&gt;It's tedious but instructive — you'll usually find one vendor accounting for half your budget.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three traps that make tools lie to you
&lt;/h2&gt;

&lt;p&gt;Doing this by hand or with a naive script, there are three places to go wrong. I've seen all three produce confidently incorrect output.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. &lt;code&gt;redirect=&lt;/code&gt; uses &lt;code&gt;=&lt;/code&gt;, not &lt;code&gt;:&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;It's a modifier, not a mechanism, and both humans and regexes skip it. Plenty of production domains are nothing but a redirect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;hubspot.com.  TXT  "v=spf1 redirect=_hspf.hubspot.com"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Miss that single term and you conclude the domain uses zero lookups — when in fact every include and every cost lives behind it. If a checker tells you a domain like this uses no lookups, the checker is broken.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Void lookups have a separate budget
&lt;/h3&gt;

&lt;p&gt;A lookup that returns nothing is a &lt;em&gt;void lookup&lt;/em&gt;, and RFC 7208 caps those at &lt;strong&gt;two&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;So an include pointing at a vendor you stopped paying for last year, whose record no longer exists, hurts you twice: once against the ten, and once against the void limit. Dead includes aren't free just because they resolve to nothing.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. TXT records arrive in pieces, joined with nothing
&lt;/h3&gt;

&lt;p&gt;This is the one that bites anyone writing their own tooling.&lt;/p&gt;

&lt;p&gt;DNS transmits TXT records as one or more character-strings of at most 255 bytes. A long SPF record arrives in several chunks, and &lt;a href="https://www.rfc-editor.org/rfc/rfc7208#section-3.3" rel="noopener noreferrer"&gt;RFC 7208 section 3.3&lt;/a&gt; says they're concatenated &lt;strong&gt;with no separator&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Real records split mid-token. One production record ends a chunk with &lt;code&gt;...ip4&lt;/code&gt; and starts the next with &lt;code&gt;:161.38.192.0/20&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Join them correctly and you get &lt;code&gt;ip4:161.38.192.0/20&lt;/code&gt;. Join them with a space — the obvious-looking thing to do — and you get &lt;code&gt;ip4 :161.38.192.0/20&lt;/code&gt;, which isn't valid syntax.&lt;/p&gt;

&lt;p&gt;If you're parsing SPF yourself, handle this first. If you're using someone else's tool and the output looks subtly mangled, this is usually why.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fixing it, in the order worth trying
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Remove senders you no longer use.&lt;/strong&gt; The only fix with no downside, and it's almost always available. Most domains over the limit are carrying includes for tools nobody has logged into in years, and deleting one buys back its entire subtree. Get the include list in front of whoever owns marketing and billing and ask what's still live.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ask the vendor for a narrower include.&lt;/strong&gt; Some publish a broad include covering all their infrastructure plus a tighter one scoped to a product or region. This is rarely documented — you have to ask support.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Flatten, but as little as possible.&lt;/strong&gt; Replacing &lt;code&gt;include:vendor.com&lt;/code&gt; with the IPs it resolves to turns a five-lookup subtree into zero, because &lt;code&gt;ip4:&lt;/code&gt; and &lt;code&gt;ip6:&lt;/code&gt; are free.&lt;/p&gt;

&lt;p&gt;The cost is permanent and easy to underestimate: you've taken on that vendor's maintenance burden. When they add a sending IP, their record updates and yours doesn't. Your mail from that provider starts failing SPF silently, and you find out when someone notices deliverability dropped a month ago.&lt;/p&gt;

&lt;p&gt;If you flatten:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Only flatten enough to get under ten. Retiring the single most expensive include is often sufficient, and leaves everyone else free to rotate their own IPs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never flatten a provider using macros.&lt;/strong&gt; A term like &lt;code&gt;exists:%{i}._spf.example.com&lt;/code&gt; expands against the connecting IP. There's no fixed set of addresses to inline, and flattening it silently drops every sender it would have matched.&lt;/li&gt;
&lt;li&gt;Re-check monthly, in a calendar, not in your head.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Worth knowing: large providers increasingly flatten their own records for this reason. &lt;code&gt;_spf.google.com&lt;/code&gt; today contains only &lt;code&gt;ip4:&lt;/code&gt; and &lt;code&gt;ip6:&lt;/code&gt; entries with no nested includes, so &lt;code&gt;include:_spf.google.com&lt;/code&gt; costs you exactly one lookup rather than four.&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't aim for ten
&lt;/h2&gt;

&lt;p&gt;Ten is legal. Ten is not a target.&lt;/p&gt;

&lt;p&gt;A record sitting at exactly ten breaks the day someone in marketing signs up for one more tool, and nobody will connect those two events. Aim for seven or eight so there's headroom to add a sender without causing an outage.&lt;/p&gt;




&lt;p&gt;I maintain &lt;a href="https://notspoofed.com/?utm_source=devto" rel="noopener noreferrer"&gt;notspoofed&lt;/a&gt;, a free checker that walks the whole include tree, follows &lt;code&gt;redirect=&lt;/code&gt;, counts against the real ceiling, and generates a corrected record that flattens only what it has to. It handles all three traps above, because I got them wrong first. No signup, and the domains you check aren't logged.&lt;/p&gt;

&lt;p&gt;If you've hit this in an interesting way — particularly a vendor whose record silently blew your budget — I'd like to hear about it.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>security</category>
      <category>dns</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
