- [Post Office Protocol - Version 3, RFC 1939](https://tools.ietf.org/html/rfc1939)
- [Simple Mail Transfer Protocol, RFC 5321](https://tools.ietf.org/html/rfc5321)
- [SMTP Service Extension for Secure SMTP over Transport Layer Security, RFC 3207](https://tools.ietf.org/html/rfc3207)
+- [SMTP Service Extension for Authentication, RFC 2554](https://tools.ietf.org/html/rfc2554)
+- [The PLAIN Simple Authentication and Security Layer (SASL) Mechanism, RFC 4616](https://tools.ietf.org/html/rfc4616)
- [POP3 Extension Mechanism, RFC 2449](https://tools.ietf.org/html/rfc2449)
return smtp.ReplyOK
}
+func (server *smtpServer) Authenticate(authz, authc, passwd string) bool {
+ authcAddr, err := mail.ParseAddress(authc)
+ if err != nil {
+ return false
+ }
+
+ authzAddr, err := mail.ParseAddress(authz)
+ if authz != "" && err != nil {
+ return false
+ }
+
+ domain := smtp.DomainForAddress(*authcAddr)
+ for _, s := range server.config.Servers {
+ if domain == s.Domain {
+ authOk := authc == MailboxAccount+s.Domain && passwd == s.MailboxPassword
+ if authzAddr != nil {
+ authOk = authOk && smtp.DomainForAddress(*authzAddr) == domain
+ }
+ return authOk
+ }
+ }
+ return false
+}
+
func (server *smtpServer) OnMessageDelivered(en smtp.Envelope) *smtp.ReplyLine {
maildrop := server.maildropForAddress(en.RcptTo[0])
if maildrop == "" {
import (
"crypto/rand"
"crypto/tls"
+ "encoding/base64"
"fmt"
"net"
"net/mail"
esmtp bool
tls *tls.ConnectionState
+ // The authcid from a PLAIN SASL login. Non-empty iff tls is non-nil and
+ // doAUTH() succeeded.
+ authc string
+
log zap.Logger
state
conn.doEHLO()
case "STARTTLS":
conn.doSTARTTLS()
+ case "AUTH":
+ conn.doAUTH()
case "MAIL":
conn.doMAIL()
case "RCPT":
if conn.server.TLSConfig() != nil && conn.tls == nil {
conn.tp.PrintfLine("250-STARTTLS")
}
+ if conn.tls != nil {
+ conn.tp.PrintfLine("250-AUTH PLAIN")
+ }
conn.tp.PrintfLine("250 SIZE %d", 40960000)
}
conn.log.Info("TLS connection done", zap.String("state", conn.getTransportString()))
}
+func (conn *connection) doAUTH() {
+ if conn.state != stateInitial || conn.tls == nil {
+ conn.reply(ReplyBadSequence)
+ return
+ }
+
+ if conn.authc != "" {
+ conn.writeReply(503, "already authenticated")
+ return
+ }
+
+ var cmd, authType string
+ _, err := fmt.Sscanf(conn.line, "%s %s", &cmd, &authType)
+ if err != nil {
+ conn.reply(ReplyBadSyntax)
+ return
+ }
+
+ if authType != "PLAIN" {
+ conn.writeReply(504, "unrecognized auth type")
+ return
+ }
+
+ conn.log.Info("doAUTH()")
+
+ conn.writeReply(334, " ")
+
+ authLine, err := conn.tp.ReadLine()
+ if err != nil {
+ conn.log.Error("failed to read auth line", zap.Error(err))
+ conn.reply(ReplyBadSyntax)
+ return
+ }
+
+ authBytes, err := base64.StdEncoding.DecodeString(authLine)
+ if err != nil {
+ conn.reply(ReplyBadSyntax)
+ return
+ }
+
+ authParts := strings.Split(string(authBytes), "\x00")
+ if len(authParts) != 3 {
+ conn.log.Error("bad auth line syntax")
+ conn.reply(ReplyBadSyntax)
+ return
+ }
+
+ if !conn.server.Authenticate(authParts[0], authParts[1], authParts[2]) {
+ conn.log.Error("failed to authenticate", zap.String("authc", authParts[1]))
+ conn.writeReply(535, "invalid credentials")
+ return
+ }
+
+ conn.log.Info("authenticated", zap.String("authz", authParts[0]), zap.String("authc", authParts[1]))
+ conn.authc = authParts[1]
+ conn.reply(ReplyOK)
+}
+
func (conn *connection) doMAIL() {
if conn.state != stateInitial {
conn.reply(ReplyBadSequence)
import (
"crypto/tls"
+ "encoding/base64"
"fmt"
"net"
"net/mail"
return l
}
+type userAuth struct {
+ authz, authc, passwd string
+}
+
type testServer struct {
EmptyServerCallbacks
blockList []string
tlsConfig *tls.Config
+ *userAuth
}
func (s *testServer) Name() string {
return ReplyOK
}
+func (s *testServer) Authenticate(authz, authc, passwd string) bool {
+ return s.userAuth.authz == authz &&
+ s.userAuth.authc == authc &&
+ s.userAuth.passwd == passwd
+}
+
func createClient(t *testing.T, addr net.Addr) *textproto.Conn {
conn, err := textproto.Dial(addr.Network(), addr.String())
if err != nil {
}
}
-func TestTLS(t *testing.T) {
- l := runServer(t, &testServer{tlsConfig: getTLSConfig(t)})
- defer l.Close()
-
- nc, err := net.Dial(l.Addr().Network(), l.Addr().String())
+func setupTLSClient(t *testing.T, addr net.Addr) *textproto.Conn {
+ nc, err := net.Dial(addr.Network(), addr.String())
ok(t, err)
conn := textproto.NewConn(nc)
if strings.Contains(resp, "STARTTLS\n") {
t.Errorf("STARTTLS advertised when already started")
}
+
+ return conn
+}
+
+func TestTLS(t *testing.T) {
+ l := runServer(t, &testServer{tlsConfig: getTLSConfig(t)})
+ defer l.Close()
+
+ setupTLSClient(t, l.Addr())
+}
+
+func TestAuthWithoutTLS(t *testing.T) {
+ l := runServer(t, &testServer{})
+ defer l.Close()
+
+ conn := createClient(t, l.Addr())
+ readCodeLine(t, conn, 220)
+
+ ok(t, conn.PrintfLine("EHLO test"))
+ _, resp, err := conn.ReadResponse(250)
+ ok(t, err)
+
+ if strings.Contains(resp, "AUTH") {
+ t.Errorf("AUTH should not be advertised over plaintext")
+ }
+}
+
+func TestAuth(t *testing.T) {
+ l := runServer(t, &testServer{
+ tlsConfig: getTLSConfig(t),
+ userAuth: &userAuth{
+ authz: "-authz-",
+ authc: "-authc-",
+ passwd: "goats",
+ },
+ })
+ defer l.Close()
+
+ conn := setupTLSClient(t, l.Addr())
+
+ b64enc := func(s string) string {
+ return string(base64.StdEncoding.EncodeToString([]byte(s)))
+ }
+
+ runTableTest(t, conn, []requestResponse{
+ {"AUTH", 501, nil},
+ {"AUTH OAUTHBEARER", 504, nil},
+ {"AUTH PLAIN", 334, nil},
+ {b64enc("abc\x00def\x00ghf"), 535, nil},
+ {"AUTH PLAIN", 334, nil},
+ {b64enc("\x00"), 501, nil},
+ {"AUTH PLAIN", 334, nil},
+ {"this isn't base 64", 501, nil},
+ {"AUTH PLAIN", 334, nil},
+ {b64enc("-authz-\x00-authc-\x00goats"), 250, nil},
+ {"AUTH PLAIN", 503, nil}, // already authenticated
+ {"NOOP", 250, nil},
+ })
}
Name() string
TLSConfig() *tls.Config
VerifyAddress(mail.Address) ReplyLine
+ // Verify that the authc+passwd identity can send mail as authz.
+ Authenticate(authz, authc, passwd string) bool
OnMessageDelivered(Envelope) *ReplyLine
}
return ReplyOK
}
+func (*EmptyServerCallbacks) Authenticate(authz, authc, passwd string) bool {
+ return false
+}
+
func (*EmptyServerCallbacks) OnMessageDelivered(Envelope) *ReplyLine {
return nil
}
--- /dev/null
+package main
+
+import (
+ "testing"
+)
+
+var testConfig = Config{
+ Servers: []Server{
+ Server{
+ Domain: "domain1.net",
+ MailboxPassword: "d1",
+ },
+ Server{
+ Domain: "domain2.xyz",
+ MailboxPassword: "d2",
+ },
+ },
+}
+
+func TestAuthenticate(t *testing.T) {
+ server := smtpServer{config: testConfig}
+
+ authTests := []struct {
+ authz, authc, passwd string
+ ok bool
+ }{
+ {"foo@domain1.net", "mailbox@domain1.net", "d1", true},
+ {"", "mailbox@domain1.net", "d1", true},
+ {"foo@domain2.xyz", "mailbox@domain1.xyz", "d1", false},
+ {"foo@domain2.xyz", "mailbox@domain1.xyz", "d2", false},
+ {"foo@domain2.xyz", "mailbox@domain2.xyz", "d2", true},
+ {"invalid", "mailbox@domain2.xyz", "d2", false},
+ {"", "mailbox@domain2.xyz", "d2", true},
+ {"", "", "", false},
+ }
+
+ for i, test := range authTests {
+ actual := server.Authenticate(test.authz, test.authc, test.passwd)
+ if actual != test.ok {
+ t.Errorf("Test %d, got %v, expected %v", i, actual, test.ok)
+ }
+ }
+}