Merge branch 'outbound'
authorRobert Sesek <rsesek@bluestatic.org>
Sun, 7 Jun 2020 04:26:43 +0000 (00:26 -0400)
committerRobert Sesek <rsesek@bluestatic.org>
Sun, 7 Jun 2020 04:26:43 +0000 (00:26 -0400)
 Conflicts:
smtp_test.go

1  2 
pop3.go
smtp/conn.go
smtp_test.go

diff --combined pop3.go
index 56e37711ca8d2196220b651fd8026f5d0d0601b0,085cfdfe09ed46170c0c825a5400ed577a0ee594..20f94bf6a36cfb5c6c618a242962757c89eb9230
+++ b/pop3.go
@@@ -104,7 -104,7 +104,7 @@@ func (server *pop3Server) Name() strin
  
  func (server *pop3Server) OpenMailbox(user, pass string) (pop3.Mailbox, error) {
        for _, s := range server.config.Servers {
-               if user == "mailbox@"+s.Domain && pass == s.MailboxPassword {
+               if user == MailboxAccount+s.Domain && pass == s.MailboxPassword {
                        return server.openMailbox(s.MaildropPath)
                }
        }
@@@ -177,7 -177,7 +177,7 @@@ func (mb *mailbox) ListMessages() ([]po
  }
  
  func (mb *mailbox) GetMessage(id int) pop3.Message {
 -      if id > len(mb.messages) {
 +      if id == 0 || id > len(mb.messages) {
                return nil
        }
        return &mb.messages[id-1]
diff --combined smtp/conn.go
index 8c59c2d4c3252a59ccca00765c56d79a30fd6ac8,9d807d437e9fcf9148928c483c21cb25952b8a8d..9020073086237127febeddcce57375aa1814f009
@@@ -7,8 -7,9 +7,9 @@@
  package smtp
  
  import (
-       "crypto/rand"
+       "bytes"
        "crypto/tls"
+       "encoding/base64"
        "fmt"
        "net"
        "net/mail"
@@@ -29,6 -30,26 +30,26 @@@ const 
        stateData
  )
  
+ type delivery int
+ func (d delivery) String() string {
+       switch d {
+       case deliverUnknown:
+               return "unknown"
+       case deliverInbound:
+               return "inbound"
+       case deliverOutbound:
+               return "outbound"
+       }
+       panic("Unknown delivery")
+ }
+ const (
+       deliverUnknown  delivery = iota
+       deliverInbound           // Mail is not from one of this server's domains.
+       deliverOutbound          // Mail IS from one of this server's domains.
+ )
  type connection struct {
        server Server
  
  
        log *zap.Logger
  
+       // The authcid from a PLAIN SASL login. Non-empty iff tls is non-nil and
+       // doAUTH() succeeded.
+       authc string
        state
        line string
  
+       delivery
+       // For deliverOutbound, replaces the From and Reply-To values.
+       sendAs *mail.Address
        ehlo     string
        mailFrom *mail.Address
        rcptTo   []mail.Address
@@@ -73,7 -102,12 +102,12 @@@ func AcceptConnection(netConn net.Conn
                        return
                }
  
-               conn.log.Info("ReadLine()", zap.String("line", conn.line))
+               lineForLog := conn.line
+               const authPlain = "AUTH PLAIN "
+               if strings.HasPrefix(conn.line, authPlain) {
+                       lineForLog = authPlain + "[redacted]"
+               }
+               conn.log.Info("ReadLine()", zap.String("line", lineForLog))
  
                var cmd string
                if _, err = fmt.Sscanf(conn.line, "%s", &cmd); err != nil {
                        conn.doEHLO()
                case "STARTTLS":
                        conn.doSTARTTLS()
+               case "AUTH":
+                       conn.doAUTH()
                case "MAIL":
                        conn.doMAIL()
                case "RCPT":
@@@ -171,6 -207,9 +207,9 @@@ func (conn *connection) doEHLO() 
                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)
        }
  
@@@ -210,6 -249,72 +249,72 @@@ func (conn *connection) doSTARTTLS() 
        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, authString string
+       n, err := fmt.Sscanf(conn.line, "%s %s %s", &cmd, &authType, &authString)
+       if n < 2 {
+               conn.reply(ReplyBadSyntax)
+               return
+       }
+       if authType != "PLAIN" {
+               conn.writeReply(504, "unrecognized auth type")
+               return
+       }
+       // If only 2 tokens were scanned, then an initial response was not provided.
+       if n == 2 && conn.line[len(conn.line)-1] != ' ' {
+               conn.reply(ReplyBadSyntax)
+               return
+       }
+       conn.log.Info("doAUTH()")
+       if authString == "" {
+               conn.writeReply(334, " ")
+               authString, 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(authString)
+       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)
                return
        }
  
+       if conn.server.VerifyAddress(*conn.mailFrom) == ReplyOK {
+               if DomainForAddress(*conn.mailFrom) != DomainForAddressString(conn.authc) {
+                       conn.writeReply(550, "not authenticated")
+                       return
+               }
+               conn.delivery = deliverOutbound
+       } else {
+               conn.delivery = deliverInbound
+       }
        conn.log.Info("doMAIL()", zap.String("address", conn.mailFrom.Address))
  
        conn.state = stateMail
@@@ -253,7 -368,7 +368,7 @@@ func (conn *connection) doRCPT() 
                return
        }
  
-       if reply := conn.server.VerifyAddress(*address); reply != ReplyOK {
+       if reply := conn.server.VerifyAddress(*address); reply != ReplyOK && conn.delivery == deliverInbound {
                conn.log.Warn("invalid address",
                        zap.String("address", address.Address),
                        zap.Stringer("reply", reply))
                return
        }
  
-       conn.log.Info("doRCPT()", zap.String("address", address.Address))
+       conn.log.Info("doRCPT()",
+               zap.String("address", address.Address),
+               zap.String("delivery", conn.delivery.String()))
  
        conn.rcptTo = append(conn.rcptTo, *address)
  
@@@ -294,46 -411,106 +411,106 @@@ func (conn *connection) doDATA() 
                MailFrom:   *conn.mailFrom,
                RcptTo:     conn.rcptTo,
                Received:   received,
-               ID:         conn.envelopeID(received),
+               ID:         generateEnvelopeId("m", received),
+               Data:       data,
        }
  
+       conn.handleSendAs(&env)
        conn.log.Info("received message",
                zap.Int("bytes", len(data)),
                zap.Time("date", received),
-               zap.String("id", env.ID))
+               zap.String("id", env.ID),
+               zap.String("delivery", conn.delivery.String()))
  
        trace := conn.getReceivedInfo(env)
  
-       env.Data = append(trace, data...)
+       env.Data = append(trace, env.Data...)
  
-       if reply := conn.server.OnMessageDelivered(env); reply != nil {
-               conn.log.Warn("message was rejected", zap.String("id", env.ID))
-               conn.reply(*reply)
-               return
+       if conn.delivery == deliverInbound {
+               if reply := conn.server.OnMessageDelivered(env); reply != nil {
+                       conn.log.Warn("message was rejected", zap.String("id", env.ID))
+                       conn.reply(*reply)
+                       return
+               }
+       } else if conn.delivery == deliverOutbound {
+               conn.server.RelayMessage(env)
        }
  
        conn.state = stateInitial
+       conn.resetBuffers()
        conn.reply(ReplyOK)
  }
  
- func (conn *connection) envelopeID(t time.Time) string {
-       var idBytes [4]byte
-       rand.Read(idBytes[:])
-       return fmt.Sprintf("m.%d.%x", t.UnixNano(), idBytes)
- }
+ func (conn *connection) handleSendAs(env *Envelope) {
+       if conn.delivery != deliverOutbound {
+               return
+       }
  
- func (conn *connection) getReceivedInfo(envelope Envelope) []byte {
-       rhost, _, err := net.SplitHostPort(conn.remoteAddr.String())
-       if err != nil {
-               rhost = conn.remoteAddr.String()
+       // Find the separator between the message header and body.
+       headerIdx := bytes.Index(env.Data, []byte("\n\n"))
+       if headerIdx == -1 {
+               conn.log.Error("send-as: could not find headers index")
+               return
        }
  
-       rhosts, err := net.LookupAddr(rhost)
-       if err == nil {
-               rhost = fmt.Sprintf("%s [%s]", rhosts[0], rhost)
+       var buf bytes.Buffer
+       headers := bytes.SplitAfter(env.Data[:headerIdx], []byte("\n"))
+       var fromIdx, subjectIdx int
+       for i, header := range headers {
+               if bytes.HasPrefix(header, []byte("From:")) {
+                       fromIdx = i
+                       continue
+               }
+               if bytes.HasPrefix(header, []byte("Subject:")) {
+                       subjectIdx = i
+                       continue
+               }
+       }
+       if subjectIdx == -1 {
+               conn.log.Error("send-as: could not find Subject header")
+               return
+       }
+       if fromIdx == -1 {
+               conn.log.Error("send-as: could not find From header")
+               return
        }
  
-       base := fmt.Sprintf("Received: from %s (%s)\r\n        ", conn.ehlo, rhost)
+       sendAs := SendAsSubject.FindSubmatchIndex(headers[subjectIdx])
+       if sendAs == nil {
+               // No send-as modification.
+               return
+       }
+       // Submatch 0 is the whole sendas magic. Submatch 1 is the address prefix.
+       sendAsUser := headers[subjectIdx][sendAs[2]:sendAs[3]]
+       sendAsAddress := string(sendAsUser) + "@" + DomainForAddressString(conn.authc)
+       for i, header := range headers {
+               if i == subjectIdx {
+                       buf.Write(header[:sendAs[0]])
+                       buf.Write(header[sendAs[1]:])
+               } else if i == fromIdx {
+                       addressStart := bytes.LastIndexByte(header, byte('<'))
+                       buf.Write(header[:addressStart+1])
+                       buf.WriteString(sendAsAddress)
+                       buf.WriteString(">\n")
+               } else {
+                       buf.Write(header)
+               }
+       }
+       buf.Write(env.Data[headerIdx:])
+       env.Data = buf.Bytes()
+       env.MailFrom.Address = sendAsAddress
+ }
+ func (conn *connection) getReceivedInfo(envelope Envelope) []byte {
+       base := fmt.Sprintf("Received: from %s (%s)\r\n        ", conn.ehlo, lookupRemoteHost(conn.remoteAddr))
  
        with := "SMTP"
        if conn.esmtp {
        }
        base += fmt.Sprintf("by %s (mailpopbox) with %s id %s\r\n        ", conn.server.Name(), with, envelope.ID)
  
-       base += fmt.Sprintf("for <%s>\r\n        ", envelope.RcptTo[0].Address)
+       if len(envelope.RcptTo) > 0 {
+               base += fmt.Sprintf("for <%s>\r\n        ", envelope.RcptTo[0].Address)
+       }
  
        transport := conn.getTransportString()
        date := envelope.Received.Format(time.RFC1123Z) // Same as RFC 5322 ยง 3.3
@@@ -359,38 -538,29 +538,38 @@@ func (conn *connection) getTransportStr
        }
  
        ciphers := map[uint16]string{
 -              tls.TLS_RSA_WITH_RC4_128_SHA:                "TLS_RSA_WITH_RC4_128_SHA",
 -              tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA:           "TLS_RSA_WITH_3DES_EDE_CBC_SHA",
 -              tls.TLS_RSA_WITH_AES_128_CBC_SHA:            "TLS_RSA_WITH_AES_128_CBC_SHA",
 -              tls.TLS_RSA_WITH_AES_256_CBC_SHA:            "TLS_RSA_WITH_AES_256_CBC_SHA",
 -              tls.TLS_RSA_WITH_AES_128_GCM_SHA256:         "TLS_RSA_WITH_AES_128_GCM_SHA256",
 -              tls.TLS_RSA_WITH_AES_256_GCM_SHA384:         "TLS_RSA_WITH_AES_256_GCM_SHA384",
 -              tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA:        "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA",
 -              tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA:    "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA",
 -              tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA:    "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA",
 -              tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA:          "TLS_ECDHE_RSA_WITH_RC4_128_SHA",
 -              tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA:     "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA",
 -              tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA:      "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA",
 -              tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA:      "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA",
 -              tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256:   "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
 -              tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256: "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
 -              tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384:   "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
 -              tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384: "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
 +              tls.TLS_RSA_WITH_RC4_128_SHA:                      "TLS_RSA_WITH_RC4_128_SHA",
 +              tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA:                 "TLS_RSA_WITH_3DES_EDE_CBC_SHA",
 +              tls.TLS_RSA_WITH_AES_128_CBC_SHA:                  "TLS_RSA_WITH_AES_128_CBC_SHA",
 +              tls.TLS_RSA_WITH_AES_256_CBC_SHA:                  "TLS_RSA_WITH_AES_256_CBC_SHA",
 +              tls.TLS_RSA_WITH_AES_128_CBC_SHA256:               "TLS_RSA_WITH_AES_128_CBC_SHA256",
 +              tls.TLS_RSA_WITH_AES_128_GCM_SHA256:               "TLS_RSA_WITH_AES_128_GCM_SHA256",
 +              tls.TLS_RSA_WITH_AES_256_GCM_SHA384:               "TLS_RSA_WITH_AES_256_GCM_SHA384",
 +              tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA:              "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA",
 +              tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA:          "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA",
 +              tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA:          "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA",
 +              tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA:                "TLS_ECDHE_RSA_WITH_RC4_128_SHA",
 +              tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA:           "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA",
 +              tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA:            "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA",
 +              tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA:            "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA",
 +              tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256:       "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256",
 +              tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256:         "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256",
 +              tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256:         "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
 +              tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256:       "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
 +              tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384:         "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
 +              tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384:       "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
 +              tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256:   "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256",
 +              tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256: "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256",
 +              tls.TLS_AES_128_GCM_SHA256:                        "TLS_AES_128_GCM_SHA256",
 +              tls.TLS_AES_256_GCM_SHA384:                        "TLS_AES_256_GCM_SHA384",
 +              tls.TLS_CHACHA20_POLY1305_SHA256:                  "TLS_CHACHA20_POLY1305_SHA256",
        }
        versions := map[uint16]string{
                tls.VersionSSL30: "SSLv3.0",
                tls.VersionTLS10: "TLSv1.0",
                tls.VersionTLS11: "TLSv1.1",
                tls.VersionTLS12: "TLSv1.2",
 +              tls.VersionTLS13: "TLSv1.3",
        }
  
        state := conn.tls
@@@ -421,6 -591,8 +600,8 @@@ func (conn *connection) doRSET() 
  }
  
  func (conn *connection) resetBuffers() {
+       conn.delivery = deliverUnknown
+       conn.sendAs = nil
        conn.mailFrom = nil
        conn.rcptTo = make([]mail.Address, 0)
  }
diff --combined smtp_test.go
index 89eeb3f125ee285ec187d6861d6de8adda449162,2eef868d941de7bfa2b90138c45478c30b8b1a0f..d1239ea99a3bce649d6f04abdfef2f98ed7939c0
 +// mailpopbox
 +// Copyright 2020 Blue Static <https://www.bluestatic.org>
 +// This program is free software licensed under the GNU General Public License,
 +// version 3.0. The full text of the license can be found in LICENSE.txt.
 +// SPDX-License-Identifier: GPL-3.0-only
 +
  package main
  
  import (
 +      "bytes"
 +      "io/ioutil"
 +      "net/mail"
 +      "os"
 +      "path/filepath"
        "testing"
 +
 +      "go.uber.org/zap"
 +
 +      "src.bluestatic.org/mailpopbox/smtp"
  )
  
 -var testConfig = Config{
 -      Servers: []Server{
 -              Server{
 -                      Domain:          "domain1.net",
 -                      MailboxPassword: "d1",
 +func TestVerifyAddress(t *testing.T) {
 +      dir, err := ioutil.TempDir("", "maildrop")
 +      if err != nil {
 +              t.Errorf("Failed to create temp dir: %v", err)
 +              return
 +      }
 +      defer os.RemoveAll(dir)
 +
 +      s := smtpServer{
 +              config: Config{
 +                      Hostname: "mx.example.com",
 +                      Servers: []Server{
 +                              {
 +                                      Domain:       "example.com",
 +                                      MaildropPath: dir,
 +                              },
 +                      },
                },
 -              Server{
 -                      Domain:          "domain2.xyz",
 -                      MailboxPassword: "d2",
 +              log: zap.NewNop(),
 +      }
 +
 +      if s.VerifyAddress(mail.Address{Address: "example@example.com"}) != smtp.ReplyOK {
 +              t.Errorf("Valid mailbox is not reported to be valid")
 +      }
 +      if s.VerifyAddress(mail.Address{Address: "mailbox@example.com"}) != smtp.ReplyOK {
 +              t.Errorf("Valid mailbox is not reported to be valid")
 +      }
 +      if s.VerifyAddress(mail.Address{Address: "hello@other.net"}) == smtp.ReplyOK {
 +              t.Errorf("Invalid mailbox reports to be valid")
 +      }
 +      if s.VerifyAddress(mail.Address{Address: "hello@mx.example.com"}) == smtp.ReplyOK {
 +              t.Errorf("Invalid mailbox reports to be valid")
 +      }
 +      if s.VerifyAddress(mail.Address{Address: "unknown"}) == smtp.ReplyOK {
 +              t.Errorf("Invalid mailbox reports to be valid")
 +      }
 +}
 +
 +func TestMessageDelivery(t *testing.T) {
 +      dir, err := ioutil.TempDir("", "maildrop")
 +      if err != nil {
 +              t.Errorf("Failed to create temp dir: %v", err)
 +              return
 +      }
 +      defer os.RemoveAll(dir)
 +
 +      s := smtpServer{
 +              config: Config{
 +                      Hostname: "mx.example.com",
 +                      Servers: []Server{
 +                              {
 +                                      Domain:       "example.com",
 +                                      MaildropPath: dir,
 +                              },
 +                      },
                },
 -      },
 +              log: zap.NewNop(),
 +      }
 +
 +      env := smtp.Envelope{
 +              MailFrom: mail.Address{Address: "sender@mail.net"},
 +              RcptTo:   []mail.Address{{Address: "receive@example.com"}},
 +              Data:     []byte("Hello, world"),
 +              ID:       "msgid",
 +      }
 +
 +      if rl := s.OnMessageDelivered(env); rl != nil {
 +              t.Errorf("Failed to deliver message: %v", rl)
 +      }
 +
 +      f, err := os.Open(filepath.Join(dir, "msgid.msg"))
 +      if err != nil {
 +              t.Errorf("Failed to open delivered message: %v", err)
 +      }
 +      defer f.Close()
 +
 +      data, err := ioutil.ReadAll(f)
 +      if err != nil {
 +              t.Errorf("Failed to read message: %v", err)
 +      }
 +
 +      if !bytes.Contains(data, env.Data) {
 +              t.Errorf("Could not find expected data in message")
 +      }
  }
 -      server := smtpServer{config: testConfig}
+ func TestAuthenticate(t *testing.T) {
++      server := smtpServer{
++              config: Config{
++                      Servers: []Server{
++                              Server{
++                                      Domain:          "domain1.net",
++                                      MailboxPassword: "d1",
++                              },
++                              Server{
++                                      Domain:          "domain2.xyz",
++                                      MailboxPassword: "d2",
++                              },
++                      },
++              },
++      }
+       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)
+               }
+       }
+ }