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 actual, message, err := conn.ReadCodeLine(code)
33 t.Errorf("%s ReadCodeLine error, expected %d, got %d: %v", _fl(1), code, actual, 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 TestSendAsRelay(t *testing.T) {
500 server, 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 {"DATA", 354, func(t testing.TB, conn *textproto.Conn) {
507 readCodeLine(t, conn, 354)
509 ok(t, conn.PrintfLine("From: <mailbox@example.com>"))
510 ok(t, conn.PrintfLine("To: <valid@dest.xyz>"))
511 ok(t, conn.PrintfLine("Subject: Send-as relay [sendas:source]\n"))
512 ok(t, conn.PrintfLine("We've switched the senders!"))
513 ok(t, conn.PrintfLine("."))
514 readCodeLine(t, conn, 250)
518 if len(server.relayed) != 1 {
519 t.Fatalf("Expected 1 relayed message, got %d", len(server.relayed))
522 replaced := "source@example.com"
523 original := "mailbox@example.com"
525 en := server.relayed[0]
526 if en.MailFrom.Address != replaced {
527 t.Errorf("Expected mail to be from %q, got %q", replaced, en.MailFrom.Address)
530 if len(en.RcptTo) != 1 {
531 t.Errorf("Expected 1 recipient, got %d", len(en.RcptTo))
533 if en.RcptTo[0].Address != "valid@dest.xyz" {
534 t.Errorf("Unexpected RcptTo %q", en.RcptTo[0].Address)
537 msg := string(en.Data)
539 if strings.Index(msg, original) != -1 {
540 t.Errorf("Should not find %q in message %q", original, msg)
543 if strings.Index(msg, "\nFrom: <source@example.com>\n") == -1 {
544 t.Errorf("Could not find From: header in message %q", msg)
547 if strings.Index(msg, "\nSubject: Send-as relay \n") == -1 {
548 t.Errorf("Could not find modified Subject: header in message %q", msg)
552 func TestSendMultipleRelay(t *testing.T) {
553 server, l, conn := setupRelayTest(t)
556 runTableTest(t, conn, []requestResponse{
557 {"MAIL FROM:<mailbox@example.com>", 250, nil},
558 {"RCPT TO:<valid@dest.xyz>", 250, nil},
559 {"RCPT TO:<another@dest.org>", 250, nil},
560 {"DATA", 354, func(t testing.TB, conn *textproto.Conn) {
561 readCodeLine(t, conn, 354)
563 ok(t, conn.PrintfLine("To: Cindy <valid@dest.xyz>, Sam <another@dest.org>"))
564 ok(t, conn.PrintfLine("From: Finn <mailbox@example.com>"))
565 ok(t, conn.PrintfLine("Subject: Two destinations [sendas:source]\n"))
566 ok(t, conn.PrintfLine("And we've switched the senders!"))
567 ok(t, conn.PrintfLine("."))
568 readCodeLine(t, conn, 250)
572 if len(server.relayed) != 1 {
573 t.Fatalf("Expected 1 relayed message, got %d", len(server.relayed))
576 replaced := "source@example.com"
577 original := "mailbox@example.com"
579 en := server.relayed[0]
580 if en.MailFrom.Address != replaced {
581 t.Errorf("Expected mail to be from %q, got %q", replaced, en.MailFrom.Address)
584 if len(en.RcptTo) != 2 {
585 t.Errorf("Expected 2 recipient, got %d", len(en.RcptTo))
587 if en.RcptTo[0].Address != "valid@dest.xyz" {
588 t.Errorf("Unexpected RcptTo %q", en.RcptTo[0].Address)
591 msg := string(en.Data)
593 if strings.Index(msg, original) != -1 {
594 t.Errorf("Should not find %q in message %q", original, msg)
597 if strings.Index(msg, "\nFrom: Finn <source@example.com>\n") == -1 {
598 t.Errorf("Could not find From: header in message %q", msg)
601 if strings.Index(msg, "\nSubject: Two destinations \n") == -1 {
602 t.Errorf("Could not find modified Subject: header in message %q", msg)