feat: harden SMTP transport

Add explicit TLS modes, context-aware delivery, and typed transport errors.
Preserve raw MIME messages and cover the new delivery paths with local SMTP tests.
This commit is contained in:
2026-08-18 18:16:22 -06:00
parent 537fbeebd9
commit b3c22c261e
12 changed files with 1560 additions and 652 deletions
+94 -21
View File
@@ -1,33 +1,106 @@
# Email Sender
## Description
`email-sender` is a focused SMTP transport for caller-built RFC/MIME messages,
with a secondary convenience API for HTML messages and attachments. Network
operations are context-aware and bounded, and TLS uses Go's standard
certificate-chain and hostname verification.
`email-sender` is a simple Go library designed to send emails with optional attachments. It's built on top of the standard Go `net/smtp` library with additional support for sending HTML emails and handling multiple attachments.
## Canonical raw API
## Features
```go
service, err := email.New(email.Config{
Auth: smtp.PlainAuth("", username, password, "smtp.example.com"),
Host: "smtp.example.com",
Port: "465",
From: "bounce@example.com",
TLSMode: email.TLSModeImplicit,
ConnectTimeout: 10 * time.Second,
OperationTimeout: 30 * time.Second,
})
if err != nil {
return err
}
- Send HTML emails.
- Attach multiple files to the email.
- Built-in support for TLS encryption.
- Simple API for sending emails.
## Installation
Clone this repository:
```bash
git clone https://gitea.urkob.com/urko/emailsender.git
err = service.SendRawContext(ctx, email.RawMessage{
EnvelopeFrom: "bounce@example.com",
EnvelopeRecipients: []string{"recipient@example.com"},
Data: rawMIMEBytes,
})
```
## Usage
`Data` is transmitted as the caller supplied it. The transport does not
rebuild MIME, replace multipart boundaries, or generate a second `Message-ID`.
SMTP envelope addresses are explicit and are not inferred from arbitrary MIME
headers.
Check examples in [examples](https://gitea.urkob.com/urko/emailsender/examples)
## TLS modes
## Dependencies
- `TLSModeImplicit`: TLS is established and verified before the SMTP greeting.
- `TLSModeSTARTTLS`: the server must advertise STARTTLS; absence is an error and
the transport never downgrades to plaintext.
- `TLSModeNone`: explicit plaintext SMTP for trusted local development systems
such as Mailpit. Authentication is rejected in this mode.
- Go's standard `net/smtp` package
- Go's standard `crypto/tls` package for secure email sending.
TLS 1.2 or later is required. The configured SMTP hostname becomes
`tls.Config.ServerName`; standard certificate-chain and hostname validation are
always enabled. There is no `InsecureSkipVerify` option.
## Contribution
By default Go uses the operating-system certificate pool. `Config.RootCAs`,
when non-nil, deliberately replaces that pool. To add a private CA while
retaining public roots, start with `x509.SystemCertPool()` and append the CA.
Feel free to submit issues or pull requests if you find any bugs or have suggestions for improvements.
## Contexts and timeouts
`SendRawContext` and `SendEmailContext` are canonical. TCP establishment uses
`net.Dialer.DialContext` and `ConnectTimeout`. Every greeting, SMTP command,
TLS upgrade, DATA write, and response is bounded by the earlier of the context
deadline and `OperationTimeout`. Context cancellation closes the underlying
connection to interrupt blocked SMTP I/O; no detached send goroutine remains.
Legacy `SendRaw` and `SendEmail` methods remain as compatibility wrappers. They
use an explicit bounded context derived from the configured timeouts. Legacy
`NewSecure` and `NewSecure465` now perform normal verified TLS. The legacy
`NewInsecure` name is retained for source compatibility but also requires
verified STARTTLS; insecure certificate behavior was intentionally removed.
## Error classification
Transport failures wrap `*email.Error`:
```go
var transportError *email.Error
if errors.As(err, &transportError) && transportError.Temporary() {
// Let the caller's job system retry.
}
```
SMTP 4xx responses and ordinary network failures are generally transient.
SMTP 5xx responses, invalid addresses/configuration, missing required STARTTLS,
and certificate failures are permanent. Cancellation preserves
`errors.Is(err, context.Canceled)` and
`errors.Is(err, context.DeadlineExceeded)`.
Errors include a bounded operation-stage description but never message bodies,
SMTP passwords, or authentication data.
## SMTP delivery ambiguity
SMTP is not an exactly-once protocol. A server can accept the final DATA while
the connection fails before the client receives the acknowledgement. A retry
may then deliver a duplicate. A deterministic caller-supplied `Message-ID`
helps downstream clients deduplicate presentation, but cannot provide
mathematical exactly-once delivery.
## Development and tests
Plain local SMTP must be selected deliberately with `TLSModeNone` and no
authentication. Automated tests use local deterministic SMTP/TLS servers and
never connect to internet SMTP.
```bash
gofmt -w .
go vet ./...
go test ./... -count=1
go test -race ./... -count=1
git diff --check
```