{"id":1268,"date":"2026-09-02T09:30:00","date_gmt":"2026-09-01T23:30:00","guid":{"rendered":"https:\/\/catharsis.net.au\/blog\/spf-dkim-dmarc-microsoft-365-com-au\/"},"modified":"2026-11-03T21:47:18","modified_gmt":"2026-11-03T10:47:18","slug":"spf-dkim-dmarc-microsoft-365-com-au","status":"publish","type":"post","link":"https:\/\/catharsis.net.au\/blog\/spf-dkim-dmarc-microsoft-365-com-au\/","title":{"rendered":"How to Set Up SPF, DKIM and DMARC for .com.au in Microsoft 365"},"content":{"rendered":"\n<p class=\"has-pale-cyan-blue-background-color has-background wp-block-paragraph\">SPF, DKIM and DMARC stop criminals sending email that looks like it came from your exact domain. This guide gives the exact records Microsoft 365 needs for a .com.au domain, how to enter them at your DNS host, and a staged rollout from p=none to p=reject with a check at every step.<\/p>\n\n\n\n<div class=\"wp-block-group has-background\" style=\"background-color:#f4f6f8;padding-top:28px;padding-right:32px;padding-bottom:28px;padding-left:32px\"><div class=\"wp-block-group__inner-container is-layout-constrained wp-container-core-group-is-layout-40de7cf3 wp-block-group-is-layout-constrained\">\n\n<p class=\"wp-block-paragraph\"><strong>Related guides<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n\n<li><a href=\"https:\/\/catharsis.net.au\/blog\/business-email-hacked-first-hour\/\" target=\"_blank\" rel=\"noreferrer noopener\">Business Email Hacked? What to Do in the First Hour<\/a><\/li>\n\n\n<li><a href=\"https:\/\/catharsis.net.au\/blog\/christmas-invoice-fraud-call-back-checklist\/\" target=\"_blank\" rel=\"noreferrer noopener\">Christmas Invoice Fraud: A Call-Back Checklist for Accounts Teams<\/a><\/li>\n\n<\/ul>\n\n<\/div><\/div>\n\n\n\n<div class=\"wp-block-group has-background\" style=\"background-color:#f4f6f8;padding-top:28px;padding-right:32px;padding-bottom:28px;padding-left:32px\"><div class=\"wp-block-group__inner-container is-layout-constrained wp-container-core-group-is-layout-40de7cf3 wp-block-group-is-layout-constrained\">\n\n<h3 class=\"wp-block-heading\">TL;DR<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n\n<li>For a .com.au domain that sends only from Microsoft 365, publish one SPF TXT record (<code>v=spf1 include:spf.protection.outlook.com -all<\/code>), enable DKIM in the Defender portal with the two CNAME records it gives you, then publish a DMARC record at <code>_dmarc<\/code> starting with <code>p=none<\/code>.<\/li>\n\n\n<li>Copy the DKIM CNAME values from your own tenant. Microsoft changed the format for new domains in May 2025, so values from older guides may be wrong.<\/li>\n\n\n<li>Move DMARC from <code>p=none<\/code> to <code>p=quarantine<\/code> to <code>p=reject<\/code> only after the aggregate reports show every legitimate sender passing.<\/li>\n\n\n<li>Check every stage in real message headers: you want <code>spf=pass<\/code>, <code>dkim=pass<\/code> and <code>dmarc=pass<\/code> for mail from your domain.<\/li>\n\n\n<li>The usual mistakes are two SPF records, more than 10 DNS lookups in SPF, and forgetting the other services that send as your domain.<\/li>\n\n<\/ul>\n\n<\/div><\/div>\n\n\n\n<div class=\"wp-block-group has-background\" style=\"background-color:#f4f6f8;padding-top:28px;padding-right:32px;padding-bottom:28px;padding-left:32px\"><div class=\"wp-block-group__inner-container is-layout-constrained wp-container-core-group-is-layout-40de7cf3 wp-block-group-is-layout-constrained\">\n\n<h3 class=\"wp-block-heading\">In this post<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n\n<li><a href=\"#what-do-spf-dkim-and-dmarc-actually-do\">What do SPF, DKIM and DMARC actually do?<\/a><\/li>\n\n\n<li><a href=\"#before-you-start\">Before you start<\/a><\/li>\n\n\n<li><a href=\"#step-1-publish-one-spf-record\">Step 1: Publish one SPF record<\/a><\/li>\n\n\n<li><a href=\"#step-2-enable-dkim-in-microsoft-365\">Step 2: Enable DKIM in Microsoft 365<\/a><\/li>\n\n\n<li><a href=\"#step-3-publish-a-dmarc-record-in-monitoring-mode\">Step 3: Publish a DMARC record in monitoring mode<\/a><\/li>\n\n\n<li><a href=\"#staged-rollout-from-p-none-to-p-reject\">Staged rollout: from p=none to p=reject<\/a><\/li>\n\n\n<li><a href=\"#how-to-read-dmarc-aggregate-reports\">How to read DMARC aggregate reports<\/a><\/li>\n\n\n<li><a href=\"#dns-host-notes-for-au-domains\">DNS host notes for .au domains<\/a><\/li>\n\n\n<li><a href=\"#lock-down-parked-domains-and-onmicrosoft-com\">Lock down parked domains (and onmicrosoft.com)<\/a><\/li>\n\n\n<li><a href=\"#common-mistakes\">Common mistakes<\/a><\/li>\n\n\n<li><a href=\"#key-takeaways\">Key takeaways<\/a><\/li>\n\n\n<li><a href=\"#sources\">Sources<\/a><\/li>\n\n<\/ul>\n\n<\/div><\/div>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"what-do-spf-dkim-and-dmarc-actually-do\">What do SPF, DKIM and DMARC actually do?<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">All three are DNS records that let receiving mail servers decide whether an email claiming to be from your domain really is.<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n\n<li><strong>SPF<\/strong> lists the servers allowed to send mail for your domain. It checks the hidden &quot;envelope&quot; sender, not the From address people see.<\/li>\n\n\n<li><strong>DKIM<\/strong> adds a cryptographic signature to each outgoing message. The receiving server checks it against a public key published in your DNS.<\/li>\n\n\n<li><strong>DMARC<\/strong> ties them together. It requires the domain that passed SPF or DKIM to match (align with) the From address, tells receivers what to do with mail that fails, and sends you reports.<\/li>\n\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">ASD&#8217;s Australian Cyber Security Centre (ACSC) puts it bluntly: &quot;Irrespective of where your organisation is in terms of implementing anti-spoofing controls, implement DMARC immediately, even if only in a monitoring mode (&#8216;p=none&#8217;) configuration.&quot; It matters for invoice fraud too. Without enforcement, a scammer can send email that appears to come from your exact address to your customers. (Our <a href=\"https:\/\/catharsis.net.au\/blog\/christmas-invoice-fraud-call-back-checklist\/\" target=\"_blank\" rel=\"noreferrer noopener\">call-back checklist for accounts teams<\/a> covers the process side.)<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"before-you-start\">Before you start<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">You need:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n\n<li><strong>Admin access to Microsoft 365<\/strong>, with permission to change email authentication settings in the Microsoft Defender portal, and ideally Exchange Online PowerShell.<\/li>\n\n\n<li><strong>Access to wherever your domain&#8217;s DNS is hosted.<\/strong> For a .com.au domain this is often, but not always, the registrar you bought it from. It could also be your web host or a separate DNS service. Look up your domain at <a href=\"https:\/\/whois.auda.org.au\/\" target=\"_blank\" rel=\"noreferrer noopener\">whois.auda.org.au<\/a> and check the name servers listed: whoever runs those name servers is where you make the changes.<\/li>\n\n\n<li><strong>A list of everything that sends email as your domain.<\/strong> This is the step people skip. Think beyond Microsoft 365: your accounting or invoicing system, CRM, newsletter platform, website contact form, booking system, payroll, multifunction printers and scanners, and any helpdesk tool. Each one needs to pass SPF or DKIM in a way that aligns with your domain, or DMARC enforcement will block it.<\/li>\n\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Throughout this guide, <code>example.com.au<\/code> stands for your domain and <code>example<\/code> for the first part of your initial <code>example.onmicrosoft.com<\/code> domain.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-1-publish-one-spf-record\">Step 1: Publish one SPF record<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">If Microsoft 365 is the only thing that sends mail as your domain, Microsoft&#8217;s recommended record is:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Type:  TXT\nHost:  @ (the domain itself)\nValue: v=spf1 include:spf.protection.outlook.com -all\nTTL:   3600\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Microsoft recommends <code>-all<\/code> (hard fail) rather than <code>~all<\/code> (soft fail) for Microsoft 365 domains, because you&#8217;ll also have DKIM and DMARC. Its documentation notes that DMARC policy is effectively ignored for <code>~all<\/code> failures on messages that also lack a DKIM signature.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">If other services send as your domain, add their <code>include:<\/code> (or <code>ip4:<\/code>) values before <code>-all<\/code>, using the value each service documents. For example:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>v=spf1 include:spf.protection.outlook.com include:mail.your-invoicing-service.example -all\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Rules that matter:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n\n<li><strong>One SPF record per domain.<\/strong> Two TXT records starting <code>v=spf1<\/code> make SPF return <code>permerror<\/code>. If you already have one, edit it; don&#8217;t add a second.<\/li>\n\n\n<li><strong>No more than 10 DNS lookups.<\/strong> Each <code>include:<\/code>, <code>a<\/code>, <code>mx<\/code>, <code>ptr<\/code>, <code>exists<\/code> and <code>redirect<\/code> counts, including lookups nested inside included records. <code>ip4:<\/code>, <code>ip6:<\/code> and <code>all<\/code> don&#8217;t count. Go over 10 and SPF fails with <code>permerror<\/code>.<\/li>\n\n\n<li><strong>Don&#8217;t &quot;flatten&quot; Microsoft&#8217;s include into IP addresses.<\/strong> Microsoft says its sending IPs change frequently.<\/li>\n\n\n<li><strong>Each subdomain that sends mail needs its own SPF record.<\/strong> The record on <code>example.com.au<\/code> doesn&#8217;t cover <code>news.example.com.au<\/code>.<\/li>\n\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Check:<\/strong> look the record up from your own computer.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Resolve-DnsName -Name example.com.au -Type TXT\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">or on macOS and Linux:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>dig +short TXT example.com.au\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">You should see exactly one string starting <code>v=spf1<\/code>.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-2-enable-dkim-in-microsoft-365\">Step 2: Enable DKIM in Microsoft 365<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Microsoft 365 already signs mail with your <code>onmicrosoft.com<\/code> domain, but that signature doesn&#8217;t align with your .com.au From address, so it doesn&#8217;t help DMARC. You need DKIM signing with your own domain.<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n\n<li>In the Defender portal at <a href=\"https:\/\/security.microsoft.com\" target=\"_blank\" rel=\"noreferrer noopener\">security.microsoft.com<\/a>, go to <strong>Email &amp; collaboration<\/strong> &gt; <strong>Policies &amp; rules<\/strong> &gt; <strong>Threat policies<\/strong> &gt; <strong>Email authentication settings<\/strong>, or go straight to <code>https:\/\/security.microsoft.com\/authentication<\/code>.<\/li>\n\n\n<li>Select the <strong>DKIM<\/strong> tab, then select your domain.<\/li>\n\n\n<li>In the flyout, select <strong>Create DKIM keys<\/strong>. (A &quot;Client error&quot; dialog may appear with the CNAME values in it. That&#8217;s expected; close it.)<\/li>\n\n\n<li>Reopen the domain. The <strong>Publish CNAMEs<\/strong> section now shows the two records to create. Copy them exactly.<\/li>\n\n\n<li>At your DNS host, create two CNAME records:<\/li>\n\n<\/ol>\n\n\n\n<pre class=\"wp-block-code\"><code>Type:  CNAME\nHost:  selector1._domainkey\nValue: (Selector1CNAME value from your tenant)\n\nType:  CNAME\nHost:  selector2._domainkey\nValue: (Selector2CNAME value from your tenant)\n<\/code><\/pre>\n\n\n\n<ol class=\"wp-block-list\">\n\n<li>Wait for Microsoft to detect the records (Microsoft says a few minutes, possibly longer), then go back to the domain flyout and turn on <strong>Sign messages for this domain with DKIM signatures<\/strong>.<\/li>\n\n<\/ol>\n\n\n\n<h3 class=\"wp-block-heading\">What the CNAME values look like<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Microsoft introduced a new format for <strong>new<\/strong> custom domains in May 2025:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>selector1-example-com-au._domainkey.example.&lt;partition&gt;-v1.dkim.mail.microsoft\nselector2-example-com-au._domainkey.example.&lt;partition&gt;-v1.dkim.mail.microsoft\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The dots in your domain become dashes (<code>example.com.au<\/code> becomes <code>example-com-au<\/code>), and <code>&lt;partition&gt;<\/code> is a single character (for example <code>r<\/code> or <code>n<\/code>) that Microsoft assigns and you can&#8217;t choose. Domains added before the change keep the older format:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>selector1-example-com-au._domainkey.example.onmicrosoft.com\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The two formats can&#8217;t be mixed for the same selector, which is why you should never copy values from a guide (including this one). Get your exact values from the portal, or in Exchange Online PowerShell:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Get-DkimSigningConfig -Identity example.com.au | Format-List Name,Enabled,Status,Selector1CNAME,Selector2CNAME\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Key size<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Keys created with <code>New-DkimSigningConfig<\/code> default to 1024-bit unless you pass <code>-KeySize 2048<\/code>. Once DKIM is working, you can move to a 2048-bit key with:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Rotate-DkimSigningConfig -Identity example.com.au -KeySize 2048\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Microsoft notes a rotation takes four days (96 hours) to take effect, and the old key keeps signing until then.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Check:<\/strong> send an email from your domain to an external mailbox you control (a personal webmail account is fine), open the message headers and find:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>DKIM-Signature: v=1; a=rsa-sha256; c=relaxed\/relaxed; d=example.com.au; s=selector1; ...\nAuthentication-Results: ... dkim=pass header.d=example.com.au ...\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><code>d=<\/code> must be your domain, not <code>onmicrosoft.com<\/code>. The Defender portal should also show the domain&#8217;s status as <strong>Valid<\/strong> with the toggle <strong>Enabled<\/strong>.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-3-publish-a-dmarc-record-in-monitoring-mode\">Step 3: Publish a DMARC record in monitoring mode<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Start with <code>p=none<\/code> so nothing is blocked while you find every sender:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Type:  TXT\nHost:  _dmarc\nValue: v=DMARC1; p=none; rua=mailto:dmarc-reports@example.com.au\nTTL:   3600\n<\/code><\/pre>\n\n\n\n<ul class=\"wp-block-list\">\n\n<li><code>p=none<\/code> asks receivers to take no DMARC action. You&#8217;re collecting data.<\/li>\n\n\n<li><code>rua=<\/code> is where aggregate reports go. Microsoft recommends a dedicated shared mailbox or Microsoft 365 Group, not a person&#8217;s inbox, because the volume adds up.<\/li>\n\n\n<li>Leave out <code>ruf=<\/code> (forensic reports) to start. Microsoft suggests <code>rua<\/code> only at first; forensic reports can be high-volume and many receivers, including Microsoft 365, don&#8217;t send them.<\/li>\n\n\n<li>If the report address is on a <strong>different domain<\/strong> (for example, a reporting service), that domain must publish an authorisation record (<code>example.com.au._report._dmarc.otherdomain<\/code> with <code>v=DMARC1;<\/code>), or receivers won&#8217;t send reports there.<\/li>\n\n\n<li>Your DMARC record covers subdomains that don&#8217;t have their own DMARC record, including subdomains that don&#8217;t exist. That&#8217;s useful: it stops scammers inventing <code>invoices.example.com.au<\/code>.<\/li>\n\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">The tool below builds SPF, DKIM and DMARC records and explains an SPF or DMARC record you paste in. It runs entirely in your browser and doesn&#8217;t look anything up in DNS, so use it to draft and sanity-check a record, then confirm what&#8217;s live with the <code>dig<\/code> check.<\/p>\n\n\n\n<div id=\"cat-dmarc-builder\">\n<style>\n#cat-dmarc-builder{background:#f4f6f8;border-radius:12px;padding:20px;margin:8px 0 24px;color:#1f2328;max-width:100%;overflow-wrap:anywhere}\n#cat-dmarc-builder *{box-sizing:border-box}\n#cat-dmarc-builder [role=tablist]{display:flex;flex-wrap:wrap;gap:6px;margin:16px 0;border-bottom:2px solid #dde3ea;padding-bottom:10px}\n#cat-dmarc-builder [role=tab]{font-size:15px;padding:8px 14px;border:1px solid #c5ced8;border-radius:999px;background:#fff;color:#32373c;cursor:pointer}\n#cat-dmarc-builder [role=tab][aria-selected=true]{background:#32373c;color:#fff;border-color:#32373c}\n#cat-dmarc-builder label,#cat-dmarc-builder legend{display:block;font-size:15px;font-weight:600;margin:0 0 6px}\n#cat-dmarc-builder fieldset{border:0;padding:0;margin:12px 0 0;min-width:0}\n#cat-dmarc-builder fieldset:disabled{opacity:.5}\n#cat-dmarc-builder .cat-field{margin:12px 0 0}\n#cat-dmarc-builder .cat-row{display:grid;grid-template-columns:1fr 1fr;gap:12px}\n#cat-dmarc-builder input[type=text],#cat-dmarc-builder textarea,#cat-dmarc-builder select{width:100%;font-size:16px;padding:9px 11px;border:1px solid #b8c2cc;border-radius:8px;background:#fff;color:#1f2328;font-family:ui-monospace,Menlo,Consolas,monospace}\n#cat-dmarc-builder select{font-family:inherit}\n#cat-dmarc-builder textarea{min-height:64px;resize:vertical}\n#cat-dmarc-builder .cat-check label,#cat-dmarc-builder .cat-radios label{font-weight:400;display:flex;gap:8px;align-items:flex-start;margin:6px 0}\n#cat-dmarc-builder .cat-check input,#cat-dmarc-builder .cat-radios input{margin-top:4px;flex:none}\n#cat-dmarc-builder input:focus-visible,#cat-dmarc-builder textarea:focus-visible,#cat-dmarc-builder select:focus-visible,#cat-dmarc-builder button:focus-visible{outline:3px solid #0a66c2;outline-offset:1px}\n#cat-dmarc-builder .cat-btn{font-size:15px;padding:9px 16px;border-radius:8px;border:1px solid #32373c;background:#32373c;color:#fff;cursor:pointer;margin:10px 8px 0 0}\n#cat-dmarc-builder .cat-btn-alt{background:#fff;color:#32373c}\n#cat-dmarc-builder .cat-copy{background:#fff;color:#32373c;border:1px solid #b8c2cc;border-radius:999px;padding:3px 10px;font-size:13px;cursor:pointer;margin:4px 0 0 8px;vertical-align:middle}\n#cat-dmarc-builder .cat-err{color:#a4002b;font-weight:600;margin:12px 0 0}\n#cat-dmarc-builder .cat-note,#cat-dmarc-builder .cat-hint{font-size:14px;color:#55606b;margin:8px 0 0}\n#cat-dmarc-builder .cat-hint{margin:4px 0 0;font-weight:400}\n#cat-dmarc-builder .cat-out{margin-top:14px}\n#cat-dmarc-builder table{width:100%;border-collapse:collapse;background:#fff;font-size:15px;margin-top:6px;table-layout:fixed}\n#cat-dmarc-builder th,#cat-dmarc-builder td{padding:7px 9px;border-bottom:1px solid #e3e8ee;text-align:left;vertical-align:top;overflow-wrap:anywhere}\n#cat-dmarc-builder .cat-rec th:nth-child(1){width:30%}\n#cat-dmarc-builder .cat-rec th:nth-child(2){width:16%}\n#cat-dmarc-builder .cat-explain th:nth-child(1){width:34%}\n#cat-dmarc-builder code{font-family:ui-monospace,Menlo,Consolas,monospace;font-size:14px;background:#eef1f4;padding:1px 4px;border-radius:4px;overflow-wrap:anywhere;word-break:break-all}\n#cat-dmarc-builder .cat-msgs{margin:10px 0 0;padding-left:20px;font-size:15px}\n#cat-dmarc-builder .cat-msgs li{margin:4px 0}\n#cat-dmarc-builder .cat-bad{color:#a4002b}\n#cat-dmarc-builder .cat-warn{color:#7a4a00}\n#cat-dmarc-builder .cat-info{color:#33404d}\n#cat-dmarc-builder .cat-good{color:#1d7a3a}\n#cat-dmarc-builder .cat-summary{font-weight:600;margin:10px 0 0}\n#cat-dmarc-builder .cat-lv-bad td:last-child{color:#a4002b}\n#cat-dmarc-builder .cat-lv-warn td:last-child{color:#7a4a00}\n#cat-dmarc-builder .cat-h{font-size:17px;margin:18px 0 6px}\n#cat-dmarc-builder .cat-plan{padding-left:22px;margin:0}\n#cat-dmarc-builder .cat-plan li{margin:0 0 12px}\n#cat-dmarc-builder .cat-plan p{margin:4px 0 0}\n#cat-dmarc-builder .cat-samples{display:flex;flex-wrap:wrap;gap:6px;margin:10px 0 0}\n#cat-dmarc-builder .cat-samples button{background:#fff;color:#32373c;border:1px solid #b8c2cc;border-radius:999px;padding:4px 11px;font-size:13px;cursor:pointer}\n#cat-dmarc-builder .cat-scroll{overflow-x:auto}\n#cat-dmarc-builder .cat-foot{font-size:13px;color:#55606b;margin:16px 0 0}\n#cat-dmarc-builder .cat-foot a{color:#0a5fb4}\n@media (max-width:640px){#cat-dmarc-builder{padding:14px}#cat-dmarc-builder .cat-row{grid-template-columns:1fr}#cat-dmarc-builder .cat-rec th:nth-child(1){width:34%}#cat-dmarc-builder .cat-rec th:nth-child(2){width:18%}#cat-dmarc-builder th,#cat-dmarc-builder td{padding:6px;font-size:14px}}\n@media print{#cat-dmarc-builder [role=tablist],#cat-dmarc-builder .cat-copy,#cat-dmarc-builder .cat-btn{display:none}}\n<\/style>\n\n<div class=\"cat-field\">\n<label for=\"cat-dm-domain\">Your email domain<\/label>\n<input type=\"text\" id=\"cat-dm-domain\" autocomplete=\"off\" spellcheck=\"false\" maxlength=\"253\" value=\"example.com.au\" aria-describedby=\"cat-dm-domain-hint\">\n<p id=\"cat-dm-domain-hint\" class=\"cat-hint\">The domain after the @ in your staff email addresses. Nothing is looked up: the records are built in your browser.<\/p>\n<p id=\"cat-dm-domain-error\" class=\"cat-err\" role=\"alert\" hidden><\/p>\n<\/div>\n\n<div role=\"tablist\" aria-label=\"Email authentication tools\">\n<button type=\"button\" role=\"tab\" id=\"cat-dm-tab-spf\" aria-controls=\"cat-dm-panel-spf\" aria-selected=\"true\">1. SPF<\/button>\n<button type=\"button\" role=\"tab\" id=\"cat-dm-tab-dkim\" aria-controls=\"cat-dm-panel-dkim\" aria-selected=\"false\" tabindex=\"-1\">2. DKIM<\/button>\n<button type=\"button\" role=\"tab\" id=\"cat-dm-tab-dmarc\" aria-controls=\"cat-dm-panel-dmarc\" aria-selected=\"false\" tabindex=\"-1\">3. DMARC<\/button>\n<button type=\"button\" role=\"tab\" id=\"cat-dm-tab-explain\" aria-controls=\"cat-dm-panel-explain\" aria-selected=\"false\" tabindex=\"-1\">Explain my record<\/button>\n<\/div>\n\n<div role=\"tabpanel\" id=\"cat-dm-panel-spf\" aria-labelledby=\"cat-dm-tab-spf\">\n<form id=\"cat-spf-form\" novalidate>\n<div class=\"cat-check\"><label><input type=\"checkbox\" id=\"cat-spf-parked\"> This domain never sends email (a parked domain)<\/label><\/div>\n<fieldset id=\"cat-spf-sources\">\n<legend>Who sends email as this domain?<\/legend>\n<div class=\"cat-check\"><label><input type=\"checkbox\" id=\"cat-spf-m365\" checked> Microsoft 365 (adds include:spf.protection.outlook.com)<\/label><\/div>\n<div class=\"cat-field\">\n<label for=\"cat-spf-includes\">Other sending services: their SPF include domains<\/label>\n<textarea id=\"cat-spf-includes\" rows=\"2\" maxlength=\"2000\" spellcheck=\"false\" aria-describedby=\"cat-spf-includes-hint\" placeholder=\"e.g. _spf.mailer.example.net\"><\/textarea>\n<p id=\"cat-spf-includes-hint\" class=\"cat-hint\">One per line. Copy the exact value from each service&#8217;s setup guide. Bulk or marketing senders are better on a subdomain.<\/p>\n<\/div>\n<div class=\"cat-row\">\n<div class=\"cat-field\"><label for=\"cat-spf-ip4\">IPv4 addresses or ranges<\/label><textarea id=\"cat-spf-ip4\" rows=\"2\" maxlength=\"2000\" spellcheck=\"false\" placeholder=\"e.g. 203.0.113.10\"><\/textarea><\/div>\n<div class=\"cat-field\"><label for=\"cat-spf-ip6\">IPv6 addresses or ranges<\/label><textarea id=\"cat-spf-ip6\" rows=\"2\" maxlength=\"2000\" spellcheck=\"false\" placeholder=\"e.g. 2001:db8::\/48\"><\/textarea><\/div>\n<\/div>\n<div class=\"cat-check\">\n<label><input type=\"checkbox\" id=\"cat-spf-a\"> The server your website address points to also sends email (a)<\/label>\n<label><input type=\"checkbox\" id=\"cat-spf-mx\"> The servers in your MX record send email (mx)<\/label>\n<\/div>\n<fieldset class=\"cat-radios\">\n<legend>Mail from anywhere else<\/legend>\n<label><input type=\"radio\" name=\"cat-spf-all\" value=\"-all\" checked> <span><strong>-all<\/strong> (hard fail): not authorised. Microsoft&#8217;s recommendation once DKIM and DMARC are in place.<\/span><\/label>\n<label><input type=\"radio\" name=\"cat-spf-all\" value=\"~all\"> <span><strong>~all<\/strong> (soft fail): probably not authorised. A stop-gap while you find every sender.<\/span><\/label>\n<\/fieldset>\n<\/fieldset>\n<\/form>\n<div id=\"cat-spf-out\" class=\"cat-out\" aria-live=\"polite\"><\/div>\n<\/div>\n\n<div role=\"tabpanel\" id=\"cat-dm-panel-dkim\" aria-labelledby=\"cat-dm-tab-dkim\" hidden>\n<form id=\"cat-dkim-form\" novalidate>\n<div class=\"cat-row\">\n<div class=\"cat-field\">\n<label for=\"cat-dkim-initial\">Initial Microsoft 365 domain<\/label>\n<input type=\"text\" id=\"cat-dkim-initial\" autocomplete=\"off\" spellcheck=\"false\" maxlength=\"80\" value=\"contoso.onmicrosoft.com\" aria-describedby=\"cat-dkim-initial-hint\">\n<p id=\"cat-dkim-initial-hint\" class=\"cat-hint\">The name.onmicrosoft.com domain your tenant was created with.<\/p>\n<\/div>\n<div class=\"cat-field\">\n<label for=\"cat-dkim-part\">Partition letter (if Microsoft has shown it)<\/label>\n<input type=\"text\" id=\"cat-dkim-part\" autocomplete=\"off\" spellcheck=\"false\" maxlength=\"1\" aria-describedby=\"cat-dkim-part-hint\">\n<p id=\"cat-dkim-part-hint\" class=\"cat-hint\">New format only: the single letter before -v1, for example r or n.<\/p>\n<\/div>\n<\/div>\n<fieldset class=\"cat-radios\">\n<legend>When was the domain added to Microsoft 365?<\/legend>\n<label><input type=\"radio\" name=\"cat-dkim-format\" value=\"new\" checked> <span>May 2025 or later (new format, ends in dkim.mail.microsoft)<\/span><\/label>\n<label><input type=\"radio\" name=\"cat-dkim-format\" value=\"old\"> <span>Before May 2025 (older format, ends in onmicrosoft.com)<\/span><\/label>\n<\/fieldset>\n<\/form>\n<div id=\"cat-dkim-out\" class=\"cat-out\" aria-live=\"polite\"><\/div>\n<\/div>\n\n<div role=\"tabpanel\" id=\"cat-dm-panel-dmarc\" aria-labelledby=\"cat-dm-tab-dmarc\" hidden>\n<form id=\"cat-dmarc-form\" novalidate>\n<div class=\"cat-row\">\n<div class=\"cat-field\">\n<label for=\"cat-dmarc-p\">Policy (p)<\/label>\n<select id=\"cat-dmarc-p\">\n<option value=\"none\" selected>none: monitor and report only<\/option>\n<option value=\"quarantine\">quarantine: send failures to junk<\/option>\n<option value=\"reject\">reject: refuse failures<\/option>\n<\/select>\n<\/div>\n<div class=\"cat-field\">\n<label for=\"cat-dmarc-sp\">Subdomain policy (sp)<\/label>\n<select id=\"cat-dmarc-sp\">\n<option value=\"\" selected>Same as the main policy<\/option>\n<option value=\"none\">none<\/option>\n<option value=\"quarantine\">quarantine<\/option>\n<option value=\"reject\">reject<\/option>\n<\/select>\n<\/div>\n<\/div>\n<div class=\"cat-row\">\n<div class=\"cat-field\">\n<label for=\"cat-dmarc-rua\">Aggregate report address (rua)<\/label>\n<input type=\"text\" id=\"cat-dmarc-rua\" autocomplete=\"off\" spellcheck=\"false\" maxlength=\"400\" value=\"dmarc-reports@example.com.au\" aria-describedby=\"cat-dmarc-rua-hint\">\n<p id=\"cat-dmarc-rua-hint\" class=\"cat-hint\">A shared mailbox or your reporting service. Separate several with commas.<\/p>\n<\/div>\n<div class=\"cat-field\">\n<label for=\"cat-dmarc-ruf\">Failure report address (ruf, optional)<\/label>\n<input type=\"text\" id=\"cat-dmarc-ruf\" autocomplete=\"off\" spellcheck=\"false\" maxlength=\"400\" aria-describedby=\"cat-dmarc-ruf-hint\">\n<p id=\"cat-dmarc-ruf-hint\" class=\"cat-hint\">Often left out: few receivers send these.<\/p>\n<\/div>\n<\/div>\n<div class=\"cat-row\">\n<div class=\"cat-field\">\n<label for=\"cat-dmarc-pct\">Percentage (pct, optional)<\/label>\n<input type=\"text\" id=\"cat-dmarc-pct\" autocomplete=\"off\" inputmode=\"numeric\" maxlength=\"4\" placeholder=\"100\" aria-describedby=\"cat-dmarc-pct-hint\">\n<p id=\"cat-dmarc-pct-hint\" class=\"cat-hint\">Leave blank for 100. Historic in RFC 9989.<\/p>\n<\/div>\n<div class=\"cat-field\">\n<label for=\"cat-dmarc-fo\">Failure report options (fo)<\/label>\n<select id=\"cat-dmarc-fo\" disabled>\n<option value=\"0\" selected>0: when SPF and DKIM both fail (default)<\/option>\n<option value=\"1\">1: when either fails<\/option>\n<option value=\"d\">d: DKIM failures<\/option>\n<option value=\"s\">s: SPF failures<\/option>\n<\/select>\n<\/div>\n<\/div>\n<div class=\"cat-row\">\n<fieldset class=\"cat-radios\">\n<legend>DKIM alignment (adkim)<\/legend>\n<label><input type=\"radio\" name=\"cat-dmarc-adkim\" value=\"r\" checked> <span>Relaxed (default)<\/span><\/label>\n<label><input type=\"radio\" name=\"cat-dmarc-adkim\" value=\"s\"> <span>Strict<\/span><\/label>\n<\/fieldset>\n<fieldset class=\"cat-radios\">\n<legend>SPF alignment (aspf)<\/legend>\n<label><input type=\"radio\" name=\"cat-dmarc-aspf\" value=\"r\" checked> <span>Relaxed (default)<\/span><\/label>\n<label><input type=\"radio\" name=\"cat-dmarc-aspf\" value=\"s\"> <span>Strict<\/span><\/label>\n<\/fieldset>\n<\/div>\n<\/form>\n<div id=\"cat-dmarc-out\" class=\"cat-out\" aria-live=\"polite\"><\/div>\n<\/div>\n\n<div role=\"tabpanel\" id=\"cat-dm-panel-explain\" aria-labelledby=\"cat-dm-tab-explain\" hidden>\n<form id=\"cat-explain-form\" novalidate>\n<div class=\"cat-field\">\n<label for=\"cat-explain-in\">Paste an SPF or DMARC record (the TXT value)<\/label>\n<textarea id=\"cat-explain-in\" rows=\"3\" maxlength=\"2048\" spellcheck=\"false\" aria-describedby=\"cat-explain-hint\" placeholder=\"v=spf1 include:spf.protection.outlook.com -all\"><\/textarea>\n<p id=\"cat-explain-hint\" class=\"cat-hint\">The text is only read in your browser. Nothing is looked up or sent anywhere.<\/p>\n<\/div>\n<button type=\"submit\" class=\"cat-btn\">Explain this record<\/button>\n<div class=\"cat-samples\" role=\"group\" aria-label=\"Try an example\">\n<button type=\"button\" data-sample=\"v=spf1 include:spf.protection.outlook.com -all\">Example: Microsoft 365 SPF<\/button>\n<button type=\"button\" data-sample=\"v=spf1 ip4:203.0.113.10 include:spf.protection.outlook.com include:_spf.mailer.example.net ptr ~all\">Example: SPF with problems<\/button>\n<button type=\"button\" data-sample=\"v=DMARC1; p=none; pct=100; rua=mailto:dmarc@example.com.au\">Example: DMARC monitoring<\/button>\n<button type=\"button\" data-sample=\"v=DMARC1; p=reject; sp=reject; adkim=s; rua=mailto:dmarc@example.com.au; ruf=mailto:dmarc@example.com.au; fo=1\">Example: DMARC reject<\/button>\n<\/div>\n<\/form>\n<div id=\"cat-explain-out\" class=\"cat-out\" aria-live=\"polite\"><\/div>\n<\/div>\n\n<p class=\"cat-foot\">Indicative only. General information, not legal advice or a formal assessment. Check against Microsoft&#8217;s current documentation and your DNS host before publishing, and change records carefully, because a wrong SPF or DMARC record can stop your email being delivered. Built from RFC 7208 (SPF), RFC 6376 (DKIM), RFC 7489 and RFC 9989 (DMARC) and Microsoft Learn: <a href=\"https:\/\/learn.microsoft.com\/en-us\/defender-office-365\/email-authentication-spf-configure\" target=\"_blank\" rel=\"noopener noreferrer\">SPF<\/a>, <a href=\"https:\/\/learn.microsoft.com\/en-us\/defender-office-365\/email-authentication-dkim-configure\" target=\"_blank\" rel=\"noopener noreferrer\">DKIM<\/a>, <a href=\"https:\/\/learn.microsoft.com\/en-us\/defender-office-365\/email-authentication-dmarc-configure\" target=\"_blank\" rel=\"noopener noreferrer\">DMARC<\/a>. Share this tool with your domain filled in by adding #d=yourdomain to the page address.<\/p>\n<script>(function () {\n  'use strict';\n  \/\/ Catharsis SPF, DKIM and DMARC builder for Microsoft 365 domains.\n  \/\/ Runs entirely in the browser: no DNS lookups, no network requests, no storage, no eval.\n  \/\/ Every value shown is written with textContent (never as markup). All input is validated against\n  \/\/ strict patterns and every list is capped. The source contains no ampersand character, because\n  \/\/ WordPress rewrites it in post content: nested ifs are used instead of the logical AND operator.\n  \/\/ Sources: RFC 7208 (SPF), RFC 6376 (DKIM), RFC 7489 and RFC 9989 (DMARC), and Microsoft Learn\n  \/\/ (email-authentication-spf-configure, -dkim-configure, -dmarc-configure), checked November 2026.\n\n  var MAX_TEXT = 2048;     \/\/ longest record accepted by \"explain\"\n  var MAX_ITEMS = 20;      \/\/ most includes, IP ranges or report addresses accepted per field\n  var MAX_TERMS = 64;      \/\/ most SPF terms or DMARC tags explained\n  var M365_INCLUDE = 'spf.protection.outlook.com';\n\n  var OCTET = '(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])';\n  var IPV4 = new RegExp('^' + OCTET + '\\\\.' + OCTET + '\\\\.' + OCTET + '\\\\.' + OCTET + '$');\n  var HOST = \/^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z](?:[a-z0-9-]{0,61}[a-z0-9])?$\/;\n  \/\/ SPF targets may contain underscore labels such as _spf.example.com\n  var SPF_HOST = \/^(?=.{1,253}$)(?:[a-z0-9_](?:[a-z0-9_-]{0,61}[a-z0-9])?\\.)+[a-z](?:[a-z0-9-]{0,61}[a-z0-9])?$\/;\n  var LOCAL = \/^[a-z0-9!#$%'*+\\\/=?^_`{|}~.-]{1,64}$\/;\n  var LABEL = \/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$\/;\n  var SECOND_LEVEL = { com: 1, net: 1, org: 1, gov: 1, edu: 1, asn: 1, id: 1, co: 1, ac: 1 };\n\n  function str(v) { return String(v === undefined || v === null ? '' : v); }\n  function trimDot(s) { return s.charAt(s.length - 1) === '.' ? s.slice(0, -1) : s; }\n\n  \/** Validate and normalise a domain name the user owns (no underscores). *\/\n  function normDomain(text) {\n    var d = trimDot(str(text).trim().toLowerCase());\n    if (d === '') return { error: 'Enter your email domain, for example example.com.au.' };\n    if (d.length > 253) return { error: 'That domain name is too long.' };\n    if (!HOST.test(d)) return { error: 'Enter a domain name like example.com.au (letters, numbers, dots and hyphens only).' };\n    return { domain: d };\n  }\n\n  \/** A close approximation of the organisational domain (handles .com.au style suffixes). *\/\n  function orgDomain(d) {\n    var parts = d.split('.');\n    if (parts.length <= 2) return d;\n    var tld = parts[parts.length - 1], sld = parts[parts.length - 2];\n    if (tld.length === 2) { if (SECOND_LEVEL[sld]) return parts.slice(-3).join('.'); }\n    return parts.slice(-2).join('.');\n  }\n\n  function isIp4Cidr(s) {\n    var m = \/^([0-9.]{7,15})(?:\\\/([0-9]{1,2}))?$\/.exec(s);\n    if (!m) return null;\n    if (!IPV4.test(m[1])) return null;\n    var p = m[2] === undefined ? 32 : +m[2];\n    if (p > 32) return null;\n    return { prefix: p };\n  }\n  function isIp6(addr) {\n    if (addr.length > 45) return false;\n    if (!\/^[0-9a-f:.]+$\/.test(addr)) return false;\n    var halves = addr.split('::');\n    if (halves.length > 2) return false;\n    var groups = 0;\n    for (var h = 0; h < halves.length; h++) {\n      if (halves[h] === '') continue;\n      var g = halves[h].split(':');\n      for (var i = 0; i < g.length; i++) {\n        if (i === g.length - 1) {\n          if (h === halves.length - 1) { if (IPV4.test(g[i])) { groups += 2; continue; } }\n        }\n        if (!\/^[0-9a-f]{1,4}$\/.test(g[i])) return false;\n        groups++;\n      }\n    }\n    if (halves.length === 2) return groups <= 7;\n    return groups === 8;\n  }\n  function isIp6Cidr(s) {\n    var m = \/^([0-9a-f:.]{2,45})(?:\\\/([0-9]{1,3}))?$\/.exec(s);\n    if (!m) return null;\n    if (!isIp6(m[1])) return null;\n    var p = m[2] === undefined ? 128 : +m[2];\n    if (p > 128) return null;\n    return { prefix: p };\n  }\n\n  \/** Split a free-text list (spaces, commas, semicolons or new lines), de-duplicate, cap. *\/\n  function list(text) {\n    var raw = str(text).toLowerCase().split(\/[\\s,;]+\/), out = [], seen = Object.create(null);\n    for (var i = 0; i < raw.length; i++) {\n      var v = raw[i].trim();\n      if (v === '') continue;\n      if (seen[v]) continue;\n      seen[v] = true; out.push(v);\n      if (out.length > MAX_ITEMS) break;\n    }\n    return out;\n  }\n\n  \/\/ ---------------------------------------------------------------- SPF builder\n  function buildSpf(o) {\n    o = o || {};\n    var errors = [], warnings = [], terms = ['v=spf1'], lookups = 0;\n    if (o.parked) {\n      return { record: 'v=spf1 -all', lookups: 0, errors: [], warnings: [],\n        notes: ['A parked domain sends no email, so the record authorises nothing. Pair it with the DMARC record v=DMARC1; p=reject; (Microsoft Learn, parked domains).'] };\n    }\n    var ip4 = list(o.ip4), ip6 = list(o.ip6), inc = list(o.includes);\n    if (ip4.length > MAX_ITEMS) errors.push('List at most ' + MAX_ITEMS + ' IPv4 addresses or ranges.');\n    if (ip6.length > MAX_ITEMS) errors.push('List at most ' + MAX_ITEMS + ' IPv6 addresses or ranges.');\n    if (inc.length > MAX_ITEMS) errors.push('List at most ' + MAX_ITEMS + ' other sending services.');\n    var i, v;\n    for (i = 0; i < Math.min(ip4.length, MAX_ITEMS); i++) {\n      v = ip4[i].replace(\/^ip4:\/, '');\n      var r4 = isIp4Cidr(v);\n      if (!r4) { errors.push('\"' + v.slice(0, 60) + '\" is not an IPv4 address or range (for example 203.0.113.10 or 203.0.113.0\/28).'); continue; }\n      if (r4.prefix < 16) warnings.push('ip4:' + v + ' is a very large range. Microsoft advises keeping ranges no bigger than you need.');\n      terms.push('ip4:' + v);\n    }\n    for (i = 0; i < Math.min(ip6.length, MAX_ITEMS); i++) {\n      v = ip6[i].replace(\/^ip6:\/, '');\n      var r6 = isIp6Cidr(v);\n      if (!r6) { errors.push('\"' + v.slice(0, 60) + '\" is not an IPv6 address or range (for example 2001:db8::1 or 2001:db8::\/48).'); continue; }\n      if (r6.prefix < 32) warnings.push('ip6:' + v + ' is a very large range.');\n      terms.push('ip6:' + v);\n    }\n    if (o.a) { terms.push('a'); lookups++; }\n    if (o.mx) {\n      terms.push('mx'); lookups++;\n      if (o.m365) warnings.push('With Microsoft 365 your MX record points to Microsoft, which the Microsoft 365 include already covers. You probably do not need \"mx\".');\n    }\n    if (o.m365) { terms.push('include:' + M365_INCLUDE); lookups++; }\n    var seenInc = Object.create(null);\n    for (i = 0; i < Math.min(inc.length, MAX_ITEMS); i++) {\n      v = trimDot(inc[i].replace(\/^include:\/, ''));\n      if (v === M365_INCLUDE) { if (o.m365) continue; }\n      if (seenInc[v]) continue;\n      seenInc[v] = true;\n      if (!SPF_HOST.test(v)) { errors.push('\"' + v.slice(0, 60) + '\" is not a valid include domain. Copy the exact value from the sending service\\'s documentation (for example _spf.example.net).'); continue; }\n      terms.push('include:' + v); lookups++;\n    }\n    if (terms.length === 1) errors.push('Choose at least one source of email: Microsoft 365, another service, or an IP address. If the domain never sends email, tick \"This domain never sends email\".');\n    var all = o.all === '~all' ? '~all' : '-all';\n    terms.push(all);\n    if (all === '~all') warnings.push('~all (soft fail) is a reasonable stop-gap while you are still finding every sender. Microsoft recommends -all once DKIM and DMARC are in place, because DMARC can then act on SPF failures.');\n    if (lookups > 10) errors.push('This record needs ' + lookups + ' DNS lookups before counting any lookups inside the includes. SPF allows at most 10 (RFC 7208 section 4.6.4); more causes a permanent error. Move some senders to a subdomain or replace includes with the provider\\'s IP ranges.');\n    else if (lookups >= 8) warnings.push('This record already uses ' + lookups + ' of the 10 DNS lookups SPF allows, and includes often contain further lookups. Check the total with your DNS provider\\'s tools.');\n    var record = terms.join(' ');\n    var notes = ['Publish it as a TXT record on the domain itself (the host is usually \"@\"). Only one SPF record per domain: merge rather than add a second (two records cause a permanent error).'];\n    if (record.length > 255) notes.push('The record is ' + record.length + ' characters. DNS stores TXT values in strings of up to 255 characters, so your DNS host must split it into several quoted strings (most do this automatically).');\n    if (record.length > 450) warnings.push('The record is longer than RFC 7208 recommends (about 450 characters). Consider moving some senders to a subdomain.');\n    return { record: record, lookups: lookups, errors: errors, warnings: warnings, notes: notes };\n  }\n\n  \/\/ ---------------------------------------------------------------- DKIM (Microsoft 365 CNAMEs)\n  function dkimRecords(domainText, initialText, format, partitionText) {\n    var d = normDomain(domainText);\n    if (d.error) return d;\n    var init = str(initialText).trim().toLowerCase().replace(\/\\.onmicrosoft\\.com\\.?$\/, '');\n    if (init === '') return { error: 'Enter your initial Microsoft 365 domain, for example contoso.onmicrosoft.com (or just contoso).' };\n    if (!LABEL.test(init)) return { error: 'The initial domain is the part before .onmicrosoft.com: letters, numbers and hyphens only.' };\n    var dashed = d.domain.split('.').join('-');\n    var part = str(partitionText).trim().toLowerCase();\n    if (part !== '') { if (!\/^[a-z]$\/.test(part)) return { error: 'The partition character is a single letter, such as r or n.' }; }\n    var fmt = format === 'old' ? 'old' : 'new';\n    var rows = [];\n    for (var s = 1; s <= 2; s++) {\n      var value;\n      if (fmt === 'old') value = 'selector' + s + '-' + dashed + '._domainkey.' + init + '.onmicrosoft.com';\n      else value = 'selector' + s + '-' + dashed + '._domainkey.' + init + '.' + (part === '' ? '<letter>' : part) + '-v1.dkim.mail.microsoft';\n      rows.push({ host: 'selector' + s + '._domainkey', fqdn: 'selector' + s + '._domainkey.' + d.domain, type: 'CNAME', value: value });\n    }\n    var notes = [];\n    if (fmt === 'new') {\n      if (part === '') notes.push('<letter> is the dynamic partition character Microsoft assigns to your domain (for example r or n). You can\\'t guess it: copy the exact values from the Microsoft Defender portal or Get-DkimSigningConfig.');\n      notes.push('This is the format Microsoft introduced in May 2025 for newly added custom domains. Domains added earlier keep the older onmicrosoft.com format, and the two formats can\\'t be mixed for the same selector.');\n    } else {\n      notes.push('This is the older format, still used by custom domains added before May 2025. If Microsoft shows you a value ending in dkim.mail.microsoft, use that instead.');\n    }\n    notes.push('Treat these as a preview. Microsoft says the values in its documentation are illustrations only: always copy the exact values from Microsoft Defender (Email authentication settings, DKIM tab) or run Get-DkimSigningConfig -Identity ' + d.domain + ' | Format-List Selector1CNAME,Selector2CNAME in Exchange Online PowerShell.');\n    notes.push('Create both CNAME records, wait for Microsoft to detect them, then turn on DKIM signing for the domain. Only one selector is active at a time; the other is used after a key rotation.');\n    return { domain: d.domain, rows: rows, notes: notes, format: fmt };\n  }\n\n  \/\/ ---------------------------------------------------------------- DMARC builder\n  var POLICIES = { none: 1, quarantine: 1, reject: 1 };\n  function parseAddresses(text, label, errors) {\n    var raw = list(text), out = [];\n    if (raw.length > 5) { errors.push('Use at most 5 ' + label + ' addresses.'); raw = raw.slice(0, 5); }\n    for (var i = 0; i < raw.length; i++) {\n      var a = raw[i].replace(\/^mailto:\/, '');\n      var at = a.lastIndexOf('@');\n      var local = at > 0 ? a.slice(0, at) : '', dom = at > 0 ? trimDot(a.slice(at + 1)) : '';\n      var ok = false;\n      if (LOCAL.test(local)) { if (HOST.test(dom)) ok = true; }\n      if (!ok) { errors.push('\"' + a.slice(0, 60) + '\" is not a valid ' + label + ' email address.'); continue; }\n      out.push({ addr: local + '@' + dom, domain: dom });\n    }\n    return out;\n  }\n  function dmarcValue(p, extra) {\n    var t = ['v=DMARC1', 'p=' + p];\n    for (var i = 0; i < extra.length; i++) t.push(extra[i]);\n    return t.join('; ');\n  }\n\n  function buildDmarc(o) {\n    o = o || {};\n    var errors = [], warnings = [], notes = [];\n    var d = normDomain(o.domain);\n    if (d.error) return { error: d.error };\n    var p = POLICIES[o.p] ? o.p : 'none';\n    var extra = [];\n    if (o.sp) {\n      if (!POLICIES[o.sp]) errors.push('The subdomain policy must be none, quarantine or reject.');\n      else if (o.sp !== p) extra.push('sp=' + o.sp);\n    }\n    var pctText = str(o.pct).trim().replace('%', '');\n    if (pctText !== '') {\n      if (!\/^[0-9]{1,3}$\/.test(pctText)) errors.push('The percentage must be a whole number from 0 to 100.');\n      else if (+pctText > 100) errors.push('The percentage must be a whole number from 0 to 100.');\n      else if (+pctText !== 100) {\n        extra.push('pct=' + (+pctText));\n        if (p === 'none') warnings.push('pct has no effect with p=none.');\n        notes.push('pct is still shown in Microsoft\\'s DMARC guidance, but RFC 9989 (May 2026), the new DMARC standard, lists it as historic because receivers rarely applied values other than 0 or 100 accurately. Treat partial percentages as approximate.');\n      }\n    }\n    if (o.adkim === 's') extra.push('adkim=s');\n    if (o.aspf === 's') extra.push('aspf=s');\n    if (o.adkim === 's') warnings.push('Strict DKIM alignment (adkim=s) fails mail signed by a parent or sibling domain. Relaxed (the default) suits most organisations.');\n    if (o.aspf === 's') warnings.push('Strict SPF alignment (aspf=s) fails services that send with a subdomain in the envelope sender, such as bounces.' + d.domain + '.');\n    var rua = parseAddresses(o.rua, 'aggregate report', errors);\n    var ruf = parseAddresses(o.ruf, 'failure report', errors);\n    var i, ext = [];\n    if (rua.length) {\n      var u = [];\n      for (i = 0; i < rua.length; i++) { u.push('mailto:' + rua[i].addr); if (orgDomain(rua[i].domain) !== orgDomain(d.domain)) ext.push(rua[i].domain); }\n      extra.push('rua=' + u.join(','));\n    } else {\n      warnings.push('Add an aggregate report address (rua). Without reports you can\\'t see who is sending as your domain, which is the whole point of p=none.');\n    }\n    if (ruf.length) {\n      var f = [];\n      for (i = 0; i < ruf.length; i++) { f.push('mailto:' + ruf[i].addr); if (orgDomain(ruf[i].domain) !== orgDomain(d.domain)) ext.push(ruf[i].domain); }\n      extra.push('ruf=' + f.join(','));\n      if (o.fo) { if (\/^(0|1|d|s)$\/.test(o.fo)) { if (o.fo !== '0') extra.push('fo=' + o.fo); } }\n      notes.push('Failure (forensic) reports can contain parts of real messages, and many receivers don\\'t send them; Microsoft 365 never does. Use a dedicated mailbox and consider the privacy of the contents.');\n    }\n    var seenExt = Object.create(null);\n    for (i = 0; i < ext.length; i++) {\n      if (seenExt[ext[i]]) continue;\n      seenExt[ext[i]] = true;\n      notes.push('Reports go to another domain (' + ext[i] + '). That domain must publish a TXT record at ' + d.domain + '._report._dmarc.' + ext[i] + ' with the value v=DMARC1; or receivers won\\'t send the reports. Reporting services usually do this for you.');\n    }\n    if (p === 'reject') { if (!rua.length) warnings.push('Going straight to p=reject without reports risks blocking legitimate mail you didn\\'t know about.'); }\n    if (o.sp) {\n      var rank = { none: 0, quarantine: 1, reject: 2 };\n      if (POLICIES[o.sp]) { if (rank[o.sp] < rank[p]) warnings.push('Your subdomain policy (sp=' + o.sp + ') is weaker than the main policy, so attackers could spoof made-up subdomains. Only do this while you are still fixing subdomain senders.'); }\n    }\n    if (errors.length) return { errors: errors, warnings: warnings, notes: notes, host: '_dmarc.' + d.domain };\n    var record = dmarcValue(p, extra);\n    return { host: '_dmarc.' + d.domain, record: record, errors: [], warnings: warnings, notes: notes, plan: rollout(d.domain, rua, o) };\n  }\n\n  \/** Staged rollout following Microsoft's guidance (none, then quarantine, then reject). *\/\n  function rollout(domain, rua, o) {\n    var tail = [];\n    if (o.adkim === 's') tail.push('adkim=s');\n    if (o.aspf === 's') tail.push('aspf=s');\n    if (rua.length) { var u = []; for (var i = 0; i < rua.length; i++) u.push('mailto:' + rua[i].addr); tail.push('rua=' + u.join(',')); }\n    else tail.push('rua=mailto:dmarc-reports@' + domain);\n    return [\n      { title: 'Before you start', record: '', text: 'Publish SPF for ' + domain + ', turn on DKIM signing with your own domain in Microsoft 365, and list every service that sends email as ' + domain + ' (invoicing, marketing, website forms, scanners). Each one needs SPF or, better, DKIM signing with your domain, or should move to a subdomain.' },\n      { title: 'Stage 1: monitor', record: dmarcValue('none', tail), text: 'Collect aggregate reports and fix every legitimate source that fails. Use a shared mailbox or a reporting service, not someone\\'s inbox. Move on only when the reports show your real mail passing.' },\n      { title: 'Stage 2: quarantine', record: dmarcValue('quarantine', tail), text: 'Failing mail now goes to junk or quarantine. Microsoft\\'s guidance allows stepping up with pct=10, 25, 50, 75 then 100; RFC 9989 makes pct historic, so expect receivers to treat it loosely. Keep watching the reports.' },\n      { title: 'Stage 3: reject', record: dmarcValue('reject', tail), text: 'The goal for every domain you use. Failing mail is refused. Keep reading reports so a new sending service doesn\\'t get blocked.' },\n      { title: 'Subdomains and parked domains', record: 'v=DMARC1; p=reject;', text: 'Microsoft suggests starting with low-volume subdomains and doing the main domain last. Domains you own but don\\'t use for email get SPF v=spf1 -all and this DMARC record, and so does your onmicrosoft.com domain if you don\\'t send from it (set in the Microsoft 365 admin centre).' }\n    ];\n  }\n\n  \/\/ ---------------------------------------------------------------- explain a pasted record\n  \/** Remove surrounding quotes and join \"split\" \"strings\" the way DNS resolvers do (RFC 7208 3.3). *\/\n  function unquote(text) {\n    var t = str(text).replace(\/[\\r\\n\\t]+\/g, ' ').trim();\n    if (t.indexOf('\"') >= 0) {\n      var parts = t.match(\/\"[^\"]*\"\/g);\n      if (parts) { var j = ''; for (var i = 0; i < parts.length; i++) j += parts[i].slice(1, -1); t = j.trim(); }\n    }\n    return t;\n  }\n\n  function detect(text) {\n    var t = unquote(text).toLowerCase();\n    if (\/^v=spf1(\\s|$)\/.test(t)) return 'spf';\n    if (\/^v\\s*=\\s*dmarc1\/.test(t)) return 'dmarc';\n    if (\/^v=dkim1\/.test(t)) return 'dkim';\n    return '';\n  }\n\n  function explain(text) {\n    var raw = str(text);\n    if (raw.trim() === '') return { error: 'Paste an SPF record (starts with v=spf1) or a DMARC record (starts with v=DMARC1).' };\n    if (raw.length > MAX_TEXT) return { error: 'That is longer than ' + MAX_TEXT + ' characters, which no real SPF or DMARC record should be.' };\n    var kind = detect(raw);\n    if (kind === 'spf') return explainSpf(raw);\n    if (kind === 'dmarc') return explainDmarc(raw);\n    if (kind === 'dkim') return { error: 'That looks like a DKIM key record (v=DKIM1). In Microsoft 365 you don\\'t publish the key yourself: you create two CNAME records that point to Microsoft (see the DKIM tab).' };\n    return { error: 'This doesn\\'t start with v=spf1 or v=DMARC1. Paste just the TXT value, for example v=spf1 include:spf.protection.outlook.com -all.' };\n  }\n\n  var LOOKUP_MECH = { include: 1, a: 1, mx: 1, ptr: 1, exists: 1 };\n\n  function explainSpf(text) {\n    var rec = unquote(text);\n    var rows = [], warnings = [], errors = [], lookups = 0, allSeen = false, redirect = 0, exp = 0;\n    var versions = rec.toLowerCase().match(\/(^|\\s)v=spf1(\\s|$)\/g);\n    if (versions) { if (versions.length > 1) errors.push('This contains more than one v=spf1. A domain must have exactly one SPF record; two cause a permanent error (RFC 7208 section 3.2). Merge them.'); }\n    var terms = rec.split(\/\\s+\/);\n    if (terms.length > MAX_TERMS) { warnings.push('Only the first ' + MAX_TERMS + ' terms are explained.'); terms = terms.slice(0, MAX_TERMS); }\n    for (var i = 0; i < terms.length; i++) {\n      var t = terms[i], lt = t.toLowerCase();\n      if (i === 0) { rows.push({ term: t, level: 'ok', text: 'Version: marks this TXT record as an SPF record.' }); continue; }\n      if (lt === 'v=spf1') { rows.push({ term: t, level: 'bad', text: 'A second version tag: this is really two records pasted together.' }); continue; }\n      if (allSeen) {\n        rows.push({ term: t, level: 'warn', text: 'Never checked: receivers stop at \"all\", so anything after it is ignored.' });\n        continue;\n      }\n      var mod = \/^([a-z][a-z0-9_.-]*)=(.*)$\/.exec(lt);\n      if (mod) {\n        if (mod[1] === 'redirect') {\n          redirect++; lookups++;\n          rows.push({ term: t, level: redirect > 1 ? 'bad' : 'ok', text: redirect > 1 ? 'A second redirect: this is a permanent error.' : 'If nothing else matches, use the SPF record of ' + mod[2] + ' instead. Uses 1 DNS lookup. Ignored if the record also has \"all\".' });\n        } else if (mod[1] === 'exp') {\n          exp++;\n          rows.push({ term: t, level: exp > 1 ? 'bad' : 'ok', text: exp > 1 ? 'A second exp: this is a permanent error.' : 'Explanation: text receivers may show when a message fails. Rarely needed.' });\n        } else if (LOOKUP_MECH[mod[1]] || mod[1] === 'ip4' || mod[1] === 'ip6') {\n          rows.push({ term: t, level: 'bad', text: 'Uses \"=\" instead of \":\". Write ' + mod[1] + ':' + mod[2] + ' (Microsoft lists this as a common mistake). As written, receivers treat it as an unknown modifier and ignore it.' });\n          errors.push('\"' + t.slice(0, 60) + '\" uses \"=\" instead of \":\".');\n        } else {\n          rows.push({ term: t, level: 'warn', text: 'Unknown modifier: receivers ignore it (RFC 7208 section 6). Check for a typo.' });\n        }\n        continue;\n      }\n      var m = \/^([+~?-]?)([a-z0-9]+)(?:([:\\\/])(.*))?$\/.exec(lt);\n      if (!m) { rows.push({ term: t, level: 'bad', text: 'Not valid SPF syntax. A stray character, an \"=\" instead of \":\", or a space after a colon all break the whole record.' }); errors.push('\"' + t.slice(0, 60) + '\" is not valid SPF syntax.'); continue; }\n      var q = m[1] || '+', name = m[2], sep = m[3] || '', arg = m[4] === undefined ? '' : m[4];\n      var who = q === '-' ? 'fail' : q === '~' ? 'soft fail' : q === '?' ? 'neutral' : 'pass';\n      var qualText = q === '+' ? '' : ' Qualifier \"' + q + '\" makes a match a ' + who + '.';\n      var macro = arg.indexOf('%{') >= 0;\n      if (name === 'all') {\n        allSeen = true;\n        if (sep !== '') { rows.push({ term: t, level: 'bad', text: '\"all\" takes no value.' }); continue; }\n        if (q === '-') rows.push({ term: t, level: 'ok', text: 'Hard fail: every server not listed is unauthorised. Microsoft recommends -all for Microsoft 365 domains that also use DKIM and DMARC.' });\n        else if (q === '~') rows.push({ term: t, level: 'warn', text: 'Soft fail: unlisted servers are probably unauthorised, so mail is accepted but marked. Fine while you are still finding senders; Microsoft recommends -all once DKIM and DMARC are set up.' });\n        else if (q === '?') rows.push({ term: t, level: 'warn', text: 'Neutral: says nothing about unlisted servers. Meant for testing only.' });\n        else rows.push({ term: t, level: 'bad', text: 'Pass for everyone: any server on the internet may send as this domain. This makes SPF useless. Change it to -all.' });\n        continue;\n      }\n      if (LOOKUP_MECH[name]) lookups++;\n      if (name === 'include') {\n        if (arg === '' || sep !== ':') { rows.push({ term: t, level: 'bad', text: 'include needs a domain, for example include:' + M365_INCLUDE + ' (with a colon and no space).' }); errors.push('An include has no domain.'); continue; }\n        var target = trimDot(arg);\n        var known = target === M365_INCLUDE ? ' This is Microsoft 365 (Exchange Online) for commercial and GCC tenants.' : '';\n        var bad = !macro ? !SPF_HOST.test(target) : false;\n        rows.push({ term: t, level: bad ? 'bad' : (q === '+' ? 'ok' : 'warn'), text: (bad ? 'Not a valid domain name. ' : '') + 'Also authorise the servers in the SPF record of ' + target + '.' + known + ' Uses 1 DNS lookup, plus any lookups inside that record.' + qualText });\n        if (arg.charAt(arg.length - 1) === '.') warnings.push('Remove the trailing dot after ' + target + ' (Microsoft lists this as a common mistake).');\n        continue;\n      }\n      if (name === 'a' || name === 'mx') {\n        var where = sep === ':' ? (arg.split('\/')[0] || 'this domain') : 'this domain';\n        var what = name === 'a' ? 'the IP addresses that ' + where + ' resolves to (A and AAAA records)' : 'the mail servers in the MX record of ' + where;\n        rows.push({ term: t, level: 'ok', text: 'Authorise ' + what + '. Uses 1 DNS lookup towards the limit of 10' + (name === 'mx' ? ' (the address lookups for its mail servers have a separate limit of 10; more is a permanent error)' : '') + '.' + qualText });\n        continue;\n      }\n      if (name === 'ptr') {\n        rows.push({ term: t, level: 'warn', text: 'ptr is deprecated: RFC 7208 says not to use it because it is slow and unreliable. Replace it with ip4, ip6 or include. Uses 1 DNS lookup.' });\n        continue;\n      }\n      if (name === 'ip4' || name === 'ip6') {\n        var ok = name === 'ip4' ? isIp4Cidr(arg) : isIp6Cidr(arg);\n        if (!ok) { rows.push({ term: t, level: 'bad', text: 'Not a valid ' + (name === 'ip4' ? 'IPv4' : 'IPv6') + ' address or range.' }); errors.push('\"' + t.slice(0, 60) + '\" is not a valid address.'); continue; }\n        var big = name === 'ip4' ? ok.prefix < 16 : ok.prefix < 32;\n        rows.push({ term: t, level: big ? 'warn' : 'ok', text: 'Authorise ' + (ok.prefix === (name === 'ip4' ? 32 : 128) ? 'the single address ' + arg : 'the range ' + arg) + '. No DNS lookup needed.' + (big ? ' That is a very large range: list only addresses you control.' : '') + qualText });\n        continue;\n      }\n      if (name === 'exists') {\n        rows.push({ term: t, level: 'ok', text: 'Advanced: authorise the sender if a lookup of ' + (arg || '(missing domain)') + ' returns any address. Usually built with SPF macros. Uses 1 DNS lookup.' + qualText });\n        continue;\n      }\n      rows.push({ term: t, level: 'bad', text: 'Unknown mechanism \"' + name.slice(0, 30) + '\": receivers return a permanent error and the whole record fails.' });\n      errors.push('Unknown mechanism \"' + name.slice(0, 30) + '\".');\n    }\n    if (!allSeen) { if (!redirect) warnings.push('There is no \"all\" at the end, so unlisted servers get a neutral result. End the record with -all (or ~all while testing).'); }\n    if (allSeen) { if (redirect) warnings.push('redirect is ignored because the record also contains \"all\".'); }\n    if (lookups > 10) errors.push('At least ' + lookups + ' DNS lookups before counting nested includes. The limit is 10 (RFC 7208 section 4.6.4); beyond that SPF returns a permanent error.');\n    else if (lookups >= 8) warnings.push(lookups + ' of 10 DNS lookups used directly, and includes usually contain more. Check the full count with your DNS provider\\'s tools.');\n    if (rec.length > 450) warnings.push('The record is ' + rec.length + ' characters, longer than RFC 7208 recommends (about 450).');\n    var summary;\n    if (errors.length) summary = 'This SPF record has errors: receivers would return a permanent error, or skip the broken part, so legitimate mail may fail SPF.';\n    else if (warnings.length) summary = 'Valid SPF syntax, with a few things to check.';\n    else summary = 'Valid SPF syntax with no obvious problems.';\n    return { kind: 'spf', record: rec, rows: rows, lookups: lookups, errors: errors, warnings: warnings, summary: summary };\n  }\n\n  function uriList(v, errors) {\n    var items = v.split(','), out = [];\n    if (items.length > 10) { errors.push('More than 10 report addresses.'); items = items.slice(0, 10); }\n    for (var i = 0; i < items.length; i++) {\n      var u = items[i].trim();\n      var m = \/^mailto:([^@\\s]{1,64})@([a-z0-9._-]{1,253})(?:!([0-9]{1,10})([kmgt]?))?$\/i.exec(u);\n      var ok = false;\n      if (m) { if (LOCAL.test(m[1].toLowerCase())) { if (HOST.test(trimDot(m[2].toLowerCase()))) ok = true; } }\n      if (!ok) errors.push('\"' + u.slice(0, 60) + '\" is not a valid mailto: address.');\n      else out.push(m[1] + '@' + m[2] + (m[3] ? ' (size limit ' + m[3] + (m[4] || '') + ')' : ''));\n    }\n    return out;\n  }\n\n  var DMARC_TAGS = {\n    p: 'Policy for mail that fails DMARC',\n    sp: 'Policy for subdomains',\n    np: 'Policy for subdomains that don\\'t exist (RFC 9989)',\n    pct: 'Percentage of failing mail the policy applies to',\n    rua: 'Where to send aggregate (daily summary) reports',\n    ruf: 'Where to send failure (forensic) reports',\n    adkim: 'DKIM alignment mode',\n    aspf: 'SPF alignment mode',\n    fo: 'When to send failure reports',\n    rf: 'Failure report format',\n    ri: 'Requested aggregate report interval',\n    t: 'Test mode (RFC 9989)',\n    psd: 'Public suffix domain flag (RFC 9989)'\n  };\n  var POLICY_TEXT = {\n    none: 'none: monitor only. Receivers take no DMARC action, but you get reports. The right place to start, not to stay.',\n    quarantine: 'quarantine: failing mail should be treated as suspicious, usually sent to junk or quarantine.',\n    reject: 'reject: failing mail should be refused. The strongest protection against spoofing of this domain.'\n  };\n\n  function explainDmarc(text) {\n    var rec = unquote(text);\n    var rows = [], warnings = [], errors = [];\n    var parts = rec.split(';'), seen = Object.create(null), tags = Object.create(null);\n    if (parts.length > MAX_TERMS) { warnings.push('Only the first ' + MAX_TERMS + ' tags are explained.'); parts = parts.slice(0, MAX_TERMS); }\n    var idx = 0;\n    for (var i = 0; i < parts.length; i++) {\n      var raw = parts[i].trim();\n      if (raw === '') continue;\n      var m = \/^([a-z]{1,10})\\s*=\\s*(.*)$\/i.exec(raw);\n      if (!m) { rows.push({ term: raw, level: 'bad', text: 'Not a tag=value pair. Tags are separated by semicolons.' }); errors.push('\"' + raw.slice(0, 60) + '\" is not a valid tag.'); idx++; continue; }\n      var name = m[1].toLowerCase(), v = m[2].trim(), lv = v.toLowerCase();\n      if (idx === 0) {\n        if (name !== 'v') errors.push('A DMARC record must start with v=DMARC1.');\n      }\n      idx++;\n      if (seen[name]) { rows.push({ term: raw, level: 'warn', text: 'Duplicate tag: only one ' + name + ' is expected. Remove the extra one.' }); warnings.push('The ' + name + ' tag appears more than once.'); continue; }\n      seen[name] = true; tags[name] = lv;\n      if (name === 'v') {\n        if (v !== 'DMARC1') { rows.push({ term: raw, level: v.toLowerCase() === 'dmarc1' ? 'warn' : 'bad', text: 'The version must be exactly DMARC1.' }); if (v.toLowerCase() !== 'dmarc1') errors.push('The version must be DMARC1.'); }\n        else rows.push({ term: raw, level: idx === 1 ? 'ok' : 'bad', text: idx === 1 ? 'Version: marks this TXT record (at _dmarc.yourdomain) as a DMARC policy.' : 'v=DMARC1 must be the first tag.' });\n        continue;\n      }\n      if (name === 'p' || name === 'sp' || name === 'np') {\n        if (!POLICIES[lv]) { rows.push({ term: raw, level: 'bad', text: DMARC_TAGS[name] + ': must be none, quarantine or reject.' }); errors.push(name + ' must be none, quarantine or reject.'); continue; }\n        rows.push({ term: raw, level: lv === 'none' ? 'warn' : 'ok', text: DMARC_TAGS[name] + ' is ' + POLICY_TEXT[lv] });\n        if (name === 'p') { if (idx !== 2) warnings.push('RFC 7489 expects p to come straight after v=DMARC1. Most receivers cope, but put it second to be safe.'); }\n        continue;\n      }\n      if (name === 'pct') {\n        if (!\/^[0-9]{1,3}$\/.test(v)) { rows.push({ term: raw, level: 'bad', text: 'pct must be a whole number from 0 to 100.' }); errors.push('pct must be 0 to 100.'); continue; }\n        if (+v > 100) { rows.push({ term: raw, level: 'bad', text: 'pct must be a whole number from 0 to 100.' }); errors.push('pct must be 0 to 100.'); continue; }\n        rows.push({ term: raw, level: +v === 100 ? 'ok' : 'warn', text: 'Apply the policy to ' + (+v) + '% of failing mail' + (+v === 100 ? ' (the default, so this tag can be left out).' : '. Used for gradual rollout in Microsoft\\'s guidance, but RFC 9989 (May 2026) makes pct historic because receivers rarely honoured values other than 0 and 100.') });\n        continue;\n      }\n      if (name === 'rua' || name === 'ruf') {\n        var errs = [];\n        var addrs = uriList(v, errs);\n        if (errs.length) { rows.push({ term: raw, level: 'bad', text: errs.join(' ') }); for (var e = 0; e < errs.length; e++) errors.push(errs[e]); continue; }\n        rows.push({ term: raw, level: 'ok', text: DMARC_TAGS[name] + ': ' + addrs.join(', ') + '.' + (name === 'ruf' ? ' Many receivers don\\'t send failure reports (Microsoft 365 doesn\\'t), and they can contain message content.' : ' Reports are XML files sent by receiving mail systems, usually daily.') });\n        continue;\n      }\n      if (name === 'adkim' || name === 'aspf') {\n        if (lv !== 'r') { if (lv !== 's') { rows.push({ term: raw, level: 'bad', text: name + ' must be r (relaxed) or s (strict).' }); errors.push(name + ' must be r or s.'); continue; } }\n        rows.push({ term: raw, level: 'ok', text: DMARC_TAGS[name] + ': ' + (lv === 's' ? 'strict. The ' + (name === 'adkim' ? 'DKIM signing domain' : 'envelope sender domain') + ' must exactly match the From domain; subdomains fail.' : 'relaxed (the default). Subdomains of the same organisational domain count as a match.') });\n        continue;\n      }\n      if (name === 'fo') {\n        if (!\/^[01ds](:[01ds]){0,3}$\/.test(lv)) { rows.push({ term: raw, level: 'bad', text: 'fo takes 0, 1, d or s, separated by colons.' }); errors.push('fo is not valid.'); continue; }\n        rows.push({ term: raw, level: 'ok', text: DMARC_TAGS[name] + ': 0 = when SPF and DKIM both fail to align (default), 1 = when either fails, d = DKIM failures, s = SPF failures. Only used with ruf.' });\n        continue;\n      }\n      if (name === 'rf' || name === 'ri') {\n        rows.push({ term: raw, level: 'warn', text: DMARC_TAGS[name] + '. Defined in RFC 7489 but historic in RFC 9989; most receivers ignore it, so it can be removed.' });\n        continue;\n      }\n      if (name === 't') {\n        if (lv !== 'y') { if (lv !== 'n') { rows.push({ term: raw, level: 'bad', text: 't must be y or n.' }); continue; } }\n        rows.push({ term: raw, level: lv === 'y' ? 'warn' : 'ok', text: DMARC_TAGS[name] + ': ' + (lv === 'y' ? 'yes. Receivers apply one step below the published policy (reject acts as quarantine). It replaces pct=0 in the new standard.' : 'no (the default).') });\n        continue;\n      }\n      if (name === 'psd') {\n        rows.push({ term: raw, level: 'warn', text: DMARC_TAGS[name] + '. Only for registries that run public suffixes (like a country-code domain); an ordinary organisation should leave it out.' });\n        continue;\n      }\n      rows.push({ term: raw, level: 'warn', text: 'Unknown tag: receivers ignore it. Check for a typo.' });\n    }\n    if (!tags.v) errors.push('The record has no v=DMARC1 tag.');\n    if (!tags.p) warnings.push('There is no p= tag. RFC 7489 requires one; the newer RFC 9989 treats a missing p as none. Add p=none, quarantine or reject.');\n    if (!tags.rua) warnings.push('No aggregate report address (rua), so you won\\'t see who is sending as your domain.');\n    if (tags.p === 'none') warnings.push('p=none protects nothing on its own. Use the reports to fix your senders, then move to quarantine and reject.');\n    var summary;\n    if (errors.length) summary = 'This DMARC record has errors. Receivers may ignore it or fall back to defaults.';\n    else if (tags.p === 'reject') summary = 'Valid DMARC record at the strongest policy (reject).';\n    else if (tags.p === 'quarantine') summary = 'Valid DMARC record at quarantine. Reject is the goal once reports look clean.';\n    else summary = 'Valid DMARC record in monitoring mode. It reports, but doesn\\'t stop spoofing yet.';\n    return { kind: 'dmarc', record: rec, rows: rows, errors: errors, warnings: warnings, summary: summary };\n  }\n\n  \/** Validate a shared-link hash: only \"#d=<domain>\" is accepted. *\/\n  function parseHash(h) {\n    var s = str(h).replace(\/^#\/, '');\n    var m = \/^d=([a-z0-9.-]{1,253})$\/i.exec(s);\n    if (!m) return null;\n    var d = normDomain(m[1]);\n    return d.error ? null : d.domain;\n  }\n\n  var api = { normDomain: normDomain, orgDomain: orgDomain, buildSpf: buildSpf, dkimRecords: dkimRecords, buildDmarc: buildDmarc, explain: explain, parseHash: parseHash, isIp6: isIp6 };\n  if (typeof module !== 'undefined') { if (module.exports) { module.exports = api; return; } }\n\n  \/\/ ---------------------------------------------------------------- page wiring\n  function el(tag, text, cls) { var e = document.createElement(tag); if (text !== undefined) e.textContent = text; if (cls) e.className = cls; return e; }\n  function clear(node) { while (node.firstChild) node.removeChild(node.firstChild); }\n  function show(node, yes) { node.hidden = !yes; }\n  function bulletList(target, items, cls) {\n    if (!items.length) return;\n    var ul = el('ul', undefined, cls);\n    for (var i = 0; i < items.length; i++) ul.appendChild(el('li', items[i]));\n    target.appendChild(ul);\n  }\n  function messages(target, r) {\n    bulletList(target, r.errors || [], 'cat-msgs cat-bad');\n    bulletList(target, r.warnings || [], 'cat-msgs cat-warn');\n    bulletList(target, r.notes || [], 'cat-msgs cat-info');\n  }\n  function copyButton(text, label) {\n    var b = el('button', 'Copy', 'cat-copy');\n    b.type = 'button';\n    b.setAttribute('aria-label', 'Copy ' + label);\n    b.addEventListener('click', function () {\n      if (!navigator.clipboard) return;\n      navigator.clipboard.writeText(text).then(function () { b.textContent = 'Copied'; setTimeout(function () { b.textContent = 'Copy'; }, 1200); }, function () {});\n    });\n    return b;\n  }\n  function recordTable(rows) {\n    var table = el('table', undefined, 'cat-rec');\n    var thead = el('thead'), hr = el('tr');\n    var heads = ['Host', 'Type', 'Value'];\n    for (var h = 0; h < heads.length; h++) { var th = el('th', heads[h]); th.scope = 'col'; hr.appendChild(th); }\n    thead.appendChild(hr); table.appendChild(thead);\n    var body = el('tbody');\n    for (var i = 0; i < rows.length; i++) {\n      var tr = el('tr');\n      tr.appendChild(el('td', rows[i].host));\n      tr.appendChild(el('td', rows[i].type));\n      var td = el('td');\n      td.appendChild(el('code', rows[i].value));\n      td.appendChild(copyButton(rows[i].value, rows[i].type + ' value for ' + rows[i].host));\n      tr.appendChild(td);\n      body.appendChild(tr);\n    }\n    table.appendChild(body);\n    return table;\n  }\n\n  function init() {\n    var root = document.getElementById('cat-dmarc-builder');\n    if (!root) return;\n    var q = function (s) { return root.querySelector(s); };\n    var val = function (s) { var n = q(s); return n ? n.value : ''; };\n    var checked = function (s) { var n = q(s); return n ? n.checked : false; };\n    var radio = function (name) { var n = root.querySelector('input[name=\"' + name + '\"]:checked'); return n ? n.value : ''; };\n\n    \/\/ Tabs\n    var tabs = root.querySelectorAll('[role=\"tab\"]');\n    function selectTab(btn) {\n      for (var i = 0; i < tabs.length; i++) {\n        var on = tabs[i] === btn;\n        tabs[i].setAttribute('aria-selected', on ? 'true' : 'false');\n        tabs[i].tabIndex = on ? 0 : -1;\n        show(document.getElementById(tabs[i].getAttribute('aria-controls')), on);\n      }\n    }\n    for (var t = 0; t < tabs.length; t++) {\n      tabs[t].addEventListener('click', function (e) { selectTab(e.currentTarget); });\n      tabs[t].addEventListener('keydown', function (e) {\n        var i = Array.prototype.indexOf.call(tabs, e.currentTarget), j = -1;\n        if (e.key === 'ArrowRight') j = (i + 1) % tabs.length; else if (e.key === 'ArrowLeft') j = (i - 1 + tabs.length) % tabs.length;\n        else if (e.key === 'Home') j = 0; else if (e.key === 'End') j = tabs.length - 1;\n        if (j >= 0) { e.preventDefault(); tabs[j].focus(); selectTab(tabs[j]); }\n      });\n    }\n\n    var domainIn = q('#cat-dm-domain'), domainErr = q('#cat-dm-domain-error');\n    function domain() {\n      var d = normDomain(domainIn.value);\n      domainErr.textContent = d.error || '';\n      show(domainErr, !!d.error);\n      return d;\n    }\n\n    \/\/ SPF\n    var spfOut = q('#cat-spf-out');\n    function renderSpf() {\n      clear(spfOut);\n      var d = domain();\n      var parked = checked('#cat-spf-parked');\n      var fs = q('#cat-spf-sources');\n      if (fs) fs.disabled = parked;\n      var r = buildSpf({ parked: parked, m365: checked('#cat-spf-m365'), includes: val('#cat-spf-includes'), ip4: val('#cat-spf-ip4'), ip6: val('#cat-spf-ip6'), a: checked('#cat-spf-a'), mx: checked('#cat-spf-mx'), all: radio('cat-spf-all') });\n      if (!r.errors.length) {\n        spfOut.appendChild(recordTable([{ host: d.error ? '@ (your domain)' : '@ (' + d.domain + ')', type: 'TXT', value: r.record }]));\n        spfOut.appendChild(el('p', 'DNS lookups used directly by this record: ' + r.lookups + ' of 10.', 'cat-summary'));\n      }\n      messages(spfOut, r);\n    }\n\n    \/\/ DKIM\n    var dkimOut = q('#cat-dkim-out');\n    function renderDkim() {\n      clear(dkimOut);\n      var r = dkimRecords(domainIn.value, val('#cat-dkim-initial'), radio('cat-dkim-format'), val('#cat-dkim-part'));\n      if (r.error) { dkimOut.appendChild(el('p', r.error, 'cat-err')); return; }\n      dkimOut.appendChild(recordTable(r.rows));\n      dkimOut.appendChild(el('p', 'Full names: ' + r.rows[0].fqdn + ' and ' + r.rows[1].fqdn + '. Most DNS hosts want only the part before your domain.', 'cat-note'));\n      bulletList(dkimOut, r.notes, 'cat-msgs cat-info');\n    }\n\n    \/\/ DMARC\n    var dmarcOut = q('#cat-dmarc-out');\n    function renderDmarc() {\n      clear(dmarcOut);\n      var ruf = val('#cat-dmarc-ruf');\n      var fo = q('#cat-dmarc-fo');\n      if (fo) fo.disabled = ruf.trim() === '';\n      var r = buildDmarc({ domain: domainIn.value, p: val('#cat-dmarc-p'), sp: val('#cat-dmarc-sp'), pct: val('#cat-dmarc-pct'), rua: val('#cat-dmarc-rua'), ruf: ruf, adkim: radio('cat-dmarc-adkim'), aspf: radio('cat-dmarc-aspf'), fo: val('#cat-dmarc-fo') });\n      domain();\n      if (r.error) { dmarcOut.appendChild(el('p', r.error, 'cat-err')); return; }\n      if (r.record) dmarcOut.appendChild(recordTable([{ host: r.host, type: 'TXT', value: r.record }]));\n      messages(dmarcOut, r);\n      if (r.plan) {\n        dmarcOut.appendChild(el('h4', 'Staged rollout plan', 'cat-h'));\n        var ol = el('ol', undefined, 'cat-plan');\n        for (var i = 0; i < r.plan.length; i++) {\n          var li = el('li');\n          li.appendChild(el('strong', r.plan[i].title));\n          li.appendChild(el('p', r.plan[i].text));\n          if (r.plan[i].record) {\n            var line = el('p', undefined, 'cat-plan-rec');\n            line.appendChild(el('code', r.plan[i].record));\n            line.appendChild(copyButton(r.plan[i].record, r.plan[i].title + ' record'));\n            li.appendChild(line);\n          }\n          ol.appendChild(li);\n        }\n        dmarcOut.appendChild(ol);\n      }\n    }\n\n    \/\/ Explain\n    var exOut = q('#cat-explain-out');\n    function renderExplain() {\n      clear(exOut);\n      var r = explain(val('#cat-explain-in'));\n      if (r.error) { exOut.appendChild(el('p', r.error, 'cat-err')); return; }\n      exOut.appendChild(el('p', r.summary, 'cat-summary ' + (r.errors.length ? 'cat-bad' : 'cat-good')));\n      var table = el('table', undefined, 'cat-explain');\n      var thead = el('thead'), hr = el('tr');\n      var th1 = el('th', r.kind === 'spf' ? 'Term' : 'Tag'); th1.scope = 'col';\n      var th2 = el('th', 'What it means'); th2.scope = 'col';\n      hr.appendChild(th1); hr.appendChild(th2); thead.appendChild(hr); table.appendChild(thead);\n      var body = el('tbody');\n      for (var i = 0; i < r.rows.length; i++) {\n        var tr = el('tr', undefined, 'cat-lv-' + r.rows[i].level);\n        var td = el('td'); td.appendChild(el('code', r.rows[i].term)); tr.appendChild(td);\n        var mark = r.rows[i].level === 'ok' ? 'OK: ' : r.rows[i].level === 'warn' ? 'Check: ' : 'Problem: ';\n        tr.appendChild(el('td', mark + r.rows[i].text));\n        body.appendChild(tr);\n      }\n      table.appendChild(body);\n      var wrap = el('div', undefined, 'cat-scroll');\n      wrap.appendChild(table);\n      exOut.appendChild(wrap);\n      if (r.kind === 'spf') exOut.appendChild(el('p', 'DNS lookups counted directly: ' + r.lookups + ' of 10 (lookups inside includes are not visible without a DNS query, which this tool never makes).', 'cat-note'));\n      messages(exOut, r);\n    }\n\n    \/\/ One debounce timer per renderer, so typing in one form never cancels another form's update.\n    function soon(fn) { var timer = null; return function () { clearTimeout(timer); timer = setTimeout(fn, 150); }; }\n    function all() { renderSpf(); renderDkim(); renderDmarc(); }\n    domainIn.addEventListener('input', soon(all));\n    q('#cat-spf-form').addEventListener('input', soon(renderSpf));\n    q('#cat-spf-form').addEventListener('change', renderSpf);\n    q('#cat-spf-form').addEventListener('submit', function (e) { e.preventDefault(); renderSpf(); });\n    q('#cat-dkim-form').addEventListener('input', soon(renderDkim));\n    q('#cat-dkim-form').addEventListener('change', renderDkim);\n    q('#cat-dkim-form').addEventListener('submit', function (e) { e.preventDefault(); renderDkim(); });\n    q('#cat-dmarc-form').addEventListener('input', soon(renderDmarc));\n    q('#cat-dmarc-form').addEventListener('change', renderDmarc);\n    q('#cat-dmarc-form').addEventListener('submit', function (e) { e.preventDefault(); renderDmarc(); });\n    q('#cat-explain-form').addEventListener('submit', function (e) { e.preventDefault(); renderExplain(); });\n    var samples = root.querySelectorAll('[data-sample]');\n    for (var s = 0; s < samples.length; s++) samples[s].addEventListener('click', function (e) {\n      q('#cat-explain-in').value = e.currentTarget.getAttribute('data-sample');\n      renderExplain();\n    });\n\n    \/\/ Start from a shared link (#d=example.com.au), validated like typed input.\n    var h = '';\n    try { h = decodeURIComponent(String(location.hash || '')); } catch (e) { h = ''; }\n    var fromHash = parseHash(h);\n    if (fromHash) domainIn.value = fromHash;\n    all();\n  }\n  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init); else init();\n})();\n<\/script>\n<\/div>\n\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Check:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>dig +short TXT _dmarc.example.com.au\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">You should see one record starting <code>v=DMARC1<\/code>. Then send another test email and look for <code>dmarc=pass<\/code> in the <code>Authentication-Results<\/code> header.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"staged-rollout-from-p-none-to-p-reject\">Staged rollout: from p=none to p=reject<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Microsoft's recommended approach is gradual, aiming for <code>p=reject<\/code>, and starting with lower-volume domains or subdomains before the main domain. Move to the next stage only when the reports from the current stage are clean.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\">\n<thead>\n<tr>\n<th>Stage<\/th>\n<th>DMARC record value<\/th>\n<th>Move on when<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>1. Monitor<\/td>\n<td><code>v=DMARC1; p=none; rua=mailto:dmarc-reports@example.com.au<\/code><\/td>\n<td>Every legitimate sender in the reports shows DMARC pass (aligned SPF or DKIM)<\/td>\n<\/tr>\n<tr>\n<td>2. Quarantine<\/td>\n<td><code>v=DMARC1; p=quarantine; rua=mailto:dmarc-reports@example.com.au<\/code><\/td>\n<td>No legitimate mail is landing in junk<\/td>\n<\/tr>\n<tr>\n<td>3. Reject<\/td>\n<td><code>v=DMARC1; p=reject; rua=mailto:dmarc-reports@example.com.au<\/code><\/td>\n<td>Keep watching reports for new senders<\/td>\n<\/tr>\n<\/tbody>\n<\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Easing in with <code>pct<\/code>.<\/strong> Microsoft's guide suggests stepping <code>pct<\/code> through 10, 25, 50, 75 and 100 at the quarantine stage (left out, it means 100). Be aware that the DMARC standard was revised in May 2026: <a href=\"https:\/\/www.rfc-editor.org\/rfc\/rfc9989.html\" target=\"_blank\" rel=\"noreferrer noopener\">RFC 9989<\/a> replaces RFC 7489, drops <code>pct<\/code> because receivers applied values other than 0 and 100 inconsistently, and adds a <code>t=y<\/code> test-mode flag instead. Records still start <code>v=DMARC1<\/code> and keep working, but don't rely on a partial percentage being honoured by every receiver. Treat <code>pct<\/code> as an optional extra step, not a safety net.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Check at each stage:<\/strong> send a test message from every sending service (Microsoft 365, the invoicing system, the newsletter tool and so on) to an external mailbox and confirm the header shows:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>spf=pass ... smtp.mailfrom=example.com.au (or a subdomain of it)\ndkim=pass ... header.d=example.com.au\ndmarc=pass ... header.from=example.com.au\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">DMARC passes if <strong>either<\/strong> SPF or DKIM passes <strong>and<\/strong> aligns. So a third-party service that passes SPF for its own domain (<code>smtp.mailfrom=bounce.servicename.example<\/code>) but doesn't sign with DKIM as your domain will fail DMARC. The fix is to set up DKIM for your domain in that service, or a custom return-path on your domain.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Check after enforcement:<\/strong> your aggregate reports should show a <code>disposition<\/code> of <code>quarantine<\/code> or <code>reject<\/code> for spoofed mail. Where the receiver is Microsoft 365 with its <strong>Honor DMARC record policy<\/strong> setting on, a spoof against <code>p=quarantine<\/code> is marked <code>dmarc=fail action=quarantine<\/code> and goes to Junk, and a spoof against <code>p=reject<\/code> is refused during delivery (<code>550 5.7.1<\/code>, recorded as <code>action=oreject<\/code>).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The same setting decides how your own tenant treats inbound mail that spoofs other domains. In the Defender portal it's <strong>Honor DMARC record policy when the message is detected as spoof<\/strong> in your anti-phishing policies; in Exchange Online PowerShell, check it with <code>Get-AntiPhishPolicy | Format-List Name,HonorDmarcPolicy<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">One more thing Microsoft points out: once your policy is <code>quarantine<\/code> or <code>reject<\/code>, outbound mail from your tenant that fails DMARC at the destination is routed through its high-risk delivery pool, with no override. That's another reason not to rush past stage 1.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"how-to-read-dmarc-aggregate-reports\">How to read DMARC aggregate reports<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Receivers send aggregate reports as compressed XML attachments, typically once a day. Raw XML is hard to read, so many organisations use a DMARC reporting service to turn them into readable summaries. Either way, the same fields matter:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\">\n<thead>\n<tr>\n<th>Field<\/th>\n<th>What it tells you<\/th>\n<th>What to do<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><code>source_ip<\/code> and <code>count<\/code><\/td>\n<td>Which server sent how many messages as your domain<\/td>\n<td>Identify every IP. Known service? Unknown?<\/td>\n<\/tr>\n<tr>\n<td><code>policy_evaluated<\/code> &gt; <code>dkim<\/code> \/ <code>spf<\/code><\/td>\n<td>Whether DKIM or SPF passed <strong>and aligned<\/strong><\/td>\n<td><code>fail<\/code> here means it didn't align with your From domain<\/td>\n<\/tr>\n<tr>\n<td><code>auth_results<\/code> &gt; <code>spf<\/code> \/ <code>dkim<\/code> &gt; <code>domain<\/code><\/td>\n<td>The domain that was actually checked<\/td>\n<td>If it isn't your domain, that's an alignment problem<\/td>\n<\/tr>\n<tr>\n<td><code>disposition<\/code><\/td>\n<td>What the receiver did (<code>none<\/code>, <code>quarantine<\/code>, <code>reject<\/code>)<\/td>\n<td>Confirms receivers are applying your policy<\/td>\n<\/tr>\n<\/tbody>\n<\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">The patterns you'll see most:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n\n<li><strong>Microsoft 365 IPs, aligned pass:<\/strong> healthy, no action.<\/li>\n\n\n<li><strong>A known service, SPF pass but not aligned:<\/strong> the service uses its own bounce domain. Set up DKIM for your domain in that service.<\/li>\n\n\n<li><strong>A known service, everything failing:<\/strong> it isn't in your SPF and isn't signing. Add it to SPF and set up DKIM.<\/li>\n\n\n<li><strong>Unknown IPs, all failing:<\/strong> likely spoofing. Once you're at <code>quarantine<\/code> or <code>reject<\/code>, your policy is doing its job.<\/li>\n\n\n<li><strong>Mailing lists and forwarders failing:<\/strong> normal. Forwarding breaks SPF, and edits to the message break DKIM.<\/li>\n\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Microsoft also notes that not every receiver sends reports, so the reports won't cover all of your mail.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"dns-host-notes-for-au-domains\">DNS host notes for .au domains<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">These differences between DNS control panels cause most failed setups:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n\n<li><strong>Host field conventions.<\/strong> Some panels want just <code>_dmarc<\/code> or <code>selector1._domainkey<\/code> and add your domain automatically. Others want the full name (<code>_dmarc.example.com.au<\/code>). If you enter the full name into a panel that appends the domain, you end up with <code>_dmarc.example.com.au.example.com.au<\/code>. After saving, always check with <code>dig<\/code> or <code>Resolve-DnsName<\/code>.<\/li>\n\n\n<li><strong>Trailing dots on CNAME values.<\/strong> Some panels need a trailing dot on the target (<code>...dkim.mail.microsoft.<\/code>) and some add it for you. A missing or extra dot is one of the DKIM problems Microsoft lists.<\/li>\n\n\n<li><strong>TXT quotes.<\/strong> Some panels add quotation marks around TXT values automatically; others need you to. Don't end up with doubled quotes.<\/li>\n\n\n<li><strong>DKIM is a CNAME, not a TXT record.<\/strong> Microsoft 365 uses CNAMEs for DKIM. Creating TXT records at <code>selector1._domainkey<\/code> won't work.<\/li>\n\n\n<li><strong>TTL.<\/strong> Microsoft recommends at least 3600 seconds (one hour) for SPF records.<\/li>\n\n\n<li><strong>Several domains.<\/strong> If you own <code>example.com.au<\/code> and also <code>example.au<\/code> or <code>example.com<\/code>, treat each separately. Each domain that sends mail needs its own SPF and DKIM. Domains that never send mail should be locked down as parked domains (below).<\/li>\n\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"lock-down-parked-domains-and-onmicrosoft-com\">Lock down parked domains (and onmicrosoft.com)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">For any domain you own that never sends email, publish records that say so:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>example.au           TXT  v=spf1 -all\n_dmarc.example.au    TXT  v=DMARC1; p=reject;\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Microsoft also recommends a DMARC record on your <code>example.onmicrosoft.com<\/code> domain if you don't send from it. You add that in the Microsoft 365 admin center under <strong>Settings<\/strong> &gt; <strong>Domains<\/strong> &gt; your onmicrosoft.com domain &gt; <strong>DNS records<\/strong> &gt; <strong>Add record<\/strong>, with TXT name <code>_dmarc<\/code> and value <code>v=DMARC1; p=reject<\/code>.<\/p>\n\n\n\n<div class=\"wp-block-group has-pale-cyan-blue-background-color has-background\" style=\"padding-top:28px;padding-right:32px;padding-bottom:28px;padding-left:32px;margin-top:32px;margin-bottom:32px\"><div class=\"wp-block-group__inner-container is-layout-constrained wp-container-core-group-is-layout-40de7cf3 wp-block-group-is-layout-constrained\">\n\n<p class=\"wp-block-paragraph\"><strong>Stop invoice fraud and spoofed email at the door.<\/strong> We set up email security properly: SPF, DKIM and DMARC, filtering, and the settings that make business email compromise harder.<\/p>\n\n\n\n<div class=\"wp-block-buttons is-layout-flex wp-block-buttons-is-layout-flex\">\n\n<div class=\"wp-block-button\"><a class=\"wp-block-button__link wp-element-button\" href=\"https:\/\/catharsis.net.au\/email-protection\/?utm_source=blog&amp;utm_medium=article&amp;utm_campaign=value-guides&amp;utm_content=spf-dkim-dmarc-microsoft-365-com-au-email-protection\" target=\"_blank\" rel=\"noreferrer noopener\">See email security<\/a><\/div>\n\n<\/div>\n\n<\/div><\/div>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"common-mistakes\">Common mistakes<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n\n<li><strong>Two SPF records.<\/strong> Usually created when a new service's setup guide says &quot;add this TXT record&quot;. Merge them into one.<\/li>\n\n\n<li><strong>More than 10 SPF lookups.<\/strong> Adding includes for every service adds up, especially as some include others. Move bulk or marketing mail to a subdomain (for example <code>news.example.com.au<\/code>) with its own SPF record; each subdomain gets its own 10-lookup budget.<\/li>\n\n\n<li><strong>Forgetting other senders.<\/strong> The invoicing system that emails your invoices is the classic one. Jumping to <code>p=reject<\/code> without finding it means your invoices stop arriving.<\/li>\n\n\n<li><strong>Leaving DMARC at p=none.<\/strong> Monitoring mode protects nobody from spoofing. It's a stage, not the destination.<\/li>\n\n\n<li><strong>Sending reports to a personal inbox.<\/strong> They'll be ignored or filtered. Use a shared mailbox or a reporting service.<\/li>\n\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">If you'd rather have this checked across all your domains and senders, including the Microsoft 365 settings around it (anti-phishing, external forwarding, MFA), our <a href=\"https:\/\/catharsis.net.au\/cloud-security\/\" target=\"_blank\" rel=\"noreferrer noopener\">Microsoft 365 security review<\/a> and <a href=\"https:\/\/catharsis.net.au\/email-protection\/\" target=\"_blank\" rel=\"noreferrer noopener\">email protection<\/a> services cover it.<\/p>\n\n\n\n<div class=\"wp-block-group has-pale-cyan-blue-background-color has-background\" style=\"padding-top:28px;padding-right:32px;padding-bottom:28px;padding-left:32px;margin-top:32px;margin-bottom:32px\"><div class=\"wp-block-group__inner-container is-layout-constrained wp-container-core-group-is-layout-40de7cf3 wp-block-group-is-layout-constrained\">\n\n<p class=\"wp-block-paragraph\"><strong>Is your Microsoft 365 set up securely?<\/strong> Our Microsoft 365 security review checks sign-in, sharing, email and admin settings against current guidance and gives you a prioritised fix list.<\/p>\n\n\n\n<div class=\"wp-block-buttons is-layout-flex wp-block-buttons-is-layout-flex\">\n\n<div class=\"wp-block-button\"><a class=\"wp-block-button__link wp-element-button\" href=\"https:\/\/catharsis.net.au\/cloud-security\/?utm_source=blog&amp;utm_medium=article&amp;utm_campaign=value-guides&amp;utm_content=spf-dkim-dmarc-microsoft-365-com-au-cloud-security\" target=\"_blank\" rel=\"noreferrer noopener\">See the Microsoft 365 review<\/a><\/div>\n\n<\/div>\n\n<\/div><\/div>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"key-takeaways\">Key takeaways<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n\n<li>Publish exactly one SPF record, <code>v=spf1 include:spf.protection.outlook.com -all<\/code>, plus any other services you use, and stay under 10 DNS lookups.<\/li>\n\n\n<li>Enable DKIM in the Defender portal and copy the CNAME values from your own tenant; new domains use the <code>dkim.mail.microsoft<\/code> format introduced in May 2025.<\/li>\n\n\n<li>Start DMARC at <code>p=none<\/code> with a <code>rua<\/code> address, then move to <code>quarantine<\/code> and <code>reject<\/code> only when the reports show every legitimate sender passing and aligned.<\/li>\n\n\n<li>Check each stage in real headers for <code>spf=pass<\/code>, <code>dkim=pass<\/code> and <code>dmarc=pass<\/code> with your domain.<\/li>\n\n\n<li>Lock down parked domains and your onmicrosoft.com domain with <code>v=spf1 -all<\/code> and <code>p=reject<\/code>.<\/li>\n\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"sources\">Sources<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n\n<li>Microsoft Learn, <a href=\"https:\/\/learn.microsoft.com\/en-us\/defender-office-365\/email-authentication-spf-configure\" target=\"_blank\" rel=\"noreferrer noopener\">Set up SPF to identify valid email sources for your Microsoft 365 domain<\/a><\/li>\n\n\n<li>Microsoft Learn, <a href=\"https:\/\/learn.microsoft.com\/en-us\/defender-office-365\/email-authentication-dkim-configure\" target=\"_blank\" rel=\"noreferrer noopener\">How to use DKIM for email in your custom domain<\/a> (updated August 2026)<\/li>\n\n\n<li>Microsoft Learn, <a href=\"https:\/\/learn.microsoft.com\/en-us\/defender-office-365\/email-authentication-dmarc-configure\" target=\"_blank\" rel=\"noreferrer noopener\">Set up DMARC to validate email in Microsoft 365<\/a><\/li>\n\n\n<li>ASD's ACSC, <a href=\"https:\/\/www.cyber.gov.au\/publication\/how-to-combat-fake-emails\" target=\"_blank\" rel=\"noreferrer noopener\">How to combat fake emails<\/a><\/li>\n\n\n<li>ASD's ACSC, <a href=\"https:\/\/www.cyber.gov.au\/publication\/protecting-against-business-email-compromise\" target=\"_blank\" rel=\"noreferrer noopener\">Protecting against business email compromise<\/a><\/li>\n\n\n<li>IETF, <a href=\"https:\/\/datatracker.ietf.org\/doc\/html\/rfc7208\" target=\"_blank\" rel=\"noreferrer noopener\">RFC 7208: Sender Policy Framework<\/a><\/li>\n\n\n<li>IETF, <a href=\"https:\/\/www.rfc-editor.org\/rfc\/rfc9989.html\" target=\"_blank\" rel=\"noreferrer noopener\">RFC 9989: DMARC<\/a> (May 2026, replaces <a href=\"https:\/\/datatracker.ietf.org\/doc\/html\/rfc7489\" target=\"_blank\" rel=\"noreferrer noopener\">RFC 7489<\/a>)<\/li>\n\n\n<li>.au Domain Administration, <a href=\"https:\/\/whois.auda.org.au\/\" target=\"_blank\" rel=\"noreferrer noopener\">WHOIS lookup<\/a><\/li>\n\n<\/ul>\n\n\n\n<div class=\"wp-block-group has-light-green-cyan-background-color has-background\" style=\"padding-top:28px;padding-right:32px;padding-bottom:28px;padding-left:32px;margin-top:40px;margin-bottom:32px\"><div class=\"wp-block-group__inner-container is-layout-constrained wp-container-core-group-is-layout-40de7cf3 wp-block-group-is-layout-constrained\">\n\n<h2 class=\"wp-block-heading\">Want a second pair of eyes?<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Catharsis Cyber Security is a Sydney cyber security company, certified to ISO\/IEC 27001:2022. We help organisations protect, test and run their technology, and explain it in plain English.<\/p>\n\n\n\n<div class=\"wp-block-buttons is-layout-flex wp-block-buttons-is-layout-flex\">\n\n<div class=\"wp-block-button\"><a class=\"wp-block-button__link wp-element-button\" href=\"https:\/\/catharsis.net.au\/contact-us\/?type=contact&amp;service=consult&amp;utm_source=blog&amp;utm_medium=article&amp;utm_campaign=value-guides&amp;utm_content=spf-dkim-dmarc-microsoft-365-com-au-contact\" target=\"_blank\" rel=\"noreferrer noopener\">Talk to us<\/a><\/div>\n\n\n<div class=\"wp-block-button is-style-outline is-style-outline--1\"><a class=\"wp-block-button__link wp-element-button\" href=\"https:\/\/catharsis.net.au\/contact-us\/?type=contact&amp;service=security-health-check&amp;utm_source=blog&amp;utm_medium=article&amp;utm_campaign=value-guides&amp;utm_content=spf-dkim-dmarc-microsoft-365-com-au-end-health-check\" target=\"_blank\" rel=\"noreferrer noopener\">Free security health check<\/a><\/div>\n\n<\/div>\n\n<\/div><\/div>\n\n\n\n<style>pre.wp-block-code{max-width:100%;overflow-x:auto}pre.wp-block-code code{white-space:pre}.wp-block-list a,.wp-block-table a{overflow-wrap:anywhere;word-break:break-word}<\/style>\n","protected":false},"excerpt":{"rendered":"<p>SPF, DKIM and DMARC stop criminals sending email that looks like it came from your exact domain. This guide gives the exact records Microsoft 365 needs for a .com.au domain, how to enter them at your DNS host, and a staged rollout from p=none to p=reject with a check at every step.<\/p>\n","protected":false},"author":2,"featured_media":1267,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[38],"tags":[251,248,246,250,249,185,176,247],"class_list":["post-1268","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-how-to-guides","tag-com-au","tag-dkim","tag-dmarc","tag-dns","tag-email-authentication","tag-email-security","tag-microsoft-365","tag-spf"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v28.8 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>How to Set Up SPF, DKIM and DMARC for .com.au in Microsoft 365 - Catharsis | Blog<\/title>\n<meta name=\"description\" content=\"How to set up SPF, DKIM and DMARC for a .com.au domain in Microsoft 365: exact records, DNS host notes and a staged rollout to p=reject with checks.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/catharsis.net.au\/blog\/spf-dkim-dmarc-microsoft-365-com-au\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"How to Set Up SPF, DKIM and DMARC for .com.au in Microsoft 365 - Catharsis | Blog\" \/>\n<meta property=\"og:description\" content=\"How to set up SPF, DKIM and DMARC for a .com.au domain in Microsoft 365: exact records, DNS host notes and a staged rollout to p=reject with checks.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/catharsis.net.au\/blog\/spf-dkim-dmarc-microsoft-365-com-au\/\" \/>\n<meta property=\"og:site_name\" content=\"Catharsis | Blog\" \/>\n<meta property=\"article:published_time\" content=\"2026-09-01T23:30:00+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2026-11-03T10:47:18+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/catharsis.net.au\/blog\/wp-content\/uploads\/2026\/11\/spf-dkim-dmarc-microsoft-365-com-au.jpg\" \/>\n\t<meta property=\"og:image:width\" content=\"1280\" \/>\n\t<meta property=\"og:image:height\" content=\"720\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/jpeg\" \/>\n<meta name=\"author\" content=\"Sajal Jain\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Sajal Jain\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"17 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/spf-dkim-dmarc-microsoft-365-com-au\\\/#article\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/spf-dkim-dmarc-microsoft-365-com-au\\\/\"},\"author\":{\"name\":\"Sajal Jain\",\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/#\\\/schema\\\/person\\\/f0826efb92a7aaf8d46b24715990e099\"},\"headline\":\"How to Set Up SPF, DKIM and DMARC for .com.au in Microsoft 365\",\"datePublished\":\"2026-09-01T23:30:00+00:00\",\"dateModified\":\"2026-11-03T10:47:18+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/spf-dkim-dmarc-microsoft-365-com-au\\\/\"},\"wordCount\":2997,\"commentCount\":0,\"publisher\":{\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/#organization\"},\"image\":{\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/spf-dkim-dmarc-microsoft-365-com-au\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/11\\\/spf-dkim-dmarc-microsoft-365-com-au.jpg\",\"keywords\":[\".com.au\",\"DKIM\",\"DMARC\",\"DNS\",\"email authentication\",\"email security\",\"Microsoft 365\",\"SPF\"],\"articleSection\":[\"How-To Guides\"],\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/spf-dkim-dmarc-microsoft-365-com-au\\\/#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/spf-dkim-dmarc-microsoft-365-com-au\\\/\",\"url\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/spf-dkim-dmarc-microsoft-365-com-au\\\/\",\"name\":\"How to Set Up SPF, DKIM and DMARC for .com.au in Microsoft 365 - Catharsis | Blog\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/spf-dkim-dmarc-microsoft-365-com-au\\\/#primaryimage\"},\"image\":{\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/spf-dkim-dmarc-microsoft-365-com-au\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/11\\\/spf-dkim-dmarc-microsoft-365-com-au.jpg\",\"datePublished\":\"2026-09-01T23:30:00+00:00\",\"dateModified\":\"2026-11-03T10:47:18+00:00\",\"description\":\"How to set up SPF, DKIM and DMARC for a .com.au domain in Microsoft 365: exact records, DNS host notes and a staged rollout to p=reject with checks.\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/spf-dkim-dmarc-microsoft-365-com-au\\\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/spf-dkim-dmarc-microsoft-365-com-au\\\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/spf-dkim-dmarc-microsoft-365-com-au\\\/#primaryimage\",\"url\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/11\\\/spf-dkim-dmarc-microsoft-365-com-au.jpg\",\"contentUrl\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/11\\\/spf-dkim-dmarc-microsoft-365-com-au.jpg\",\"width\":1280,\"height\":720,\"caption\":\"How to Set Up SPF, DKIM and DMARC for .com.au in Microsoft 365\"},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/spf-dkim-dmarc-microsoft-365-com-au\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"How to Set Up SPF, DKIM and DMARC for .com.au in Microsoft 365\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/#website\",\"url\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/\",\"name\":\"Catharsis | Blog\",\"description\":\"\",\"publisher\":{\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/#organization\",\"name\":\"Catharsis | Blog\",\"url\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/#\\\/schema\\\/logo\\\/image\\\/\",\"url\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/wp-content\\\/uploads\\\/2019\\\/01\\\/catharsis_au.jpg\",\"contentUrl\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/wp-content\\\/uploads\\\/2019\\\/01\\\/catharsis_au.jpg\",\"width\":215,\"height\":60,\"caption\":\"Catharsis | Blog\"},\"image\":{\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/#\\\/schema\\\/logo\\\/image\\\/\"}},{\"@type\":\"Person\",\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/#\\\/schema\\\/person\\\/f0826efb92a7aaf8d46b24715990e099\",\"name\":\"Sajal Jain\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/wp-content\\\/uploads\\\/2019\\\/01\\\/Sajal-copy-96x96.jpg\",\"url\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/wp-content\\\/uploads\\\/2019\\\/01\\\/Sajal-copy-96x96.jpg\",\"contentUrl\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/wp-content\\\/uploads\\\/2019\\\/01\\\/Sajal-copy-96x96.jpg\",\"caption\":\"Sajal Jain\"},\"description\":\"Cyber Security Enthusiast | OSCP | CEH | CHFI | CNSS | Master of IT Security &amp; Master of IT Management\",\"url\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/author\\\/sajal-jain\\\/\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"How to Set Up SPF, DKIM and DMARC for .com.au in Microsoft 365 - Catharsis | Blog","description":"How to set up SPF, DKIM and DMARC for a .com.au domain in Microsoft 365: exact records, DNS host notes and a staged rollout to p=reject with checks.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/catharsis.net.au\/blog\/spf-dkim-dmarc-microsoft-365-com-au\/","og_locale":"en_US","og_type":"article","og_title":"How to Set Up SPF, DKIM and DMARC for .com.au in Microsoft 365 - Catharsis | Blog","og_description":"How to set up SPF, DKIM and DMARC for a .com.au domain in Microsoft 365: exact records, DNS host notes and a staged rollout to p=reject with checks.","og_url":"https:\/\/catharsis.net.au\/blog\/spf-dkim-dmarc-microsoft-365-com-au\/","og_site_name":"Catharsis | Blog","article_published_time":"2026-09-01T23:30:00+00:00","article_modified_time":"2026-11-03T10:47:18+00:00","og_image":[{"width":1280,"height":720,"url":"https:\/\/catharsis.net.au\/blog\/wp-content\/uploads\/2026\/11\/spf-dkim-dmarc-microsoft-365-com-au.jpg","type":"image\/jpeg"}],"author":"Sajal Jain","twitter_card":"summary_large_image","twitter_misc":{"Written by":"Sajal Jain","Est. reading time":"17 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/catharsis.net.au\/blog\/spf-dkim-dmarc-microsoft-365-com-au\/#article","isPartOf":{"@id":"https:\/\/catharsis.net.au\/blog\/spf-dkim-dmarc-microsoft-365-com-au\/"},"author":{"name":"Sajal Jain","@id":"https:\/\/catharsis.net.au\/blog\/#\/schema\/person\/f0826efb92a7aaf8d46b24715990e099"},"headline":"How to Set Up SPF, DKIM and DMARC for .com.au in Microsoft 365","datePublished":"2026-09-01T23:30:00+00:00","dateModified":"2026-11-03T10:47:18+00:00","mainEntityOfPage":{"@id":"https:\/\/catharsis.net.au\/blog\/spf-dkim-dmarc-microsoft-365-com-au\/"},"wordCount":2997,"commentCount":0,"publisher":{"@id":"https:\/\/catharsis.net.au\/blog\/#organization"},"image":{"@id":"https:\/\/catharsis.net.au\/blog\/spf-dkim-dmarc-microsoft-365-com-au\/#primaryimage"},"thumbnailUrl":"https:\/\/catharsis.net.au\/blog\/wp-content\/uploads\/2026\/11\/spf-dkim-dmarc-microsoft-365-com-au.jpg","keywords":[".com.au","DKIM","DMARC","DNS","email authentication","email security","Microsoft 365","SPF"],"articleSection":["How-To Guides"],"inLanguage":"en-US","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/catharsis.net.au\/blog\/spf-dkim-dmarc-microsoft-365-com-au\/#respond"]}]},{"@type":"WebPage","@id":"https:\/\/catharsis.net.au\/blog\/spf-dkim-dmarc-microsoft-365-com-au\/","url":"https:\/\/catharsis.net.au\/blog\/spf-dkim-dmarc-microsoft-365-com-au\/","name":"How to Set Up SPF, DKIM and DMARC for .com.au in Microsoft 365 - Catharsis | Blog","isPartOf":{"@id":"https:\/\/catharsis.net.au\/blog\/#website"},"primaryImageOfPage":{"@id":"https:\/\/catharsis.net.au\/blog\/spf-dkim-dmarc-microsoft-365-com-au\/#primaryimage"},"image":{"@id":"https:\/\/catharsis.net.au\/blog\/spf-dkim-dmarc-microsoft-365-com-au\/#primaryimage"},"thumbnailUrl":"https:\/\/catharsis.net.au\/blog\/wp-content\/uploads\/2026\/11\/spf-dkim-dmarc-microsoft-365-com-au.jpg","datePublished":"2026-09-01T23:30:00+00:00","dateModified":"2026-11-03T10:47:18+00:00","description":"How to set up SPF, DKIM and DMARC for a .com.au domain in Microsoft 365: exact records, DNS host notes and a staged rollout to p=reject with checks.","breadcrumb":{"@id":"https:\/\/catharsis.net.au\/blog\/spf-dkim-dmarc-microsoft-365-com-au\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/catharsis.net.au\/blog\/spf-dkim-dmarc-microsoft-365-com-au\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/catharsis.net.au\/blog\/spf-dkim-dmarc-microsoft-365-com-au\/#primaryimage","url":"https:\/\/catharsis.net.au\/blog\/wp-content\/uploads\/2026\/11\/spf-dkim-dmarc-microsoft-365-com-au.jpg","contentUrl":"https:\/\/catharsis.net.au\/blog\/wp-content\/uploads\/2026\/11\/spf-dkim-dmarc-microsoft-365-com-au.jpg","width":1280,"height":720,"caption":"How to Set Up SPF, DKIM and DMARC for .com.au in Microsoft 365"},{"@type":"BreadcrumbList","@id":"https:\/\/catharsis.net.au\/blog\/spf-dkim-dmarc-microsoft-365-com-au\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/catharsis.net.au\/blog\/"},{"@type":"ListItem","position":2,"name":"How to Set Up SPF, DKIM and DMARC for .com.au in Microsoft 365"}]},{"@type":"WebSite","@id":"https:\/\/catharsis.net.au\/blog\/#website","url":"https:\/\/catharsis.net.au\/blog\/","name":"Catharsis | Blog","description":"","publisher":{"@id":"https:\/\/catharsis.net.au\/blog\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/catharsis.net.au\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Organization","@id":"https:\/\/catharsis.net.au\/blog\/#organization","name":"Catharsis | Blog","url":"https:\/\/catharsis.net.au\/blog\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/catharsis.net.au\/blog\/#\/schema\/logo\/image\/","url":"https:\/\/catharsis.net.au\/blog\/wp-content\/uploads\/2019\/01\/catharsis_au.jpg","contentUrl":"https:\/\/catharsis.net.au\/blog\/wp-content\/uploads\/2019\/01\/catharsis_au.jpg","width":215,"height":60,"caption":"Catharsis | Blog"},"image":{"@id":"https:\/\/catharsis.net.au\/blog\/#\/schema\/logo\/image\/"}},{"@type":"Person","@id":"https:\/\/catharsis.net.au\/blog\/#\/schema\/person\/f0826efb92a7aaf8d46b24715990e099","name":"Sajal Jain","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/catharsis.net.au\/blog\/wp-content\/uploads\/2019\/01\/Sajal-copy-96x96.jpg","url":"https:\/\/catharsis.net.au\/blog\/wp-content\/uploads\/2019\/01\/Sajal-copy-96x96.jpg","contentUrl":"https:\/\/catharsis.net.au\/blog\/wp-content\/uploads\/2019\/01\/Sajal-copy-96x96.jpg","caption":"Sajal Jain"},"description":"Cyber Security Enthusiast | OSCP | CEH | CHFI | CNSS | Master of IT Security &amp; Master of IT Management","url":"https:\/\/catharsis.net.au\/blog\/author\/sajal-jain\/"}]}},"_links":{"self":[{"href":"https:\/\/catharsis.net.au\/blog\/wp-json\/wp\/v2\/posts\/1268","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/catharsis.net.au\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/catharsis.net.au\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/catharsis.net.au\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/catharsis.net.au\/blog\/wp-json\/wp\/v2\/comments?post=1268"}],"version-history":[{"count":1,"href":"https:\/\/catharsis.net.au\/blog\/wp-json\/wp\/v2\/posts\/1268\/revisions"}],"predecessor-version":[{"id":1294,"href":"https:\/\/catharsis.net.au\/blog\/wp-json\/wp\/v2\/posts\/1268\/revisions\/1294"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/catharsis.net.au\/blog\/wp-json\/wp\/v2\/media\/1267"}],"wp:attachment":[{"href":"https:\/\/catharsis.net.au\/blog\/wp-json\/wp\/v2\/media?parent=1268"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/catharsis.net.au\/blog\/wp-json\/wp\/v2\/categories?post=1268"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/catharsis.net.au\/blog\/wp-json\/wp\/v2\/tags?post=1268"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}