16 "github.com/uber-go/zap"
19 func _fl(depth int) string {
20 _, file, line, _ := runtime.Caller(depth + 1)
21 return fmt.Sprintf("[%s:%d]", filepath.Base(file), line)
24 func ok(t testing.TB, err error) {
26 t.Errorf("%s unexpected error: %v", _fl(1), err)
30 func readCodeLine(t testing.TB, conn *textproto.Conn, code int) string {
31 _, message, err := conn.ReadCodeLine(code)
33 t.Errorf("%s ReadCodeLine error: %v", _fl(1), err)
38 // runServer creates a TCP socket, runs a listening server, and returns the connection.
39 // The server exits when the Conn is closed.
40 func runServer(t *testing.T, server Server) net.Listener {
41 l, err := net.Listen("tcp", "localhost:0")
49 conn, err := l.Accept()
53 go AcceptConnection(conn, server, zap.New(zap.NullEncoder()))
60 type userAuth struct {
61 authz, authc, passwd string
64 type testServer struct {
73 func (s *testServer) Name() string {
77 func (s *testServer) TLSConfig() *tls.Config {
81 func (s *testServer) VerifyAddress(addr mail.Address) ReplyLine {
82 if DomainForAddress(addr) != s.domain {
83 return ReplyBadMailbox
85 for _, block := range s.blockList {
86 if strings.ToLower(block) == addr.Address {
87 return ReplyBadMailbox
93 func (s *testServer) Authenticate(authz, authc, passwd string) bool {
94 return s.userAuth.authz == authz &&
95 s.userAuth.authc == authc &&
96 s.userAuth.passwd == passwd
99 func (s *testServer) RelayMessage(en Envelope) {
100 s.relayed = append(s.relayed, en)
103 func createClient(t *testing.T, addr net.Addr) *textproto.Conn {
104 conn, err := textproto.Dial(addr.Network(), addr.String())
112 type requestResponse struct {
115 handler func(testing.TB, *textproto.Conn)
118 func runTableTest(t testing.TB, conn *textproto.Conn, seq []requestResponse) {
119 for i, rr := range seq {
120 t.Logf("%s case %d", _fl(1), i)
121 ok(t, conn.PrintfLine(rr.request))
122 if rr.handler != nil {
125 readCodeLine(t, conn, rr.responseCode)
131 func TestScenarioTypical(t *testing.T) {
134 blockList: []string{"Green@foo.com"},
136 l := runServer(t, &s)
139 conn := createClient(t, l.Addr())
141 message := readCodeLine(t, conn, 220)
142 if !strings.HasPrefix(message, s.Name()) {
143 t.Errorf("Greeting does not have server name, got %q", message)
146 greet := "greeting.TestScenarioTypical"
147 ok(t, conn.PrintfLine("EHLO "+greet))
149 _, message, err := conn.ReadResponse(250)
151 if !strings.Contains(message, greet) {
152 t.Errorf("EHLO response does not contain greeting, got %q", message)
155 ok(t, conn.PrintfLine("MAIL FROM:<Smith@bar.com>"))
156 readCodeLine(t, conn, 250)
158 ok(t, conn.PrintfLine("RCPT TO:<Jones@foo.com>"))
159 readCodeLine(t, conn, 250)
161 ok(t, conn.PrintfLine("RCPT TO:<Green@foo.com>"))
162 readCodeLine(t, conn, 550)
164 ok(t, conn.PrintfLine("RCPT TO:<Brown@foo.com>"))
165 readCodeLine(t, conn, 250)
167 ok(t, conn.PrintfLine("DATA"))
168 readCodeLine(t, conn, 354)
170 ok(t, conn.PrintfLine("Blah blah blah..."))
171 ok(t, conn.PrintfLine("...etc. etc. etc."))
172 ok(t, conn.PrintfLine("."))
173 readCodeLine(t, conn, 250)
175 ok(t, conn.PrintfLine("QUIT"))
176 readCodeLine(t, conn, 221)
179 func TestVerifyAddress(t *testing.T) {
182 blockList: []string{"banned@test.mail"},
184 l := runServer(t, &s)
187 conn := createClient(t, l.Addr())
188 readCodeLine(t, conn, 220)
190 runTableTest(t, conn, []requestResponse{
191 {"EHLO test", 0, func(t testing.TB, conn *textproto.Conn) { conn.ReadResponse(250) }},
192 {"VRFY banned@test.mail", 252, nil},
193 {"VRFY allowed@test.mail", 252, nil},
194 {"MAIL FROM:<sender@example.com>", 250, nil},
195 {"RCPT TO:<banned@test.mail>", 550, nil},
200 func TestBadAddress(t *testing.T) {
201 l := runServer(t, &testServer{})
204 conn := createClient(t, l.Addr())
205 readCodeLine(t, conn, 220)
207 runTableTest(t, conn, []requestResponse{
208 {"EHLO test", 0, func(t testing.TB, conn *textproto.Conn) { conn.ReadResponse(250) }},
209 {"MAIL FROM:<sender>", 501, nil},
210 {"MAIL FROM:<sender@foo.com> SIZE=2163", 250, nil},
211 {"RCPT TO:<banned.net>", 501, nil},
216 func TestCaseSensitivty(t *testing.T) {
219 blockList: []string{"reject@mail.com"},
224 conn := createClient(t, l.Addr())
225 readCodeLine(t, conn, 220)
227 runTableTest(t, conn, []requestResponse{
229 {"ehLO test.TEST", 0, func(t testing.TB, conn *textproto.Conn) { conn.ReadResponse(250) }},
230 {"mail FROM:<sender@example.com>", 250, nil},
231 {"RcPT tO:<receive@mail.com>", 250, nil},
232 {"RCPT TO:<reject@MAIL.com>", 550, nil},
233 {"RCPT TO:<reject@mail.com>", 550, nil},
234 {"DATa", 0, func(t testing.TB, conn *textproto.Conn) {
235 readCodeLine(t, conn, 354)
237 ok(t, conn.PrintfLine("."))
238 readCodeLine(t, conn, 250)
240 {"MAIL FR:", 501, nil},
245 func TestGetReceivedInfo(t *testing.T) {
247 server: &testServer{},
248 remoteAddr: &net.IPAddr{net.IPv4(127, 0, 0, 1), ""},
254 const line1 = "Received: from remote.test. (localhost [127.0.0.1])" + crlf
255 const line2 = "by Test-Server (mailpopbox) with "
256 const msgId = "abcdef.hijk"
257 lineLast := now.Format(time.RFC1123Z) + crlf
271 {params{"remote.test.", true, false, "foo@bar.com"},
273 line2 + "ESMTP id " + msgId + crlf,
274 "for <foo@bar.com>" + crlf,
275 "(using PLAINTEXT);" + crlf,
279 for _, test := range tests {
280 t.Logf("%#v", test.params)
282 conn.ehlo = test.params.ehlo
283 conn.esmtp = test.params.esmtp
284 //conn.tls = test.params.tls
286 envelope := Envelope{
287 RcptTo: []mail.Address{{"", test.params.address}},
292 actual := conn.getReceivedInfo(envelope)
293 actualLines := strings.SplitAfter(string(actual), crlf)
295 if len(actualLines) != len(test.expect) {
296 t.Errorf("wrong numbber of lines, expected %d, got %d", len(test.expect), len(actualLines))
300 for i, line := range actualLines {
301 expect := test.expect[i]
302 if expect != strings.TrimLeft(line, " ") {
303 t.Errorf("Expected equal string %q, got %q", expect, line)
310 func getTLSConfig(t *testing.T) *tls.Config {
311 cert, err := tls.LoadX509KeyPair("../testtls/domain.crt", "../testtls/domain.key")
317 ServerName: "localhost",
318 Certificates: []tls.Certificate{cert},
319 InsecureSkipVerify: true,
323 func setupTLSClient(t *testing.T, addr net.Addr) *textproto.Conn {
324 nc, err := net.Dial(addr.Network(), addr.String())
327 conn := textproto.NewConn(nc)
328 readCodeLine(t, conn, 220)
330 ok(t, conn.PrintfLine("EHLO test-tls"))
331 _, resp, err := conn.ReadResponse(250)
333 if !strings.Contains(resp, "STARTTLS\n") {
334 t.Errorf("STARTTLS not advertised")
337 ok(t, conn.PrintfLine("STARTTLS"))
338 readCodeLine(t, conn, 220)
340 tc := tls.Client(nc, getTLSConfig(t))
344 conn = textproto.NewConn(tc)
346 ok(t, conn.PrintfLine("EHLO test-tls-started"))
347 _, resp, err = conn.ReadResponse(250)
349 if strings.Contains(resp, "STARTTLS\n") {
350 t.Errorf("STARTTLS advertised when already started")
356 func b64enc(s string) string {
357 return string(base64.StdEncoding.EncodeToString([]byte(s)))
360 func TestTLS(t *testing.T) {
361 l := runServer(t, &testServer{tlsConfig: getTLSConfig(t)})
364 setupTLSClient(t, l.Addr())
367 func TestAuthWithoutTLS(t *testing.T) {
368 l := runServer(t, &testServer{})
371 conn := createClient(t, l.Addr())
372 readCodeLine(t, conn, 220)
374 ok(t, conn.PrintfLine("EHLO test"))
375 _, resp, err := conn.ReadResponse(250)
378 if strings.Contains(resp, "AUTH") {
379 t.Errorf("AUTH should not be advertised over plaintext")
383 func TestAuth(t *testing.T) {
384 l := runServer(t, &testServer{
385 tlsConfig: getTLSConfig(t),
394 conn := setupTLSClient(t, l.Addr())
396 runTableTest(t, conn, []requestResponse{
398 {"AUTH OAUTHBEARER", 504, nil},
399 {"AUTH PLAIN", 334, nil},
400 {b64enc("abc\x00def\x00ghf"), 535, nil},
401 {"AUTH PLAIN", 334, nil},
402 {b64enc("\x00"), 501, nil},
403 {"AUTH PLAIN", 334, nil},
404 {"this isn't base 64", 501, nil},
405 {"AUTH PLAIN", 334, nil},
406 {b64enc("-authz-\x00-authc-\x00goats"), 250, nil},
407 {"AUTH PLAIN", 503, nil}, // already authenticated
412 func TestRelayRequiresAuth(t *testing.T) {
413 l := runServer(t, &testServer{
414 domain: "example.com",
415 tlsConfig: getTLSConfig(t),
418 authc: "mailbox@example.com",
424 conn := setupTLSClient(t, l.Addr())
426 runTableTest(t, conn, []requestResponse{
427 {"MAIL FROM:<apples@example.com>", 550, nil},
428 {"MAIL FROM:<mailbox@example.com>", 550, nil},
429 {"AUTH PLAIN", 334, nil},
430 {b64enc("\x00mailbox@example.com\x00test"), 250, nil},
431 {"MAIL FROM:<mailbox@example.com>", 250, nil},
435 func setupRelayTest(t *testing.T) (server *testServer, l net.Listener, conn *textproto.Conn) {
436 server = &testServer{
437 domain: "example.com",
438 tlsConfig: getTLSConfig(t),
441 authc: "mailbox@example.com",
445 l = runServer(t, server)
446 conn = setupTLSClient(t, l.Addr())
447 runTableTest(t, conn, []requestResponse{
448 {"AUTH PLAIN", 334, nil},
449 {b64enc("\x00mailbox@example.com\x00test"), 250, nil},
454 func TestBasicRelay(t *testing.T) {
455 server, l, conn := setupRelayTest(t)
458 runTableTest(t, conn, []requestResponse{
459 {"MAIL FROM:<mailbox@example.com>", 250, nil},
460 {"RCPT TO:<dest@another.net>", 250, nil},
461 {"DATA", 354, func(t testing.TB, conn *textproto.Conn) {
462 readCodeLine(t, conn, 354)
464 ok(t, conn.PrintfLine("From: <mailbox@example.com>"))
465 ok(t, conn.PrintfLine("To: <dest@example.com>"))
466 ok(t, conn.PrintfLine("Subject: Basic relay\n"))
467 ok(t, conn.PrintfLine("This is a basic relay message"))
468 ok(t, conn.PrintfLine("."))
469 readCodeLine(t, conn, 250)
473 if len(server.relayed) != 1 {
474 t.Errorf("Expected 1 relayed message, got %d", len(server.relayed))
478 func TestNoInternalRelays(t *testing.T) {
479 _, l, conn := setupRelayTest(t)
482 runTableTest(t, conn, []requestResponse{
483 {"MAIL FROM:<mailbox@example.com>", 250, nil},
484 {"RCPT TO:<valid@dest.xyz>", 250, nil},
485 {"RCPT TO:<dest@example.com>", 550, nil},
486 {"RCPT TO:<mailbox@example.com>", 550, nil},
490 func TestSendAsRelay(t *testing.T) {
491 server, l, conn := setupRelayTest(t)
494 runTableTest(t, conn, []requestResponse{
495 {"MAIL FROM:<mailbox@example.com>", 250, nil},
496 {"RCPT TO:<valid@dest.xyz>", 250, nil},
497 {"RCPT TO:<sendas+source@example.com>", 250, nil},
498 {"RCPT TO:<mailbox@example.com>", 550, nil},
499 {"DATA", 354, func(t testing.TB, conn *textproto.Conn) {
500 readCodeLine(t, conn, 354)
502 ok(t, conn.PrintfLine("From: <mailbox@example.com>"))
503 ok(t, conn.PrintfLine("To: <valid@dest.xyz>"))
504 ok(t, conn.PrintfLine("Subject: Send-as relay\n"))
505 ok(t, conn.PrintfLine("We've switched the senders!"))
506 ok(t, conn.PrintfLine("."))
507 readCodeLine(t, conn, 250)
511 if len(server.relayed) != 1 {
512 t.Errorf("Expected 1 relayed message, got %d", len(server.relayed))
515 replaced := "source@example.com"
516 original := "mailbox@example.com"
518 en := server.relayed[0]
519 if en.MailFrom.Address != replaced {
520 t.Errorf("Expected mail to be from %q, got %q", replaced, en.MailFrom.Address)
523 if len(en.RcptTo) != 1 {
524 t.Errorf("Expected 1 recipient, got %d", len(en.RcptTo))
526 if en.RcptTo[0].Address != "valid@dest.xyz" {
527 t.Errorf("Unexpected RcptTo %q", en.RcptTo[0].Address)
530 msg := string(en.Data)
532 if strings.Index(msg, original) != -1 {
533 t.Errorf("Should not find %q in message %q", original, msg)
536 if strings.Index(msg, "\nFrom: <source@example.com>\n") == -1 {
537 t.Errorf("Could not find From: header in message %q", msg)
541 func TestSendMultipleRelay(t *testing.T) {
542 server, l, conn := setupRelayTest(t)
545 runTableTest(t, conn, []requestResponse{
546 {"MAIL FROM:<mailbox@example.com>", 250, nil},
547 {"RCPT TO:<valid@dest.xyz>", 250, nil},
548 {"RCPT TO:<sendas+source@example.com>", 250, nil},
549 {"RCPT TO:<another@dest.org>", 250, nil},
550 {"DATA", 354, func(t testing.TB, conn *textproto.Conn) {
551 readCodeLine(t, conn, 354)
553 ok(t, conn.PrintfLine("To: Cindy <valid@dest.xyz>, Sam <another@dest.org>"))
554 ok(t, conn.PrintfLine("From: <mailbox@example.com>"))
555 ok(t, conn.PrintfLine("Subject: Two destinations\n"))
556 ok(t, conn.PrintfLine("And we've switched the senders!"))
557 ok(t, conn.PrintfLine("."))
558 readCodeLine(t, conn, 250)
562 if len(server.relayed) != 1 {
563 t.Errorf("Expected 1 relayed message, got %d", len(server.relayed))
566 replaced := "source@example.com"
567 original := "mailbox@example.com"
569 en := server.relayed[0]
570 if en.MailFrom.Address != replaced {
571 t.Errorf("Expected mail to be from %q, got %q", replaced, en.MailFrom.Address)
574 if len(en.RcptTo) != 2 {
575 t.Errorf("Expected 2 recipient, got %d", len(en.RcptTo))
577 if en.RcptTo[0].Address != "valid@dest.xyz" {
578 t.Errorf("Unexpected RcptTo %q", en.RcptTo[0].Address)
581 msg := string(en.Data)
583 if strings.Index(msg, original) != -1 {
584 t.Errorf("Should not find %q in message %q", original, msg)
587 if strings.Index(msg, "\nFrom: <source@example.com>\n") == -1 {
588 t.Errorf("Could not find From: header in message %q", msg)