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 len(actualLines) != len(test.expect) {
304 t.Errorf("wrong numbber of lines, expected %d, got %d", len(test.expect), len(actualLines))
308 for i, line := range actualLines {
309 expect := test.expect[i]
310 if expect != strings.TrimLeft(line, " ") {
311 t.Errorf("Expected equal string %q, got %q", expect, line)
318 func getTLSConfig(t *testing.T) *tls.Config {
319 cert, err := tls.LoadX509KeyPair("../testtls/domain.crt", "../testtls/domain.key")
325 ServerName: "localhost",
326 Certificates: []tls.Certificate{cert},
327 InsecureSkipVerify: true,
331 func setupTLSClient(t *testing.T, addr net.Addr) *textproto.Conn {
332 nc, err := net.Dial(addr.Network(), addr.String())
335 conn := textproto.NewConn(nc)
336 readCodeLine(t, conn, 220)
338 ok(t, conn.PrintfLine("EHLO test-tls"))
339 _, resp, err := conn.ReadResponse(250)
341 if !strings.Contains(resp, "STARTTLS\n") {
342 t.Errorf("STARTTLS not advertised")
345 ok(t, conn.PrintfLine("STARTTLS"))
346 readCodeLine(t, conn, 220)
348 tc := tls.Client(nc, getTLSConfig(t))
352 conn = textproto.NewConn(tc)
354 ok(t, conn.PrintfLine("EHLO test-tls-started"))
355 _, resp, err = conn.ReadResponse(250)
357 if strings.Contains(resp, "STARTTLS\n") {
358 t.Errorf("STARTTLS advertised when already started")
364 func b64enc(s string) string {
365 return string(base64.StdEncoding.EncodeToString([]byte(s)))
368 func TestTLS(t *testing.T) {
369 l := runServer(t, &testServer{tlsConfig: getTLSConfig(t)})
372 setupTLSClient(t, l.Addr())
375 func TestAuthWithoutTLS(t *testing.T) {
376 l := runServer(t, &testServer{})
379 conn := createClient(t, l.Addr())
380 readCodeLine(t, conn, 220)
382 ok(t, conn.PrintfLine("EHLO test"))
383 _, resp, err := conn.ReadResponse(250)
386 if strings.Contains(resp, "AUTH") {
387 t.Errorf("AUTH should not be advertised over plaintext")
391 func TestAuth(t *testing.T) {
392 l := runServer(t, &testServer{
393 tlsConfig: getTLSConfig(t),
402 conn := setupTLSClient(t, l.Addr())
404 runTableTest(t, conn, []requestResponse{
406 {"AUTH OAUTHBEARER", 504, nil},
407 {"AUTH PLAIN", 501, nil}, // Bad syntax, missing space.
408 {"AUTH PLAIN ", 334, nil},
409 {b64enc("abc\x00def\x00ghf"), 535, nil},
410 {"AUTH PLAIN ", 334, nil},
411 {b64enc("\x00"), 501, nil},
412 {"AUTH PLAIN ", 334, nil},
413 {"this isn't base 64", 501, nil},
414 {"AUTH PLAIN ", 334, nil},
415 {b64enc("-authz-\x00-authc-\x00goats"), 250, nil},
416 {"AUTH PLAIN ", 503, nil}, // Already authenticated.
421 func TestAuthNoInitialResponse(t *testing.T) {
422 l := runServer(t, &testServer{
423 tlsConfig: getTLSConfig(t),
427 passwd: "longpassword",
432 conn := setupTLSClient(t, l.Addr())
434 runTableTest(t, conn, []requestResponse{
435 {"AUTH PLAIN " + b64enc("\x00user\x00longpassword"), 250, nil},
439 func TestRelayRequiresAuth(t *testing.T) {
440 l := runServer(t, &testServer{
441 domain: "example.com",
442 tlsConfig: getTLSConfig(t),
445 authc: "mailbox@example.com",
451 conn := setupTLSClient(t, l.Addr())
453 runTableTest(t, conn, []requestResponse{
454 {"MAIL FROM:<apples@example.com>", 550, nil},
455 {"MAIL FROM:<mailbox@example.com>", 550, nil},
456 {"AUTH PLAIN ", 334, nil},
457 {b64enc("\x00mailbox@example.com\x00test"), 250, nil},
458 {"MAIL FROM:<mailbox@example.com>", 250, nil},
462 func setupRelayTest(t *testing.T) (server *testServer, l net.Listener, conn *textproto.Conn) {
463 server = &testServer{
464 domain: "example.com",
465 tlsConfig: getTLSConfig(t),
468 authc: "mailbox@example.com",
472 l = runServer(t, server)
473 conn = setupTLSClient(t, l.Addr())
474 runTableTest(t, conn, []requestResponse{
475 {"AUTH PLAIN ", 334, nil},
476 {b64enc("\x00mailbox@example.com\x00test"), 250, nil},
481 func TestBasicRelay(t *testing.T) {
482 server, l, conn := setupRelayTest(t)
485 runTableTest(t, conn, []requestResponse{
486 {"MAIL FROM:<mailbox@example.com>", 250, nil},
487 {"RCPT TO:<dest@another.net>", 250, nil},
488 {"DATA", 354, func(t testing.TB, conn *textproto.Conn) {
489 readCodeLine(t, conn, 354)
491 ok(t, conn.PrintfLine("From: <mailbox@example.com>"))
492 ok(t, conn.PrintfLine("To: <dest@example.com>"))
493 ok(t, conn.PrintfLine("Subject: Basic relay\n"))
494 ok(t, conn.PrintfLine("This is a basic relay message"))
495 ok(t, conn.PrintfLine("."))
496 readCodeLine(t, conn, 250)
500 if len(server.relayed) != 1 {
501 t.Errorf("Expected 1 relayed message, got %d", len(server.relayed))
505 func TestSendAsRelay(t *testing.T) {
506 server, l, conn := setupRelayTest(t)
509 runTableTest(t, conn, []requestResponse{
510 {"MAIL FROM:<mailbox@example.com>", 250, nil},
511 {"RCPT TO:<valid@dest.xyz>", 250, nil},
512 {"DATA", 354, func(t testing.TB, conn *textproto.Conn) {
513 readCodeLine(t, conn, 354)
515 ok(t, conn.PrintfLine("From: <mailbox@example.com>"))
516 ok(t, conn.PrintfLine("To: <valid@dest.xyz>"))
517 ok(t, conn.PrintfLine("Subject: Send-as relay [sendas:source]\n"))
518 ok(t, conn.PrintfLine("We've switched the senders!"))
519 ok(t, conn.PrintfLine("."))
520 readCodeLine(t, conn, 250)
524 if len(server.relayed) != 1 {
525 t.Fatalf("Expected 1 relayed message, got %d", len(server.relayed))
528 replaced := "source@example.com"
529 original := "mailbox@example.com"
531 en := server.relayed[0]
532 if en.MailFrom.Address != replaced {
533 t.Errorf("Expected mail to be from %q, got %q", replaced, en.MailFrom.Address)
536 if len(en.RcptTo) != 1 {
537 t.Errorf("Expected 1 recipient, got %d", len(en.RcptTo))
539 if en.RcptTo[0].Address != "valid@dest.xyz" {
540 t.Errorf("Unexpected RcptTo %q", en.RcptTo[0].Address)
543 msg := string(en.Data)
545 if strings.Index(msg, original) != -1 {
546 t.Errorf("Should not find %q in message %q", original, msg)
549 if strings.Index(msg, "\nFrom: <source@example.com>\n") == -1 {
550 t.Errorf("Could not find From: header in message %q", msg)
553 if strings.Index(msg, "\nSubject: Send-as relay \n") == -1 {
554 t.Errorf("Could not find modified Subject: header in message %q", msg)
558 func TestSendMultipleRelay(t *testing.T) {
559 server, l, conn := setupRelayTest(t)
562 runTableTest(t, conn, []requestResponse{
563 {"MAIL FROM:<mailbox@example.com>", 250, nil},
564 {"RCPT TO:<valid@dest.xyz>", 250, nil},
565 {"RCPT TO:<another@dest.org>", 250, nil},
566 {"DATA", 354, func(t testing.TB, conn *textproto.Conn) {
567 readCodeLine(t, conn, 354)
569 ok(t, conn.PrintfLine("To: Cindy <valid@dest.xyz>, Sam <another@dest.org>"))
570 ok(t, conn.PrintfLine("From: Finn <mailbox@example.com>"))
571 ok(t, conn.PrintfLine("Subject: Two destinations [sendas:source]\n"))
572 ok(t, conn.PrintfLine("And we've switched the senders!"))
573 ok(t, conn.PrintfLine("."))
574 readCodeLine(t, conn, 250)
578 if len(server.relayed) != 1 {
579 t.Fatalf("Expected 1 relayed message, got %d", len(server.relayed))
582 replaced := "source@example.com"
583 original := "mailbox@example.com"
585 en := server.relayed[0]
586 if en.MailFrom.Address != replaced {
587 t.Errorf("Expected mail to be from %q, got %q", replaced, en.MailFrom.Address)
590 if len(en.RcptTo) != 2 {
591 t.Errorf("Expected 2 recipient, got %d", len(en.RcptTo))
593 if en.RcptTo[0].Address != "valid@dest.xyz" {
594 t.Errorf("Unexpected RcptTo %q", en.RcptTo[0].Address)
597 msg := string(en.Data)
599 if strings.Index(msg, original) != -1 {
600 t.Errorf("Should not find %q in message %q", original, msg)
603 if strings.Index(msg, "\nFrom: Finn <source@example.com>\n") == -1 {
604 t.Errorf("Could not find From: header in message %q", msg)
607 if strings.Index(msg, "\nSubject: Two destinations \n") == -1 {
608 t.Errorf("Could not find modified Subject: header in message %q", msg)