This is a work in progress. Sending email reliably isn’t an easy thing to do. These are some of my personal notes of things that have worked for me as well as some reference articles from other people who have also spent a lot of time working on this problem.

Sample dkim header

DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; s=0; d=mail.greatclips.com; h=Message-ID:List-Unsubscribe:MIME-Version:From:To:Date:Subject:Content-Type; i=greatclips@mail.greatclips.com; bh=ygnW58q8p3EZxckvuXDdtK0cs26o6UfWAMzbc4CrfNo=; b=CgNvr+0AqaCGICrXG9kkS24/yyCLGiw1JopyxHKpGucuQSlOcWXoRfkF6e58YD60A7wh9OvjRnax vLQSq5B18D8ajwVrKtEU83dUp7H8BUkUjL3Y3Ae0LmsOkDphc1fEaFPnayfIauE5NYNNICJZfn74 Km+XB06HaADLkD2ETfg=
  • d: Domain: The email domain you’re sending email on behalf of (eg mail.greatclips.com)
  • s: Selector: A public key can be found at $s._domainkey.domain
  • The public key is stored in a TXT type dns record
  • Fetch the record with dig 0._domainkey.mail.greatclips.com txt in this example

Email auth headers

auth headers example

  • dkim=pass DKIM signature check passed on the receiving esp side
  • spf=pass SPF check passed on the receiving esp side
  • dmarc=pass (p=NONE sp=NONE dis=NONE)
    • policy=NONE Can be either NONE, QUARANTINE or REJECT. Usually when you turn on dmarc you start in policy mode NONE so you can start collecting reports on how well you are aligned with you message from header, return-path (for spf) and dkim signature header
    • sp=NONE tells the receiving esp whether to apply dmarc policy to subdomains
    • dis=NONE Gmail specific extension (Are others doing it?) saying policy=. was ignored (Happens when mail is forwarded?) source
  • DNS
    • Sending email
      • Reverse (PTR “pointer” records): A ptr record must be setup mapping an ip address to the email sending domain (hence the “reverse” dns part since dns usually takes a domain and returns 1 or more ip addresses). For AWS and Azure I can set this up myself but less progressive hosting providers may need a phone call and a special request
        • -> mail.leskowsky.net (PTR record)
      • Forward: The email sending domain should have a dns “A” record setup pointing at the vm running postfix (or other mail transport agent (mta))
        • mail.leskowsky.net -> (A record)
      • SPF
      • DKIM
      • DMARC
    • Receiving email
      • MX
  • Reputation
    • IP
      • Warming
    • Domain
      • Domain splitting: It’s a good idea to isolate different kinds of email from each other through the use of subdomains. (eg Transactional email vs bulk or marketing email is an easy example to think about)
        • no-reply@mail.company.com (or no-reply@company.com) vs no-reply@marketing.company.com
        • Reputation of email sent at the root domain affects email sent from subdomains. The reverse is not true.
        • Email sent from 1 subdomain doesn’t impact the rep of email sent from another subdomain
        • Domains and subdomains must be warmed for sending email (Google and Microsoft offer tools to see a domain’s rep from their point of view)
          • Every email service provider will have their own internal way of measuring email domain rep involving multiple factors
          • IPs have rep too but it is thought that since services exist that provide shared pools of ip addresses for use by multiple senders this signal is not as good as it was for ESPs
        • If you split too much you may get to a place where you’re not sending enough email in a channel for it to build the rep it needs for email to deliver
  • Postfix
  • Transactional vs bulk (unsolicited) email
  • Feedback loops: Register with email service providers so that you can receive notifications whenever a recipient marks one of your emails as spam. Add these mailboxes to a do-not-email list to protect your email domain / ip rep. The ESPs you’re probably particularly worried about not offending are gmail, outlook / hotmail and yahoo

Gmail

Microsoft, outlook, hotmail

Links

From the trenches

Message authentication

SPF

DKIM

DMARC

Tools

Using email subdomains

Sample DMARC records

[cleskowsky@ip-10-0-10-192.ec2.internal opendkim]$ dig _dmarc.amazon.com txt

; <<>> DiG 9.11.4-P2-RedHat-9.11.4-26.P2.el7_9.15 <<>> _dmarc.amazon.com txt
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 45656
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;_dmarc.amazon.com.		IN	TXT

;; ANSWER SECTION:
_dmarc.amazon.com.	273	IN	TXT	"v=DMARC1;" "p=quarantine;" "pct=100;" "rua=mailto:report@dmarc.amazon.com;" "ruf=mailto:report@dmarc.amazon.com"

;; Query time: 1 msec
;; SERVER: 10.0.0.2#53(10.0.0.2)
;; WHEN: Fri Nov 17 18:00:18 EST 2023
;; MSG SIZE  rcvd: 162
[cleskowsky@ip-10-0-10-192.ec2.internal opendkim]$ dig _dmarc.google.com txt

; <<>> DiG 9.11.4-P2-RedHat-9.11.4-26.P2.el7_9.15 <<>> _dmarc.google.com txt
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 14921
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;_dmarc.google.com.		IN	TXT

;; ANSWER SECTION:
_dmarc.google.com.	300	IN	TXT	"v=DMARC1; p=reject; rua=mailto:mailauth-reports@google.com"

;; Query time: 6 msec
;; SERVER: 10.0.0.2#53(10.0.0.2)
;; WHEN: Fri Nov 17 18:00:31 EST 2023
;; MSG SIZE  rcvd: 117

Generate dkim signing key

opendkim-genkey -d leskowsky.net -s k1