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 smtp_downstream
 20
 21import (
 22	"errors"
 23	"flag"
 24	"math/rand"
 25	"os"
 26	"strconv"
 27	"testing"
 28
 29	"github.com/emersion/go-smtp"
 30	"github.com/foxcpp/maddy/framework/config"
 31	"github.com/foxcpp/maddy/framework/exterrors"
 32	"github.com/foxcpp/maddy/internal/testutils"
 33)
 34
 35var testPort string
 36
 37func TestDownstreamDelivery(t *testing.T) {
 38	be, srv := testutils.SMTPServer(t, "127.0.0.1:"+testPort)
 39	defer srv.Close()
 40	defer testutils.CheckSMTPConnLeak(t, srv)
 41
 42	tarpit := testutils.FailOnConn(t, "127.0.0.2:"+testPort)
 43	defer tarpit.Close()
 44
 45	mod := &Downstream{
 46		hostname: "mx.example.invalid",
 47		endpoints: []config.Endpoint{
 48			{
 49				Scheme: "tcp",
 50				Host:   "127.0.0.1",
 51				Port:   testPort,
 52			},
 53			{
 54				Scheme: "tcp",
 55				Host:   "127.0.0.2",
 56				Port:   testPort,
 57			},
 58		},
 59		log: testutils.Logger(t, "target.smtp"),
 60	}
 61
 62	testutils.DoTestDelivery(t, mod, "test@example.invalid", []string{"rcpt@example.invalid"})
 63	be.CheckMsg(t, 0, "test@example.invalid", []string{"rcpt@example.invalid"})
 64}
 65
 66func TestDownstreamDelivery_LMTP(t *testing.T) {
 67	be, srv := testutils.SMTPServer(t, "127.0.0.1:"+testPort, func(srv *smtp.Server) {
 68		srv.LMTP = true
 69	})
 70	be.LMTPDataErr = []error{
 71		nil,
 72		&smtp.SMTPError{
 73			Code:    501,
 74			Message: "nop",
 75		},
 76	}
 77	defer srv.Close()
 78	defer testutils.CheckSMTPConnLeak(t, srv)
 79
 80	mod := &Downstream{
 81		hostname: "mx.example.invalid",
 82		endpoints: []config.Endpoint{
 83			{
 84				Scheme: "tcp",
 85				Host:   "127.0.0.1",
 86				Port:   testPort,
 87			},
 88		},
 89		modName: "target.lmtp",
 90		lmtp:    true,
 91		log:     testutils.Logger(t, "lmtp_downstream"),
 92	}
 93
 94	sc := make(statusCollector)
 95
 96	testutils.DoTestDeliveryNonAtomic(t, &sc, mod, "test@example.invalid", []string{"rcpt1@example.invalid", "rcpt2@example.invalid"})
 97	be.CheckMsg(t, 0, "test@example.invalid", []string{"rcpt1@example.invalid", "rcpt2@example.invalid"})
 98
 99	if len(sc) != 2 {
100		t.Fatal("Two statuses should be set")
101	}
102	if err := sc["rcpt1@example.invalid"]; err != nil {
103		t.Fatal("Unexpected error for rcpt1:", err)
104	}
105	if sc["rcpt2@example.invalid"] == nil {
106		t.Fatal("Expected an error for rcpt2")
107	}
108	var rcptErr *exterrors.SMTPError
109	if !errors.As(sc["rcpt2@example.invalid"], &rcptErr) {
110		t.Fatalf("Not SMTPError: %T", rcptErr)
111	}
112	if rcptErr.Code != 501 {
113		t.Fatal("Wrong SMTP code:", rcptErr.Code)
114	}
115}
116
117func TestDownstreamDelivery_LMTP_ErrorCoerce(t *testing.T) {
118	be, srv := testutils.SMTPServer(t, "127.0.0.1:"+testPort, func(srv *smtp.Server) {
119		srv.LMTP = true
120	})
121	be.LMTPDataErr = []error{
122		nil,
123		&smtp.SMTPError{
124			Code:    501,
125			Message: "nop",
126		},
127	}
128	defer srv.Close()
129	defer testutils.CheckSMTPConnLeak(t, srv)
130
131	mod := &Downstream{
132		hostname: "mx.example.invalid",
133		endpoints: []config.Endpoint{
134			{
135				Scheme: "tcp",
136				Host:   "127.0.0.1",
137				Port:   testPort,
138			},
139		},
140		modName: "target.lmtp",
141		lmtp:    true,
142		log:     testutils.Logger(t, "lmtp_downstream"),
143	}
144
145	_, err := testutils.DoTestDeliveryErr(t, mod, "test@example.invalid", []string{"rcpt1@example.invalid", "rcpt2@example.invalid"})
146	if err == nil {
147		t.Error("expected failure")
148	}
149}
150
151type statusCollector map[string]error
152
153func (sc *statusCollector) SetStatus(rcptTo string, err error) {
154	(*sc)[rcptTo] = err
155}
156
157func TestDownstreamDelivery_Fallback(t *testing.T) {
158	be, srv := testutils.SMTPServer(t, "127.0.0.2:"+testPort)
159	defer srv.Close()
160	defer testutils.CheckSMTPConnLeak(t, srv)
161
162	mod := &Downstream{
163		hostname: "mx.example.invalid",
164		endpoints: []config.Endpoint{
165			{
166				Scheme: "tcp",
167				Host:   "127.0.0.1",
168				Port:   testPort,
169			},
170			{
171				Scheme: "tcp",
172				Host:   "127.0.0.2",
173				Port:   testPort,
174			},
175		},
176		log: testutils.Logger(t, "target.smtp"),
177	}
178
179	testutils.DoTestDelivery(t, mod, "test@example.invalid", []string{"rcpt@example.invalid"})
180	be.CheckMsg(t, 0, "test@example.invalid", []string{"rcpt@example.invalid"})
181}
182
183func TestDownstreamDelivery_MAILErr(t *testing.T) {
184	be, srv := testutils.SMTPServer(t, "127.0.0.1:"+testPort)
185	defer srv.Close()
186	defer testutils.CheckSMTPConnLeak(t, srv)
187
188	be.MailErr = &smtp.SMTPError{
189		Code:         550,
190		EnhancedCode: smtp.EnhancedCode{5, 1, 2},
191		Message:      "Hey",
192	}
193
194	mod := &Downstream{
195		hostname: "mx.example.invalid",
196		endpoints: []config.Endpoint{
197			{
198				Scheme: "tcp",
199				Host:   "127.0.0.1",
200				Port:   testPort,
201			},
202		},
203		log: testutils.Logger(t, "target.smtp"),
204	}
205
206	_, err := testutils.DoTestDeliveryErr(t, mod, "test@example.invalid", []string{"rcpt@example.invalid"})
207	testutils.CheckSMTPErr(t, err, 550, exterrors.EnhancedCode{5, 1, 2}, "Hey")
208}
209
210func TestDownstreamDelivery_StartTLS(t *testing.T) {
211	clientCfg, be, srv := testutils.SMTPServerSTARTTLS(t, "127.0.0.1:"+testPort)
212	defer srv.Close()
213	defer testutils.CheckSMTPConnLeak(t, srv)
214
215	mod := &Downstream{
216		hostname: "mx.example.invalid",
217		endpoints: []config.Endpoint{
218			{
219				Scheme: "tcp",
220				Host:   "127.0.0.1",
221				Port:   testPort,
222			},
223		},
224		tlsConfig: clientCfg.Clone(),
225		starttls:  true,
226		log:       testutils.Logger(t, "target.smtp"),
227	}
228
229	testutils.DoTestDelivery(t, mod, "test@example.invalid", []string{"rcpt@example.invalid"})
230	be.CheckMsg(t, 0, "test@example.invalid", []string{"rcpt@example.invalid"})
231
232	tlsState, ok := be.Messages[0].Conn.TLSConnectionState()
233	if !ok || !tlsState.HandshakeComplete {
234		t.Fatal("Message was not delivered over TLS")
235	}
236}
237
238func TestDownstreamDelivery_StartTLS_NoFallback(t *testing.T) {
239	_, srv := testutils.SMTPServer(t, "127.0.0.1:"+testPort)
240	defer srv.Close()
241	defer testutils.CheckSMTPConnLeak(t, srv)
242
243	mod := &Downstream{
244		hostname: "mx.example.invalid",
245		endpoints: []config.Endpoint{
246			{
247				Scheme: "tcp",
248				Host:   "127.0.0.1",
249				Port:   testPort,
250			},
251		},
252		starttls: true,
253		log:      testutils.Logger(t, "target.smtp"),
254	}
255
256	_, err := testutils.DoTestDeliveryErr(t, mod, "test@example.invalid", []string{"rcpt@example.invalid"})
257	if err == nil {
258		t.Error("Expected an error, got none")
259	}
260}
261
262func TestMain(m *testing.M) {
263	remoteSmtpPort := flag.String("test.smtpport", "random", "(maddy) SMTP port to use for connections in tests")
264	flag.Parse()
265
266	if *remoteSmtpPort == "random" {
267		*remoteSmtpPort = strconv.Itoa(rand.Intn(65536-10000) + 10000)
268	}
269
270	testPort = *remoteSmtpPort
271	os.Exit(m.Run())
272}