Billionmail/BillionMail · error

SMTP STARTTLS: %w

Error message

SMTP STARTTLS: %w

What it means

This error is returned when client.StartTLS fails after a successful plain dial on port 587. The server either rejected the STARTTLS command (e.g. 454 TLS not available), does not support it, or the subsequent TLS handshake failed. The client is closed before returning so no socket leaks.

Source

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

	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)
	}

	// Check if STARTTLS is needed
	if e.Port == "587" {
		if err = client.StartTLS(&tls.Config{
			MinVersion:         tls.VersionTLS12,
			InsecureSkipVerify: true,
			ServerName:         e.SNI,
		}); err != nil {
			client.Close()
			return fmt.Errorf("SMTP STARTTLS: %w", err)
		}
	}

	var auth smtp.Auth

	if e.Port == "25" {
		auth = &customAuth{e.UserName, e.Password}
	} else {
		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. Check the server advertises STARTTLS in EHLO (swaks or openssl s_client -starttls smtp -connect host:587).
  2. Set e.SNI to the SMTP hostname so the TLS handshake presents the correct ServerName; an empty SNI is a frequent cause.
  3. If STARTTLS is genuinely unavailable on the relay, use port 25 without STARTTLS or port 465 implicit TLS instead.
  4. Check the wrapped alert code: handshake failures with old servers require server upgrades since MinVersion is fixed at TLS 1.2.

Example fix

// before
if err = client.StartTLS(&tls.Config{
    MinVersion:         tls.VersionTLS12,
    InsecureSkipVerify: true,
    ServerName:         e.SNI,
}); err != nil {
    client.Close()
    return fmt.Errorf("SMTP STARTTLS: %w", err)
}
// after
serverName := e.SNI
if serverName == "" {
    serverName = e.Host // fall back so SNI is always sent
}
if err = client.StartTLS(&tls.Config{
    MinVersion:         tls.VersionTLS12,
    InsecureSkipVerify: true,
    ServerName:         serverName,
}); err != nil {
    client.Close()
    return fmt.Errorf("SMTP STARTTLS to %s: %w", serverName, err)
}
Defensive patterns

Strategy: validation

Validate before calling

func supportsSTARTTLS(host, port, serverName string) error {
    conn, err := smtp.Dial(net.JoinHostPort(host, port))
    if err != nil {
        return err
    }
    defer conn.Close()
    if ok, _ := conn.Extension("STARTTLS"); !ok {
        return fmt.Errorf("%s:%s does not advertise STARTTLS", host, port)
    }
    return conn.StartTLS(&tls.Config{MinVersion: tls.VersionTLS12, ServerName: serverName})
}

Type guard

func isStartTLSErr(err error) bool {
    var protoErr *textproto.Error
    return errors.As(err, &protoErr) || errors.Is(err, tls.RecordHeaderError{}) || strings.Contains(err.Error(), "STARTTLS")
}

Try / catch

if err := sender.Send(msg, rcpts); err != nil {
    if strings.Contains(err.Error(), "SMTP STARTTLS") {
        // fall back to port 465 implicit TLS or plaintext port 25 relay
        log.Printf("STARTTLS failed on %s:%s, switching transport", host, port)
    }
}

Prevention

When it happens

Trigger: connectPlain dials a port-587 server and calls client.StartTLS with MinVersion TLS 1.2 and ServerName e.SNI; the command fails or the handshake fails because STARTTLS is unsupported, e.SNI is empty/mismatched, or the server cannot negotiate TLS 1.2+.

Common situations: Configuring port 587 on a relay that only offers plaintext or already-TLS service; e.SNI left empty so the handshake sends wrong/no SNI and strict servers abort; old server limited to TLS 1.0/1.1 while MinVersion is fixed at 1.2; middleboxes stripping STARTTLS capabilities.

Related errors


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