urko b3c22c261e 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.
2026-08-18 18:16:22 -06:00
2026-08-18 18:16:22 -06:00
2023-10-21 20:31:31 +02:00
2026-08-18 18:16:22 -06:00
2023-10-21 20:31:31 +02:00
2026-08-18 18:16:22 -06:00
2026-08-18 18:16:22 -06:00
2026-08-18 18:16:22 -06:00
2026-08-18 18:16:22 -06:00

Email Sender

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.

Canonical raw API

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
}

err = service.SendRawContext(ctx, email.RawMessage{
    EnvelopeFrom:       "bounce@example.com",
    EnvelopeRecipients: []string{"recipient@example.com"},
    Data:               rawMIMEBytes,
})

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.

TLS modes

  • 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.

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.

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.

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:

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.

gofmt -w .
go vet ./...
go test ./... -count=1
go test -race ./... -count=1
git diff --check
S
Description
A Go package that facilitates sending emails with optional attachments using SMTP. It supports TLS for secure connections.
Readme 107 KiB
Languages
Go 98.2%
Makefile 1.8%