In smtp/conn_test.go, testServer.verifyAddress should check a common domain.
[mailpopbox.git] / smtp / conn_test.go
1 package smtp
2
3 import (
4 "crypto/tls"
5 "encoding/base64"
6 "fmt"
7 "net"
8 "net/mail"
9 "net/textproto"
10 "path/filepath"
11 "runtime"
12 "strings"
13 "testing"
14 "time"
15
16 "github.com/uber-go/zap"
17 )
18
19 func _fl(depth int) string {
20 _, file, line, _ := runtime.Caller(depth + 1)
21 return fmt.Sprintf("[%s:%d]", filepath.Base(file), line)
22 }
23
24 func ok(t testing.TB, err error) {
25 if err != nil {
26 t.Errorf("%s unexpected error: %v", _fl(1), err)
27 }
28 }
29
30 func readCodeLine(t testing.TB, conn *textproto.Conn, code int) string {
31 _, message, err := conn.ReadCodeLine(code)
32 if err != nil {
33 t.Errorf("%s ReadCodeLine error: %v", _fl(1), err)
34 }
35 return message
36 }
37
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")
42 if err != nil {
43 t.Fatal(err)
44 return nil
45 }
46
47 go func() {
48 for {
49 conn, err := l.Accept()
50 if err != nil {
51 return
52 }
53 go AcceptConnection(conn, server, zap.New(zap.NullEncoder()))
54 }
55 }()
56
57 return l
58 }
59
60 type userAuth struct {
61 authz, authc, passwd string
62 }
63
64 type testServer struct {
65 EmptyServerCallbacks
66 domain string
67 blockList []string
68 tlsConfig *tls.Config
69 *userAuth
70 }
71
72 func (s *testServer) Name() string {
73 return "Test-Server"
74 }
75
76 func (s *testServer) TLSConfig() *tls.Config {
77 return s.tlsConfig
78 }
79
80 func (s *testServer) VerifyAddress(addr mail.Address) ReplyLine {
81 if DomainForAddress(addr) != s.domain {
82 return ReplyBadMailbox
83 }
84 for _, block := range s.blockList {
85 if strings.ToLower(block) == addr.Address {
86 return ReplyBadMailbox
87 }
88 }
89 return ReplyOK
90 }
91
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
96 }
97
98 func createClient(t *testing.T, addr net.Addr) *textproto.Conn {
99 conn, err := textproto.Dial(addr.Network(), addr.String())
100 if err != nil {
101 t.Fatal(err)
102 return nil
103 }
104 return conn
105 }
106
107 type requestResponse struct {
108 request string
109 responseCode int
110 handler func(testing.TB, *textproto.Conn)
111 }
112
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 {
118 rr.handler(t, conn)
119 } else {
120 readCodeLine(t, conn, rr.responseCode)
121 }
122 }
123 }
124
125 // RFC 5321 ยง D.1
126 func TestScenarioTypical(t *testing.T) {
127 s := testServer{
128 domain: "foo.com",
129 blockList: []string{"Green@foo.com"},
130 }
131 l := runServer(t, &s)
132 defer l.Close()
133
134 conn := createClient(t, l.Addr())
135
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)
139 }
140
141 greet := "greeting.TestScenarioTypical"
142 ok(t, conn.PrintfLine("EHLO "+greet))
143
144 _, message, err := conn.ReadResponse(250)
145 ok(t, err)
146 if !strings.Contains(message, greet) {
147 t.Errorf("EHLO response does not contain greeting, got %q", message)
148 }
149
150 ok(t, conn.PrintfLine("MAIL FROM:<Smith@bar.com>"))
151 readCodeLine(t, conn, 250)
152
153 ok(t, conn.PrintfLine("RCPT TO:<Jones@foo.com>"))
154 readCodeLine(t, conn, 250)
155
156 ok(t, conn.PrintfLine("RCPT TO:<Green@foo.com>"))
157 readCodeLine(t, conn, 550)
158
159 ok(t, conn.PrintfLine("RCPT TO:<Brown@foo.com>"))
160 readCodeLine(t, conn, 250)
161
162 ok(t, conn.PrintfLine("DATA"))
163 readCodeLine(t, conn, 354)
164
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)
169
170 ok(t, conn.PrintfLine("QUIT"))
171 readCodeLine(t, conn, 221)
172 }
173
174 func TestVerifyAddress(t *testing.T) {
175 s := testServer{
176 domain: "test.mail",
177 blockList: []string{"banned@test.mail"},
178 }
179 l := runServer(t, &s)
180 defer l.Close()
181
182 conn := createClient(t, l.Addr())
183 readCodeLine(t, conn, 220)
184
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},
191 {"QUIT", 221, nil},
192 })
193 }
194
195 func TestBadAddress(t *testing.T) {
196 l := runServer(t, &testServer{})
197 defer l.Close()
198
199 conn := createClient(t, l.Addr())
200 readCodeLine(t, conn, 220)
201
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},
207 {"QUIT", 221, nil},
208 })
209 }
210
211 func TestCaseSensitivty(t *testing.T) {
212 s := &testServer{
213 domain: "mail.com",
214 blockList: []string{"reject@mail.com"},
215 }
216 l := runServer(t, s)
217 defer l.Close()
218
219 conn := createClient(t, l.Addr())
220 readCodeLine(t, conn, 220)
221
222 runTableTest(t, conn, []requestResponse{
223 {"nOoP", 250, nil},
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)
231
232 ok(t, conn.PrintfLine("."))
233 readCodeLine(t, conn, 250)
234 }},
235 {"MAIL FR:", 501, nil},
236 {"QUiT", 221, nil},
237 })
238 }
239
240 func TestGetReceivedInfo(t *testing.T) {
241 conn := connection{
242 server: &testServer{},
243 remoteAddr: &net.IPAddr{net.IPv4(127, 0, 0, 1), ""},
244 }
245
246 now := time.Now()
247
248 const crlf = "\r\n"
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
253
254 type params struct {
255 ehlo string
256 esmtp bool
257 tls bool
258 address string
259 }
260
261 tests := []struct {
262 params params
263
264 expect []string
265 }{
266 {params{"remote.test.", true, false, "foo@bar.com"},
267 []string{line1,
268 line2 + "ESMTP id " + msgId + crlf,
269 "for <foo@bar.com>" + crlf,
270 "(using PLAINTEXT);" + crlf,
271 lineLast, ""}},
272 }
273
274 for _, test := range tests {
275 t.Logf("%#v", test.params)
276
277 conn.ehlo = test.params.ehlo
278 conn.esmtp = test.params.esmtp
279 //conn.tls = test.params.tls
280
281 envelope := Envelope{
282 RcptTo: []mail.Address{{"", test.params.address}},
283 Received: now,
284 ID: msgId,
285 }
286
287 actual := conn.getReceivedInfo(envelope)
288 actualLines := strings.SplitAfter(string(actual), crlf)
289
290 if len(actualLines) != len(test.expect) {
291 t.Errorf("wrong numbber of lines, expected %d, got %d", len(test.expect), len(actualLines))
292 continue
293 }
294
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)
299 }
300 }
301 }
302
303 }
304
305 func getTLSConfig(t *testing.T) *tls.Config {
306 cert, err := tls.LoadX509KeyPair("../testtls/domain.crt", "../testtls/domain.key")
307 if err != nil {
308 t.Fatal(err)
309 return nil
310 }
311 return &tls.Config{
312 ServerName: "localhost",
313 Certificates: []tls.Certificate{cert},
314 InsecureSkipVerify: true,
315 }
316 }
317
318 func setupTLSClient(t *testing.T, addr net.Addr) *textproto.Conn {
319 nc, err := net.Dial(addr.Network(), addr.String())
320 ok(t, err)
321
322 conn := textproto.NewConn(nc)
323 readCodeLine(t, conn, 220)
324
325 ok(t, conn.PrintfLine("EHLO test-tls"))
326 _, resp, err := conn.ReadResponse(250)
327 ok(t, err)
328 if !strings.Contains(resp, "STARTTLS\n") {
329 t.Errorf("STARTTLS not advertised")
330 }
331
332 ok(t, conn.PrintfLine("STARTTLS"))
333 readCodeLine(t, conn, 220)
334
335 tc := tls.Client(nc, getTLSConfig(t))
336 err = tc.Handshake()
337 ok(t, err)
338
339 conn = textproto.NewConn(tc)
340
341 ok(t, conn.PrintfLine("EHLO test-tls-started"))
342 _, resp, err = conn.ReadResponse(250)
343 ok(t, err)
344 if strings.Contains(resp, "STARTTLS\n") {
345 t.Errorf("STARTTLS advertised when already started")
346 }
347
348 return conn
349 }
350
351 func TestTLS(t *testing.T) {
352 l := runServer(t, &testServer{tlsConfig: getTLSConfig(t)})
353 defer l.Close()
354
355 setupTLSClient(t, l.Addr())
356 }
357
358 func TestAuthWithoutTLS(t *testing.T) {
359 l := runServer(t, &testServer{})
360 defer l.Close()
361
362 conn := createClient(t, l.Addr())
363 readCodeLine(t, conn, 220)
364
365 ok(t, conn.PrintfLine("EHLO test"))
366 _, resp, err := conn.ReadResponse(250)
367 ok(t, err)
368
369 if strings.Contains(resp, "AUTH") {
370 t.Errorf("AUTH should not be advertised over plaintext")
371 }
372 }
373
374 func TestAuth(t *testing.T) {
375 l := runServer(t, &testServer{
376 tlsConfig: getTLSConfig(t),
377 userAuth: &userAuth{
378 authz: "-authz-",
379 authc: "-authc-",
380 passwd: "goats",
381 },
382 })
383 defer l.Close()
384
385 conn := setupTLSClient(t, l.Addr())
386
387 b64enc := func(s string) string {
388 return string(base64.StdEncoding.EncodeToString([]byte(s)))
389 }
390
391 runTableTest(t, conn, []requestResponse{
392 {"AUTH", 501, nil},
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
403 {"NOOP", 250, nil},
404 })
405 }