Skip to content

Commit

Permalink
feat: add smtp auth login support
Browse files Browse the repository at this point in the history
Signed-off-by: Steven Kreitzer <[email protected]>
  • Loading branch information
buroa committed Feb 29, 2024
1 parent 1d6cd8c commit 550d42a
Show file tree
Hide file tree
Showing 4 changed files with 111 additions and 3 deletions.
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

0 comments on commit 550d42a

Please sign in to comment.