Skip to content

AUTH LOGIN sends initial response instead of waiting for server challenge #117

@arashnn0uri

Description

@arashnn0uri

Version

v2.10.0 (also tested on latest)

Description

When connecting to an SMTP server that advertises only AUTH LOGIN (no STARTTLS, no AUTH PLAIN), authentication always fails with 535 Authentication Failed.

Server capabilities

250-localhost
250-AUTH LOGIN
250 HELP

No STARTTLS, no SSL — plain connection on port 587 with EncryptionNone.

Root cause

In smtp.go, the authenticate() function sends the initial response immediately alongside AUTH LOGIN:

code, msg64, err := c.cmd(0, strings.TrimSpace(fmt.Sprintf("AUTH %s %s", mech, resp64)))

This sends AUTH LOGIN <base64> in one shot. However, the AUTH LOGIN protocol requires a challenge-response flow:

  1. Client sends: AUTH LOGIN
  2. Server responds: 334 VXNlcm5hbWU6 (base64 of "Username:")
  3. Client sends: <base64 username>
  4. Server responds: 334 UGFzc3dvcmQ6 (base64 of "Password:")
  5. Client sends: <base64 password>
  6. Server responds: 235 Authentication successful

By sending the initial response immediately, the library skips step 2 and confuses the server, causing 535.

Proof

The same credentials work perfectly using net/smtp with a custom LoginAuth that returns nil as the initial response:

func (a *loginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) {
    return "LOGIN", nil, nil // sends just "AUTH LOGIN", then waits for challenge
}

func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
    if more {
        switch string(fromServer) {
        case "Username:":
            return []byte(a.username), nil
        case "Password:":
            return []byte(a.password), nil
        default:
            return nil, fmt.Errorf("unexpected server challenge: %s", fromServer)
        }
    }
    return nil, nil
}

Expected behavior

AUTH LOGIN should send no initial response and wait for the server's 334 Username: challenge before sending credentials.

Suggested fix

In authenticate(), when the mechanism is LOGIN, the initial resp returned by a.start() should be nil, and the first cmd should send only AUTH LOGIN without appending any base64 data.

Thanks for maintaining this library — it's been really useful. I hope this report helps improve compatibility with servers that strictly follow the AUTH LOGIN challenge-response flow.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions