The wire
What actually happens in the twenty seconds after you press send
The code that has this conversation with another organisation's mail server is ours, and it is named after this domain. This page walks the session it runs, in the order it runs it.
Handing your outbound mail to an outside marketing service means handing that service your contact list. The reason to run the dialogue yourself is not that it is clever — it is not, it is a 1980s line protocol — it is that the list then has one fewer place to be.
Everything below is injected rather than hardcoded: the socket, the encryption upgrade, the address lookup. That is why the session can be exercised end to end against a scripted fake server with no network at all. It is a property of how the module is built, and it is not the same thing as a claim about production traffic, which is covered further down.
Step by step
8 steps, in the order the code performs them
Pick a step. Each panel shows what goes on the wire, what comes back, and the decision the code makes about it.
1. Greeting
The far side speaks first, and we are allowed to be refused here
A mail server greets the connection before we say anything. A 220 means carry on. Anything else ends the session there, and the reply is classified rather than ignored: a 4xx becomes a deferral we will retry, a 5xx becomes a bounce we will not.
The important part is what does not happen. There is no branch that treats a refused greeting as a partial success, because a message that never got past the greeting was never sent and the record should say so.
mitto-smtp-transport.ts:298 (runDialogue, step 1)
2. EHLO
We introduce ourselves and read back what the server can do
The introduction asks the far side to list its capabilities. Those come back as a multiline reply, and parsing one correctly is fussier than it looks: non-final lines are marked with a hyphen after the code and the final line with a space, and the code that matters is the one on the last line.
That parser is a pure, total function with no socket in it, which is why it can be tested against captured replies rather than against a live server that may answer differently tomorrow.
mitto-smtp-transport.ts:298 (step 2) · :407 parseReplyLines
3. STARTTLS
If the server advertises encryption, we take it -- and never quietly decline it
When the capability list includes STARTTLS the session is upgraded to an encrypted channel and the introduction is repeated over it, because the capabilities advertised before the upgrade cannot be trusted afterwards. That repeat is required by the protocol and it is easy to skip; it is not skipped here.
If the upgrade is refused or fails, the attempt is recorded as a deferral. It does not fall back to sending the message in the clear. A quiet downgrade is the kind of thing that never shows up in a delivery report, which is exactly why it has to be a rule in the code and not a preference.
mitto-smtp-transport.ts:237 offersStartTls · :298 (step 3)
4. MAIL FROM
The envelope sender is a per-message address, not the address a human reads
There are two senders on an email and most people only ever see one. The from-line a recipient reads is a header. The envelope sender is the address the receiving server will send a failure notice to, and here it is unique per message: it carries the queue-row identifier for this exact send.
That means a bounce comes back attributable to one message instead of arriving in a shared mailbox where nobody can tell which of nine hundred sends it belongs to. It is a small decision with a large effect on whether a delivery problem is diagnosable at all.
mitto-smtp-transport.ts:176 buildVerpEnvelopeFrom
5. RCPT TO
One recipient, stated to the server, and refusable by it
The recipient is named to the server, which may accept or refuse it. A refusal here is the most informative failure in the whole dialogue, because it is the server telling you something about that specific address rather than about your message.
A 5xx at this step is a bounce and the address should stop being mailed. That is not this module's decision to make -- it hands the classified attempt back and the address-level suppression stop is what acts on it.
mitto-smtp-transport.ts:298 (step 5)
6. DATA
A 354 is the one reply in the session that means neither yes nor no
DATA asks permission to start sending the message body, and the expected answer is a 354, which is an intermediate code: not a success, not a failure, an instruction to continue. Code that treats every 3xx as a soft error gets this wrong and defers messages that were about to be accepted.
Anything other than 354 here is mapped by its own class, so an overloaded server saying 421 defers and a policy refusal saying 550 bounces.
mitto-smtp-transport.ts:298 (step 6)
7. The body
Dot-stuffing, the signature if there is one, and the terminator
The message is terminated by a line containing a single dot, which creates an obvious problem: a line in your text that happens to start with a dot would end the message early. The protocol's answer is to double a leading dot on the way out, and the receiving server undoes it. Skip that and a paragraph beginning with an ellipsis truncates the email.
If a signer is wired in, its header is prepended here. If there is no key, the message goes out UNSIGNED and is recorded as unsigned. There is no code path that writes a signature it could not compute.
mitto-smtp-transport.ts:201 dotStuff · :212 composeMessage
8. The verdict
The reply to the body is the whole answer, and QUIT cannot change it
The reply that follows the message body is the authoritative outcome: 2xx delivered, 4xx deferred, 5xx bounced. QUIT is a courtesy afterwards and its failure is caught and discarded, because a connection that dropped after the server already accepted the message did not un-accept it.
Getting that order wrong is a real and common bug. It turns an accepted message into a retry, and the recipient gets it twice.
mitto-smtp-transport.ts:298 (step 8)
Reading the answer
Five kinds of reply, and what each one entitles us to say
A delivery report is only worth reading if the thing that wrote it was strict about what a reply code means. These are the five outcomes and the conclusion each one licenses.
2xx
The server accepted responsibility for the message.
We do: Record a delivery attempt as delivered, and seal the idempotency key so a retry cannot send it twice.
We do not: Claim it reached an inbox. Acceptance and placement are different facts and only the first one is ours.
354
Intermediate: send the body now.
We do: Write the dot-stuffed message and wait for the real verdict.
We do not: Treat it as a soft failure. It is neither success nor error, and mapping it to either is a bug.
4xx
Try again later -- the server is busy, greylisting, or temporarily unhappy.
We do: Record a deferral, which keeps the message eligible for a later send with backoff.
We do not: Give up, and never mark it delivered.
5xx
Permanent refusal -- no mailbox, policy block, or a rejected sender.
We do: Record a bounce so the address-level suppression stop can act on it.
We do not: Retry it into the ground. Repeatedly mailing a dead address is how a domain earns a reputation problem for everyone else on it.
0
We never got a usable reply: no socket factory, a refused connection, or the peer hung up mid-dialogue.
We do: Record a deferral with the reason attached, including the plain string 'not provisioned' when the transport has no socket factory at all.
We do not: Fabricate an outcome. There is no branch anywhere in the module that returns delivered without a 2xx from a real server.
The distinction that matters most is the first one. A 2xx means a server accepted responsibility for the message. It does not mean the message reached an inbox, and no software on earth can tell you that it did. A product that reports "delivered" and means "accepted" is not lying so much as quietly redefining a word its customer already understood.
Around the session
The four things that surround a send
A retry after a slow acceptance must not become a second email
Every send derives a stable key from the channel, the address, and the rendered message, so the same logical message always presents the same key however many times a runner attempts it.
A success seals the key; a failure does not. That asymmetry is the whole point: a transient failure stays legitimately retryable, while a message the receiving server already accepted cannot be sent twice because a socket hiccuped on the way back.
packages/notifications/src/delivery/idempotency.ts:45 (deriveIdempotencyKey)
An address that has told us to stop is stopped on every lane
A hard bounce, a complaint, or a global opt-out binds to the ADDRESS rather than to a campaign, so it is honoured identically by every caller, including a one-off send that never went near a planner.
It is fail-closed. If the suppression store cannot answer, the send is blocked rather than attempted. That costs something — a store outage stops mail — and it is the choice we would make again, because sending into uncertainty about a complaint address damages deliverability for everything else on the domain.
packages/notifications/src/delivery/suppression.ts:53 (suppressionGuard)
An unsubscribe link that stops working is worse than none
The unsubscribe target is a signed, stateless token with no expiry, because someone may click a link in a year-old message and it has to still work.
That is a deliberate design decision recorded in the module rather than an accident of implementation, and it is the kind of thing that only ever gets noticed when it is missing.
packages/notifications/src/unsubscribe-token.ts:64 (mintUnsubscribeToken)
What happens today, with the latch off
A transport built without a socket factory is not provisioned. Its delivery call returns a deferred attempt carrying the plain reason not provisioned, and deferred is the retry class, so the message stays eligible for a real send later.
It never fakes a delivery. That sentence is worth being precise about, because it is the one a buyer should check: there is no code path in the module that returns a delivered outcome without a success reply from a real server.
packages/notifications/src/mitto-smtp-transport.ts:262 (provisioned) · apps/api/src/services/comms-send-transport.ts:82 (honestOffOwnedMta)
Built and not built
The table where we lose two rows on purpose
Product categories, not named vendors. Two rows go against us, and they are the two a buyer most needs to know before signing anything.
| Question a buyer should ask | A bulk marketing tool | A big free provider | This mailbox |
|---|---|---|---|
| Who holds your contact list | The marketing vendor | Your provider, plus their partners | Our own private system |
| Bounce attributable to one message | Usually, inside their dashboard | One shared bounce mailbox | Per-message envelope sender |
| Encryption declined quietly if the upgrade fails | Varies by product | Varies by product | Never -- the attempt defers instead |
| 'Delivered' means accepted, not placed | Reported as delivered | Reported as delivered | Recorded as accepted, and said so |
| Runs a campaign builder and open-rate dashboard | Yes | Sometimes | No -- and that is a real loss for some buyers |
| Sending is live today | Yes | Yes | No -- queued, and recorded as queued |
If a comparison table wins every row, it was built backwards from its conclusion. The last two rows here are honest losses: there is no campaign tooling, and we are not accepting mail yet.
Elsewhere on this site
The rest of it
Overview
What a sending address is, and why the from-line is the whole product.
Read it →Authentication
DKIM, SPF, DMARC, the return path and BIMI -- what each does and which are live today.
Read it →What we do not claim
The promises this page refuses to make, and the reason each one is refusable.
Read it →Pricing
Catalog figures for planning a budget. Checkout is not open and nothing here bills.
Read it →