One Recipient or a List
Ten lines that convert a recipient list carry four separate defences — against a value that is not a list, a single recipient that was not wrapped in one, a key spelled with a different capital letter, and an entry with no address in it. All four are worth having, and one of them is the interesting one.
There is a spot in every integration where two worlds meet, and it is not where anybody expects. It is not the network call. It is the small function that takes the shape your system produced and turns it into the shape somebody else's system wants — the place where an assumption from one side meets a requirement from the other, and neither of them is written down.
What the function is defending against
A recipient list arrives as part of an internally built message and comes out as the list this provider wants. Between those two things sit four checks, and each is guarding against something real.
| The defence | What it catches | Without it |
|---|---|---|
| Is this a list at all? | A value that is not what was expected | A crash on iterating something that is not iterable |
| Is this one recipient rather than a list of them? | A single object where a list was assumed | The recipient silently lost |
| Which spelling of the key? | The same field with a different capital letter | Every address read as empty |
| Is there actually an address? | An entry with a name and nothing else | A request the provider refuses |
Three of these fail loudly and one of them fails silently. The silent one is the reason the function is worth a post.
The one that is genuinely dangerous
The second row. A single recipient supplied as one object rather than as a list of one.
It is an easy thing to produce — most email formats accept both, and code building a message for one person naturally writes the one person rather than a list containing them. It is entirely valid input.
Iterated as though it were a list, it produces nothing usable: the loop walks the object's fields rather than a set of recipients, none of which is a recipient, so the resulting list is empty. And an empty recipient list on a translated request is not an error anywhere in this chain. The message is sent to nobody, successfully.
That is the shape of failure this whole series keeps returning to: no exception, no log line, a green result and an absence that only somebody expecting the email would ever notice — days later, when a supplier says they never received the purchase order.
So the function checks whether what it was handed looks like a single recipient and wraps it in a list if so. Two lines.
Why a translator has to be more careful than ordinary code
A reasonable objection: surely the callers are all in this codebase, so the shape is known and this is defensive programming for its own sake?
The answer is about where the function sits.
-
The input format is not ours
Messages are built in a real provider's format rather than an invented one, and that format legitimately allows several shapes for the same thing. Accepting only the one our callers currently use is accepting a subset of a specification.
-
There are many callers and they are far away
Every place in the product that sends one of these emails builds a body. They were written at different times by different people, and none of them is looking at this function.
-
Adding a caller is routine
A new document type that sends an email is ordinary work. Whether its author happens to wrap a single recipient in a list is not something anybody will think about.
-
The failure is silent
If it failed loudly, the first new caller to get it wrong would find out immediately. It does not, so the cost of being strict is paid by whoever notices a missing email weeks later.
That is the test for where defensive parsing earns its keep: many callers, a format you do not own, and a silent failure mode. All three here. Where any of the three is absent, the same code is clutter.
Skipping rather than failing
The other two defences drop bad entries rather than rejecting the whole list, which is a decision rather than an obvious default.
An entry that is not shaped like a recipient is skipped. An entry with a name and no address is skipped. The other recipients still get the message.
The alternative is to refuse the whole send, and the argument for it is real: silently dropping a recipient is its own kind of quiet failure. What decides it here is the same reasoning as the fall-through in the delivery chain — these are commercial documents somebody is waiting on, and one malformed entry costing four suppliers their purchase order is a worse outcome than one supplier missing it.
And unlike the empty-list case, this one does not pass silently at the end: a body that produces no valid recipients at all yields nothing sent, which the verdict reports as a failure rather than as a success.
Two spellings of the same field
The last defence is the smallest and the most revealing about how systems actually accumulate.
Both capitalisations of the address and name keys are accepted. The internal format capitalises them; a body assembled by hand somewhere, or adapted from an example written against a different provider, might not.
Reading both is a handful of characters. The failure it prevents is an address read as empty, an entry skipped, and a message sent to fewer people than intended — which is, once again, the silent one.
There is a respectable argument that this is exactly the kind of leniency that lets inconsistency spread. It is a fair criticism. The counterweight is that the strict version enforces a convention by losing mail, and losing mail is a very expensive way to teach somebody about capital letters.
What is in place, layer by layer
The translation seam
A single recipient accepted as one
An object supplied where a list was expected is wrapped rather than iterated, because iterating it produces an empty list and a message sent to nobody.
Both key spellings read
Either capitalisation of the address and name fields is accepted, so a body assembled from an example does not silently lose its recipients.
Entries without an address dropped
Skipped rather than sent, so a partially filled entry does not become a request the provider refuses for everybody.
Non-list input handled
Anything that is not a list at all yields an empty result rather than an error deep inside a conversion.
One bad entry costs itself
The other recipients still receive the message, on the same reasoning as the delivery chain — these are documents somebody is waiting on.
No valid recipients is still a failure
A body that produces nothing sendable results in nothing sent, which the verdict reports rather than passing as fine.
The same care on the other translator
The connector that converts to a form request flattens recipients with equivalent handling, because it sits at the same kind of seam.
Names omitted rather than blanked
A recipient with no name carries no name field rather than an empty one, which some clients render as a stray space before the address.
Defensive parsing earns its place where there are many callers, a format you do not own, and a silent failure mode. Where any of those three is missing, the same code is clutter.
Three positions held on purpose
- Leniency is applied where the strict version fails silently. Rejecting an unexpected shape is defensible when the rejection is visible; here it would have been a message sent to nobody, reported as a success.
- A malformed entry costs itself rather than the batch. One supplier missing a purchase order is a worse outcome than four suppliers missing theirs, and that comparison is what decides it rather than a general principle.
- The leniency is bounded and stated. Accepting two spellings of a field is a real invitation to inconsistency, and the reason it is accepted anyway is that the alternative teaches the lesson by losing mail.
Five questions about a translation layer
What if a single recipient is not wrapped in a list?
A good answer sounds like
It still sends.
What ours actually is
Detected and wrapped, because iterating it produces an empty list and a message sent to nobody.
What if one recipient entry is malformed?
A good answer sounds like
The others still receive it.
What ours actually is
Skipped individually rather than failing the whole send.
Can a message end up going to nobody?
A good answer sounds like
Not silently.
What ours actually is
A body with no valid recipients produces nothing sent, which the verdict reports as a failure.
Why accept two spellings of a field?
A good answer sounds like
A reasoned answer.
What ours actually is
Because the strict version enforces a convention by losing mail, which is an expensive way to teach capitalisation.
Is this defensiveness everywhere?
A good answer sounds like
No — at the seams.
What ours actually is
Where there are many callers, a format we do not own, and a silent failure mode. Elsewhere it would be clutter.
Our take
Defensive programming has a bad reputation because most of it is performed rather than reasoned — checks added everywhere out of unease, none of which has a failure in mind. The version worth doing is narrow and specific: identify the seams where your assumptions meet somebody else's format, work out which mistakes there fail loudly and which fail quietly, and defend only against the quiet ones. A crash tells you something is wrong. A recipient list that silently comes out empty tells a supplier you never sent the purchase order, and it tells you nothing at all.
The failures worth engineering against are the silent ones
A crash gets fixed the same afternoon. A message sent successfully to nobody gets found by a customer, weeks later, in a conversation you did not want to be having.
Talk through email deliveryFrequently asked questions
Could an email be sent to nobody without us knowing?
That is precisely the case this handling exists to prevent. A body producing no valid recipients results in nothing sent, and the verdict reports a failure rather than a success — so the outcome is visible rather than an absence somebody discovers later.
If one recipient address is malformed, do the others receive the email?
Yes. Entries without a usable address are skipped and the rest are sent. The reasoning is the same as elsewhere in the mail path: these are commercial documents somebody is waiting on, and one bad entry costing everybody their copy is the worse outcome.
Why does the translator accept shapes our own code does not produce?
Because the internal format is a real provider's specification rather than something invented here, and it legitimately allows several shapes for the same thing. Accepting only what today's callers happen to produce means the next caller written by somebody reading the provider's documentation is quietly wrong.
Is this kind of checking applied everywhere in the codebase?
No, and it should not be. It earns its place at seams with many callers, a format we do not own, and a failure mode that is silent. Applied generally it becomes noise that obscures the checks that matter.
Does the other email translator do the same thing?
Yes — the connector that converts to a form-encoded request flattens its recipients with equivalent handling, because it sits at the same kind of boundary and faces the same inputs.