feat: add MailerMailgun, an HTTP API transport - #10
Merged
Conversation
The library shipped one transport, SMTP, and docs/examples/custom-backend.md
sketched an "APIMailer" as something each caller would write for themselves.
This makes that concrete for the provider most likely to be reached for.
Why an API transport is worth having alongside SMTP:
- SMTP needs an outbound connection on a port many hosting environments
block outright. The messages API is HTTPS on 443.
- SMTP reports most delivery failures asynchronously, by bounce. The API
answers synchronously, so "Domain not found" arrives as an error the
caller can act on rather than as a message that silently never lands.
Notes on the implementation, in case they read as arbitrary:
- The whole messages URL is configuration, not a host plus a domain. Mailgun
puts the sending domain in the path AND runs a separate EU host
(api.eu.mailgun.net), which a host+domain pair cannot express.
- Basic auth with the literal username "api" is what Mailgun expects; the
key is the password, not the account address.
- The body is form-encoded, not JSON, and `text` and `html` are different
fields. Choosing between them from MailContent's MIME type is what stops
an HTML template arriving as visible source -- a failure no status code
would reveal.
- A non-https URL is refused at construction. The key is a basic-auth header
on every request, and a mail provider is on the public internet by
definition, so there is no private-network case to allow for.
- HTTPClient is optional but expected: a service that already has a
configured client should pass it, so this transport inherits that timeout,
retry policy and pool instead of quietly keeping its own.
- The API key never appears in an error. An error message is the one place
guaranteed to reach a log file, and a test asserts it.
Configuration errors surface from NewMailerMailgun rather than from the first
Send, because the first message a service sends is usually a password reset or
an account verification -- a misconfiguration found there is found as a user who
cannot get in.
Tests assert the request against Mailgun's documented wire format field by
field: method, path, content type, basic-auth pair, and the form fields. They
deliberately do not model a response body the code parses, because there is none
-- only a status -- so there is no way for the test to agree with the code about
a shape neither shares with the real provider.
Two of them found real things while being written: the builder validates a
display name with len() BEFORE trimming, so " " is a valid three-character
name and the empty-name path is reachable rather than defensive; and the
html/text mutation was verified to fail the suite.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The library ships one transport, SMTP, and
docs/examples/custom-backend.mdsketches an "APIMailer" as something each caller writes for themselves. This
makes it concrete for the provider most likely to be reached for.
Why an API transport alongside SMTP
outright. The messages API is HTTPS on 443.
answers synchronously, so
Domain not foundarrives as an error the callercan act on rather than as a message that silently never lands.
Decisions that might otherwise read as arbitrary
Mailgun puts the sending domain in the path and runs a separate EU host
(
api.eu.mailgun.net), which a host+domain pair cannot express.apiis what Mailgun expects; thekey is the password, not the account address.
text/htmlare different fields.Choosing between them from the
MailContentMIME type is what stops an HTMLtemplate arriving as visible source — a failure no status code would reveal.
httpsURL is refused at construction. The key is a basic-authheader on every request, and a mail provider is on the public internet by
definition, so there is no private-network case to allow for.
HTTPClientis optional but expected. A service that already has aconfigured client should pass it, so this transport inherits that timeout,
retry policy and pool rather than quietly keeping its own.
guaranteed to reach a log file; a test asserts it.
Configuration errors come from
NewMailerMailgun, not the firstSend— thefirst message a service sends is usually a password reset or an account
verification, so a misconfiguration found there is found as a user who cannot
get in.
On the tests
They assert the request against Mailgun's documented wire format field by field:
method, path, content type, the basic-auth pair, and the form fields. They
deliberately do not model a response body the code parses, because there is
none — only a status — so there is no way for the test to agree with the code
about a shape neither shares with the real provider.
Two found real things while being written:
len()before trimming, so" "is a valid three-character name — the empty-name path is reachable,not defensive;
html/textselection was verified by mutation: forcing the wrong branchfails the suite.
Not verified against a live Mailgun account — there is none in this project
to capture golden fixtures from, so the request shape rests on the published API
documentation. Worth stating plainly rather than letting "tested" be read as
"verified against the real thing".
go vet,go test -raceandgofmtare clean; README,doc.go, CHANGELOG andthe custom-backend example are updated.
🤖 Generated with Claude Code