SMTP

On this page

SMTP is responsible for handing off the email to the next hop until the receiving system takes over and delivers it; it is not an inbox synchronization protocol, nor does it prove that the recipient has read the message. Understanding it requires tracking two sets of state simultaneously: the commands and responses on the connection, and the emails in the server queue that have not yet completed delivery.

CheckpointCore Understanding
MAIL FROM vs. FromDelivery envelope and message content are different layers
Does RCPT TO success mean the email was received?Only that recipient is accepted; the body has not yet been submitted
Does 250 after DATA mean the user received it?The next hop assumes responsibility; subsequent failures are still possible
Does timeout retry automatically deduplicate?SMTP does not provide end-to-end exactly-once delivery
465 vs. 587Implicit TLS submission vs. common STARTTLS submission
SPF / DKIM / DMARCSource authorization, signature verification, and alignment with the From domain

The Path of an Email and Two Address Layers

Clients typically submit emails to an MSA (Mail Submission Agent), while the MTA (Mail Transfer Agent) handles queuing and relaying; on the receiving side, the email is handed over to the mailbox delivery system. The client/server distinction in SMTP refers to the current connection role: the same MTA acts as a server when receiving mail and as a client when sending to the next hop.

Each hop assumes delivery responsibility, but it must still enter the recipient's mailbox User Client Submit Email Sending Service Queue / Select Next Hop Receiving Service Receive / Filter / Deliver Submit SMTP Final DATA Response 250 This hop has accepted responsibility; the sender can remove the queue entry Processing may continue after receipt Forwarding, quarantine, or subsequent delivery failure 250 does not mean the recipient has read the email; if the response is lost, the sender's retry may produce duplicate emails.

The SMTP envelope is defined by MAIL FROM and one or more RCPT TO commands. The former is the return path, while the latter determines the actual recipients for this transaction; the message body section contains RFC 5322 headers and content, where From, To, and Cc are the message fields seen by the user. These may differ; for example, bounce addresses use a dedicated bounce subdomain, and BCC recipients exist in the envelope but do not appear in the public recipient list.

Return-Path is typically generated by the final delivery system based on the envelope return path and should not be altered by the sending client arbitrarily adding a header to change bounce routing. Message-ID is a message identifier, not a global idempotent transaction ID for SMTP. SMTP RFC 5321

The MTA selects candidate servers based on the MX records of the receiving domain, preferring those with smaller preference values. The absence of an MX record is different from explicitly stating that mail is not accepted: the former may fall back to address records according to rules, while MX 0 . (Null MX) explicitly indicates that mail is not accepted. Null MX RFC 7505

From EHLO to Accepting the Body

The following shows a transaction after TLS and necessary authentication have been completed. Each line actually ends with CRLF; S: and C: are reading markers and are not part of the online bytes.

DirectionCommand or ResponseMeaning
C→SEHLO sender.exampleRequest extension capabilities
S→C250-..., ending with 250 ...Multi-line response: hyphen continues, space ends
C→SMAIL FROM:<bounce@example.com>Start transaction, declare return path
S→C250 2.1.0 OKAccept sender path
C→SRCPT TO:<alice@example.net>Request delivery to Alice
S→C250 2.1.5 OKAccept this recipient
C→SRCPT TO:<missing@example.net>Request second recipient
S→C550 5.1.1 No such userAddress rejected; Alice is not affected
C→SDATARequest to send message content
S→C354 Start mail inputAllow body transmission; whole email not yet accepted
C→SMessage headers, blank line, body, termination lineTransmit actual content
S→C250 2.0.0 QueuedAccept delivery responsibility for this body and authorized recipients

Multi-recipient transactions may be partially successful; clients must retain results per recipient and stage, rather than saving a single "send failed" boolean. 4xx indicates a temporary failure, while 5xx indicates a permanent failure nature for that request; retry strategies should be combined with specific stages. For example, if credential configuration is rejected, the configuration should be corrected first, rather than infinitely replaying the email.

How DATA Avoids Premature End-of-Message Due to Dots in the Body

DATA uses a dot on a line by itself to terminate. If a line in the message originally starts with a dot, the sender adds an extra dot during transmission, and the receiver removes this transparent encoding character. The following model assumes the input already uses canonical CRLF and ends with CRLF; it is not a MIME generator for arbitrary emails.

message = (b'From: sender@example.com\r\n'
           b'To: alice@example.net\r\n'
           b'Subject: dot example\r\n\r\n'
           b'.first\r\n.\r\n..third\r\n')
lines = message[:-2].split(b'\r\n')
wire = b'\r\n'.join(b'.' + line if line.startswith(b'.') else line
                    for line in lines) + b'\r\n.\r\n'
assert b'\r\n..first\r\n..\r\n...third\r\n.\r\n' in wire
received = []
for line in wire.split(b'\r\n'):
    if line == b'.':
        break
    received.append(line[1:] if line.startswith(b'.') else line)
assert b'\r\n'.join(received) + b'\r\n' == message
assert len(wire) - len(message) == 6  # Three dot-stuff bytes + final dot line (3 bytes)
print('DATA dot-stuff, termination, and recovery passed')

Connections that have negotiated CHUNKING can use BDAT to transmit content in explicit byte counts, with the last block marked as LAST. This is not an alternative way to write the body within DATA, nor does it use DATA's dot-termination rules for delimiting. CHUNKING RFC 3030

Queues, 250, and the Failure Window for Duplicate Emails

When the sender finishes sending the body but does not receive a final response, there are two possibilities: the recipient has not yet accepted, or the recipient has queued the email but the 250 response was lost in transit. The sender cannot distinguish between these two cases based solely on a timeout; to avoid losing the email, the sender may retry, potentially creating duplicate emails. Keeping the same Message-ID helps correlate them, but not all receiving systems guarantee deduplication based on this alone.

If the receiving system cannot deliver after the final 250, it should typically generate a Non-Delivery Report (NDR) according to the protocol. Bounce messages use an empty reverse path MAIL FROM:<> to avoid failure notifications triggering infinite bounce loops. Receiving services should not promise success to any address first and then generate a large volume of bounces to potentially spoofed senders. SMTP Accept Responsibility and Failure Handling

When applications send verification codes or order notifications, the business side should still record notification events and delivery attempts. An SMTP success response is not part of a payment transaction; you cannot re-charge after a send timeout; email duplicates and business side-effect duplicates should be handled separately.

Why Might There Already Be One Email for the Same Timeout?

Both failures leave the sender in a TIMEOUT state, but the receiving queue has either zero or one email. After one retry each, this becomes one and two emails. This unobservable window explains why uncertainty must be retained between "successfully finished sending the body" and "confirmed that the recipient has accepted."

Preparing the visual
Why Might There Already Be One Email for the Same Timeout?

The timeout seen by the sender is the same, but the receiving queue may differ. Retries can remedy unaccepted cases but may also duplicate already accepted emails; Message-ID does not provide universal deduplication.

Submission, Server Relaying, and TLS

PortCommon RoleTLS Behavior
TCP 25MTA to MTA relaySTARTTLS negotiable; requirements depend on receiving domain policy
TCP 587Mail submissionSTARTTLS commonly used; clients should require a secure connection before authentication
TCP 465submissionsTLS first after connection, then SMTP begins

Port 465 has been registered by RFC 8314 as an implicit TLS service for mail submission and should no longer be labeled as "non-standard SMTPS." The authentication model for submission differs from that of public MTA relays; it is not required that every sending MTA on the internet hold an account on the receiving server. RFC 8314, Submission RFC 6409

STARTTLS returns ready, after which the TLS handshake occurs. Both parties clear the SMTP state learned previously, and the client re-sends EHLO to obtain current capabilities; you cannot continue to cache AUTH capabilities from the plaintext phase as trusted. AUTH PLAIN's Base64 is merely encoding, not password encryption; whether authentication is allowed and which SASL mechanism is used is determined by server capabilities and policies. STARTTLS RFC 3207

Opportunistic TLS may suffer from downgrade attacks. MTA-STS publishes and caches TLS/MX policies for the receiving domain via HTTPS; DANE for SMTP relies on DNSSEC-authenticated TLSA records. These are used to constrain relay links and cannot replace message signing, nor do they make all transit servers unable to read the email. MTA-STS RFC 8461, SMTP DANE RFC 7672

SPF: Is the Source IP Authorized to Use This Envelope Domain?

SPF evaluates authorization based on the source IP of the current SMTP connection and the envelope domain; in scenarios with an empty return path, HELO identity rules are used. It does not directly verify the visible From header, nor does it prove that the body has not been modified.

For example, v=spf1 ip4:192.0.2.0/24 -all indicates that this educational domain authorizes the specified IPv4 range, and any other match results in -all (fail). Results also include softfail, neutral, none, temperror, and permerror; DNS temporary failures and explicit non-authorization cannot be conflated into a single failure. SPF RFC 7208

include is not text pasting. In recursive evaluation, terms such as include, a, mx, ptr, exists, and redirect are constrained by the total limit of DNS query terms; exceeding 10 returns permerror. This is not just counting the outermost includes, nor is it simply counting all DNS packets in a packet capture. When maintaining multiple sending vendors, you must track the actual expansion path and record failures, rather than endlessly appending includes.

Forwarding changes the connection source, so the original envelope domain's SPF may fail; this does not mean the original email was necessarily forged, and it explains why content signing and alignment checks are also needed.

DKIM: The Signature Domain Endorses Selected Message Content

The d= in DKIM-Signature is the signing domain, and s= is the selector; the verifier looks up the public key at s._domainkey.d. h= specifies the headers involved in the signature, bh= is the digest of the canonicalized body, and b= is the signature value. The signature input also involves the DKIM-Signature header processed according to rules; it cannot be simplified to "randomly concatenating header hash and body hash for RSA." The From header must be included in the signature. DKIM RFC 6376

Canonicalization rules determine which whitespace adjustments are tolerated; mailing list footers, Subject modifications, or body conversions may still break the signature. DKIM pass proves that the holder of the domain's key signed the verified content, but it does not prove that the signing domain is the visible From domain, nor does it prove the good faith of the email content.

When forwarding only changes the external envelope while preserving the signed content, DKIM has a chance to pass. Multiple signatures can coexist; a pass for an unrelated domain's signature does not directly imply DMARC pass.

DMARC: Binding Authentication Results to the Visible From Domain

DMARC: Authentication passed, but must also align with the visible From domain From: billing@example.com SPF: Verify current connection source MAIL FROM domain passes + aligns with From DKIM: Verify signature and body digest At least one passing signature's d= aligns with From If either path holds → DMARC pass SPF pass or DKIM pass alone, without corresponding alignment, is not sufficient for DMARC.

The condition for DMARC pass is that at least one path simultaneously satisfies "authentication pass" and "alignment with the Author Domain." Strict alignment requires the domain names to be identical; relaxed alignment is determined based on organizational domain relationships, cannot use string suffix matching, and cannot simply take the last two domain segments. DMARC RFC 9989

As of September 2026, RFC 9989 replaced RFC 7489/9091 in May 2026: organizational domain and policy discovery uses DNS Tree Walk, the old pct tag was removed and a testing t tag introduced, and aggregate and failure reports were split into RFC 9990/9991. Actual receiving implementations may still be in a migration period, so do not assume that old PSL determinations and new tree traversals yield identical results for all domain structures. New Changes

The following is a strict alignment local teaching model that does not implement DNS policy discovery; it helps clarify that "pass" and "aligned" must belong to the same verification path.

def dmarc_strict(author, spf_result, mail_from_domain, dkim_results):
    spf_ok = spf_result == 'pass' and mail_from_domain.lower() == author.lower()
    dkim_ok = any(result == 'pass' and domain.lower() == author.lower()
                  for result, domain in dkim_results)
    return spf_ok or dkim_ok

assert dmarc_strict('example.com', 'fail', 'example.com', [('pass', 'example.com')])
assert not dmarc_strict('example.com', 'pass', 'vendor.example', [('fail', 'example.com')])
assert not dmarc_strict('example.com', 'pass', 'bounce.example.com', [])
assert dmarc_strict('example.com', 'pass', 'example.com', [])
print('Strict alignment and SPF fail but DKIM pass branch passed')

Publishing v=DMARC1; p=reject; rua=mailto:dmarc@example.com expresses the domain owner's handling preferences and reporting requests; receivers still make judgments based on local policies. p=none does not require isolation or rejection of DMARC failures, nor does it guarantee the email enters the inbox; p=reject does not cause all receivers to unconditionally reject all SPF fail emails, because an aligned DKIM pass can still result in DMARC pass.

ARC can record the chain of authentication results as the email passes through intermediate systems, helping receivers evaluate forwarding scenarios; it does not magically repair failed DKIM signatures, nor does it require receivers to trust arbitrary ARC signers. ARC RFC 8617

Why Might SPF Pass and DKIM Align, Yet Fail Combined?

The initial setup intentionally splits the two valid conditions into different paths, resulting in an overall failure. Completing just one line allows it to pass; the other line does not need to pass simultaneously. What we get here are authentication results; how the receiver ultimately handles the email remains up to their own policy.

Preparing the visual
Why Might SPF Pass and DKIM Align, Yet Fail Combined?

pass and aligned must appear on the same path. You cannot combine SPF pass with failed DKIM alignment to create success.

MIME, Internationalization, and Final Diagnosis

SMTP transports the message, while MIME describes the body type, segmentation, and transfer encoding. Base64 attachments typically increase data volume by about one-third, plus line folding and message headers; 8BITMIME does not mean any internationalized address is valid; non-ASCII email addresses require corresponding support such as SMTPUTF8. MIME RFC 2045, SMTPUTF8 RFC 6531

When troubleshooting, first obtain the accurate stage, response code, and queue ID: if there is no connection response, check the network; if AUTH fails, check submission credentials; if RCPT is rejected, check the address and policy; if queuing succeeds after DATA, check subsequent queues and receiving-side processing. Authentication result headers must also distinguish records generated by trusted receiving boundaries from text inserted by the sender; do not treat a single spf=pass line as a verification conclusion.