maddy

Fork https://github.com/foxcpp/maddy

git clone git://git.lin.moe/go/maddy.git

  1/*
  2Maddy Mail Server - Composable all-in-one email server.
  3Copyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors
  4
  5This program is free software: you can redistribute it and/or modify
  6it under the terms of the GNU General Public License as published by
  7the Free Software Foundation, either version 3 of the License, or
  8(at your option) any later version.
  9
 10This program is distributed in the hope that it will be useful,
 11but WITHOUT ANY WARRANTY; without even the implied warranty of
 12MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 13GNU General Public License for more details.
 14
 15You should have received a copy of the GNU General Public License
 16along with this program.  If not, see <https://www.gnu.org/licenses/>.
 17*/
 18
 19package testutils
 20
 21import (
 22	"crypto/tls"
 23	"crypto/x509"
 24	"fmt"
 25	"io"
 26	"net"
 27	"reflect"
 28	"sort"
 29	"sync/atomic"
 30	"testing"
 31	"time"
 32
 33	"github.com/emersion/go-sasl"
 34	"github.com/emersion/go-smtp"
 35	"github.com/foxcpp/maddy/framework/exterrors"
 36)
 37
 38type SMTPMessage struct {
 39	From     string
 40	Opts     smtp.MailOptions
 41	To       []string
 42	Data     []byte
 43	Conn     *smtp.Conn
 44	AuthUser string
 45	AuthPass string
 46}
 47
 48type SMTPBackend struct {
 49	Messages        []*SMTPMessage
 50	MailFromCounter int
 51	SessionCounter  int
 52	SourceEndpoints map[string]struct{}
 53
 54	AuthErr     error
 55	MailErr     error
 56	RcptErr     map[string]error
 57	DataErr     error
 58	LMTPDataErr []error
 59
 60	ActiveSessionsCounter atomic.Int32
 61}
 62
 63func (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	}, nil
 74}
 75
 76func (be *SMTPBackend) ConnectionCount() int {
 77	return int(be.ActiveSessionsCounter.Load())
 78}
 79
 80func (be *SMTPBackend) CheckMsg(t *testing.T, indx int, from string, rcptTo []string) {
 81	t.Helper()
 82
 83	if len(be.Messages) <= indx {
 84		t.Errorf("Expected at least %d messages in mailbox, got %d", indx+1, len(be.Messages))
 85		return
 86	}
 87
 88	msg := be.Messages[indx]
 89	if msg.From != from {
 90		t.Errorf("Wrong MAIL FROM: %v", msg.From)
 91	}
 92
 93	sort.Strings(msg.To)
 94	sort.Strings(rcptTo)
 95
 96	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}
103
104type session struct {
105	backend  *SMTPBackend
106	user     string
107	password string
108	conn     *smtp.Conn
109	msg      *SMTPMessage
110}
111
112func (s *session) AuthMechanisms() []string {
113	return []string{sasl.Plain}
114}
115
116func (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.AuthErr
123		}
124		s.user = username
125		s.password = password
126		return nil
127	}), nil
128}
129
130func (s *session) Reset() {
131	s.msg = &SMTPMessage{}
132}
133
134func (s *session) Logout() error {
135	s.backend.ActiveSessionsCounter.Add(-1)
136	return nil
137}
138
139func (s *session) Mail(from string, opts *smtp.MailOptions) error {
140	s.backend.MailFromCounter++
141
142	if s.backend.MailErr != nil {
143		return s.backend.MailErr
144	}
145
146	s.Reset()
147	s.msg.From = from
148	s.msg.Opts = *opts
149	return nil
150}
151
152func (s *session) Rcpt(to string, _ *smtp.RcptOptions) error {
153	if err := s.backend.RcptErr[to]; err != nil {
154		return err
155	}
156
157	s.msg.To = append(s.msg.To, to)
158	return nil
159}
160
161func (s *session) Data(r io.Reader) error {
162	if s.backend.DataErr != nil {
163		return s.backend.DataErr
164	}
165
166	b, err := io.ReadAll(r)
167	if err != nil {
168		return err
169	}
170	s.msg.Data = b
171	s.msg.Conn = s.conn
172	s.msg.AuthUser = s.user
173	s.msg.AuthPass = s.password
174	s.backend.Messages = append(s.backend.Messages, s.msg)
175	return nil
176}
177
178func (s *session) LMTPData(r io.Reader, status smtp.StatusCollector) error {
179	if s.backend.DataErr != nil {
180		return s.backend.DataErr
181	}
182
183	b, err := io.ReadAll(r)
184	if err != nil {
185		return err
186	}
187	s.msg.Data = b
188	s.msg.Conn = s.conn
189	s.msg.AuthUser = s.user
190	s.msg.AuthPass = s.password
191	s.backend.Messages = append(s.backend.Messages, s.msg)
192
193	for i, rcpt := range s.msg.To {
194		status.SetStatus(rcpt, s.backend.LMTPDataErr[i])
195	}
196
197	return nil
198}
199
200type SMTPServerConfigureFunc func(*smtp.Server)
201
202func SMTPServer(t *testing.T, addr string, fn ...SMTPServerConfigureFunc) (*SMTPBackend, *smtp.Server) {
203	t.Helper()
204
205	l, err := net.Listen("tcp", addr)
206	if err != nil {
207		t.Fatal(err)
208	}
209
210	be := new(SMTPBackend)
211	s := smtp.NewServer(be)
212	s.Domain = "localhost"
213	s.AllowInsecureAuth = true
214	for _, f := range fn {
215		f(s)
216	}
217
218	go func() {
219		if err := s.Serve(l); err != nil {
220			t.Error(err)
221		}
222	}()
223
224	// Dial it once it make sure Server completes its initialization before
225	// 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 a
227	// nil Server.listener (Serve sets it to a non-nil value, so it is racy and
228	// happens only sometimes).
229	testConn, err := net.Dial("tcp", addr)
230	if err != nil {
231		t.Fatal(err)
232	}
233	testConn.Close()
234
235	return be, s
236}
237
238// RSA 1024, valid for *.example.invalid, 127.0.0.1, 127.0.0.2,, 127.0.0.3
239// until Nov 18 17:13:45 2029 GMT.
240const testServerCert = `-----BEGIN CERTIFICATE-----
241MIICDzCCAXigAwIBAgIRAJ1x+qCW7L+Hs6sRU8BHmWkwDQYJKoZIhvcNAQELBQAw
242EjEQMA4GA1UEChMHQWNtZSBDbzAeFw0xOTExMTgxNzEzNDVaFw0yOTExMTUxNzEz
243NDVaMBIxEDAOBgNVBAoTB0FjbWUgQ28wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJ
244AoGBAPINKMyuu3AvzndLDS2/BroA+DRUcAhWPBxMxG1b1BkkHisAZWteKajKmwdO
245O13N8HHBRPPOD56AAPLZGNxYLHn6nel7AiH8k40/xC5tDOthqA82+00fwJHDFCnW
246oDLOLcO17HulPvfCSWfefc+uee4kajPa+47hutqZH2bGMTXhAgMBAAGjZTBjMA4G
247A1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATAMBgNVHRMBAf8EAjAA
248MC4GA1UdEQQnMCWCESouZXhhbXBsZS5pbnZhbGlkhwR/AAABhwR/AAAChwR/AAAD
249MA0GCSqGSIb3DQEBCwUAA4GBAGRn3C2NbwR4cyQmTRm5jcaqi1kAYyEu6U8Q9PJW
250Q15BXMKUTx2lw//QScK9MH2JpKxDuzWDSvaxZMnTxgri2uiplqpe8ydsWj6Wl0q9
2512XMGJ9LIxTZk5+cyZP2uOolvmSP/q8VFTyk9Udl6KUZPQyoiiDq4rBFUIxUyb+bX
252pHkR
253-----END CERTIFICATE-----`
254
255const testServerKey = `-----BEGIN PRIVATE KEY-----
256MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBAPINKMyuu3AvzndL
257DS2/BroA+DRUcAhWPBxMxG1b1BkkHisAZWteKajKmwdOO13N8HHBRPPOD56AAPLZ
258GNxYLHn6nel7AiH8k40/xC5tDOthqA82+00fwJHDFCnWoDLOLcO17HulPvfCSWfe
259fc+uee4kajPa+47hutqZH2bGMTXhAgMBAAECgYEAgPjSDH3uEdDnSlkLJJzskJ+D
260oR58s3R/gvTElSCg2uSLzo3ffF4oBHAwOqxMpabdvz8j5mSdne7Gkp9qx72TtEG2
261wt6uX1tZhm2UTAkInH8IQDthj98P8vAWQsS6HHEIMErsrW2CyUrAt/+o1BRg/hWW
262zixA3CLTthhZTJkaUCECQQD5EM16UcTAKfhr3IZppgq+ZsAOMkeCl3XVV9gHo32i
263DL6UFAb27BAYyjfcZB1fPou4RszX0Ryu9yU0P5qm6N47AkEA+MpdAPkaPziY0ok4
264e9Tcee6P0mIR+/AHk9GliVX2P74DDoOHyMXOSRBwdb+z2tYjrdjkNEL1Txe+sHny
265k/EukwJBAOBqlmqPwNNRPeiaRHZvSSD0XjqsbSirJl48D4gadPoNt66fOQNGAt8D
266Xj/z6U9HgQdiq/IOFmVEhT5FzSh1jL8CQQD3Myth8iGQO84tM0c6U3CWfuHMqsEv
2670XnV+HNAmHdLMqOa4joi1dh4ZKs5dDdi828UJ/PnsbhI1FEWzLSpJvWdAkAkVWqf
268AC/TvWvEZLA6Z5CllyNzZJ7XvtIaNOosxHDolyZ1HMWMlfEb2K2ZXWLy5foKPeoY
269Xi3olS9rB0J+Rvjz
270-----END PRIVATE KEY-----`
271
272// SMTPServerSTARTTLS starts a server listening on the specified addr with the
273// STARTTLS extension supported.
274//
275// Returned *tls.Config is for the client and is set to trust the server
276// certificate.
277func SMTPServerSTARTTLS(t *testing.T, addr string, fn ...SMTPServerConfigureFunc) (*tls.Config, *SMTPBackend, *smtp.Server) {
278	t.Helper()
279
280	cert, err := tls.X509KeyPair([]byte(testServerCert), []byte(testServerKey))
281	if err != nil {
282		panic(err)
283	}
284
285	l, err := net.Listen("tcp", addr)
286	if err != nil {
287		t.Fatal(err)
288	}
289
290	be := new(SMTPBackend)
291	s := smtp.NewServer(be)
292	s.Domain = "localhost"
293	s.AllowInsecureAuth = true
294	s.TLSConfig = &tls.Config{
295		Certificates: []tls.Certificate{cert},
296	}
297	for _, f := range fn {
298		f(s)
299	}
300
301	pool := x509.NewCertPool()
302	pool.AppendCertsFromPEM([]byte(testServerCert))
303
304	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	}
311
312	go func() {
313		if err := s.Serve(l); err != nil {
314			t.Error(err)
315		}
316	}()
317
318	// Dial it once it make sure Server completes its initialization before
319	// 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 a
321	// nil Server.listener (Serve sets it to a non-nil value, so it is racy and
322	// happens only sometimes).
323	testConn, err := net.Dial("tcp", addr)
324	if err != nil {
325		t.Fatal(err)
326	}
327	testConn.Close()
328
329	return clientCfg, be, s
330}
331
332// SMTPServerTLS starts a SMTP server listening on the specified addr with
333// Implicit TLS.
334func SMTPServerTLS(t *testing.T, addr string, fn ...SMTPServerConfigureFunc) (*tls.Config, *SMTPBackend, *smtp.Server) {
335	t.Helper()
336
337	cert, err := tls.X509KeyPair([]byte(testServerCert), []byte(testServerKey))
338	if err != nil {
339		panic(err)
340	}
341
342	l, err := tls.Listen("tcp", addr, &tls.Config{
343		Certificates: []tls.Certificate{cert},
344	})
345	if err != nil {
346		t.Fatal(err)
347	}
348
349	be := new(SMTPBackend)
350	s := smtp.NewServer(be)
351	s.Domain = "localhost"
352	for _, f := range fn {
353		f(s)
354	}
355
356	pool := x509.NewCertPool()
357	pool.AppendCertsFromPEM([]byte(testServerCert))
358
359	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	}
366
367	go func() {
368		if err := s.Serve(l); err != nil {
369			t.Error(err)
370		}
371	}()
372
373	// Dial it once it make sure Server completes its initialization before
374	// 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 a
376	// nil Server.listener (Serve sets it to a non-nil value, so it is racy and
377	// happens only sometimes).
378	testConn, err := net.Dial("tcp", addr)
379	if err != nil {
380		t.Fatal(err)
381	}
382	testConn.Close()
383
384	return clientCfg, be, s
385}
386
387type smtpBackendConnCounter interface {
388	ConnectionCount() int
389}
390
391func CheckSMTPConnLeak(t *testing.T, srv *smtp.Server) {
392	t.Helper()
393
394	ccb, ok := srv.Backend.(smtpBackendConnCounter)
395	if !ok {
396		t.Error("CheckSMTPConnLeak used for smtp.Server with backend without ConnectionCount method")
397		return
398	}
399
400	// Connection closure is handled asynchronously, so before failing
401	// 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			return
405		}
406		time.Sleep(100 * time.Millisecond)
407	}
408	t.Error("Non-closed connections present after test completion")
409}
410
411func WaitForConnsClose(t *testing.T, srv *smtp.Server) {
412	t.Helper()
413	CheckSMTPConnLeak(t, srv)
414}
415
416// FailOnConn fails the test if attempt is made to connect the
417// specified endpoint.
418func FailOnConn(t *testing.T, addr string) net.Listener {
419	t.Helper()
420
421	tarpit, err := net.Listen("tcp", addr)
422	if err != nil {
423		t.Fatal(err)
424	}
425	go func() {
426		t.Helper()
427
428		_, err := tarpit.Accept()
429		if err == nil {
430			t.Error("No connection expected")
431		}
432	}()
433	return tarpit
434}
435
436func CheckSMTPErr(t *testing.T, err error, code int, enchCode exterrors.EnhancedCode, msg string) {
437	t.Helper()
438
439	if err == nil {
440		t.Error("Expected an error, got none")
441		return
442	}
443
444	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}