# SMTP

SMTP is the protocol one mail server uses to hand a message to another. A client connects, names the sender and the recipients, offers the message, and gets a numeric reply. A `250` means the receiving server has taken responsibility for the message. It does not mean delivered, it does not mean in the inbox, and it certainly does not mean read.

- **Status:** Reference
- **Audience:** both, developer
- **Last verified:** 2026-09-10
- **Canonical:** https://connectbyjbrh.com/docs/technology/smtp/

## The envelope and the letter are different documents

This is the single most useful thing to understand about SMTP, and it explains SPF, forwarding, bounces and most "the From address is wrong" questions at once.

```text
MAIL FROM:<bounces@example.net>      <- envelope sender (Return-Path)
RCPT TO:<someone@example.net>       <- envelope recipient
DATA
From: Acme Traders <hello@example.net>   <- header, what a reader sees
To: Purchasing <someone@example.net>
Subject: Your order
.
```

**Envelope** — The `MAIL FROM` and `RCPT TO` commands. This is what the servers act on. The envelope sender becomes the `Return-Path` and receives bounces.
**Headers** — `From`, `To`, `Cc`, `Subject`. This is what the reader's client displays, and it need not match the envelope at all.
**Bcc** — Not a header on the delivered message. It is an extra `RCPT TO` and nothing else, which is why a Bcc recipient sees no trace of themselves.
**`To:` with nobody in it** — Perfectly legal. A mailing list message often has a list address in `To` and your address only in the envelope.

SPF authorises the *envelope* sender. DKIM signs the *message*, headers included. DMARC exists to require that one of those two aligns with the domain in the visible `From` header — which is the only part a reader ever sees.

## Reading the reply codes

| Class | Meaning | Correct response |
|---|---|---|
| `2xx` | Accepted at this hop | Record it as accepted, with the server's text. Stop retrying |
| `4xx` | Temporary failure — greylisting, rate limit, full mailbox, server busy | Retry with backoff over hours, not seconds. Give up after a policy window |
| `5xx` | Permanent failure — no such user, blocked, message refused | Do not retry. Suppress the address if the code says the mailbox does not exist |
| No reply at all | Timeout after `DATA` was sent | **Uncertain.** The message may have been accepted. Retrying risks a duplicate |

> **Careful** The timeout after `DATA` is the case worth designing for. A retry sends the message twice; a silent give-up loses it. Neither is correct, so the state has to be representable — anything that collapses it into *sent* or *failed* is guessing on the customer's behalf.

Later signals are separate events with separate mechanics. A bounce arrives as a new message to the `Return-Path` address, often minutes or days afterwards, and a complaint arrives through a provider's feedback loop. Neither is an SMTP reply, and neither can be waited for at send time.

## Ports, transport security and submission

- **587 — submission.** What a client uses to send its own mail, authenticated, upgrading to TLS with `STARTTLS`. This is the correct port for an application sending through a provider.
- **465 — implicit TLS submission.** TLS from the first byte. Widely supported and, where offered, marginally safer because there is no plaintext moment to strip.
- **25 — relay between servers.** Not for client submission, and blocked outbound by most networks. A configuration that sends on 25 usually works in a data centre and fails everywhere else.
- **`STARTTLS` is strippable.** An attacker on the path can remove the offer and the client may fall back to plaintext. MTA-STS and DANE exist to stop that; neither is universal.

> **Note** Authentication failures on submission are frequently reported as generic 5xx text by the provider. When a previously working mailbox starts refusing, an expired or revoked credential is more likely than anything about the message.

## Does Connect use SMTP?

**Used, for every mailbox connected as IMAP/SMTP.** A workspace may connect Gmail through the Gmail API, Microsoft Graph, or any IMAP/SMTP server; `providers.build()` selects the transport from the mailbox's own configuration, and the rest of Connect does not know or care which was chosen.

Whatever the transport, sending goes through one boundary — `outbound.py` — used identically by the agent and by a person clicking send. That boundary is where the reply code is turned into a state, and it holds a rule this page has been building towards: **a message is reported as sent only when the provider has acknowledged it.** Where there is no acknowledgement, the state is recorded as **uncertain** and shown as uncertain, because re-sending on a maybe is how a customer receives the same reply twice.

Two consequences follow for anyone reading a conversation. A message marked sent has a provider acknowledgement behind it rather than an optimistic write. A message marked uncertain is a real state with a real cause, not a rendering glitch, and it is the state SMTP itself produces when a connection dies at the wrong moment.

## Questions

### Does a 250 response mean the message reached the inbox?

No. It means the server you handed it to accepted responsibility for it. That server may still route it to spam, apply a filter, forward it somewhere, or bounce it minutes later. Delivery to a folder is a decision made after the SMTP conversation has ended, and nothing in the protocol reports it back.

### Why can the From header differ from the envelope sender?

Because they serve different purposes: the envelope routes and receives bounces, the header addresses the reader. Legitimate mail relies on this — bounce handling, mailing lists and forwarding all depend on the two differing. It is also why authentication needed DMARC, which is the piece that ties the visible `From` back to something authorised.

### Should an application retry after a 4xx?

Yes, with backoff, and with a limit. Greylisting deliberately answers 4xx to a first attempt and accepts the second, so a client that does not retry loses mail to a very common anti-spam technique. Retrying a 5xx, by contrast, achieves nothing and damages sender reputation.

## Related

- [IMAP](https://connectbyjbrh.com/docs/technology/imap/)
- [MIME](https://connectbyjbrh.com/docs/technology/mime/)
- [SPF](https://connectbyjbrh.com/docs/technology/spf/)
- [Bounces](https://connectbyjbrh.com/docs/technology/bounce/)
- [Why 'sent' must require provider evidence](https://connectbyjbrh.com/research/provider-evidence-for-sent/)
- [Email in Connect](https://connectbyjbrh.com/docs/email/)

## What this page is based on

- RFC 5321 — Simple Mail Transfer Protocol, https://www.rfc-editor.org/rfc/rfc5321
- RFC 6409 — Message Submission for Mail, https://www.rfc-editor.org/rfc/rfc6409
- Connect channel source pack — docs-source/sources/CHANNELS.md §1, `outbound.py` as the one send boundary and the sent/uncertain states
- Connect capability registry (docs-source/facts.py) — imap_smtp, send_evidence
