add more documentation, examples with tests to illustrate reusable components

This commit is contained in:
Mechiel Lukkien
2023-12-12 15:47:26 +01:00
parent 810cbdc61d
commit d1b66035a9
40 changed files with 973 additions and 119 deletions

View File

@ -1,4 +1,29 @@
// Package smtpclient is an SMTP client, used by the queue for sending outgoing messages.
// Package smtpclient is an SMTP client, for submitting to an SMTP server or
// delivering from a queue.
//
// Email clients can submit a message to SMTP server, after which the server is
// responsible for delivery to the final destination. A submission client
// typically connects with TLS, and PKIX-verifies the server's certificate. The
// client then authenticates using a SASL mechanism.
//
// Email servers manage a message queue, from which they will try to deliver
// messages. In case of temporary failures, the message is kept in the queue and
// tried again later. For delivery, no authentication is done. TLS is opportunistic
// by default (TLS certificates not verified), but TLS and certificate verification
// can be opted into by domains by specifying an MTA-STS policy for the domain, or
// DANE TLSA records for their MX hosts.
//
// Delivering a message from a queue would involve:
// 1. Looking up an MTA-STS policy, through a cache.
// 2. Resolving the MX targets for a domain, through smtpclient.GatherDestinations,
// and for each destination try delivery through:
// 3. Looking up IP addresses for the destination, with smtpclient.GatherIPs.
// 4. Looking up TLSA records for DANE, in case of authentic DNS responses
// (DNSSEC), with smtpclient.GatherTLSA.
// 5. Dialing the MX target with smtpclient.Dial.
// 6. Initializing a SMTP session with smtpclient.New, with proper TLS
// configuration based on discovered MTA-STS and DANE policies, and finally calling
// client.Deliver.
package smtpclient
import (
@ -201,22 +226,28 @@ type Opts struct {
// returned on which eventually Close must be called. Otherwise an error is
// returned and the caller is responsible for closing the connection.
//
// Connecting to the correct host is outside the scope of the client. The queue
// managing outgoing messages decides which host to deliver to, taking multiple MX
// records with preferences, other DNS records, MTA-STS, retries and special
// cases into account.
// Connecting to the correct host for delivery can be done using the Gather
// functions, and with Dial. The queue managing outgoing messages typically decides
// which host to deliver to, taking multiple MX records with preferences, other DNS
// records, MTA-STS, retries and special cases into account.
//
// tlsMode indicates if and how TLS may/must (not) be used. tlsVerifyPKIX
// indicates if TLS certificates must be validated against the PKIX/WebPKI
// certificate authorities (if TLS is done). DANE-verification is done when
// opts.DANERecords is not nil. TLS verification errors will be ignored if
// opts.IgnoreTLSVerification is set. If TLS is done, PKIX verification is
// always performed for tracking the results for TLS reporting, but if
// tlsVerifyPKIX is false, the verification result does not affect the
// connection. At the time of writing, delivery of email on the internet is done
// with opportunistic TLS without PKIX verification by default. Recipient domains
// can opt-in to PKIX verification by publishing an MTA-STS policy, or opt-in to
// DANE verification by publishing DNSSEC-protected TLSA records in DNS.
// tlsMode indicates if and how TLS may/must (not) be used.
//
// tlsVerifyPKIX indicates if TLS certificates must be validated against the
// PKIX/WebPKI certificate authorities (if TLS is done).
//
// DANE-verification is done when opts.DANERecords is not nil.
//
// TLS verification errors will be ignored if opts.IgnoreTLSVerification is set.
//
// If TLS is done, PKIX verification is always performed for tracking the results
// for TLS reporting, but if tlsVerifyPKIX is false, the verification result does
// not affect the connection.
//
// At the time of writing, delivery of email on the internet is done with
// opportunistic TLS without PKIX verification by default. Recipient domains can
// opt-in to PKIX verification by publishing an MTA-STS policy, or opt-in to DANE
// verification by publishing DNSSEC-protected TLSA records in DNS.
func New(ctx context.Context, elog *slog.Logger, conn net.Conn, tlsMode TLSMode, tlsVerifyPKIX bool, ehloHostname, remoteHostname dns.Domain, opts Opts) (*Client, error) {
ensureResult := func(r *tlsrpt.Result) *tlsrpt.Result {
if r == nil {

View File

@ -41,8 +41,9 @@ type Dialer interface {
// Dial connects to host by dialing ips, taking previous attempts in dialedIPs into
// accounts (for greylisting, blocklisting and ipv4/ipv6).
//
// If the previous attempt used IPv4, this attempt will use IPv6 (in case one of
// the IPs is in a DNSBL).
// If the previous attempt used IPv4, this attempt will use IPv6 (useful in case
// one of the IPs is in a DNSBL).
//
// The second attempt for an address family we prefer the same IP as earlier, to
// increase our chances if remote is doing greylisting.
//

View File

@ -0,0 +1,58 @@
package smtpclient_test
import (
"context"
"log"
"net"
"strings"
"golang.org/x/exp/slog"
"github.com/mjl-/mox/dns"
"github.com/mjl-/mox/sasl"
"github.com/mjl-/mox/smtpclient"
)
func ExampleClient() {
// Submit a message to an SMTP server, with authentication. The SMTP server is
// responsible for getting the message delivered.
// Make TCP connection to submission server.
conn, err := net.Dial("tcp", "submit.example.org:465")
if err != nil {
log.Fatalf("dial submission server: %v", err)
}
defer conn.Close()
// Initialize the SMTP session, with a EHLO, STARTTLS and authentication.
// Verify the server TLS certificate with PKIX/WebPKI.
ctx := context.Background()
tlsVerifyPKIX := true
opts := smtpclient.Opts{
Auth: []sasl.Client{
// Prefer strongest authentication mechanism, allow up to older CRAM-MD5.
sasl.NewClientSCRAMSHA256("mjl", "test1234"),
sasl.NewClientSCRAMSHA1("mjl", "test1234"),
sasl.NewClientCRAMMD5("mjl", "test1234"),
},
}
localname := dns.Domain{ASCII: "localhost"}
remotename := dns.Domain{ASCII: "submit.example.org"}
client, err := smtpclient.New(ctx, slog.Default(), conn, smtpclient.TLSImmediate, tlsVerifyPKIX, localname, remotename, opts)
if err != nil {
log.Fatalf("initialize smtp to submission server: %v", err)
}
defer client.Close()
// Send the message to the server, which will add it to its queue.
req8bitmime := false // ASCII-only, so 8bitmime not required.
reqSMTPUTF8 := false // No UTF-8 headers, so smtputf8 not required.
requireTLS := false // Not supported by most servers at the time of writing.
msg := "From: <mjl@example.org>\r\nTo: <other@example.org>\r\nSubject: hi\r\n\r\nnice to test you.\r\n"
err = client.Deliver(ctx, "mjl@example.org", "other@example.com", int64(len(msg)), strings.NewReader(msg), req8bitmime, reqSMTPUTF8, requireTLS)
if err != nil {
log.Fatalf("submit message to smtp server: %v", err)
}
// Message has been submitted.
}

View File

@ -42,11 +42,11 @@ var (
// expandedNextHopAuthentic indicates if the DNS records after following CNAMEs were
// DNSSEC secure.
//
// These authentic flags are used by DANE, to determine where to look up TLSA
// These authentic results are needed for DANE, to determine where to look up TLSA
// records, and which names to allow in the remote TLS certificate. If MX records
// were found, both the original and expanded next-hops must be authentic for DANE
// to apply. For a non-IP with no MX records found, the authentic result can be
// used to decide which of the names to use as TLSA base domain.
// to be option. For a non-IP with no MX records found, the authentic result can
// be used to decide which of the names to use as TLSA base domain.
func GatherDestinations(ctx context.Context, elog *slog.Logger, resolver dns.Resolver, origNextHop dns.IPDomain) (haveMX, origNextHopAuthentic, expandedNextHopAuthentic bool, expandedNextHop dns.Domain, hosts []dns.IPDomain, permanent bool, err error) {
// ../rfc/5321:3824