package usecase import ( "crypto/tls" "crypto/x509" "fmt" "net" "net/smtp" "strconv" "strings" "time" ) const relayDialTimeout = 15 * time.Second // RelayConfig is the smarthost of PLAN.md section 10.4, used when the host's // outbound port 25 is blocked. In this mode the relay takes responsibility for // transport security to the final destination, so DANE and MTA-STS are not // evaluated here - they would only describe the wrong hop. type RelayConfig struct { Host string Port int Username string Password string // RootCAs trusts an internal relay presenting a certificate from a // private CA. Left nil, the system trust store is used. There is // deliberately no option to skip verification: the credentials sent over // this connection are worth protecting. RootCAs *x509.CertPool } // Configured reports whether delivery should go through the relay. func (c RelayConfig) Configured() bool { return c.Host != "" } // Address renders the dial target, defaulting to the submission port. func (c RelayConfig) Address() string { port := c.Port if port == 0 { port = 587 } return net.JoinHostPort(c.Host, strconv.Itoa(port)) } // SpfInclude renders the mechanism to add to the domain's SPF record. Without // it the relay's own IP is not authorised to send for the domain and every // message it forwards fails SPF (PLAN.md section 10.4). func (c RelayConfig) SpfInclude() string { if !c.Configured() { return "" } // A relay host is normally a subdomain of the provider's sending // infrastructure; including its organisational domain is what the // provider documents. The host itself is used when it cannot be reduced. host := strings.TrimSuffix(strings.ToLower(c.Host), ".") labels := strings.Split(host, ".") if len(labels) > 2 { host = strings.Join(labels[len(labels)-2:], ".") } return "include:" + host } // authFor selects the SASL mechanism to use against the relay. PLAIN is // preferred, and CRAM-MD5 is deliberately not offered: it requires the server // to store a reversible secret. func (c RelayConfig) authFor(serverName string) smtp.Auth { if c.Username == "" { return nil } return smtp.PlainAuth("", c.Username, c.Password, serverName) } // relayTLSConfig requires a valid certificate. Unlike opportunistic delivery // to an arbitrary MX, the relay is a host we chose and configured, so there is // no reason to accept an unverified certificate - and the credentials sent // over this connection make it worth refusing. func relayTLSConfig(relay RelayConfig) *tls.Config { return &tls.Config{ ServerName: relay.Host, MinVersion: tls.VersionTLS12, RootCAs: relay.RootCAs, } } // deliverViaRelay hands one message to the smarthost. func (w *OutboundWorkerUseCase) deliverViaRelay(from, to string, payload []byte) error { relay := w.relay addr := relay.Address() conn, err := net.DialTimeout("tcp", addr, relayDialTimeout) if err != nil { return fmt.Errorf("dial relay %s: %w", addr, err) } defer conn.Close() client, err := smtp.NewClient(conn, relay.Host) if err != nil { return fmt.Errorf("relay handshake: %w", err) } defer client.Close() if err := client.Hello(w.localHost); err != nil { return fmt.Errorf("relay EHLO failed: %w", err) } if ok, _ := client.Extension("STARTTLS"); ok { if err := client.StartTLS(relayTLSConfig(relay)); err != nil { return fmt.Errorf("relay STARTTLS failed: %w", err) } } else if relay.Username != "" { // Sending the relay credentials over a cleartext link would leak them // to anyone on the path, which is worse than deferring the message. return fmt.Errorf("relay %s does not offer STARTTLS and credentials must not be sent in the clear", addr) } if auth := relay.authFor(relay.Host); auth != nil { if err := client.Auth(auth); err != nil { return fmt.Errorf("relay authentication failed: %w", err) } } if err := client.Mail(from); err != nil { return fmt.Errorf("relay MAIL FROM failed: %w", err) } if err := client.Rcpt(to); err != nil { return fmt.Errorf("relay RCPT TO failed: %w", err) } writer, err := client.Data() if err != nil { return fmt.Errorf("relay DATA command failed: %w", err) } if _, err := writer.Write(payload); err != nil { writer.Close() return fmt.Errorf("relay write DATA failed: %w", err) } if err := writer.Close(); err != nil { return fmt.Errorf("relay close DATA failed: %w", err) } return client.Quit() }