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 {
72 func (s *testServer) Name() string {
76 func (s *testServer) TLSConfig() *tls.Config {
80 func (s *testServer) VerifyAddress(addr mail.Address) ReplyLine {
81 if DomainForAddress(addr) != s.domain {
82 return ReplyBadMailbox
84 for _, block := range s.blockList {
85 if strings.ToLower(block) == addr.Address {
86 return ReplyBadMailbox
92 func (s *testServer) Authenticate(authz, authc, passwd string) bool {
93 return s.userAuth.authz == authz &&
94 s.userAuth.authc == authc &&
95 s.userAuth.passwd == passwd
98 func createClient(t *testing.T, addr net.Addr) *textproto.Conn {
99 conn, err := textproto.Dial(addr.Network(), addr.String())
107 type requestResponse struct {
110 handler func(testing.TB, *textproto.Conn)
113 func runTableTest(t testing.TB, conn *textproto.Conn, seq []requestResponse) {
114 for i, rr := range seq {
115 t.Logf("%s case %d", _fl(1), i)
116 ok(t, conn.PrintfLine(rr.request))
117 if rr.handler != nil {
120 readCodeLine(t, conn, rr.responseCode)
126 func TestScenarioTypical(t *testing.T) {
129 blockList: []string{"Green@foo.com"},
131 l := runServer(t, &s)
134 conn := createClient(t, l.Addr())
136 message := readCodeLine(t, conn, 220)
137 if !strings.HasPrefix(message, s.Name()) {
138 t.Errorf("Greeting does not have server name, got %q", message)
141 greet := "greeting.TestScenarioTypical"
142 ok(t, conn.PrintfLine("EHLO "+greet))
144 _, message, err := conn.ReadResponse(250)
146 if !strings.Contains(message, greet) {
147 t.Errorf("EHLO response does not contain greeting, got %q", message)
150 ok(t, conn.PrintfLine("MAIL FROM:<Smith@bar.com>"))
151 readCodeLine(t, conn, 250)
153 ok(t, conn.PrintfLine("RCPT TO:<Jones@foo.com>"))
154 readCodeLine(t, conn, 250)
156 ok(t, conn.PrintfLine("RCPT TO:<Green@foo.com>"))
157 readCodeLine(t, conn, 550)
159 ok(t, conn.PrintfLine("RCPT TO:<Brown@foo.com>"))
160 readCodeLine(t, conn, 250)
162 ok(t, conn.PrintfLine("DATA"))
163 readCodeLine(t, conn, 354)
165 ok(t, conn.PrintfLine("Blah blah blah..."))
166 ok(t, conn.PrintfLine("...etc. etc. etc."))
167 ok(t, conn.PrintfLine("."))
168 readCodeLine(t, conn, 250)
170 ok(t, conn.PrintfLine("QUIT"))
171 readCodeLine(t, conn, 221)
174 func TestVerifyAddress(t *testing.T) {
177 blockList: []string{"banned@test.mail"},
179 l := runServer(t, &s)
182 conn := createClient(t, l.Addr())
183 readCodeLine(t, conn, 220)
185 runTableTest(t, conn, []requestResponse{
186 {"EHLO test", 0, func(t testing.TB, conn *textproto.Conn) { conn.ReadResponse(250) }},
187 {"VRFY banned@test.mail", 252, nil},
188 {"VRFY allowed@test.mail", 252, nil},
189 {"MAIL FROM:<sender@example.com>", 250, nil},
190 {"RCPT TO:<banned@test.mail>", 550, nil},
195 func TestBadAddress(t *testing.T) {
196 l := runServer(t, &testServer{})
199 conn := createClient(t, l.Addr())
200 readCodeLine(t, conn, 220)
202 runTableTest(t, conn, []requestResponse{
203 {"EHLO test", 0, func(t testing.TB, conn *textproto.Conn) { conn.ReadResponse(250) }},
204 {"MAIL FROM:<sender>", 501, nil},
205 {"MAIL FROM:<sender@foo.com> SIZE=2163", 250, nil},
206 {"RCPT TO:<banned.net>", 501, nil},
211 func TestCaseSensitivty(t *testing.T) {
214 blockList: []string{"reject@mail.com"},
219 conn := createClient(t, l.Addr())
220 readCodeLine(t, conn, 220)
222 runTableTest(t, conn, []requestResponse{
224 {"ehLO test.TEST", 0, func(t testing.TB, conn *textproto.Conn) { conn.ReadResponse(250) }},
225 {"mail FROM:<sender@example.com>", 250, nil},
226 {"RcPT tO:<receive@mail.com>", 250, nil},
227 {"RCPT TO:<reject@MAIL.com>", 550, nil},
228 {"RCPT TO:<reject@mail.com>", 550, nil},
229 {"DATa", 0, func(t testing.TB, conn *textproto.Conn) {
230 readCodeLine(t, conn, 354)
232 ok(t, conn.PrintfLine("."))
233 readCodeLine(t, conn, 250)
235 {"MAIL FR:", 501, nil},
240 func TestGetReceivedInfo(t *testing.T) {
242 server: &testServer{},
243 remoteAddr: &net.IPAddr{net.IPv4(127, 0, 0, 1), ""},
249 const line1 = "Received: from remote.test. (localhost [127.0.0.1])" + crlf
250 const line2 = "by Test-Server (mailpopbox) with "
251 const msgId = "abcdef.hijk"
252 lineLast := now.Format(time.RFC1123Z) + crlf
266 {params{"remote.test.", true, false, "foo@bar.com"},
268 line2 + "ESMTP id " + msgId + crlf,
269 "for <foo@bar.com>" + crlf,
270 "(using PLAINTEXT);" + crlf,
274 for _, test := range tests {
275 t.Logf("%#v", test.params)
277 conn.ehlo = test.params.ehlo
278 conn.esmtp = test.params.esmtp
279 //conn.tls = test.params.tls
281 envelope := Envelope{
282 RcptTo: []mail.Address{{"", test.params.address}},
287 actual := conn.getReceivedInfo(envelope)
288 actualLines := strings.SplitAfter(string(actual), crlf)
290 if len(actualLines) != len(test.expect) {
291 t.Errorf("wrong numbber of lines, expected %d, got %d", len(test.expect), len(actualLines))
295 for i, line := range actualLines {
296 expect := test.expect[i]
297 if expect != strings.TrimLeft(line, " ") {
298 t.Errorf("Expected equal string %q, got %q", expect, line)
305 func getTLSConfig(t *testing.T) *tls.Config {
306 cert, err := tls.LoadX509KeyPair("../testtls/domain.crt", "../testtls/domain.key")
312 ServerName: "localhost",
313 Certificates: []tls.Certificate{cert},
314 InsecureSkipVerify: true,
318 func setupTLSClient(t *testing.T, addr net.Addr) *textproto.Conn {
319 nc, err := net.Dial(addr.Network(), addr.String())
322 conn := textproto.NewConn(nc)
323 readCodeLine(t, conn, 220)
325 ok(t, conn.PrintfLine("EHLO test-tls"))
326 _, resp, err := conn.ReadResponse(250)
328 if !strings.Contains(resp, "STARTTLS\n") {
329 t.Errorf("STARTTLS not advertised")
332 ok(t, conn.PrintfLine("STARTTLS"))
333 readCodeLine(t, conn, 220)
335 tc := tls.Client(nc, getTLSConfig(t))
339 conn = textproto.NewConn(tc)
341 ok(t, conn.PrintfLine("EHLO test-tls-started"))
342 _, resp, err = conn.ReadResponse(250)
344 if strings.Contains(resp, "STARTTLS\n") {
345 t.Errorf("STARTTLS advertised when already started")
351 func TestTLS(t *testing.T) {
352 l := runServer(t, &testServer{tlsConfig: getTLSConfig(t)})
355 setupTLSClient(t, l.Addr())
358 func TestAuthWithoutTLS(t *testing.T) {
359 l := runServer(t, &testServer{})
362 conn := createClient(t, l.Addr())
363 readCodeLine(t, conn, 220)
365 ok(t, conn.PrintfLine("EHLO test"))
366 _, resp, err := conn.ReadResponse(250)
369 if strings.Contains(resp, "AUTH") {
370 t.Errorf("AUTH should not be advertised over plaintext")
374 func TestAuth(t *testing.T) {
375 l := runServer(t, &testServer{
376 tlsConfig: getTLSConfig(t),
385 conn := setupTLSClient(t, l.Addr())
387 b64enc := func(s string) string {
388 return string(base64.StdEncoding.EncodeToString([]byte(s)))
391 runTableTest(t, conn, []requestResponse{
393 {"AUTH OAUTHBEARER", 504, nil},
394 {"AUTH PLAIN", 334, nil},
395 {b64enc("abc\x00def\x00ghf"), 535, nil},
396 {"AUTH PLAIN", 334, nil},
397 {b64enc("\x00"), 501, nil},
398 {"AUTH PLAIN", 334, nil},
399 {"this isn't base 64", 501, nil},
400 {"AUTH PLAIN", 334, nil},
401 {b64enc("-authz-\x00-authc-\x00goats"), 250, nil},
402 {"AUTH PLAIN", 503, nil}, // already authenticated