How SMTP Works: Ports, MX Lookup, SPF, DKIM and DMARC
How SMTP works, step by step: the EHLO to DATA dialogue, ports 25, 587 and 465, the MX lookup, and how SPF, DKIM and DMARC decide if your email lands.
By Matthews WongReviewed on
Key takeaway
SMTP, the Simple Mail Transfer Protocol, is how mail servers hand a message to each other: EHLO, MAIL FROM with the envelope sender, RCPT TO, then DATA. Apps submit mail on port 587 or 465; servers relay it on port 25 to the recipient's MX. The receiver then checks SPF on the envelope domain, the DKIM signature, and DMARC alignment with the From header.
You will be able to
- Trace one message through submission, the MX lookup and the relay on port 25, and read the EHLO to DATA conversation.
- Say which domain SPF, DKIM and DMARC each check, and read the verdicts in an Authentication-Results header.
- Explain why an app sends from its own domain and puts a visitor's address in Reply-To instead of From.
Step through the diagram
Press Play, or step with the arrows. Every step is written out below the diagram.
One email from the contact form to the inbox
The app hands the message to a provider, which finds the recipient's MX in DNS and relays it on port 25; the receiving server checks SPF on the envelope, the DKIM signature and DMARC alignment with From before it delivers.
Keys: ← → step · Home / End · K play or pause
One contact-form message, start to finish. The app hands it to an email provider, DNS points the way, and the receiving server runs three checks before the inbox.
The app never opens port 25 itself. It hands the message to a provider: this site calls an HTTPS API; others submit over SMTP on 587 or 465.
POST https://api.resend.com/emailsPredict the next step The provider now holds a message for an address at gmail.com. How does it find the server that accepts it? (It asks DNS for the MX records of gmail.com · It connects to the website at gmail.com · The app tells it which server to use)The provider asks DNS for the recipient domain's MX records and tries the lowest preference number first. A domain with no MX records at all serves as its own implicit MX.
dig @one.one.one.one MX gmail.com +short 5 gmail-smtp-in.l.google.com. 10 alt1.gmail-smtp-in.l.google.com.The provider's server opens SMTP to the MX on port 25: EHLO, MAIL FROM with the envelope sender, RCPT TO, then DATA with the From header and body.
EHLO MAIL FROM the envelope sender RCPT TO the recipient DATA headers, body, a lone . 250 OKPredict the next step The envelope sender is on send.noreply.matthewswong.com, the From header on noreply.matthewswong.com. Which domain does SPF check? (The domain in the From header · The envelope sender's domain, from MAIL FROM)SPF asks DNS whether this server may send for the MAIL FROM domain. DKIM fetches the public key at selector._domainkey under the signing domain and checks the signature.
send.noreply.matthewswong.com v=spf1 include:amazonses.com ~all resend._domainkey.noreply.matthewswong.com p=MIGfMA0G...DMARC reads _dmarc under the From domain. It passes only if SPF or DKIM passed for a domain aligned with From; otherwise the p= tag asks for none, quarantine or reject.
From domain noreply.matthewswong.com MAIL FROM domain send.noreply.matthewswong.com relaxed: same organizational domain strict: identical domainThe server records each verdict in an Authentication-Results header and delivers. A DMARC failure under p=reject could instead get a 5xx reply to DATA, which the sender must report.
Authentication-Results: mx.example.com; spf=pass smtp.mailfrom=example.com; dkim=pass header.d=example.com; dmarc=pass header.from=example.com
What is SMTP, and which port does what?
SMTP, the Simple Mail Transfer Protocol, moves a mail object from one server to the next. RFC 5321 splits that object in two. The envelope holds an originator address, which is where error reports are directed, and one or more recipient addresses. The content is the header section and the body, the part a person reads.
Read more (1)Show less
Three ports matter. Port 25 is where servers relay to each other. RFC 6409 keeps relay there and reserves port 587 for submission, where an app or mail client hands over a new message, and a submission server must by default refuse MAIL until the session has authenticated. Port 465 is submission over implicit TLS: the handshake starts the moment the connection opens. RFC 8314 prefers it to STARTTLS on 587, and asks for both during the transition.
What does an SMTP conversation look like?
A session is a short dialogue of commands and three-digit replies. The server greets with 220. The client sends EHLO with its own name, then MAIL FROM:<address> with the envelope sender, one RCPT TO:<address> per recipient, then DATA. After a 354 reply it sends the header section and body, and ends them with a line holding a single period. 250 means accepted, and QUIT closes the session with 221.
Read more (2)Show less
The first digit is what software acts on. 2yz is success and 3yz asks for more input. 4yz is a temporary failure, so the same request may be tried again later; 5yz is permanent, and the client should not repeat it.
On port 587 the session starts in cleartext, the server lists STARTTLS among its extensions, and the client upgrades. After the TLS handshake SMTP resets: the client sends EHLO again, and the list of extensions may differ. Gmail's submission server shows it. On 2026-10-18 it listed STARTTLS but no AUTH before the upgrade, and AUTH only after it.
Envelope sender vs From header: which is which?
MAIL FROM sets the envelope sender, also called the reverse-path. Error reports go there, and the server that makes final delivery copies it into a Return-Path line at the top of the message. The From header lives in the content: RFC 5322 defines it as the author of the message, and Reply-To as the address the author suggests replies go to.
Read more (1)Show less
The two often differ on purpose. Resend uses a send subdomain of your domain as the Return-Path by default, while From stays the address you chose. For this site that is send.noreply.matthewswong.com, whose MX record points at an amazonses.com feedback host, so bounces go back to the provider.
How does mail find the recipient's server, and why not send it yourself?
To deliver to an address, a server looks up the MX records of the domain after the @. Each MX names a host and a preference, and lower numbers are tried first. If a domain has no MX records at all, the domain itself is used as an implicit MX with preference 0. On 2026-10-18, gmail.com listed five MX hosts, with preferences from 5 to 40.
Read more (1)Show less
Delivering straight from an app server means outbound port 25, and many cloud hosts block it. Google Compute Engine blocks connections to destination port 25 outside your VPC, citing the risk of abuse, but leaves 587 and 465 open. Amazon EC2 blocks port 25 to public addresses by default and lifts the restriction on request. So apps hand mail to a provider, over SMTP submission or an HTTPS API, and the provider's servers do the relay on port 25.
What does SPF check?
SPF lets a domain say which hosts may use it in the MAIL FROM or HELO identity. The record is a DNS TXT record starting with v=spf1, and the receiver tests the connecting server against it during the SMTP session. DMARC uses only the MAIL FROM result, so for DMARC purposes SPF is about the envelope, not the From header.
Read more (2)Show less
The terms read left to right. include:amazonses.com runs the same check against amazonses.com's record and counts a pass there as a match here. The qualifier on the final all covers every other host: -all is a fail, and ~all is a softfail, a weak statement that the host is probably not authorized. send.noreply.matthewswong.com publishes v=spf1 include:amazonses.com ~all, the value Resend's DNS guide asks for on the send subdomain.
Two limits catch people out. A domain must have exactly one v=spf1 record, because two of them make the result permerror. And include, a, mx, ptr, exists and redirect each cost a DNS lookup, with a limit of 10 per evaluation; past that, the result is permerror too.
How does DKIM sign a message?
DKIM lets a domain take responsibility for a message with a cryptographic signature in a DKIM-Signature header. Its d= tag names the signing domain and s= the selector, and the receiver fetches the public key from a TXT record at the selector, then _domainkey, then the domain. The From header must be one of the signed fields, and an empty p= in the key record means the key has been revoked.
Read more (1)Show less
Selectors let one domain hold several keys at once. The key Resend uses for this site's sending domain is published at resend._domainkey.noreply.matthewswong.com: a TXT record holding p= and the base64 public key. Relays typically make no substantive change to the content, so the signature usually survives the trip, including a hop through a forwarder that SPF does not know about.
What does DMARC add on top of SPF and DKIM?
SPF and DKIM each vouch for a domain, but not necessarily the domain the reader sees. DMARC closes that gap: a message passes only when SPF or DKIM passed for a domain aligned with the domain in the From header. Relaxed alignment, the default, means both share an Organizational Domain; strict means they are identical. One aligned pass is enough.
Read more (2)Show less
The policy is a TXT record at _dmarc under the From domain, starting v=DMARC1. p=none expresses no preference, quarantine calls failing mail suspicious, and reject says a failure is not a valid use of the domain; sp= covers subdomains. On 2026-10-18 yahoo.com published p=reject, and gmail.com published p=none with sp=quarantine. The receiver still decides: final handling is its local policy.
DMARC is now RFC 9989, published in May 2026, which obsoletes RFC 7489. It retired the pct tag, now listed as historic, so a record that still carries pct=100 is using a deprecated tag. It also finds the Organizational Domain with a DNS tree walk instead of a public suffix list. Google's sender guidelines ask every sender for SPF or DKIM, and bulk senders for both plus DMARC, with From aligned.
The real config behind this lesson
Excerpts from the repository that builds and deploys this website, quoted as they are.
const fromEmail =
process.env.CONTACT_FROM_EMAIL ||
"Matthews Wong <noreply@noreply.matthewswong.com>";
const toEmail = process.env.CONTACT_TO_EMAIL || "matthewswong2610@gmail.com";The sender defaults to an address on noreply.matthewswong.com, the sending subdomain whose SPF and DKIM records the try-it reads, and an environment variable can override either address. Resend recommends sending from a subdomain rather than the root domain. The recipient is the owner's own inbox, so the provider delivers to that domain's MX.
const { error } = await getResend(apiKey).emails.send({
from: fromEmail,
to: toEmail,
replyTo: email,
template: {
id: isHiring ? TEMPLATE_HIRING : TEMPLATE_PROJECT,
variables,
},
});from stays on the site's own sending domain, and the visitor's address goes into replyTo, which the SDK sends as reply_to and becomes the Reply-To header. Pressing Reply answers the visitor. With the visitor in from, their domain would be the one DMARC checks, nothing this site signs could align with it, and a domain publishing p=reject asks receivers to refuse the message.
Mistakes that cost real time
A visitor's address in From fails DMARC at their domain
A form that sends From the visitor makes their domain the one DMARC checks. Your SPF and DKIM domains cannot align with it, so a domain like yahoo.com, which published p=reject on 2026-10-18, asks receivers to refuse the message. Google's guidelines also say not to impersonate Gmail From headers, and Resend refuses a from address on a domain you have not verified. Send From your own domain and put the visitor in Reply-To.
Port 25 is blocked from a cloud server
An app that delivers straight to recipients' MX hosts needs outbound port 25, and on many cloud VMs the connection never gets through. Compute Engine blocks destination port 25 outside the VPC, and EC2 blocks it to public addresses until you ask for the restriction to be lifted. Hand mail to a provider over 587, 465 or its HTTPS API instead.
Two SPF records, or more than 10 lookups
Adding a second provider by publishing a second v=spf1 record breaks SPF for both: more than one record makes the result permerror. Merge them into one record with two include terms, and count the terms that cost a DNS lookup, because each include pulls in the other domain's own lookups and more than 10 is also permerror.
Forwarded mail fails SPF
When a mailbox forwards your message, the forwarder's server connects to the next MX, and your SPF record does not list it. Unless the forwarder rewrites MAIL FROM into its own domain, SPF cannot pass. A DKIM signature over unchanged content still verifies, which is why signing with DKIM matters even when SPF is set up.
Login refused on 587 before STARTTLS
A client that skips STARTTLS on 587 may never see AUTH at all: Gmail's submission server offered AUTH only after the upgrade when checked on 2026-10-18, and RFC 3207 allows the extension list to change after TLS. Turn on STARTTLS for 587, or use 465, where TLS starts before the first command.
| Port | Used for | TLS | Note |
|---|---|---|---|
| 25 | Relay between servers, to the recipient's MX | STARTTLS when offered; a public MX must not require it | Often blocked outbound on cloud VMs |
| 587 | Submission from an app or mail client | STARTTLS, then EHLO again | Refuses MAIL until AUTH, by default |
| 465 | Submission from an app or mail client | Implicit TLS from the first byte | Preferred over STARTTLS by RFC 8314 |
Do it on your own machine
Everything here runs on a laptop with dig and openssl, and nothing sends mail: the SMTP steps stop at EHLO and you never type a password. Some networks block 587 and 465; if a connection hangs, try another network.
Find where gmail.com accepts mail. Each line is a preference and a host; the lowest number is tried first.
dig @one.one.one.one MX gmail.com +shortRead the SPF record on this site's bounce domain, the send subdomain Resend uses as the Return-Path.
dig @one.one.one.one TXT send.noreply.matthewswong.com +shortFetch the DKIM public key for selector resend. The p= value is the key a receiver verifies the signature with.
dig @one.one.one.one TXT resend._domainkey.noreply.matthewswong.com +shortCompare two large mailbox providers' DMARC records. Read p=, sp= and rua=, and notice which one still carries the retired pct tag.
dig @one.one.one.one TXT _dmarc.yahoo.com +short dig @one.one.one.one TXT _dmarc.gmail.com +shortOpen a submission session with STARTTLS. When it goes quiet, type EHLO laptop.example and look for AUTH in the reply, then type QUIT. Do not log in.
openssl s_client -starttls smtp -connect smtp.gmail.com:587 -crlf -quietDo the same on 465. TLS starts immediately there, so there is no -starttls flag, and the 220 greeting arrives over TLS.
openssl s_client -connect smtp.gmail.com:465 -crlf -quietOpen the full header of a message you received (in Gmail: More, then Show original) and find the Authentication-Results lines: spf=, dkim= and dmarc=, each with the domain it checked.
Three questions before you move on
Pick an answer to see why it is right or wrong. Nothing is graded, so guess freely; a miss is kept in this browser so the course can ask it again later.
Two questions from earlier lessons
Recalling an older lesson after a gap is what makes it stick. Answer from memory before you look anything up.
Answer in your head, then check
1.What is the difference between the envelope sender and the From header?Show the answer
Answer:The envelope sender is the MAIL FROM address in the SMTP session; error reports go there, and it is saved as Return-Path on delivery. The From header is part of the content and names the author. SPF checks the first, and DMARC protects the second.
2.A DKIM-Signature carries d=example.com and s=mail. Where does the receiver look for the public key?Show the answer
Answer:In a TXT record at mail._domainkey.example.com: the selector, then _domainkey, then the signing domain.
3.Why does the contact route put the visitor's address in replyTo instead of from?Show the answer
Answer:From has to be on a domain the site sends and signs for, or DMARC alignment fails at the visitor's domain, and the provider refuses an unverified from domain anyway. Reply-To still sends the owner's reply straight to the visitor.
4.When does a message pass DMARC?Show the answer
Answer:When SPF or DKIM passed for a domain that aligns with the From domain: the same Organizational Domain under relaxed alignment, the identical domain under strict. One aligned pass is enough.
Related deep dives on this site
Sources
Primary sources checked on the research date. Follow them for the current version.
- RFC 5321: Simple Mail Transfer Protocol
- RFC 5322: Internet Message Format (From, Reply-To)
- RFC 6409: Message Submission for Mail (port 587)
- RFC 8314: Use of TLS for Email Submission and Access (port 465)
- RFC 3207: SMTP Service Extension for Secure SMTP over TLS
- RFC 7208: Sender Policy Framework (SPF)
- RFC 6376: DomainKeys Identified Mail (DKIM) Signatures
- RFC 9989: DMARC (obsoletes RFC 7489)
- RFC 8601: the Authentication-Results header field
- Google: Email sender guidelines
- Google Cloud: Sending email from an instance (port 25)
- AWS: Amazon EC2 restriction on email sent using port 25
- Resend docs: Custom Return Path (the send subdomain)
- Resend docs: Add a domain (send from a subdomain)
- Resend docs: Send email (POST /emails, reply_to)
- Resend docs: the MX, SPF and DKIM records to add
- Resend docs: 403 error from a domain mismatch
- Google Workspace: Gmail SMTP server on 465 and 587
- Gmail Help: Trace an email with its full header