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.
Checkpoint
Core Understanding
MAIL FROM vs. From
Delivery 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. 587
Implicit TLS submission vs. common STARTTLS submission
SPF / DKIM / DMARC
Source 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.
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.
Direction
Command or Response
Meaning
C→S
EHLO sender.example
Request extension capabilities
S→C
250-..., ending with 250 ...
Multi-line response: hyphen continues, space ends
C→S
MAIL FROM:<bounce@example.com>
Start transaction, declare return path
S→C
250 2.1.0 OK
Accept sender path
C→S
RCPT TO:<alice@example.net>
Request delivery to Alice
S→C
250 2.1.5 OK
Accept this recipient
C→S
RCPT TO:<missing@example.net>
Request second recipient
S→C
550 5.1.1 No such user
Address rejected; Alice is not affected
C→S
DATA
Request to send message content
S→C
354 Start mail input
Allow body transmission; whole email not yet accepted
C→S
Message headers, blank line, body, termination line
Transmit actual content
S→C
250 2.0.0 Queued
Accept 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'.'+lineifline.startswith(b'.')elselineforlineinlines)+b'\r\n.\r\n'assertb'\r\n..first\r\n..\r\n...third\r\n.\r\n'inwirereceived=[]forlineinwire.split(b'\r\n'):ifline==b'.':breakreceived.append(line[1:]ifline.startswith(b'.')elseline)assertb'\r\n'.join(received)+b'\r\n'==messageassertlen(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.
{"id":"smtp-acceptance-ambiguity","title":"Why Might There Already Be One Email for the Same Timeout?","summary":"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.","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"presets\" role=\"group\" data-i18n-label=\"mode\"><button data-mode=\"before\" data-i18n=\"before\"></button><button data-mode=\"after\" data-i18n=\"after\"></button></div><div class=\"actions\"><button id=\"first\" data-i18n=\"first\"></button><button id=\"retry\" data-i18n=\"retry\"></button><button id=\"reset\" data-i18n=\"reset\"></button></div><div class=\"track-row\"><span data-i18n=\"queue\"></span><div id=\"queue\" class=\"track\"></div></div><div class=\"metrics\"><div><span data-i18n=\"view\"></span><strong id=\"view\"></strong></div></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n/* Action labels must remain readable across locales and narrow embeds. */\n.actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}\n.actions button{overflow-wrap:anywhere;white-space:normal;min-height:58px;padding:8px}\nbutton{overflow-wrap:anywhere}\n@media(min-width:640px){.actions{grid-template-columns:repeat(3,minmax(0,1fr))}}\n\n.metrics span{overflow-wrap:anywhere}\n.panel{border:1px solid var(--rule);border-radius:9px;padding:12px;margin:14px 0;min-width:0}.panel h3{font-size:13px;margin:0 0 10px;color:var(--muted)}.source{font:13px/1.8 var(--viz-font-mono);white-space:pre-wrap;overflow-wrap:anywhere;margin:12px 0}.source mark{background:var(--accent-soft);color:var(--ink);border-bottom:2px solid var(--accent)}.source .fault{background:var(--second-soft);border-color:var(--second)}.diagram{display:block;width:100%;height:auto;max-height:320px;margin:14px 0;overflow:visible}.diagram text{fill:var(--ink);font:12px var(--viz-font-ui)}.diagram .edge{stroke:var(--muted);stroke-width:1.5;fill:none}.diagram .edge.on{stroke:var(--accent);stroke-width:3}.diagram .node{fill:var(--surface);stroke:var(--rule);stroke-width:1.5}.diagram .node.on{fill:var(--accent-soft);stroke:var(--accent)}.diagram .node.gold{fill:var(--second-soft);stroke:var(--second)}.log{font:12px/1.8 var(--viz-font-mono);white-space:pre-line;overflow-wrap:anywhere;min-height:7.2em}.table{display:grid;gap:5px;margin:14px 0}.table .tr{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:5px}.table .tr>*{padding:8px 5px;background:var(--surface);border-radius:4px;min-width:0;overflow-wrap:anywhere;font-size:12px}.table .tr .on{background:var(--accent-soft)}.table .tr .gold{background:var(--second-soft)}.choices button{min-height:44px}.metrics strong{min-height:1.6em}.code-line{display:block}.code-line.on{background:var(--accent-soft);border-left:2px solid var(--accent);padding-left:6px}.status-badge{font-size:12px;color:var(--accent)}.tile-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px}.tile-grid .cell{min-height:40px}.actions button{min-height:48px}.form-field{display:grid;gap:8px;margin:14px 0;font-size:13px}.form-field input{width:100%;font-family:var(--viz-font-ui)}.metrics.three{grid-template-columns:repeat(3,minmax(0,1fr))}.formula{min-height:3.2em}.presets{flex-wrap:wrap}.presets button{flex-basis:25%;min-height:44px}\n\n.budget{display:flex;height:28px;margin:16px 0;background:var(--surface);border-radius:5px;overflow:hidden}.budget i{display:block;background:var(--accent);transition:width .2s}.budget i.gold{background:var(--second)}.intervals{margin:16px 0}.interval{position:relative;height:36px;background:var(--surface);margin:6px 0;border-radius:4px;overflow:hidden}.interval i{position:absolute;height:100%;background:var(--accent-soft);border-left:2px solid var(--accent)}.interval i.gold{background:var(--second-soft);border-color:var(--second)}.interval span{position:relative;z-index:1;font:12px/36px var(--viz-font-ui);padding-left:7px}.table .tr>*{min-height:4.8em}.formula{white-space:pre-line;min-height:7.2em}.actions:has(>button:only-child){grid-template-columns:1fr}.metrics strong{min-height:3.2em}.rangegrid{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:3px;margin:14px 0}.rangegrid .cell{min-height:30px;padding:3px 1px;font-size:10px}.message{min-height:7.2em;white-space:pre-line;font:13px/1.8 var(--viz-font-ui);overflow-wrap:anywhere}\n/* Allow CJK hanging punctuation without overflowing a metric's border box. */\n.metrics>div{padding-right:4px}.metrics span{line-break:anywhere}\n\n#queue{display:grid;grid-template-columns:repeat(4,minmax(0,1fr))}#queue .cell{min-height:68px}.metrics strong{font-size:18px}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}const esc=s=>String(s).replace(/[&<>\"']/g,c=>({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));const fmt=s=>'{'+[...s].sort().join(', ')+'}';function graph(id,nodes,edges){const el=$('#'+id);el.setAttribute('viewBox','0 0 360 200');el.innerHTML='<defs><marker id=\"arrow-'+id+'\" viewBox=\"0 0 10 10\" refX=\"8\" refY=\"5\" markerWidth=\"5\" markerHeight=\"5\" orient=\"auto-start-reverse\"><path d=\"M 0 0 L 10 5 L 0 10 z\" fill=\"var(--muted)\"/></marker></defs>'+edges.map(e=>{const a=nodes[e[0]],b=nodes[e[1]],dx=b.x-a.x,dy=b.y-a.y,d=Math.hypot(dx,dy)||1;if(edges.some(r=>r[0]===e[1]&&r[1]===e[0])){const nx=-dy/d,ny=dx/d;return '<path class=\"edge '+(e[2]?'on':'')+'\" d=\"M '+(a.x+dx/d*22+nx*12)+' '+(a.y+dy/d*22+ny*12)+' Q '+((a.x+b.x)/2+nx*38)+' '+((a.y+b.y)/2+ny*38)+' '+(b.x-dx/d*25+nx*12)+' '+(b.y-dy/d*25+ny*12)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}return '<line class=\"edge '+(e[2]?'on':'')+'\" x1=\"'+(a.x+dx/d*25)+'\" y1=\"'+(a.y+dy/d*25)+'\" x2=\"'+(b.x-dx/d*28)+'\" y2=\"'+(b.y-dy/d*28)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}).join('')+nodes.map(n=>'<g><circle class=\"node '+(n.cls||'')+'\" cx=\"'+n.x+'\" cy=\"'+n.y+'\" r=\"25\"/><text x=\"'+n.x+'\" y=\"'+(n.y+4)+'\" text-anchor=\"middle\">'+esc(n.name)+'</text>'+(n.sub?'<text x=\"'+n.x+'\" y=\"'+(n.y+43)+'\" text-anchor=\"middle\">'+esc(n.sub)+'</text>':'')+'</g>').join('');}function table(id,rows){$('#'+id).classList.add('table');$('#'+id).replaceChildren(...rows.map(r=>{const row=document.createElement('div');row.className='tr';r.forEach(x=>{const c=document.createElement('div');if(typeof x==='object'){c.textContent=x.text;c.className=x.cls||'';}else c.textContent=x;row.append(c)});return row;}));}function band(id,parts,total){$('#'+id).replaceChildren(...parts.map((p,i)=>{const e=document.createElement('i');e.style.width=(100*p.value/total)+'%';e.className=i%2?'gold':'on';e.title=p.name+': '+p.value;return e}));}function intervals(id,items,total){$('#'+id).replaceChildren(...items.map(p=>{const row=document.createElement('div');row.className='interval';const bar=document.createElement('i');bar.style.left=(100*p.start/total)+'%';bar.style.width=(100*(p.end-p.start)/total)+'%';bar.className=p.cls||'';const label=document.createElement('span');label.textContent=p.label;row.append(bar,label);return row;}));}let mode='after',started=false,count=0,last='∅';function draw(){cells('queue',Array.from({length:4},(_,i)=>({text:i<count?'Message-ID X':'·',cls:i<count?'on':'empty'})));set('view',last);$('#first').disabled=started;$('#retry').disabled=!started||count>=4;$('#queue').dataset.count=count;pressed(mode);}function reset(){started=false;count=0;last='∅';draw()}$('#first').onclick=()=>{started=true;count=mode==='after'?1:0;last='TIMEOUT';draw()};$('#retry').onclick=()=>{count++;last='250 Queued';draw()};$('#reset').onclick=reset;document.querySelectorAll('[data-mode]').forEach(b=>b.onclick=()=>{mode=b.dataset.mode;reset()});draw();","audio":false,"strings":{"after":"250 lost after queuing","before":"Disconnect before queuing","first":"Execute first attempt","heading":"Why Might There Already Be One Email for the Same Timeout?","intro":"Choose whether the first failure occurred before queuing or during the return of the 250 response, then decide whether to retry.","mode":"First Failure Location","queue":"Receiver queue copy","reset":"Reset","retry":"Retry the same email","scope":"One email with a fixed Message-ID; the first failure is optional, subsequent attempts succeed; the receiver does not deduplicate additionally. Queuing indicates assumption of responsibility, not entry into the inbox or read status.","scopeLabel":"Model Scope and Conventions","view":"Sender-visible result"}}
Submission, Server Relaying, and TLS
Port
Common Role
TLS Behavior
TCP 25
MTA to MTA relay
STARTTLS negotiable; requirements depend on receiving domain policy
TCP 587
Mail submission
STARTTLS commonly used; clients should require a secure connection before authentication
TCP 465
submissions
TLS 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
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.
defdmarc_strict(author,spf_result,mail_from_domain,dkim_results):spf_ok=spf_result=='pass'andmail_from_domain.lower()==author.lower()dkim_ok=any(result=='pass'anddomain.lower()==author.lower()forresult,domainindkim_results)returnspf_okordkim_okassertdmarc_strict('example.com','fail','example.com',[('pass','example.com')])assertnotdmarc_strict('example.com','pass','vendor.example',[('fail','example.com')])assertnotdmarc_strict('example.com','pass','bounce.example.com',[])assertdmarc_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.
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.