mirror of
https://github.com/mjl-/mox.git
synced 2025-06-28 01:48:15 +03:00
add "mox smtp dial" subcommand, for debugging connectivity issues
with various options for the tls connection.
This commit is contained in:
parent
70bbfc8f10
commit
4a14abc254
46
doc.go
46
doc.go
@ -114,6 +114,7 @@ any parameters. Followed by the help and usage information for each command.
|
|||||||
mox rdap domainage domain
|
mox rdap domainage domain
|
||||||
mox retrain [accountname]
|
mox retrain [accountname]
|
||||||
mox sendmail [-Fname] [ignoredflags] [-t] [<message]
|
mox sendmail [-Fname] [ignoredflags] [-t] [<message]
|
||||||
|
mox smtp dial host[:port]
|
||||||
mox spf check domain ip
|
mox spf check domain ip
|
||||||
mox spf lookup domain
|
mox spf lookup domain
|
||||||
mox spf parse txtrecord
|
mox spf parse txtrecord
|
||||||
@ -1527,6 +1528,51 @@ binary should be setgid that group:
|
|||||||
|
|
||||||
usage: mox sendmail [-Fname] [ignoredflags] [-t] [<message]
|
usage: mox sendmail [-Fname] [ignoredflags] [-t] [<message]
|
||||||
|
|
||||||
|
# mox smtp dial
|
||||||
|
|
||||||
|
Dial the address, initialize the SMTP session, including using STARTTLS to enable TLS if the server supports it.
|
||||||
|
|
||||||
|
If no port is specified, SMTP port 25 is used.
|
||||||
|
|
||||||
|
Data is copied between connection and stdin/stdout until either side closes the
|
||||||
|
connection.
|
||||||
|
|
||||||
|
The flags influence the TLS configuration, useful for debugging interoperability
|
||||||
|
issues.
|
||||||
|
|
||||||
|
No MTA-STS or DANE verification is done.
|
||||||
|
|
||||||
|
Hint: Use "mox -loglevel trace smtp dial ..." to see the protocol messages
|
||||||
|
exchanged during connection set up.
|
||||||
|
|
||||||
|
usage: mox smtp dial host[:port]
|
||||||
|
-ehlohostname string
|
||||||
|
our hostname to use during the SMTP EHLO command
|
||||||
|
-forcetls
|
||||||
|
use TLS, even if remote SMTP server does not announce STARTTLS extension
|
||||||
|
-notls
|
||||||
|
do not use TLS
|
||||||
|
-remotehostname string
|
||||||
|
remote hostname to use for TLS verification, if enabled; the hostname from the parameter is used by default
|
||||||
|
-tlscerts string
|
||||||
|
path to root ca certificates in pem form, for verification
|
||||||
|
-tlsciphersuites string
|
||||||
|
ciphersuites to allow, comma-separated, order is ignored, only for TLS 1.2 and earlier, empty value uses TLS stack defaults; values: tls_ecdhe_ecdsa_with_aes_128_cbc_sha, tls_ecdhe_ecdsa_with_aes_128_gcm_sha256, tls_ecdhe_ecdsa_with_aes_256_cbc_sha, tls_ecdhe_ecdsa_with_aes_256_gcm_sha384, tls_ecdhe_ecdsa_with_chacha20_poly1305_sha256, tls_ecdhe_rsa_with_aes_128_cbc_sha, tls_ecdhe_rsa_with_aes_128_gcm_sha256, tls_ecdhe_rsa_with_aes_256_cbc_sha, tls_ecdhe_rsa_with_aes_256_gcm_sha384, tls_ecdhe_rsa_with_chacha20_poly1305_sha256, and insecure: tls_ecdhe_ecdsa_with_aes_128_cbc_sha256, tls_ecdhe_ecdsa_with_rc4_128_sha, tls_ecdhe_rsa_with_3des_ede_cbc_sha, tls_ecdhe_rsa_with_aes_128_cbc_sha256, tls_ecdhe_rsa_with_rc4_128_sha, tls_rsa_with_3des_ede_cbc_sha, tls_rsa_with_aes_128_cbc_sha, tls_rsa_with_aes_128_cbc_sha256, tls_rsa_with_aes_128_gcm_sha256, tls_rsa_with_aes_256_cbc_sha, tls_rsa_with_aes_256_gcm_sha384, tls_rsa_with_rc4_128_sha
|
||||||
|
-tlscurves string
|
||||||
|
tls ecc key exchange mechanisms to allow, comma-separated, order is ignored, empty value uses TLS stack defaults; values: curvep256, curvep384, curvep521, x25519, x25519mlkem768
|
||||||
|
-tlsnodynamicrecordsizing
|
||||||
|
disable TLS dynamic record sizing
|
||||||
|
-tlsnosessiontickets
|
||||||
|
disable TLS session tickets
|
||||||
|
-tlsrenegotiation string
|
||||||
|
when to allow renegotiation; only applies to tls1.2 and earlier, not tls1.3; values: never, once, always (default "never")
|
||||||
|
-tlsverify
|
||||||
|
verify remote hostname during TLS
|
||||||
|
-tlsversionmax string
|
||||||
|
maximum TLS version, empty value uses TLS stack default; values: tls1.2, etc.
|
||||||
|
-tlsversionmin string
|
||||||
|
minimum TLS version, empty value uses TLS stack default; values: tls1.2, etc.
|
||||||
|
|
||||||
# mox spf check
|
# mox spf check
|
||||||
|
|
||||||
Check the status of IP for the policy published in DNS for the domain.
|
Check the status of IP for the policy published in DNS for the domain.
|
||||||
|
224
main.go
224
main.go
@ -12,6 +12,7 @@ import (
|
|||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"crypto/sha512"
|
"crypto/sha512"
|
||||||
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@ -23,6 +24,7 @@ import (
|
|||||||
"io/fs"
|
"io/fs"
|
||||||
"log"
|
"log"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"maps"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
@ -201,6 +203,7 @@ var commands = []struct {
|
|||||||
{"rdap domainage", cmdRDAPDomainage},
|
{"rdap domainage", cmdRDAPDomainage},
|
||||||
{"retrain", cmdRetrain},
|
{"retrain", cmdRetrain},
|
||||||
{"sendmail", cmdSendmail},
|
{"sendmail", cmdSendmail},
|
||||||
|
{"smtp dial", cmdSMTPDial},
|
||||||
{"spf check", cmdSPFCheck},
|
{"spf check", cmdSPFCheck},
|
||||||
{"spf lookup", cmdSPFLookup},
|
{"spf lookup", cmdSPFLookup},
|
||||||
{"spf parse", cmdSPFParse},
|
{"spf parse", cmdSPFParse},
|
||||||
@ -1897,6 +1900,227 @@ with DKIM, by mox.
|
|||||||
xcheckf(err, "writing rsa private key")
|
xcheckf(err, "writing rsa private key")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// todo: options for specifying the domain this is the mx host of, and enabling dane and/or mta-sts verification
|
||||||
|
func cmdSMTPDial(c *cmd) {
|
||||||
|
c.params = "host[:port]"
|
||||||
|
|
||||||
|
var tlsCerts, tlsCiphersuites, tlsCurves, tlsVersionMin, tlsVersionMax, tlsRenegotiation string
|
||||||
|
var tlsVerify, noTLS, forceTLS, tlsNoSessionTickets, tlsNoDynamicRecordSizing bool
|
||||||
|
var ehloHostnameStr, remoteHostnameStr string
|
||||||
|
|
||||||
|
ciphersuites := map[string]*tls.CipherSuite{}
|
||||||
|
ciphersuitesInsecure := map[string]*tls.CipherSuite{}
|
||||||
|
for _, v := range tls.CipherSuites() {
|
||||||
|
if slices.Contains(v.SupportedVersions, tls.VersionTLS10) || slices.Contains(v.SupportedVersions, tls.VersionTLS11) || slices.Contains(v.SupportedVersions, tls.VersionTLS12) {
|
||||||
|
ciphersuites[strings.ToLower(v.Name)] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, v := range tls.InsecureCipherSuites() {
|
||||||
|
if slices.Contains(v.SupportedVersions, tls.VersionTLS10) || slices.Contains(v.SupportedVersions, tls.VersionTLS11) || slices.Contains(v.SupportedVersions, tls.VersionTLS12) {
|
||||||
|
ciphersuitesInsecure[strings.ToLower(v.Name)] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
curvesList := []tls.CurveID{
|
||||||
|
tls.CurveP256,
|
||||||
|
tls.CurveP384,
|
||||||
|
tls.CurveP521,
|
||||||
|
tls.X25519,
|
||||||
|
tls.X25519MLKEM768,
|
||||||
|
}
|
||||||
|
curves := map[string]tls.CurveID{}
|
||||||
|
for _, a := range curvesList {
|
||||||
|
curves[strings.ToLower(a.String())] = a
|
||||||
|
}
|
||||||
|
|
||||||
|
c.flag.StringVar(&tlsCiphersuites, "tlsciphersuites", "", "ciphersuites to allow, comma-separated, order is ignored, only for TLS 1.2 and earlier, empty value uses TLS stack defaults; values: "+strings.Join(slices.Sorted(maps.Keys(ciphersuites)), ", ")+", and insecure: "+strings.Join(slices.Sorted(maps.Keys(ciphersuitesInsecure)), ", "))
|
||||||
|
c.flag.StringVar(&tlsCurves, "tlscurves", "", "tls ecc key exchange mechanisms to allow, comma-separated, order is ignored, empty value uses TLS stack defaults; values: "+strings.Join(slices.Sorted(maps.Keys(curves)), ", "))
|
||||||
|
c.flag.StringVar(&tlsCerts, "tlscerts", "", "path to root ca certificates in pem form, for verification")
|
||||||
|
c.flag.StringVar(&tlsVersionMin, "tlsversionmin", "", "minimum TLS version, empty value uses TLS stack default; values: tls1.2, etc.")
|
||||||
|
c.flag.StringVar(&tlsVersionMax, "tlsversionmax", "", "maximum TLS version, empty value uses TLS stack default; values: tls1.2, etc.")
|
||||||
|
c.flag.BoolVar(&tlsVerify, "tlsverify", false, "verify remote hostname during TLS")
|
||||||
|
c.flag.BoolVar(&tlsNoSessionTickets, "tlsnosessiontickets", false, "disable TLS session tickets")
|
||||||
|
c.flag.BoolVar(&tlsNoDynamicRecordSizing, "tlsnodynamicrecordsizing", false, "disable TLS dynamic record sizing")
|
||||||
|
c.flag.BoolVar(&noTLS, "notls", false, "do not use TLS")
|
||||||
|
c.flag.BoolVar(&forceTLS, "forcetls", false, "use TLS, even if remote SMTP server does not announce STARTTLS extension")
|
||||||
|
c.flag.StringVar(&tlsRenegotiation, "tlsrenegotiation", "never", "when to allow renegotiation; only applies to tls1.2 and earlier, not tls1.3; values: never, once, always")
|
||||||
|
c.flag.StringVar(&ehloHostnameStr, "ehlohostname", "", "our hostname to use during the SMTP EHLO command")
|
||||||
|
c.flag.StringVar(&remoteHostnameStr, "remotehostname", "", "remote hostname to use for TLS verification, if enabled; the hostname from the parameter is used by default")
|
||||||
|
|
||||||
|
c.help = `Dial the address, initialize the SMTP session, including using STARTTLS to enable TLS if the server supports it.
|
||||||
|
|
||||||
|
If no port is specified, SMTP port 25 is used.
|
||||||
|
|
||||||
|
Data is copied between connection and stdin/stdout until either side closes the
|
||||||
|
connection.
|
||||||
|
|
||||||
|
The flags influence the TLS configuration, useful for debugging interoperability
|
||||||
|
issues.
|
||||||
|
|
||||||
|
No MTA-STS or DANE verification is done.
|
||||||
|
|
||||||
|
Hint: Use "mox -loglevel trace smtp dial ..." to see the protocol messages
|
||||||
|
exchanged during connection set up.
|
||||||
|
`
|
||||||
|
args := c.Parse()
|
||||||
|
if len(args) != 1 {
|
||||||
|
c.Usage()
|
||||||
|
}
|
||||||
|
|
||||||
|
if noTLS && forceTLS {
|
||||||
|
log.Fatalf("cannot have both -notls and -forcetls")
|
||||||
|
}
|
||||||
|
|
||||||
|
parseTLSVersion := func(s string) uint16 {
|
||||||
|
switch s {
|
||||||
|
case "tls1.0":
|
||||||
|
return tls.VersionTLS10
|
||||||
|
case "tls1.1":
|
||||||
|
return tls.VersionTLS11
|
||||||
|
case "tls1.2":
|
||||||
|
return tls.VersionTLS12
|
||||||
|
case "tls1.3":
|
||||||
|
return tls.VersionTLS13
|
||||||
|
case "":
|
||||||
|
return 0
|
||||||
|
default:
|
||||||
|
log.Fatalf("invalid tls version %q", s)
|
||||||
|
panic("not reached")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tlsConfig := tls.Config{
|
||||||
|
MinVersion: parseTLSVersion(tlsVersionMin),
|
||||||
|
MaxVersion: parseTLSVersion(tlsVersionMax),
|
||||||
|
InsecureSkipVerify: !tlsVerify,
|
||||||
|
SessionTicketsDisabled: tlsNoSessionTickets,
|
||||||
|
DynamicRecordSizingDisabled: tlsNoDynamicRecordSizing,
|
||||||
|
}
|
||||||
|
|
||||||
|
switch tlsRenegotiation {
|
||||||
|
case "never":
|
||||||
|
tlsConfig.Renegotiation = tls.RenegotiateNever
|
||||||
|
case "once":
|
||||||
|
tlsConfig.Renegotiation = tls.RenegotiateOnceAsClient
|
||||||
|
case "always":
|
||||||
|
tlsConfig.Renegotiation = tls.RenegotiateFreelyAsClient
|
||||||
|
default:
|
||||||
|
log.Fatalf("invalid value %q for -tlsrenegotation", tlsRenegotiation)
|
||||||
|
}
|
||||||
|
if tlsCerts != "" {
|
||||||
|
pool := x509.NewCertPool()
|
||||||
|
pembuf, err := os.ReadFile(tlsCerts)
|
||||||
|
xcheckf(err, "reading tls certificates")
|
||||||
|
ok := pool.AppendCertsFromPEM(pembuf)
|
||||||
|
if !ok {
|
||||||
|
c.log.Warn("no tls certificates found", slog.String("path", tlsCerts))
|
||||||
|
}
|
||||||
|
tlsConfig.RootCAs = pool
|
||||||
|
}
|
||||||
|
if tlsCiphersuites != "" {
|
||||||
|
for s := range strings.SplitSeq(tlsCiphersuites, ",") {
|
||||||
|
s = strings.TrimSpace(s)
|
||||||
|
c, ok := ciphersuites[s]
|
||||||
|
if !ok {
|
||||||
|
c, ok = ciphersuitesInsecure[s]
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
log.Fatalf("unknown ciphersuite %q", s)
|
||||||
|
}
|
||||||
|
tlsConfig.CipherSuites = append(tlsConfig.CipherSuites, c.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if tlsCurves != "" {
|
||||||
|
for s := range strings.SplitSeq(tlsCurves, ",") {
|
||||||
|
s = strings.TrimSpace(s)
|
||||||
|
if c, ok := curves[s]; !ok {
|
||||||
|
log.Fatalf("unknown ecc key exchange algorithm %q", s)
|
||||||
|
} else {
|
||||||
|
tlsConfig.CurvePreferences = append(tlsConfig.CurvePreferences, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var host, portStr string
|
||||||
|
var err error
|
||||||
|
host, portStr, err = net.SplitHostPort(args[0])
|
||||||
|
if err != nil {
|
||||||
|
host = args[0]
|
||||||
|
portStr = "25"
|
||||||
|
}
|
||||||
|
port, err := strconv.ParseInt(portStr, 10, 64)
|
||||||
|
xcheckf(err, "parsing port %q", portStr)
|
||||||
|
|
||||||
|
if remoteHostnameStr == "" {
|
||||||
|
remoteHostnameStr = host
|
||||||
|
}
|
||||||
|
remoteHostname, err := dns.ParseDomain(remoteHostnameStr)
|
||||||
|
xcheckf(err, "parsing remote host")
|
||||||
|
tlsConfig.ServerName = remoteHostname.Name()
|
||||||
|
|
||||||
|
resolver := dns.StrictResolver{Pkg: "smtpdial"}
|
||||||
|
_, _, _, ips, _, err := smtpclient.GatherIPs(context.Background(), c.log.Logger, resolver, "ip", dns.IPDomain{Domain: remoteHostname}, nil)
|
||||||
|
xcheckf(err, "resolve host")
|
||||||
|
c.log.Info("resolved remote address", slog.Any("ips", ips))
|
||||||
|
|
||||||
|
dialer := &net.Dialer{Timeout: 5 * time.Second}
|
||||||
|
dialedIPs := map[string][]net.IP{}
|
||||||
|
conn, ip, err := smtpclient.Dial(context.Background(), c.log.Logger, dialer, dns.IPDomain{Domain: remoteHostname}, ips, int(port), dialedIPs, nil)
|
||||||
|
xcheckf(err, "dial")
|
||||||
|
c.log.Info("connected to remote host", slog.Any("ip", ip))
|
||||||
|
|
||||||
|
tlsMode := smtpclient.TLSOpportunistic
|
||||||
|
if forceTLS {
|
||||||
|
tlsMode = smtpclient.TLSRequiredStartTLS
|
||||||
|
} else if noTLS {
|
||||||
|
tlsMode = smtpclient.TLSSkip
|
||||||
|
}
|
||||||
|
var ehloHostname dns.Domain
|
||||||
|
if ehloHostnameStr == "" {
|
||||||
|
name, err := os.Hostname()
|
||||||
|
xcheckf(err, "get hostname")
|
||||||
|
ehloHostnameStr = name
|
||||||
|
}
|
||||||
|
ehloHostname, err = dns.ParseDomain(ehloHostnameStr)
|
||||||
|
xcheckf(err, "parse hostname")
|
||||||
|
|
||||||
|
opts := smtpclient.Opts{
|
||||||
|
TLSConfig: &tlsConfig,
|
||||||
|
}
|
||||||
|
client, err := smtpclient.New(context.Background(), c.log.Logger, conn, tlsMode, false, ehloHostname, dns.Domain{}, opts)
|
||||||
|
xcheckf(err, "new smtp client")
|
||||||
|
|
||||||
|
cs := client.TLSConnectionState()
|
||||||
|
if cs == nil {
|
||||||
|
c.log.Info("smtp initialized without tls")
|
||||||
|
} else {
|
||||||
|
c.log.Info("smtp initialized with tls",
|
||||||
|
slog.String("version", tls.VersionName(cs.Version)),
|
||||||
|
slog.String("ciphersuite", strings.ToLower(tls.CipherSuiteName(cs.CipherSuite))),
|
||||||
|
slog.String("sni", cs.ServerName),
|
||||||
|
)
|
||||||
|
for _, chain := range cs.VerifiedChains {
|
||||||
|
var l []string
|
||||||
|
for _, cert := range chain {
|
||||||
|
s := fmt.Sprintf("dns names %q, common name %q, %s - %s, issuer %q)", strings.Join(cert.DNSNames, ","), cert.Subject.CommonName, cert.NotBefore.Format("2006-01-02T15:04:05"), cert.NotAfter.Format("2006-01-02T15:04:05"), cert.Issuer.CommonName)
|
||||||
|
l = append(l, s)
|
||||||
|
}
|
||||||
|
c.log.Info("tls certificate verification chain", slog.String("chain", strings.Join(l, "; ")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err = client.Conn()
|
||||||
|
xcheckf(err, "get smtp session connection")
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
_, err := io.Copy(os.Stdout, conn)
|
||||||
|
xcheckf(err, "copy from connection to stdout")
|
||||||
|
err = conn.Close()
|
||||||
|
c.log.Check(err, "closing connection")
|
||||||
|
}()
|
||||||
|
_, err = io.Copy(conn, os.Stdin)
|
||||||
|
xcheckf(err, "copy from stdin to connection")
|
||||||
|
}
|
||||||
|
|
||||||
func cmdDANEDial(c *cmd) {
|
func cmdDANEDial(c *cmd) {
|
||||||
c.params = "host:port"
|
c.params = "host:port"
|
||||||
var usages string
|
var usages string
|
||||||
|
@ -114,6 +114,7 @@ type Client struct {
|
|||||||
daneMoreHostnames []dns.Domain // Additional allowed names in TLS certificate for DANE-TA.
|
daneMoreHostnames []dns.Domain // Additional allowed names in TLS certificate for DANE-TA.
|
||||||
daneVerifiedRecord *adns.TLSA // If non-nil, then will be set to verified DANE record if any.
|
daneVerifiedRecord *adns.TLSA // If non-nil, then will be set to verified DANE record if any.
|
||||||
clientCert *tls.Certificate // If non-nil, tls client authentication is done.
|
clientCert *tls.Certificate // If non-nil, tls client authentication is done.
|
||||||
|
tlsConfigOpts *tls.Config // If non-nil, tls config to use.
|
||||||
|
|
||||||
// TLS connection success/failure are added. These are always non-nil, regardless
|
// TLS connection success/failure are added. These are always non-nil, regardless
|
||||||
// of what was passed in opts. It lets us unconditionally dereference them.
|
// of what was passed in opts. It lets us unconditionally dereference them.
|
||||||
@ -235,6 +236,12 @@ type Opts struct {
|
|||||||
// tracked.
|
// tracked.
|
||||||
RecipientDomainResult *tlsrpt.Result // MTA-STS or no policy.
|
RecipientDomainResult *tlsrpt.Result // MTA-STS or no policy.
|
||||||
HostResult *tlsrpt.Result // DANE or no policy.
|
HostResult *tlsrpt.Result // DANE or no policy.
|
||||||
|
|
||||||
|
// If not nil, the TLS config to use instead of the default. Useful for custom
|
||||||
|
// certificate verification or TLS parameters. The other DANE/TLS/certificate
|
||||||
|
// fields in [Opts], and the tlsVerifyPKIX and remoteHostname parameters to [New]
|
||||||
|
// have no effect when TLSConfig is set.
|
||||||
|
TLSConfig *tls.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
// New initializes an SMTP session on the given connection, returning a client that
|
// New initializes an SMTP session on the given connection, returning a client that
|
||||||
@ -290,6 +297,7 @@ func New(ctx context.Context, elog *slog.Logger, conn net.Conn, tlsMode TLSMode,
|
|||||||
cmds: []string{"(none)"},
|
cmds: []string{"(none)"},
|
||||||
recipientDomainResult: ensureResult(opts.RecipientDomainResult),
|
recipientDomainResult: ensureResult(opts.RecipientDomainResult),
|
||||||
hostResult: ensureResult(opts.HostResult),
|
hostResult: ensureResult(opts.HostResult),
|
||||||
|
tlsConfigOpts: opts.TLSConfig,
|
||||||
}
|
}
|
||||||
c.log = mlog.New("smtpclient", elog).WithFunc(func() []slog.Attr {
|
c.log = mlog.New("smtpclient", elog).WithFunc(func() []slog.Attr {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
@ -354,6 +362,10 @@ func (c *Client) tlsConfig() *tls.Config {
|
|||||||
// failures. And we may have to verify both PKIX and DANE, record errors for
|
// failures. And we may have to verify both PKIX and DANE, record errors for
|
||||||
// each, and possibly ignore the errors.
|
// each, and possibly ignore the errors.
|
||||||
|
|
||||||
|
if c.tlsConfigOpts != nil {
|
||||||
|
return c.tlsConfigOpts
|
||||||
|
}
|
||||||
|
|
||||||
verifyConnection := func(cs tls.ConnectionState) error {
|
verifyConnection := func(cs tls.ConnectionState) error {
|
||||||
// Collect verification errors. If there are none at the end, TLS validation
|
// Collect verification errors. If there are none at the end, TLS validation
|
||||||
// succeeded. We may find validation problems below, record them for a TLS report
|
// succeeded. We may find validation problems below, record them for a TLS report
|
||||||
@ -1453,9 +1465,10 @@ func (c *Client) Close() (rerr error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Conn returns the connection with initialized SMTP session. Once the caller uses
|
// Conn returns the connection with the initialized SMTP session, possibly wrapping
|
||||||
// this connection it is in control, and responsible for closing the connection,
|
// a TLS connection, and handling protocol trace logging. Once the caller uses this
|
||||||
// and other functions on the client must not be called anymore.
|
// connection it is in control, and responsible for closing the connection, and
|
||||||
|
// other functions on the client must not be called anymore.
|
||||||
func (c *Client) Conn() (net.Conn, error) {
|
func (c *Client) Conn() (net.Conn, error) {
|
||||||
if err := c.conn.SetDeadline(time.Time{}); err != nil {
|
if err := c.conn.SetDeadline(time.Time{}); err != nil {
|
||||||
return nil, fmt.Errorf("clearing io deadlines: %w", err)
|
return nil, fmt.Errorf("clearing io deadlines: %w", err)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user