diff --git a/docs/reference/targets/smtp.md b/docs/reference/targets/smtp.md index 7ee56abd..d9fbe46a 100644 --- a/docs/reference/targets/smtp.md +++ b/docs/reference/targets/smtp.md @@ -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. @@ -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 diff --git a/internal/target/smtp/sasl.go b/internal/target/smtp/sasl.go index 75f5d4e5..7f21b762 100644 --- a/internal/target/smtp/sasl.go +++ b/internal/target/smtp/sasl.go @@ -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 { diff --git a/internal/target/smtp/sasl_test.go b/internal/target/smtp/sasl_test.go index 63b52a5e..c929eb20 100644 --- a/internal/target/smtp/sasl_test.go +++ b/internal/target/smtp/sasl_test.go @@ -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, "test@example.invalid", []string{"rcpt@example.invalid"}) + be.CheckMsg(t, 0, "test@example.invalid", []string{"rcpt@example.invalid"}) + 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, "test@example.invalid", []string{"rcpt@example.invalid"}) + 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() diff --git a/internal/testutils/smtp_server.go b/internal/testutils/smtp_server.go index 6fb105a6..97069e2b 100644 --- a/internal/testutils/smtp_server.go +++ b/internal/testutils/smtp_server.go @@ -29,6 +29,7 @@ import ( "testing" "time" + "github.com/emersion/go-sasl" "github.com/emersion/go-smtp" "github.com/foxcpp/maddy/framework/exterrors" ) @@ -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 { @@ -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{ @@ -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)