# 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.

URL: https://www.matthewswong.com/en/learn/devops-from-zero/how-smtp-works-spf-dkim-dmarc/

DevOps from zero · Lesson 18 of 25 · Reviewed on 18 October 2026 · By Matthews Wong (https://www.matthewswong.com/en/about/)

> 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.

## 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.

1. **Meet the parts.** 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.
2. **Hand it to a provider.** 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/emails
   ```

   *Predict:* 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)
3. **Look up the MX.** 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.
   ```
4. **Relay on port 25.** 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 OK
   ```

   *Predict:* 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)
5. **Check SPF and DKIM.** 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...
   ```
6. **DMARC asks for alignment.** 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 domain
   ```
7. **Deliver with a verdict.** The 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.

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.

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.

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.

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.

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.

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.

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

`app/api/contact/route.ts:105-108`

```typescript
  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.

`app/api/contact/route.ts:138-146`

```typescript
  const payload = {
    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.

## What breaks

- **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.

### The three SMTP ports, as RFC 5321, RFC 6409 and RFC 8314 define them.

| 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 |

## Try it yourself

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.

1. Find where gmail.com accepts mail. Each line is a preference and a host; the lowest number is tried first.

   ```bash
   dig @one.one.one.one MX gmail.com +short
   ```
2. Read the SPF record on this site's bounce domain, the send subdomain Resend uses as the Return-Path.

   ```bash
   dig @one.one.one.one TXT send.noreply.matthewswong.com +short
   ```
3. Fetch the DKIM public key for selector resend. The p= value is the key a receiver verifies the signature with.

   ```bash
   dig @one.one.one.one TXT resend._domainkey.noreply.matthewswong.com +short
   ```
4. Compare two large mailbox providers' DMARC records. Read p=, sp= and rua=, and notice which one still carries the retired pct tag.

   ```bash
   dig @one.one.one.one TXT _dmarc.yahoo.com +short
   dig @one.one.one.one TXT _dmarc.gmail.com +short
   ```
5. Open 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.

   ```bash
   openssl s_client -starttls smtp -connect smtp.gmail.com:587 -crlf -quiet
   ```
6. Do the same on 465. TLS starts immediately there, so there is no -starttls flag, and the 220 greeting arrives over TLS.

   ```bash
   openssl s_client -connect smtp.gmail.com:465 -crlf -quiet
   ```
7. Open 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.

## Quick quiz

1. **Which address does SPF check when DMARC uses its result?**
   A. The address in the From header
   B. The envelope sender given in MAIL FROM
   C. The address in the Reply-To header
   Answer: B. SPF authorizes hosts for the MAIL FROM and HELO identities, and DMARC relies only on the MAIL FROM result. The From header is what DMARC then compares it with.

2. **A contact form sends with From set to the visitor's yahoo.com address, through your own provider. What is the likely result?**
   A. DMARC fails, because neither your SPF nor your DKIM domain aligns with yahoo.com, and yahoo.com asks for reject
   B. It is delivered, because SPF passes for your provider's servers
   C. It is delivered, because the receiver rewrites From to your domain
   Answer: A. An SPF pass for your envelope domain proves nothing about yahoo.com. DMARC needs a pass for a domain aligned with From, and yahoo.com published p=reject on 2026-10-18.

3. **Your app on a cloud VM cannot connect to any recipient's MX host on port 25, while HTTPS calls work. What is the most likely cause?**
   A. The recipients' MX hosts are down
   B. Your DKIM key is missing from DNS
   C. The cloud host blocks outbound port 25
   Answer: C. Compute Engine blocks destination port 25 outside the VPC, and EC2 blocks it to public addresses by default. A missing DKIM key fails a check after connecting, not the connection itself.

## Think it through

- **What is the difference between the envelope sender and the From header?** 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.
- **A DKIM-Signature carries d=example.com and s=mail. Where does the receiver look for the public key?** In a TXT record at mail._domainkey.example.com: the selector, then _domainkey, then the signing domain.
- **Why does the contact route put the visitor's address in replyTo instead of from?** 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.
- **When does a message pass DMARC?** 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.

## Go deeper

- [ERP Email Automation: Triggering Transactional Emails from Business Events](https://www.matthewswong.com/en/blog/erp-email-automation/)
- [How to Design a Scalable Notification System (Push, Email, SMS)](https://www.matthewswong.com/en/blog/design-notification-system-architecture/)
- [HTTPS and TLS Beyond Certbot: Hardening Your SSL Configuration](https://www.matthewswong.com/en/blog/https-tls-beyond-certbot/)

## Sources

- [RFC 5321: Simple Mail Transfer Protocol](https://www.rfc-editor.org/rfc/rfc5321)
- [RFC 5322: Internet Message Format (From, Reply-To)](https://www.rfc-editor.org/rfc/rfc5322)
- [RFC 6409: Message Submission for Mail (port 587)](https://www.rfc-editor.org/rfc/rfc6409)
- [RFC 8314: Use of TLS for Email Submission and Access (port 465)](https://www.rfc-editor.org/rfc/rfc8314)
- [RFC 3207: SMTP Service Extension for Secure SMTP over TLS](https://www.rfc-editor.org/rfc/rfc3207)
- [RFC 7208: Sender Policy Framework (SPF)](https://www.rfc-editor.org/rfc/rfc7208)
- [RFC 6376: DomainKeys Identified Mail (DKIM) Signatures](https://www.rfc-editor.org/rfc/rfc6376)
- [RFC 9989: DMARC (obsoletes RFC 7489)](https://www.rfc-editor.org/rfc/rfc9989)
- [RFC 8601: the Authentication-Results header field](https://www.rfc-editor.org/rfc/rfc8601)
- [Google: Email sender guidelines](https://support.google.com/a/answer/81126)
- [Google Cloud: Sending email from an instance (port 25)](https://docs.cloud.google.com/compute/docs/tutorials/sending-mail)
- [AWS: Amazon EC2 restriction on email sent using port 25](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-resource-limits.html)
- [Resend docs: Custom Return Path (the send subdomain)](https://resend.com/docs/dashboard/domains/custom-return-path)
- [Resend docs: Add a domain (send from a subdomain)](https://resend.com/docs/add-a-domain)
- [Resend docs: Send email (POST /emails, reply_to)](https://resend.com/docs/api-reference/emails/send-email)
- [Resend docs: the MX, SPF and DKIM records to add](https://resend.com/docs/knowledge-base/cloudflare)
- [Resend docs: 403 error from a domain mismatch](https://resend.com/docs/knowledge-base/403-error-domain-mismatch)
- [Google Workspace: Gmail SMTP server on 465 and 587](https://knowledge.workspace.google.com/admin/gmail/send-email-from-a-printer-scanner-or-app)
- [Gmail Help: Trace an email with its full header](https://support.google.com/mail/answer/29436)

---

- HTML version: https://www.matthewswong.com/en/learn/devops-from-zero/how-smtp-works-spf-dkim-dmarc/
- Bahasa Indonesia: https://www.matthewswong.com/id/learn/devops-from-zero/how-smtp-works-spf-dkim-dmarc/
- Site index for agents: https://www.matthewswong.com/llms.txt
- Sitemap: https://www.matthewswong.com/sitemap.xml
- API description (OpenAPI): https://www.matthewswong.com/openapi.json
