2 // Copyright 2020 Blue Static <https://www.bluestatic.org>
3 // This program is free software licensed under the GNU General Public License,
4 // version 3.0. The full text of the license can be found in LICENSE.txt.
5 // SPDX-License-Identifier: GPL-3.0-only
25 func _fl(depth int) string {
26 _, file, line, _ := runtime.Caller(depth + 1)
27 return fmt.Sprintf("[%s:%d]", filepath.Base(file), line)
30 func ok(t testing.TB, err error) {
32 t.Errorf("%s unexpected error: %v", _fl(1), err)
36 func readCodeLine(t testing.TB, conn *textproto.Conn, code int) string {
37 actual, message, err := conn.ReadCodeLine(code)
39 t.Errorf("%s ReadCodeLine error, expected %d, got %d: %v", _fl(1), code, actual, err)
44 // runServer creates a TCP socket, runs a listening server, and returns the connection.
45 // The server exits when the Conn is closed.
46 func runServer(t *testing.T, server Server) net.Listener {
47 l, err := net.Listen("tcp", "localhost:0")
55 conn, err := l.Accept()
59 go AcceptConnection(conn, server, zap.NewNop())
66 type userAuth struct {
67 authz, authc, passwd string
70 type testServer struct {
79 func (s *testServer) Name() string {
83 func (s *testServer) TLSConfig() *tls.Config {
87 func (s *testServer) VerifyAddress(addr mail.Address) ReplyLine {
88 if DomainForAddress(addr) != s.domain {
89 return ReplyBadMailbox
91 for _, block := range s.blockList {
92 if strings.ToLower(block) == addr.Address {
93 return ReplyBadMailbox
99 func (s *testServer) Authenticate(authz, authc, passwd string) bool {
100 return s.userAuth.authz == authz &&
101 s.userAuth.authc == authc &&
102 s.userAuth.passwd == passwd
105 func (s *testServer) RelayMessage(en Envelope) {
106 s.relayed = append(s.relayed, en)
109 func createClient(t *testing.T, addr net.Addr) *textproto.Conn {
110 conn, err := textproto.Dial(addr.Network(), addr.String())
118 type requestResponse struct {
121 handler func(testing.TB, *textproto.Conn)
124 func runTableTest(t testing.TB, conn *textproto.Conn, seq []requestResponse) {
125 for i, rr := range seq {
126 ok(t, conn.PrintfLine(rr.request))
127 if rr.handler != nil {
130 readCodeLine(t, conn, rr.responseCode)
133 t.Logf("%s case %d", _fl(1), i)
139 func TestScenarioTypical(t *testing.T) {
142 blockList: []string{"Green@foo.com"},
144 l := runServer(t, &s)
147 conn := createClient(t, l.Addr())
149 message := readCodeLine(t, conn, 220)
150 if !strings.HasPrefix(message, s.Name()) {
151 t.Errorf("Greeting does not have server name, got %q", message)
154 greet := "greeting.TestScenarioTypical"
155 ok(t, conn.PrintfLine("EHLO "+greet))
157 _, message, err := conn.ReadResponse(250)
159 if !strings.Contains(message, greet) {
160 t.Errorf("EHLO response does not contain greeting, got %q", message)
163 ok(t, conn.PrintfLine("MAIL FROM:<Smith@bar.com>"))
164 readCodeLine(t, conn, 250)
166 ok(t, conn.PrintfLine("RCPT TO:<Jones@foo.com>"))
167 readCodeLine(t, conn, 250)
169 ok(t, conn.PrintfLine("RCPT TO:<Green@foo.com>"))
170 readCodeLine(t, conn, 550)
172 ok(t, conn.PrintfLine("RCPT TO:<Brown@foo.com>"))
173 readCodeLine(t, conn, 250)
175 ok(t, conn.PrintfLine("DATA"))
176 readCodeLine(t, conn, 354)
178 ok(t, conn.PrintfLine("Blah blah blah..."))
179 ok(t, conn.PrintfLine("...etc. etc. etc."))
180 ok(t, conn.PrintfLine("."))
181 readCodeLine(t, conn, 250)
183 ok(t, conn.PrintfLine("QUIT"))
184 readCodeLine(t, conn, 221)
187 func TestVerifyAddress(t *testing.T) {
190 blockList: []string{"banned@test.mail"},
192 l := runServer(t, &s)
195 conn := createClient(t, l.Addr())
196 readCodeLine(t, conn, 220)
198 runTableTest(t, conn, []requestResponse{
199 {"EHLO test", 0, func(t testing.TB, conn *textproto.Conn) { conn.ReadResponse(250) }},
200 {"VRFY banned@test.mail", 252, nil},
201 {"VRFY allowed@test.mail", 252, nil},
202 {"MAIL FROM:<sender@example.com>", 250, nil},
203 {"RCPT TO:<banned@test.mail>", 550, nil},
208 func TestBadAddress(t *testing.T) {
209 l := runServer(t, &testServer{})
212 conn := createClient(t, l.Addr())
213 readCodeLine(t, conn, 220)
215 runTableTest(t, conn, []requestResponse{
216 {"EHLO test", 0, func(t testing.TB, conn *textproto.Conn) { conn.ReadResponse(250) }},
217 {"MAIL FROM:<sender>", 501, nil},
218 {"MAIL FROM:<sender@foo.com> SIZE=2163", 250, nil},
219 {"RCPT TO:<banned.net>", 501, nil},
224 func TestCaseSensitivty(t *testing.T) {
227 blockList: []string{"reject@mail.com"},
232 conn := createClient(t, l.Addr())
233 readCodeLine(t, conn, 220)
235 runTableTest(t, conn, []requestResponse{
237 {"ehLO test.TEST", 0, func(t testing.TB, conn *textproto.Conn) { conn.ReadResponse(250) }},
238 {"mail FROM:<sender@example.com>", 250, nil},
239 {"RcPT tO:<receive@mail.com>", 250, nil},
240 {"RCPT TO:<reject@MAIL.com>", 550, nil},
241 {"RCPT TO:<reject@mail.com>", 550, nil},
242 {"DATa", 0, func(t testing.TB, conn *textproto.Conn) {
243 readCodeLine(t, conn, 354)
245 ok(t, conn.PrintfLine("."))
246 readCodeLine(t, conn, 250)
248 {"MAIL FR:", 501, nil},
253 func TestGetReceivedInfo(t *testing.T) {
255 server: &testServer{},
256 remoteAddr: &net.IPAddr{net.IPv4(127, 0, 0, 1), ""},
262 const line1 = "Received: from remote.test. (localhost [127.0.0.1])" + crlf
263 const line2 = "by Test-Server (mailpopbox) with "
264 const msgId = "abcdef.hijk"
265 lineLast := now.Format(time.RFC1123Z) + crlf
279 {params{"remote.test.", true, false, "foo@bar.com"},
281 line2 + "ESMTP id " + msgId + crlf,
282 "for <foo@bar.com>" + crlf,
283 "(using PLAINTEXT);" + crlf,
287 for _, test := range tests {
288 t.Logf("%#v", test.params)
290 conn.ehlo = test.params.ehlo
291 conn.esmtp = test.params.esmtp
292 //conn.tls = test.params.tls
294 envelope := Envelope{
295 RcptTo: []mail.Address{{"", test.params.address}},
300 actual := conn.getReceivedInfo(envelope)
301 actualLines := strings.SplitAfter(string(actual), crlf)
303 if want, got := len(test.expect), len(actualLines); want != got {
304 t.Errorf("wrong numbber of lines, want %d, got %d", want, got)
308 for i, line := range actualLines {
309 if want, got := test.expect[i], strings.TrimLeft(line, " "); want != got {
310 t.Errorf("want equal string %q, got %q", want, got)
317 func getTLSConfig(t *testing.T) *tls.Config {
318 cert, err := tls.LoadX509KeyPair("../testtls/domain.crt", "../testtls/domain.key")
324 ServerName: "localhost",
325 Certificates: []tls.Certificate{cert},
326 InsecureSkipVerify: true,
330 func setupTLSClient(t *testing.T, addr net.Addr) *textproto.Conn {
331 nc, err := net.Dial(addr.Network(), addr.String())
334 conn := textproto.NewConn(nc)
335 readCodeLine(t, conn, 220)
337 ok(t, conn.PrintfLine("EHLO test-tls"))
338 _, resp, err := conn.ReadResponse(250)
340 if !strings.Contains(resp, "STARTTLS\n") {
341 t.Errorf("STARTTLS not advertised")
344 ok(t, conn.PrintfLine("STARTTLS"))
345 readCodeLine(t, conn, 220)
347 tc := tls.Client(nc, getTLSConfig(t))
351 conn = textproto.NewConn(tc)
353 ok(t, conn.PrintfLine("EHLO test-tls-started"))
354 _, resp, err = conn.ReadResponse(250)
356 if strings.Contains(resp, "STARTTLS\n") {
357 t.Errorf("STARTTLS advertised when already started")
363 func b64enc(s string) string {
364 return string(base64.StdEncoding.EncodeToString([]byte(s)))
367 func TestTLS(t *testing.T) {
368 l := runServer(t, &testServer{tlsConfig: getTLSConfig(t)})
371 setupTLSClient(t, l.Addr())
374 func TestAuthWithoutTLS(t *testing.T) {
375 l := runServer(t, &testServer{})
378 conn := createClient(t, l.Addr())
379 readCodeLine(t, conn, 220)
381 ok(t, conn.PrintfLine("EHLO test"))
382 _, resp, err := conn.ReadResponse(250)
385 if strings.Contains(resp, "AUTH") {
386 t.Errorf("AUTH should not be advertised over plaintext")
390 func TestAuth(t *testing.T) {
391 l := runServer(t, &testServer{
392 tlsConfig: getTLSConfig(t),
401 conn := setupTLSClient(t, l.Addr())
403 runTableTest(t, conn, []requestResponse{
405 {"AUTH OAUTHBEARER", 504, nil},
406 {"AUTH PLAIN", 501, nil}, // Bad syntax, missing space.
407 {"AUTH PLAIN ", 334, nil},
408 {b64enc("abc\x00def\x00ghf"), 535, nil},
409 {"AUTH PLAIN ", 334, nil},
410 {b64enc("\x00"), 501, nil},
411 {"AUTH PLAIN ", 334, nil},
412 {"this isn't base 64", 501, nil},
413 {"AUTH PLAIN ", 334, nil},
414 {b64enc("-authz-\x00-authc-\x00goats"), 250, nil},
415 {"AUTH PLAIN ", 503, nil}, // Already authenticated.
420 func TestAuthNoInitialResponse(t *testing.T) {
421 l := runServer(t, &testServer{
422 tlsConfig: getTLSConfig(t),
426 passwd: "longpassword",
431 conn := setupTLSClient(t, l.Addr())
433 runTableTest(t, conn, []requestResponse{
434 {"AUTH PLAIN " + b64enc("\x00user\x00longpassword"), 250, nil},
438 func TestRelayRequiresAuth(t *testing.T) {
439 l := runServer(t, &testServer{
440 domain: "example.com",
441 tlsConfig: getTLSConfig(t),
444 authc: "mailbox@example.com",
450 conn := setupTLSClient(t, l.Addr())
452 runTableTest(t, conn, []requestResponse{
453 {"MAIL FROM:<apples@example.com>", 550, nil},
454 {"MAIL FROM:<mailbox@example.com>", 550, nil},
455 {"AUTH PLAIN ", 334, nil},
456 {b64enc("\x00mailbox@example.com\x00test"), 250, nil},
457 {"MAIL FROM:<mailbox@example.com>", 250, nil},
461 func setupRelayTest(t *testing.T) (server *testServer, l net.Listener, conn *textproto.Conn) {
462 server = &testServer{
463 domain: "example.com",
464 tlsConfig: getTLSConfig(t),
467 authc: "mailbox@example.com",
471 l = runServer(t, server)
472 conn = setupTLSClient(t, l.Addr())
473 runTableTest(t, conn, []requestResponse{
474 {"AUTH PLAIN ", 334, nil},
475 {b64enc("\x00mailbox@example.com\x00test"), 250, nil},
480 func TestBasicRelay(t *testing.T) {
481 server, l, conn := setupRelayTest(t)
484 runTableTest(t, conn, []requestResponse{
485 {"MAIL FROM:<mailbox@example.com>", 250, nil},
486 {"RCPT TO:<dest@another.net>", 250, nil},
487 {"DATA", 354, func(t testing.TB, conn *textproto.Conn) {
488 readCodeLine(t, conn, 354)
490 ok(t, conn.PrintfLine("From: <mailbox@example.com>"))
491 ok(t, conn.PrintfLine("To: <dest@example.com>"))
492 ok(t, conn.PrintfLine("Subject: Basic relay\n"))
493 ok(t, conn.PrintfLine("This is a basic relay message"))
494 ok(t, conn.PrintfLine("."))
495 readCodeLine(t, conn, 250)
499 if want, got := 1, len(server.relayed); want != got {
500 t.Errorf("Want %d relayed message, got %d", want, got)
504 func TestSendAsRelay(t *testing.T) {
505 server, l, conn := setupRelayTest(t)
508 runTableTest(t, conn, []requestResponse{
509 {"MAIL FROM:<mailbox@example.com>", 250, nil},
510 {"RCPT TO:<valid@dest.xyz>", 250, nil},
511 {"DATA", 354, func(t testing.TB, conn *textproto.Conn) {
512 readCodeLine(t, conn, 354)
514 ok(t, conn.PrintfLine("From: <mailbox@example.com>"))
515 ok(t, conn.PrintfLine("To: <valid@dest.xyz>"))
516 ok(t, conn.PrintfLine("Subject: Send-as relay [sendas:source]\n"))
517 ok(t, conn.PrintfLine("We've switched the senders!"))
518 ok(t, conn.PrintfLine("."))
519 readCodeLine(t, conn, 250)
523 if want, got := 1, len(server.relayed); want != got {
524 t.Fatalf("Want %d relayed message, got %d", want, got)
527 replaced := "source@example.com"
528 original := "mailbox@example.com"
530 en := server.relayed[0]
531 if want, got := replaced, en.MailFrom.Address; want != got {
532 t.Errorf("Want mail to be from %q, got %q", want, got)
535 if want, got := 1, len(en.RcptTo); want != got {
536 t.Errorf("Want %d recipient, got %d", want, got)
538 if want, got := "valid@dest.xyz", en.RcptTo[0].Address; want != got {
539 t.Errorf("Unexpected RcptTo %q", got)
542 msg := string(en.Data)
544 if strings.Index(msg, original) != -1 {
545 t.Errorf("Should not find %q in message %q", original, msg)
548 if strings.Index(msg, "\nFrom: <source@example.com>\n") == -1 {
549 t.Errorf("Could not find From: header in message %q", msg)
552 if strings.Index(msg, "\nSubject: Send-as relay \n") == -1 {
553 t.Errorf("Could not find modified Subject: header in message %q", msg)
557 func TestSendMultipleRelay(t *testing.T) {
558 server, l, conn := setupRelayTest(t)
561 runTableTest(t, conn, []requestResponse{
562 {"MAIL FROM:<mailbox@example.com>", 250, nil},
563 {"RCPT TO:<valid@dest.xyz>", 250, nil},
564 {"RCPT TO:<another@dest.org>", 250, nil},
565 {"DATA", 354, func(t testing.TB, conn *textproto.Conn) {
566 readCodeLine(t, conn, 354)
568 ok(t, conn.PrintfLine("To: Cindy <valid@dest.xyz>, Sam <another@dest.org>"))
569 ok(t, conn.PrintfLine("From: Finn <mailbox@example.com>"))
570 ok(t, conn.PrintfLine("Subject: Two destinations [sendas:source]\n"))
571 ok(t, conn.PrintfLine("And we've switched the senders!"))
572 ok(t, conn.PrintfLine("."))
573 readCodeLine(t, conn, 250)
577 if len(server.relayed) != 1 {
578 t.Fatalf("Expected 1 relayed message, got %d", len(server.relayed))
581 replaced := "source@example.com"
582 original := "mailbox@example.com"
584 en := server.relayed[0]
585 if want, got := replaced, en.MailFrom.Address; want != got {
586 t.Errorf("Want mail to be from %q, got %q", want, got)
589 if want, got := 2, len(en.RcptTo); want != got {
590 t.Errorf("Want %d recipients, got %d", want, got)
592 if want, got := "valid@dest.xyz", en.RcptTo[0].Address; want != got {
593 t.Errorf("Unexpected RcptTo %q", got)
596 msg := string(en.Data)
598 if strings.Index(msg, original) != -1 {
599 t.Errorf("Should not find %q in message %q", original, msg)
602 if strings.Index(msg, "\nFrom: Finn <source@example.com>\n") == -1 {
603 t.Errorf("Could not find From: header in message %q", msg)
606 if strings.Index(msg, "\nSubject: Two destinations \n") == -1 {
607 t.Errorf("Could not find modified Subject: header in message %q", msg)