Billionmail/BillionMail · error

TLS dial: %w

Error message

TLS dial: %w

What it means

This error wraps the failure of tls.Dial when connectWithSSL tries to open an implicit-TLS (port 465) SMTP connection to e.Host:e.Port. It fires before any SMTP conversation starts, so the root cause is a TCP/TLS-level problem: server unreachable, not listening for TLS, or rejecting the handshake. The original net/tls error is preserved via %w for errors.Is/errors.As inspection.

Source

Thrown at core/internal/service/mail_service/sending.go:214

	if err != nil {
		e.connected = false
		e.client = nil
		return err
	}

	e.connected = true
	return nil
}

// connectWithSSL establishes a secure SMTP connection
func (e *EmailSender) connectWithSSL() error {
	conn, err := tls.Dial("tcp", net.JoinHostPort(e.Host, e.Port), &tls.Config{
		MinVersion:         tls.VersionTLS12,
		InsecureSkipVerify: true,
		ServerName:         e.SNI,
	})
	if err != nil {
		return fmt.Errorf("TLS dial: %w", err)
	}

	client, err := smtp.NewClient(conn, e.Host)
	if err != nil {
		conn.Close()
		return fmt.Errorf("new SMTP client: %w", err)
	}

	auth := smtp.PlainAuth("", e.UserName, e.Password, e.Host)
	if err = client.Auth(auth); err != nil {
		client.Close()
		return fmt.Errorf("SMTP auth: %w", err)
	}

	e.client = client

	return nil
}

View on GitHub (pinned to fc36c76c05)

Solutions

  1. Verify the SMTP host resolves and the port is reachable: nc -zv <host> <port> or openssl s_client -connect host:port.
  2. Make sure port matches the security mode: 465 for implicit TLS (connectWithSSL), 587/25 for STARTTLS/plain (connectPlain).
  3. Check network/firewall rules allow outbound connections to that port from the app host/container.
  4. Use tcpdump or openssl s_client to confirm the TCP handshake completes and inspect the TLS alert the server returns.

Example fix

// before
conn, err := tls.Dial("tcp", net.JoinHostPort(e.Host, e.Port), &tls.Config{ServerName: e.SNI})
// after
if e.SNI == "" {
    e.SNI = e.Host // ensure ServerName is set so the handshake presents correct SNI
}
addr := net.JoinHostPort(e.Host, e.Port)
if !portReachable(e.Host, e.Port) { // pre-check with net.DialTimeout
    return fmt.Errorf("TLS dial: server %s unreachable", addr)
}
conn, err := tls.Dial("tcp", addr, &tls.Config{ServerName: e.SNI})
Defensive patterns

Strategy: validation

Validate before calling

func smtpsReachable(host, port string) error {
    conn, err := net.DialTimeout("tcp", net.JoinHostPort(host, port), 5*time.Second)
    if err != nil {
        return fmt.Errorf("cannot reach %s:%s: %w", host, port, err)
    }
    conn.Close()
    return nil
}
// call before creating/using the sender
if err := smtpsReachable(cfg.SMTPHost, cfg.SMTPPort); err != nil { log.Fatal(err) }

Type guard

func isDialErr(err error) bool {
    var opErr *net.OpError
    var certErr *tls.CertificateVerificationError
    return errors.As(err, &opErr) || errors.As(err, &certErr)
}

Try / catch

if err := sender.Send(msg, rcpts); err != nil {
    var netErr net.Error
    if errors.As(err, &netErr) && netErr.Timeout() {
        // transient: retry with backoff
    } else {
        return fmt.Errorf("TLS dial failed permanently: %w", err)
    }
}

Prevention

When it happens

Trigger: EmailSender.Connect() is called with a secure host/port (isSecure() true, typically port 465) and tls.Dial fails: host unresolvable, port blocked/closed, or TLS handshake rejected by the remote server.

Common situations: Wrong port for implicit SSL (e.g. using 587 with connectWithSSL, since 587 expects STARTTLS); firewall/Docker network blocking outbound 465; hostname typo or internal DNS not resolving; remote mail server down; SNI (e.SNI) empty or mismatched causing handshake failure on strict servers.

Understand the failure class

Related errors


AI-assisted analysis of Billionmail/BillionMail@fc36c76c05 (2026-09-05). Data as JSON: /api/errors/11dcf8f6ca2558de. Report an issue: GitHub.