Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add smtp auth login support #683

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion docs/reference/targets/smtp.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ Refuse to pass messages over plain-text connections.

---

### auth `off` | `plain` _username_ _password_ | `forward` | `external`
### auth `off` | `plain` _username_ _password_ | `login` _username_ _password_ | `forward` | `external`
Default: `off`

Specify the way to authenticate to the remote server.
Expand All @@ -74,6 +74,10 @@ Valid values:
- `off` – No authentication.
- `plain` – Authenticate using specified username-password pair.
**Don't use** this without enforced TLS (`require_tls`).
- `login` - Authenticate using specified username-password pair.
Uses obsolete SASL LOGIN mechanism instead of SASL PLAIN.
This is required for compatibility with some SMTP services (e.g. Office 365).
**Don't use** this without enforced TLS (`require_tls`).
- `forward` – Forward credentials specified by the client.
**Don't use** this without enforced TLS (`require_tls`).
- `external` – Request "external" SASL authentication. This is usually used for
Expand Down
10 changes: 8 additions & 2 deletions internal/target/smtp/sasl.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,18 @@ func saslAuthDirective(_ *config.Map, node config.Node) (interface{}, error) {
}
return sasl.NewPlainClient("", msgMeta.Conn.AuthUser, msgMeta.Conn.AuthPassword), nil
}, nil
case "plain":
case "plain", "login":
if len(node.Args) != 3 {
return nil, config.NodeErr(node, "two additional arguments are required (username, password)")
}
return func(*module.MsgMetadata) (sasl.Client, error) {
return sasl.NewPlainClient("", node.Args[1], node.Args[2]), nil
if node.Args[0] == "plain" {
return sasl.NewPlainClient("", node.Args[1], node.Args[2]), nil
} else if node.Args[0] == "login" {
return sasl.NewLoginClient(node.Args[1], node.Args[2]), nil
} else {
return nil, config.NodeErr(node, "unknown authentication mechanism: %s", node.Args[0])
}
}, nil
case "external":
if len(node.Args) > 1 {
Expand Down
58 changes: 58 additions & 0 deletions internal/target/smtp/sasl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,64 @@ func TestSASL_Plain_AuthFail(t *testing.T) {
}
}

func TestSASL_Login(t *testing.T) {
be, srv := testutils.SMTPServer(t, "127.0.0.1:"+testPort)
defer srv.Close()
defer testutils.CheckSMTPConnLeak(t, srv)

mod := &Downstream{
hostname: "mx.example.invalid",
endpoints: []config.Endpoint{
{
Scheme: "tcp",
Host: "127.0.0.1",
Port: testPort,
},
},
saslFactory: testSaslFactory(t, "login", "test", "testpass"),
log: testutils.Logger(t, "target.smtp"),
}

testutils.DoTestDelivery(t, mod, "[email protected]", []string{"[email protected]"})
be.CheckMsg(t, 0, "[email protected]", []string{"[email protected]"})
if be.Messages[0].AuthUser != "test" {
t.Errorf("Wrong AuthUser: %v", be.Messages[0].AuthUser)
}
if be.Messages[0].AuthPass != "testpass" {
t.Errorf("Wrong AuthPass: %v", be.Messages[0].AuthPass)
}
}

func TestSASL_Login_AuthFail(t *testing.T) {
be, srv := testutils.SMTPServer(t, "127.0.0.1:"+testPort)
defer srv.Close()
defer testutils.CheckSMTPConnLeak(t, srv)

be.AuthErr = &smtp.SMTPError{
Code: 550,
EnhancedCode: smtp.EnhancedCode{5, 1, 2},
Message: "Hey",
}

mod := &Downstream{
hostname: "mx.example.invalid",
endpoints: []config.Endpoint{
{
Scheme: "tcp",
Host: "127.0.0.1",
Port: testPort,
},
},
saslFactory: testSaslFactory(t, "login", "test", "testpass"),
log: testutils.Logger(t, "target.smtp"),
}

_, err := testutils.DoTestDeliveryErr(t, mod, "[email protected]", []string{"[email protected]"})
if err == nil {
t.Error("Expected an error, got none")
}
}

func TestSASL_Forward(t *testing.T) {
be, srv := testutils.SMTPServer(t, "127.0.0.1:"+testPort)
defer srv.Close()
Expand Down
40 changes: 40 additions & 0 deletions internal/testutils/smtp_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
"testing"
"time"

"github.com/emersion/go-sasl"
"github.com/emersion/go-smtp"
"github.com/foxcpp/maddy/framework/exterrors"
)
Expand Down Expand Up @@ -202,6 +203,19 @@ func SMTPServer(t *testing.T, addr string, fn ...SMTPServerConfigureFunc) (*SMTP

be := new(SMTPBackend)
s := smtp.NewServer(be)

// Enable AUTH LOGIN with a custom handler that just calls AuthPlain.
s.EnableAuth(sasl.Login, func(conn *smtp.Conn) sasl.Server {
return sasl.NewLoginServer(func(username, password string) error {
sess := conn.Session()
if sess == nil {
panic("No session when AUTH is called")
}

return sess.AuthPlain(username, password)
})
})

s.Domain = "localhost"
s.AllowInsecureAuth = true
for _, f := range fn {
Expand Down Expand Up @@ -282,6 +296,19 @@ func SMTPServerSTARTTLS(t *testing.T, addr string, fn ...SMTPServerConfigureFunc

be := new(SMTPBackend)
s := smtp.NewServer(be)

// Enable AUTH LOGIN with a custom handler that just calls AuthPlain.
s.EnableAuth(sasl.Login, func(conn *smtp.Conn) sasl.Server {
return sasl.NewLoginServer(func(username, password string) error {
sess := conn.Session()
if sess == nil {
panic("No session when AUTH is called")
}

return sess.AuthPlain(username, password)
})
})

s.Domain = "localhost"
s.AllowInsecureAuth = true
s.TLSConfig = &tls.Config{
Expand Down Expand Up @@ -341,6 +368,19 @@ func SMTPServerTLS(t *testing.T, addr string, fn ...SMTPServerConfigureFunc) (*t

be := new(SMTPBackend)
s := smtp.NewServer(be)

// Enable AUTH LOGIN with a custom handler that just calls AuthPlain.
s.EnableAuth(sasl.Login, func(conn *smtp.Conn) sasl.Server {
return sasl.NewLoginServer(func(username, password string) error {
sess := conn.Session()
if sess == nil {
panic("No session when AUTH is called")
}

return sess.AuthPlain(username, password)
})
})

s.Domain = "localhost"
for _, f := range fn {
f(s)
Expand Down