Billionmail/BillionMail · error

new SMTP client: %w

Error message

new SMTP client: %w

What it means

This error is returned when smtp.NewClient(conn, e.Host) fails after the TLS connection was established. NewClient reads the server greeting (220 banner); it errors if the connection was closed, the greeting is malformed, or the peer is not an SMTP server. The code closes conn before returning to avoid leaking the socket.

Source

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

	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
}

// connectPlain establishes a plain SMTP connection
func (e *EmailSender) connectPlain() error {
	client, err := smtp.Dial(net.JoinHostPort(e.Host, e.Port))
	if err != nil {
		return fmt.Errorf("SMTP dial: %w", err)

View on GitHub (pinned to fc36c76c05)

Solutions

  1. Confirm the port speaks SMTPS (usually 465); test with openssl s_client -connect host:port and check the banner starts with '220'.
  2. If the server only supports STARTTLS, switch the sender to the plain path (port 587) instead of implicit SSL.
  3. Inspect the wrapped error (errors.Unwrap) — 'EOF' usually means the peer closed immediately; a protocol error means it is not SMTP.
  4. Check TLS-terminating middleboxes/proxies and ensure they pass through to the SMTP backend.

Example fix

// before
client, err := smtp.NewClient(conn, e.Host)
// after
client, err := smtp.NewClient(conn, e.Host)
if err != nil {
    conn.Close()
    return fmt.Errorf("new SMTP client for %s: %w (is port %s really SMTPS?)", e.Host, err, e.Port)
}
Defensive patterns

Strategy: validation

Validate before calling

func verifySMTPSBanner(host, port string) error {
    conn, err := tls.DialWithDialer(&net.Dialer{Timeout: 5 * time.Second}, "tcp", net.JoinHostPort(host, port), &tls.Config{ServerName: host})
    if err != nil {
        return err
    }
    defer conn.Close()
    buf := make([]byte, 256)
    conn.SetReadDeadline(time.Now().Add(5 * time.Second))
    n, err := conn.Read(buf)
    if err != nil || n < 3 || string(buf[:3]) != "220" {
        return fmt.Errorf("not an SMTPS endpoint (greeting: %q)", buf[:n])
    }
    return nil
}

Type guard

func isNotSMTPPeer(err error) bool {
    var protoErr *textproto.Error
    return errors.Is(err, io.EOF) || errors.As(err, &protoErr)
}

Try / catch

if err := sender.Send(msg, rcpts); err != nil {
    if strings.Contains(err.Error(), "new SMTP client") {
        // peer greeted incorrectly: verify port/protocol, then alert ops
        log.Printf("endpoint %s:%s is not speaking SMTPS", host, port)
    }
}

Prevention

When it happens

Trigger: connectWithSSL is called (Connect on a secure sender), tls.Dial succeeds, but the server does not send a valid SMTP 220 greeting — e.g. the port hosts HTTP, a non-SMTP TLS service, or a proxy that closes the connection.

Common situations: Pointing the client at port 443/993/other TLS service that is not SMTP; a TLS-terminating proxy (HAProxy/nginx) accepting the handshake but closing the backend; server greeting delayed past a timeout causing EOF; connecting implicit-TLS to port 587 so the plain banner arrives corrupted by TLS framing.

Related errors


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