Billionmail/BillionMail · error
new SMTP client
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.
Solutions
- Confirm the port speaks SMTPS (usually 465); test with openssl s_client -connect host:port and check the banner starts with '220'.
- If the server only supports STARTTLS, switch the sender to the plain path (port 587) instead of implicit SSL.
- Inspect the wrapped error (errors.Unwrap) — 'EOF' usually means the peer closed immediately; a protocol error means it is not SMTP.
- 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
- Only use implicit TLS against ports that actually speak SMTPS (465).
- Verify with openssl s_client that the banner begins with 220 before configuring.
- Avoid pointing the sender at TLS-terminating proxies that do not proxy raw SMTP.
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.
Appendix: 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)