1/*2Maddy Mail Server - Composable all-in-one email server.3Copyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors45This program is free software: you can redistribute it and/or modify6it under the terms of the GNU General Public License as published by7the Free Software Foundation, either version 3 of the License, or8(at your option) any later version.910This program is distributed in the hope that it will be useful,11but WITHOUT ANY WARRANTY; without even the implied warranty of12MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the13GNU General Public License for more details.1415You should have received a copy of the GNU General Public License16along with this program. If not, see <https://www.gnu.org/licenses/>.17*/1819package testutils2021import (22 "crypto/tls"23 "crypto/x509"24 "fmt"25 "io"26 "net"27 "reflect"28 "sort"29 "sync/atomic"30 "testing"31 "time"3233 "github.com/emersion/go-sasl"34 "github.com/emersion/go-smtp"35 "github.com/foxcpp/maddy/framework/exterrors"36)3738type SMTPMessage struct {39 From string40 Opts smtp.MailOptions41 To []string42 Data []byte43 Conn *smtp.Conn44 AuthUser string45 AuthPass string46}4748type SMTPBackend struct {49 Messages []*SMTPMessage50 MailFromCounter int51 SessionCounter int52 SourceEndpoints map[string]struct{}5354 AuthErr error55 MailErr error56 RcptErr map[string]error57 DataErr error58 LMTPDataErr []error5960 ActiveSessionsCounter atomic.Int3261}6263func (be *SMTPBackend) NewSession(conn *smtp.Conn) (smtp.Session, error) {64 be.SessionCounter++65 be.ActiveSessionsCounter.Add(1)66 if be.SourceEndpoints == nil {67 be.SourceEndpoints = make(map[string]struct{})68 }69 be.SourceEndpoints[conn.Conn().RemoteAddr().String()] = struct{}{}70 return &session{71 backend: be,72 conn: conn,73 }, nil74}7576func (be *SMTPBackend) ConnectionCount() int {77 return int(be.ActiveSessionsCounter.Load())78}7980func (be *SMTPBackend) CheckMsg(t *testing.T, indx int, from string, rcptTo []string) {81 t.Helper()8283 if len(be.Messages) <= indx {84 t.Errorf("Expected at least %d messages in mailbox, got %d", indx+1, len(be.Messages))85 return86 }8788 msg := be.Messages[indx]89 if msg.From != from {90 t.Errorf("Wrong MAIL FROM: %v", msg.From)91 }9293 sort.Strings(msg.To)94 sort.Strings(rcptTo)9596 if !reflect.DeepEqual(msg.To, rcptTo) {97 t.Errorf("Wrong RCPT TO: %v", msg.To)98 }99 if string(msg.Data) != DeliveryData {100 t.Errorf("Wrong DATA payload: %v (%v)", string(msg.Data), msg.Data)101 }102}103104type session struct {105 backend *SMTPBackend106 user string107 password string108 conn *smtp.Conn109 msg *SMTPMessage110}111112func (s *session) AuthMechanisms() []string {113 return []string{sasl.Plain}114}115116func (s *session) Auth(mech string) (sasl.Server, error) {117 if mech != sasl.Plain {118 return nil, fmt.Errorf("mechanisms other than plain are unsupported")119 }120 return sasl.NewPlainServer(func(identity, username, password string) error {121 if s.backend.AuthErr != nil {122 return s.backend.AuthErr123 }124 s.user = username125 s.password = password126 return nil127 }), nil128}129130func (s *session) Reset() {131 s.msg = &SMTPMessage{}132}133134func (s *session) Logout() error {135 s.backend.ActiveSessionsCounter.Add(-1)136 return nil137}138139func (s *session) Mail(from string, opts *smtp.MailOptions) error {140 s.backend.MailFromCounter++141142 if s.backend.MailErr != nil {143 return s.backend.MailErr144 }145146 s.Reset()147 s.msg.From = from148 s.msg.Opts = *opts149 return nil150}151152func (s *session) Rcpt(to string, _ *smtp.RcptOptions) error {153 if err := s.backend.RcptErr[to]; err != nil {154 return err155 }156157 s.msg.To = append(s.msg.To, to)158 return nil159}160161func (s *session) Data(r io.Reader) error {162 if s.backend.DataErr != nil {163 return s.backend.DataErr164 }165166 b, err := io.ReadAll(r)167 if err != nil {168 return err169 }170 s.msg.Data = b171 s.msg.Conn = s.conn172 s.msg.AuthUser = s.user173 s.msg.AuthPass = s.password174 s.backend.Messages = append(s.backend.Messages, s.msg)175 return nil176}177178func (s *session) LMTPData(r io.Reader, status smtp.StatusCollector) error {179 if s.backend.DataErr != nil {180 return s.backend.DataErr181 }182183 b, err := io.ReadAll(r)184 if err != nil {185 return err186 }187 s.msg.Data = b188 s.msg.Conn = s.conn189 s.msg.AuthUser = s.user190 s.msg.AuthPass = s.password191 s.backend.Messages = append(s.backend.Messages, s.msg)192193 for i, rcpt := range s.msg.To {194 status.SetStatus(rcpt, s.backend.LMTPDataErr[i])195 }196197 return nil198}199200type SMTPServerConfigureFunc func(*smtp.Server)201202func SMTPServer(t *testing.T, addr string, fn ...SMTPServerConfigureFunc) (*SMTPBackend, *smtp.Server) {203 t.Helper()204205 l, err := net.Listen("tcp", addr)206 if err != nil {207 t.Fatal(err)208 }209210 be := new(SMTPBackend)211 s := smtp.NewServer(be)212 s.Domain = "localhost"213 s.AllowInsecureAuth = true214 for _, f := range fn {215 f(s)216 }217218 go func() {219 if err := s.Serve(l); err != nil {220 t.Error(err)221 }222 }()223224 // Dial it once it make sure Server completes its initialization before225 // we try to use it. Notably, if test fails before connecting to the server,226 // it will call Server.Close which will call Server.listener.Close with a227 // nil Server.listener (Serve sets it to a non-nil value, so it is racy and228 // happens only sometimes).229 testConn, err := net.Dial("tcp", addr)230 if err != nil {231 t.Fatal(err)232 }233 testConn.Close()234235 return be, s236}237238// RSA 1024, valid for *.example.invalid, 127.0.0.1, 127.0.0.2,, 127.0.0.3239// until Nov 18 17:13:45 2029 GMT.240const testServerCert = `-----BEGIN CERTIFICATE-----241MIICDzCCAXigAwIBAgIRAJ1x+qCW7L+Hs6sRU8BHmWkwDQYJKoZIhvcNAQELBQAw242EjEQMA4GA1UEChMHQWNtZSBDbzAeFw0xOTExMTgxNzEzNDVaFw0yOTExMTUxNzEz243NDVaMBIxEDAOBgNVBAoTB0FjbWUgQ28wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJ244AoGBAPINKMyuu3AvzndLDS2/BroA+DRUcAhWPBxMxG1b1BkkHisAZWteKajKmwdO245O13N8HHBRPPOD56AAPLZGNxYLHn6nel7AiH8k40/xC5tDOthqA82+00fwJHDFCnW246oDLOLcO17HulPvfCSWfefc+uee4kajPa+47hutqZH2bGMTXhAgMBAAGjZTBjMA4G247A1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATAMBgNVHRMBAf8EAjAA248MC4GA1UdEQQnMCWCESouZXhhbXBsZS5pbnZhbGlkhwR/AAABhwR/AAAChwR/AAAD249MA0GCSqGSIb3DQEBCwUAA4GBAGRn3C2NbwR4cyQmTRm5jcaqi1kAYyEu6U8Q9PJW250Q15BXMKUTx2lw//QScK9MH2JpKxDuzWDSvaxZMnTxgri2uiplqpe8ydsWj6Wl0q92512XMGJ9LIxTZk5+cyZP2uOolvmSP/q8VFTyk9Udl6KUZPQyoiiDq4rBFUIxUyb+bX252pHkR253-----END CERTIFICATE-----`254255const testServerKey = `-----BEGIN PRIVATE KEY-----256MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBAPINKMyuu3AvzndL257DS2/BroA+DRUcAhWPBxMxG1b1BkkHisAZWteKajKmwdOO13N8HHBRPPOD56AAPLZ258GNxYLHn6nel7AiH8k40/xC5tDOthqA82+00fwJHDFCnWoDLOLcO17HulPvfCSWfe259fc+uee4kajPa+47hutqZH2bGMTXhAgMBAAECgYEAgPjSDH3uEdDnSlkLJJzskJ+D260oR58s3R/gvTElSCg2uSLzo3ffF4oBHAwOqxMpabdvz8j5mSdne7Gkp9qx72TtEG2261wt6uX1tZhm2UTAkInH8IQDthj98P8vAWQsS6HHEIMErsrW2CyUrAt/+o1BRg/hWW262zixA3CLTthhZTJkaUCECQQD5EM16UcTAKfhr3IZppgq+ZsAOMkeCl3XVV9gHo32i263DL6UFAb27BAYyjfcZB1fPou4RszX0Ryu9yU0P5qm6N47AkEA+MpdAPkaPziY0ok4264e9Tcee6P0mIR+/AHk9GliVX2P74DDoOHyMXOSRBwdb+z2tYjrdjkNEL1Txe+sHny265k/EukwJBAOBqlmqPwNNRPeiaRHZvSSD0XjqsbSirJl48D4gadPoNt66fOQNGAt8D266Xj/z6U9HgQdiq/IOFmVEhT5FzSh1jL8CQQD3Myth8iGQO84tM0c6U3CWfuHMqsEv2670XnV+HNAmHdLMqOa4joi1dh4ZKs5dDdi828UJ/PnsbhI1FEWzLSpJvWdAkAkVWqf268AC/TvWvEZLA6Z5CllyNzZJ7XvtIaNOosxHDolyZ1HMWMlfEb2K2ZXWLy5foKPeoY269Xi3olS9rB0J+Rvjz270-----END PRIVATE KEY-----`271272// SMTPServerSTARTTLS starts a server listening on the specified addr with the273// STARTTLS extension supported.274//275// Returned *tls.Config is for the client and is set to trust the server276// certificate.277func SMTPServerSTARTTLS(t *testing.T, addr string, fn ...SMTPServerConfigureFunc) (*tls.Config, *SMTPBackend, *smtp.Server) {278 t.Helper()279280 cert, err := tls.X509KeyPair([]byte(testServerCert), []byte(testServerKey))281 if err != nil {282 panic(err)283 }284285 l, err := net.Listen("tcp", addr)286 if err != nil {287 t.Fatal(err)288 }289290 be := new(SMTPBackend)291 s := smtp.NewServer(be)292 s.Domain = "localhost"293 s.AllowInsecureAuth = true294 s.TLSConfig = &tls.Config{295 Certificates: []tls.Certificate{cert},296 }297 for _, f := range fn {298 f(s)299 }300301 pool := x509.NewCertPool()302 pool.AppendCertsFromPEM([]byte(testServerCert))303304 clientCfg := &tls.Config{305 ServerName: "127.0.0.1",306 Time: func() time.Time {307 return time.Date(2019, time.November, 18, 17, 59, 41, 0, time.UTC)308 },309 RootCAs: pool,310 }311312 go func() {313 if err := s.Serve(l); err != nil {314 t.Error(err)315 }316 }()317318 // Dial it once it make sure Server completes its initialization before319 // we try to use it. Notably, if test fails before connecting to the server,320 // it will call Server.Close which will call Server.listener.Close with a321 // nil Server.listener (Serve sets it to a non-nil value, so it is racy and322 // happens only sometimes).323 testConn, err := net.Dial("tcp", addr)324 if err != nil {325 t.Fatal(err)326 }327 testConn.Close()328329 return clientCfg, be, s330}331332// SMTPServerTLS starts a SMTP server listening on the specified addr with333// Implicit TLS.334func SMTPServerTLS(t *testing.T, addr string, fn ...SMTPServerConfigureFunc) (*tls.Config, *SMTPBackend, *smtp.Server) {335 t.Helper()336337 cert, err := tls.X509KeyPair([]byte(testServerCert), []byte(testServerKey))338 if err != nil {339 panic(err)340 }341342 l, err := tls.Listen("tcp", addr, &tls.Config{343 Certificates: []tls.Certificate{cert},344 })345 if err != nil {346 t.Fatal(err)347 }348349 be := new(SMTPBackend)350 s := smtp.NewServer(be)351 s.Domain = "localhost"352 for _, f := range fn {353 f(s)354 }355356 pool := x509.NewCertPool()357 pool.AppendCertsFromPEM([]byte(testServerCert))358359 clientCfg := &tls.Config{360 ServerName: "127.0.0.1",361 Time: func() time.Time {362 return time.Date(2019, time.November, 18, 17, 59, 41, 0, time.UTC)363 },364 RootCAs: pool,365 }366367 go func() {368 if err := s.Serve(l); err != nil {369 t.Error(err)370 }371 }()372373 // Dial it once it make sure Server completes its initialization before374 // we try to use it. Notably, if test fails before connecting to the server,375 // it will call Server.Close which will call Server.listener.Close with a376 // nil Server.listener (Serve sets it to a non-nil value, so it is racy and377 // happens only sometimes).378 testConn, err := net.Dial("tcp", addr)379 if err != nil {380 t.Fatal(err)381 }382 testConn.Close()383384 return clientCfg, be, s385}386387type smtpBackendConnCounter interface {388 ConnectionCount() int389}390391func CheckSMTPConnLeak(t *testing.T, srv *smtp.Server) {392 t.Helper()393394 ccb, ok := srv.Backend.(smtpBackendConnCounter)395 if !ok {396 t.Error("CheckSMTPConnLeak used for smtp.Server with backend without ConnectionCount method")397 return398 }399400 // Connection closure is handled asynchronously, so before failing401 // wait a bit for handleQuit in go-smtp to do its work.402 for i := 0; i < 10; i++ {403 if ccb.ConnectionCount() == 0 {404 return405 }406 time.Sleep(100 * time.Millisecond)407 }408 t.Error("Non-closed connections present after test completion")409}410411func WaitForConnsClose(t *testing.T, srv *smtp.Server) {412 t.Helper()413 CheckSMTPConnLeak(t, srv)414}415416// FailOnConn fails the test if attempt is made to connect the417// specified endpoint.418func FailOnConn(t *testing.T, addr string) net.Listener {419 t.Helper()420421 tarpit, err := net.Listen("tcp", addr)422 if err != nil {423 t.Fatal(err)424 }425 go func() {426 t.Helper()427428 _, err := tarpit.Accept()429 if err == nil {430 t.Error("No connection expected")431 }432 }()433 return tarpit434}435436func CheckSMTPErr(t *testing.T, err error, code int, enchCode exterrors.EnhancedCode, msg string) {437 t.Helper()438439 if err == nil {440 t.Error("Expected an error, got none")441 return442 }443444 fields := exterrors.Fields(err)445 if val, _ := fields["smtp_code"].(int); val != code {446 t.Errorf("Wrong smtp_code: %v", val)447 }448 if val, _ := fields["smtp_enchcode"].(exterrors.EnhancedCode); val != enchCode {449 t.Errorf("Wrong smtp_enchcode: %v", val)450 }451 if val, _ := fields["smtp_msg"].(string); val != msg {452 t.Errorf("Wrong smtp_msg: %v", val)453 }454}