semaphoreui/semaphore · error

unencrypted connection

Error message

unencrypted connection

What it means

The PLAIN SMTP auth implementation refuses to send credentials unless the connection is TLS-secured or the server is localhost. Per RFC, ServerInfo cannot be trusted on a plaintext connection (an attacker could advertise PLAIN), so Start rejects it outright with this error.

Solutions

  1. Enable TLS in the mailer configuration (STARTTLS or implicit TLS on port 465) so server.TLS is true.
  2. If this is genuinely a local relay, connect via localhost so isLocalhost passes.
  3. Configure the SMTP server to require and support STARTTLS; do not weaken the client to send credentials unencrypted.

Example fix

// before
smtpConfig := mailer.SMTPConfig{Host: "smtp.example.com", Port: 25, TLS: false}
// after
smtpConfig := mailer.SMTPConfig{Host: "smtp.example.com", Port: 587, TLS: true} // STARTTLS enforced
Defensive patterns

Strategy: validation

Validate before calling

if !smtpConfig.TLS && !isLocalhost(smtpConfig.Host) {
    return errors.New("SMTP config must enable TLS (or use localhost) before PLAIN auth")
}

Try / catch

ok, enc, err := auth.Start(&smtp.ServerInfo{Name: host, TLS: tlsOn, Auth: mechs})
if err != nil && err.Error() == "unencrypted connection" {
    return fmt.Errorf("enable STARTTLS/TLS on the SMTP connection (or use localhost); credentials are refused in plaintext: %w", err)
}

Prevention

When it happens

Trigger: Using mailer plainAuth with an smtp.Client whose ServerInfo.TLS is false and whose server name is not localhost - i.e. SMTP over a non-TLS connection to a remote host, typically on port 25 without STARTTLS.

Common situations: SMTP config without TLS/STARTTLS enabled pointing at a remote relay; implicit-TLS port (465) misconfigured as plaintext; TLS handshake silently failed but auth still attempted.

Related errors


AI-assisted analysis of semaphoreui/semaphore@1774ccb71a (2026-09-07). Data as JSON: /api/errors/394c2d82961b49e0. Report an issue: GitHub.

Appendix: source

Thrown at util/mailer/auth.go:33

func isLocalhost(name string) bool {
	return name == "localhost" || name == "127.0.0.1" || name == "::1"
}

type plainOrLoginAuth struct {
	username   string
	password   string
	host       string
	authMethod string
}

func (a *plainOrLoginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) {
	// Must have TLS, or else localhost server.
	// Note: If TLS is not true, then we can't trust ANYTHING in ServerInfo.
	// In particular, it doesn't matter if the server advertises PLAIN auth.
	// That might just be the attacker saying
	// "it's ok, you can trust me with your password."
	if !server.TLS && !isLocalhost(server.Name) {
		return "", nil, errors.New("unencrypted connection")
	}
	if server.Name != a.host {
		return "", nil, errors.New("wrong host name")
	}
	if !slices.Contains(server.Auth, "PLAIN") {
		a.authMethod = "LOGIN"
		return a.authMethod, nil, nil
	} else {
		a.authMethod = "PLAIN"
		resp := []byte("\x00" + a.username + "\x00" + a.password)
		return a.authMethod, resp, nil
	}
}

func (a *plainOrLoginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
	if !more {
		return nil, nil
	}

View on GitHub (pinned to 1774ccb71a)