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