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:
@@ -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
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user