Billionmail/BillionMail · error

unexpected server response

Error message

unexpected server response

What it means

customAuth implements smtp.Auth for PLAIN. Its Next method refuses any server challenge (more == true), because PLAIN sends credentials once in Start and expects no further challenge. If the server keeps the SASL dialogue open, the client rejects it as 'unexpected server response'.

Source

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

	Port      string       `json:"port" v:"required"`     // SMTP server port
	Password  string       `json:"password" v:"required"` // SMTP password
	SNI       string       `json:"sni"`                   // SNI for TLS
	client    *smtp.Client // persistent SMTP client connection
	mutex     sync.Mutex   // mutex for thread safety
	connected bool         // connection status
}

type customAuth struct {
	Username, Password string
}

func (a *customAuth) Start(server *smtp.ServerInfo) (string, []byte, error) {
	return "PLAIN", []byte("\x00" + a.Username + "\x00" + a.Password), nil
}

func (a *customAuth) Next(fromServer []byte, more bool) ([]byte, error) {
	if more {
		return nil, errors.New("unexpected server response")
	}
	return nil, nil
}

func NewEmailSender() *EmailSender {
	e := &EmailSender{
		Host:      "localhost",
		Port:      "25",
		connected: false,
	}

	if public.IsRunningInContainer() {
		e.Host = "postfix"
		// e.Port = "587"
		// e.SNI, _ = public.DockerEnv("BILLIONMAIL_HOSTNAME")
	}

	return e

View on GitHub (pinned to fc36c76c05)

Solutions

  1. Verify username/password are correct — servers often re-challenge on bad credentials.
  2. Check the server's advertised AUTH mechanisms (EHLO) and use LOGIN auth if PLAIN isn't the negotiated mechanism.
  3. Ensure the connection uses TLS if the server only accepts AUTH after STARTTLS.
  4. If needed, implement a LOGIN customAuth instead of relying on this PLAIN-only implementation.

Example fix

// before
auth := &customAuth{Username: user, Password: pass}
// after
// confirm PLAIN is supported, else fall back
if serverAuths["PLAIN"] {
    auth = &customAuth{Username: user, Password: pass}
} else {
    auth = smtp.Plain("", user, pass) // or a LOGIN implementation
}
Defensive patterns

Strategy: try-catch

Validate before calling

conn, _ := smtp.Dial(host + ":" + port)
defer conn.Close()
conn.Extension("AUTH") // returns mechanisms map; require "PLAIN"
conn.StartTLS(&tls.Config{ServerName: host})

Try / catch

if _, _, err := auth.Start(serverInfo); err == nil {
    if _, err := auth.Next(nil, true); err != nil && err.Error() == "unexpected server response" {
        // server re-challenged: wrong mechanism or bad credentials
        return fmt.Errorf("PLAIN auth rejected by %s; check credentials and AUTH mechanism", host)
    }
}

Prevention

When it happens

Trigger: SMTP server answers the PLAIN auth with an additional challenge (334 prompt) instead of accepting, e.g. server expects a different mechanism (LOGIN/XOAUTH2) or rejects credentials while continuing the exchange.

Common situations: Authenticating to a server configured for LOGIN rather than PLAIN; wrong credentials causing the server to re-prompt; relay/MTA with non-standard SASL behavior.

Related errors


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