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 ok(t, conn.PrintfLine(rr.request))
121 if rr.handler != nil {
124 readCodeLine(t, conn, rr.responseCode)
127 t.Logf("%s case %d", _fl(1), i)
133 func TestScenarioTypical(t *testing.T) {
136 blockList: []string{"Green@foo.com"},
138 l := runServer(t, &s)
141 conn := createClient(t, l.Addr())
143 message := readCodeLine(t, conn, 220)
144 if !strings.HasPrefix(message, s.Name()) {
145 t.Errorf("Greeting does not have server name, got %q", message)
148 greet := "greeting.TestScenarioTypical"
149 ok(t, conn.PrintfLine("EHLO "+greet))
151 _, message, err := conn.ReadResponse(250)
153 if !strings.Contains(message, greet) {
154 t.Errorf("EHLO response does not contain greeting, got %q", message)
157 ok(t, conn.PrintfLine("MAIL FROM:<Smith@bar.com>"))
158 readCodeLine(t, conn, 250)
160 ok(t, conn.PrintfLine("RCPT TO:<Jones@foo.com>"))
161 readCodeLine(t, conn, 250)
163 ok(t, conn.PrintfLine("RCPT TO:<Green@foo.com>"))
164 readCodeLine(t, conn, 550)
166 ok(t, conn.PrintfLine("RCPT TO:<Brown@foo.com>"))
167 readCodeLine(t, conn, 250)
169 ok(t, conn.PrintfLine("DATA"))
170 readCodeLine(t, conn, 354)
172 ok(t, conn.PrintfLine("Blah blah blah..."))
173 ok(t, conn.PrintfLine("...etc. etc. etc."))
174 ok(t, conn.PrintfLine("."))
175 readCodeLine(t, conn, 250)
177 ok(t, conn.PrintfLine("QUIT"))
178 readCodeLine(t, conn, 221)
181 func TestVerifyAddress(t *testing.T) {
184 blockList: []string{"banned@test.mail"},
186 l := runServer(t, &s)
189 conn := createClient(t, l.Addr())
190 readCodeLine(t, conn, 220)
192 runTableTest(t, conn, []requestResponse{
193 {"EHLO test", 0, func(t testing.TB, conn *textproto.Conn) { conn.ReadResponse(250) }},
194 {"VRFY banned@test.mail", 252, nil},
195 {"VRFY allowed@test.mail", 252, nil},
196 {"MAIL FROM:<sender@example.com>", 250, nil},
197 {"RCPT TO:<banned@test.mail>", 550, nil},
202 func TestBadAddress(t *testing.T) {
203 l := runServer(t, &testServer{})
206 conn := createClient(t, l.Addr())
207 readCodeLine(t, conn, 220)
209 runTableTest(t, conn, []requestResponse{
210 {"EHLO test", 0, func(t testing.TB, conn *textproto.Conn) { conn.ReadResponse(250) }},
211 {"MAIL FROM:<sender>", 501, nil},
212 {"MAIL FROM:<sender@foo.com> SIZE=2163", 250, nil},
213 {"RCPT TO:<banned.net>", 501, nil},
218 func TestCaseSensitivty(t *testing.T) {
221 blockList: []string{"reject@mail.com"},
226 conn := createClient(t, l.Addr())
227 readCodeLine(t, conn, 220)
229 runTableTest(t, conn, []requestResponse{
231 {"ehLO test.TEST", 0, func(t testing.TB, conn *textproto.Conn) { conn.ReadResponse(250) }},
232 {"mail FROM:<sender@example.com>", 250, nil},
233 {"RcPT tO:<receive@mail.com>", 250, nil},
234 {"RCPT TO:<reject@MAIL.com>", 550, nil},
235 {"RCPT TO:<reject@mail.com>", 550, nil},
236 {"DATa", 0, func(t testing.TB, conn *textproto.Conn) {
237 readCodeLine(t, conn, 354)
239 ok(t, conn.PrintfLine("."))
240 readCodeLine(t, conn, 250)
242 {"MAIL FR:", 501, nil},
247 func TestGetReceivedInfo(t *testing.T) {
249 server: &testServer{},
250 remoteAddr: &net.IPAddr{net.IPv4(127, 0, 0, 1), ""},
256 const line1 = "Received: from remote.test. (localhost [127.0.0.1])" + crlf
257 const line2 = "by Test-Server (mailpopbox) with "
258 const msgId = "abcdef.hijk"
259 lineLast := now.Format(time.RFC1123Z) + crlf
273 {params{"remote.test.", true, false, "foo@bar.com"},
275 line2 + "ESMTP id " + msgId + crlf,
276 "for <foo@bar.com>" + crlf,
277 "(using PLAINTEXT);" + crlf,
281 for _, test := range tests {
282 t.Logf("%#v", test.params)
284 conn.ehlo = test.params.ehlo
285 conn.esmtp = test.params.esmtp
286 //conn.tls = test.params.tls
288 envelope := Envelope{
289 RcptTo: []mail.Address{{"", test.params.address}},
294 actual := conn.getReceivedInfo(envelope)
295 actualLines := strings.SplitAfter(string(actual), crlf)
297 if len(actualLines) != len(test.expect) {
298 t.Errorf("wrong numbber of lines, expected %d, got %d", len(test.expect), len(actualLines))
302 for i, line := range actualLines {
303 expect := test.expect[i]
304 if expect != strings.TrimLeft(line, " ") {
305 t.Errorf("Expected equal string %q, got %q", expect, line)
312 func getTLSConfig(t *testing.T) *tls.Config {
313 cert, err := tls.LoadX509KeyPair("../testtls/domain.crt", "../testtls/domain.key")
319 ServerName: "localhost",
320 Certificates: []tls.Certificate{cert},
321 InsecureSkipVerify: true,
325 func setupTLSClient(t *testing.T, addr net.Addr) *textproto.Conn {
326 nc, err := net.Dial(addr.Network(), addr.String())
329 conn := textproto.NewConn(nc)
330 readCodeLine(t, conn, 220)
332 ok(t, conn.PrintfLine("EHLO test-tls"))
333 _, resp, err := conn.ReadResponse(250)
335 if !strings.Contains(resp, "STARTTLS\n") {
336 t.Errorf("STARTTLS not advertised")
339 ok(t, conn.PrintfLine("STARTTLS"))
340 readCodeLine(t, conn, 220)
342 tc := tls.Client(nc, getTLSConfig(t))
346 conn = textproto.NewConn(tc)
348 ok(t, conn.PrintfLine("EHLO test-tls-started"))
349 _, resp, err = conn.ReadResponse(250)
351 if strings.Contains(resp, "STARTTLS\n") {
352 t.Errorf("STARTTLS advertised when already started")
358 func b64enc(s string) string {
359 return string(base64.StdEncoding.EncodeToString([]byte(s)))
362 func TestTLS(t *testing.T) {
363 l := runServer(t, &testServer{tlsConfig: getTLSConfig(t)})
366 setupTLSClient(t, l.Addr())
369 func TestAuthWithoutTLS(t *testing.T) {
370 l := runServer(t, &testServer{})
373 conn := createClient(t, l.Addr())
374 readCodeLine(t, conn, 220)
376 ok(t, conn.PrintfLine("EHLO test"))
377 _, resp, err := conn.ReadResponse(250)
380 if strings.Contains(resp, "AUTH") {
381 t.Errorf("AUTH should not be advertised over plaintext")
385 func TestAuth(t *testing.T) {
386 l := runServer(t, &testServer{
387 tlsConfig: getTLSConfig(t),
396 conn := setupTLSClient(t, l.Addr())
398 runTableTest(t, conn, []requestResponse{
400 {"AUTH OAUTHBEARER", 504, nil},
401 {"AUTH PLAIN", 501, nil}, // Bad syntax, missing space.
402 {"AUTH PLAIN ", 334, nil},
403 {b64enc("abc\x00def\x00ghf"), 535, nil},
404 {"AUTH PLAIN ", 334, nil},
405 {b64enc("\x00"), 501, nil},
406 {"AUTH PLAIN ", 334, nil},
407 {"this isn't base 64", 501, nil},
408 {"AUTH PLAIN ", 334, nil},
409 {b64enc("-authz-\x00-authc-\x00goats"), 250, nil},
410 {"AUTH PLAIN ", 503, nil}, // Already authenticated.
415 func TestAuthNoInitialResponse(t *testing.T) {
416 l := runServer(t, &testServer{
417 tlsConfig: getTLSConfig(t),
421 passwd: "longpassword",
426 conn := setupTLSClient(t, l.Addr())
428 runTableTest(t, conn, []requestResponse{
429 {"AUTH PLAIN " + b64enc("\x00user\x00longpassword"), 250, nil},
433 func TestRelayRequiresAuth(t *testing.T) {
434 l := runServer(t, &testServer{
435 domain: "example.com",
436 tlsConfig: getTLSConfig(t),
439 authc: "mailbox@example.com",
445 conn := setupTLSClient(t, l.Addr())
447 runTableTest(t, conn, []requestResponse{
448 {"MAIL FROM:<apples@example.com>", 550, nil},
449 {"MAIL FROM:<mailbox@example.com>", 550, nil},
450 {"AUTH PLAIN ", 334, nil},
451 {b64enc("\x00mailbox@example.com\x00test"), 250, nil},
452 {"MAIL FROM:<mailbox@example.com>", 250, nil},
456 func setupRelayTest(t *testing.T) (server *testServer, l net.Listener, conn *textproto.Conn) {
457 server = &testServer{
458 domain: "example.com",
459 tlsConfig: getTLSConfig(t),
462 authc: "mailbox@example.com",
466 l = runServer(t, server)
467 conn = setupTLSClient(t, l.Addr())
468 runTableTest(t, conn, []requestResponse{
469 {"AUTH PLAIN ", 334, nil},
470 {b64enc("\x00mailbox@example.com\x00test"), 250, nil},
475 func TestBasicRelay(t *testing.T) {
476 server, l, conn := setupRelayTest(t)
479 runTableTest(t, conn, []requestResponse{
480 {"MAIL FROM:<mailbox@example.com>", 250, nil},
481 {"RCPT TO:<dest@another.net>", 250, nil},
482 {"DATA", 354, func(t testing.TB, conn *textproto.Conn) {
483 readCodeLine(t, conn, 354)
485 ok(t, conn.PrintfLine("From: <mailbox@example.com>"))
486 ok(t, conn.PrintfLine("To: <dest@example.com>"))
487 ok(t, conn.PrintfLine("Subject: Basic relay\n"))
488 ok(t, conn.PrintfLine("This is a basic relay message"))
489 ok(t, conn.PrintfLine("."))
490 readCodeLine(t, conn, 250)
494 if len(server.relayed) != 1 {
495 t.Errorf("Expected 1 relayed message, got %d", len(server.relayed))
499 func TestNoInternalRelays(t *testing.T) {
500 _, l, conn := setupRelayTest(t)
503 runTableTest(t, conn, []requestResponse{
504 {"MAIL FROM:<mailbox@example.com>", 250, nil},
505 {"RCPT TO:<valid@dest.xyz>", 250, nil},
506 {"RCPT TO:<dest@example.com>", 550, nil},
507 {"RCPT TO:<mailbox@example.com>", 550, nil},
511 func TestSendAsRelay(t *testing.T) {
512 server, l, conn := setupRelayTest(t)
515 runTableTest(t, conn, []requestResponse{
516 {"MAIL FROM:<mailbox@example.com>", 250, nil},
517 {"RCPT TO:<valid@dest.xyz>", 250, nil},
518 {"RCPT TO:<sendas+source@example.com>", 250, nil},
519 {"RCPT TO:<mailbox@example.com>", 550, nil},
520 {"DATA", 354, func(t testing.TB, conn *textproto.Conn) {
521 readCodeLine(t, conn, 354)
523 ok(t, conn.PrintfLine("From: <mailbox@example.com>"))
524 ok(t, conn.PrintfLine("To: <valid@dest.xyz>"))
525 ok(t, conn.PrintfLine("Subject: Send-as relay\n"))
526 ok(t, conn.PrintfLine("We've switched the senders!"))
527 ok(t, conn.PrintfLine("."))
528 readCodeLine(t, conn, 250)
532 if len(server.relayed) != 1 {
533 t.Fatalf("Expected 1 relayed message, got %d", len(server.relayed))
536 replaced := "source@example.com"
537 original := "mailbox@example.com"
539 en := server.relayed[0]
540 if en.MailFrom.Address != replaced {
541 t.Errorf("Expected mail to be from %q, got %q", replaced, en.MailFrom.Address)
544 if len(en.RcptTo) != 1 {
545 t.Errorf("Expected 1 recipient, got %d", len(en.RcptTo))
547 if en.RcptTo[0].Address != "valid@dest.xyz" {
548 t.Errorf("Unexpected RcptTo %q", en.RcptTo[0].Address)
551 msg := string(en.Data)
553 if strings.Index(msg, original) != -1 {
554 t.Errorf("Should not find %q in message %q", original, msg)
557 if strings.Index(msg, "\nFrom: <source@example.com>\n") == -1 {
558 t.Errorf("Could not find From: header in message %q", msg)
562 func TestSendMultipleRelay(t *testing.T) {
563 server, l, conn := setupRelayTest(t)
566 runTableTest(t, conn, []requestResponse{
567 {"MAIL FROM:<mailbox@example.com>", 250, nil},
568 {"RCPT TO:<valid@dest.xyz>", 250, nil},
569 {"RCPT TO:<sendas+source@example.com>", 250, nil},
570 {"RCPT TO:<another@dest.org>", 250, nil},
571 {"DATA", 354, func(t testing.TB, conn *textproto.Conn) {
572 readCodeLine(t, conn, 354)
574 ok(t, conn.PrintfLine("To: Cindy <valid@dest.xyz>, Sam <another@dest.org>"))
575 ok(t, conn.PrintfLine("From: <mailbox@example.com>"))
576 ok(t, conn.PrintfLine("Subject: Two destinations\n"))
577 ok(t, conn.PrintfLine("And we've switched the senders!"))
578 ok(t, conn.PrintfLine("."))
579 readCodeLine(t, conn, 250)
583 if len(server.relayed) != 1 {
584 t.Fatalf("Expected 1 relayed message, got %d", len(server.relayed))
587 replaced := "source@example.com"
588 original := "mailbox@example.com"
590 en := server.relayed[0]
591 if en.MailFrom.Address != replaced {
592 t.Errorf("Expected mail to be from %q, got %q", replaced, en.MailFrom.Address)
595 if len(en.RcptTo) != 2 {
596 t.Errorf("Expected 2 recipient, got %d", len(en.RcptTo))
598 if en.RcptTo[0].Address != "valid@dest.xyz" {
599 t.Errorf("Unexpected RcptTo %q", en.RcptTo[0].Address)
602 msg := string(en.Data)
604 if strings.Index(msg, original) != -1 {
605 t.Errorf("Should not find %q in message %q", original, msg)
608 if strings.Index(msg, "\nFrom: <source@example.com>\n") == -1 {
609 t.Errorf("Could not find From: header in message %q", msg)