Add support for outbound SMTP delivery.
[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 relayed []Envelope
71 }
72
73 func (s *testServer) Name() string {
74 return "Test-Server"
75 }
76
77 func (s *testServer) TLSConfig() *tls.Config {
78 return s.tlsConfig
79 }
80
81 func (s *testServer) VerifyAddress(addr mail.Address) ReplyLine {
82 if DomainForAddress(addr) != s.domain {
83 return ReplyBadMailbox
84 }
85 for _, block := range s.blockList {
86 if strings.ToLower(block) == addr.Address {
87 return ReplyBadMailbox
88 }
89 }
90 return ReplyOK
91 }
92
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
97 }
98
99 func (s *testServer) RelayMessage(en Envelope) {
100 s.relayed = append(s.relayed, en)
101 }
102
103 func createClient(t *testing.T, addr net.Addr) *textproto.Conn {
104 conn, err := textproto.Dial(addr.Network(), addr.String())
105 if err != nil {
106 t.Fatal(err)
107 return nil
108 }
109 return conn
110 }
111
112 type requestResponse struct {
113 request string
114 responseCode int
115 handler func(testing.TB, *textproto.Conn)
116 }
117
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 {
123 rr.handler(t, conn)
124 } else {
125 readCodeLine(t, conn, rr.responseCode)
126 }
127 }
128 }
129
130 // RFC 5321 ยง D.1
131 func TestScenarioTypical(t *testing.T) {
132 s := testServer{
133 domain: "foo.com",
134 blockList: []string{"Green@foo.com"},
135 }
136 l := runServer(t, &s)
137 defer l.Close()
138
139 conn := createClient(t, l.Addr())
140
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)
144 }
145
146 greet := "greeting.TestScenarioTypical"
147 ok(t, conn.PrintfLine("EHLO "+greet))
148
149 _, message, err := conn.ReadResponse(250)
150 ok(t, err)
151 if !strings.Contains(message, greet) {
152 t.Errorf("EHLO response does not contain greeting, got %q", message)
153 }
154
155 ok(t, conn.PrintfLine("MAIL FROM:<Smith@bar.com>"))
156 readCodeLine(t, conn, 250)
157
158 ok(t, conn.PrintfLine("RCPT TO:<Jones@foo.com>"))
159 readCodeLine(t, conn, 250)
160
161 ok(t, conn.PrintfLine("RCPT TO:<Green@foo.com>"))
162 readCodeLine(t, conn, 550)
163
164 ok(t, conn.PrintfLine("RCPT TO:<Brown@foo.com>"))
165 readCodeLine(t, conn, 250)
166
167 ok(t, conn.PrintfLine("DATA"))
168 readCodeLine(t, conn, 354)
169
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)
174
175 ok(t, conn.PrintfLine("QUIT"))
176 readCodeLine(t, conn, 221)
177 }
178
179 func TestVerifyAddress(t *testing.T) {
180 s := testServer{
181 domain: "test.mail",
182 blockList: []string{"banned@test.mail"},
183 }
184 l := runServer(t, &s)
185 defer l.Close()
186
187 conn := createClient(t, l.Addr())
188 readCodeLine(t, conn, 220)
189
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},
196 {"QUIT", 221, nil},
197 })
198 }
199
200 func TestBadAddress(t *testing.T) {
201 l := runServer(t, &testServer{})
202 defer l.Close()
203
204 conn := createClient(t, l.Addr())
205 readCodeLine(t, conn, 220)
206
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},
212 {"QUIT", 221, nil},
213 })
214 }
215
216 func TestCaseSensitivty(t *testing.T) {
217 s := &testServer{
218 domain: "mail.com",
219 blockList: []string{"reject@mail.com"},
220 }
221 l := runServer(t, s)
222 defer l.Close()
223
224 conn := createClient(t, l.Addr())
225 readCodeLine(t, conn, 220)
226
227 runTableTest(t, conn, []requestResponse{
228 {"nOoP", 250, nil},
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)
236
237 ok(t, conn.PrintfLine("."))
238 readCodeLine(t, conn, 250)
239 }},
240 {"MAIL FR:", 501, nil},
241 {"QUiT", 221, nil},
242 })
243 }
244
245 func TestGetReceivedInfo(t *testing.T) {
246 conn := connection{
247 server: &testServer{},
248 remoteAddr: &net.IPAddr{net.IPv4(127, 0, 0, 1), ""},
249 }
250
251 now := time.Now()
252
253 const crlf = "\r\n"
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
258
259 type params struct {
260 ehlo string
261 esmtp bool
262 tls bool
263 address string
264 }
265
266 tests := []struct {
267 params params
268
269 expect []string
270 }{
271 {params{"remote.test.", true, false, "foo@bar.com"},
272 []string{line1,
273 line2 + "ESMTP id " + msgId + crlf,
274 "for <foo@bar.com>" + crlf,
275 "(using PLAINTEXT);" + crlf,
276 lineLast, ""}},
277 }
278
279 for _, test := range tests {
280 t.Logf("%#v", test.params)
281
282 conn.ehlo = test.params.ehlo
283 conn.esmtp = test.params.esmtp
284 //conn.tls = test.params.tls
285
286 envelope := Envelope{
287 RcptTo: []mail.Address{{"", test.params.address}},
288 Received: now,
289 ID: msgId,
290 }
291
292 actual := conn.getReceivedInfo(envelope)
293 actualLines := strings.SplitAfter(string(actual), crlf)
294
295 if len(actualLines) != len(test.expect) {
296 t.Errorf("wrong numbber of lines, expected %d, got %d", len(test.expect), len(actualLines))
297 continue
298 }
299
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)
304 }
305 }
306 }
307
308 }
309
310 func getTLSConfig(t *testing.T) *tls.Config {
311 cert, err := tls.LoadX509KeyPair("../testtls/domain.crt", "../testtls/domain.key")
312 if err != nil {
313 t.Fatal(err)
314 return nil
315 }
316 return &tls.Config{
317 ServerName: "localhost",
318 Certificates: []tls.Certificate{cert},
319 InsecureSkipVerify: true,
320 }
321 }
322
323 func setupTLSClient(t *testing.T, addr net.Addr) *textproto.Conn {
324 nc, err := net.Dial(addr.Network(), addr.String())
325 ok(t, err)
326
327 conn := textproto.NewConn(nc)
328 readCodeLine(t, conn, 220)
329
330 ok(t, conn.PrintfLine("EHLO test-tls"))
331 _, resp, err := conn.ReadResponse(250)
332 ok(t, err)
333 if !strings.Contains(resp, "STARTTLS\n") {
334 t.Errorf("STARTTLS not advertised")
335 }
336
337 ok(t, conn.PrintfLine("STARTTLS"))
338 readCodeLine(t, conn, 220)
339
340 tc := tls.Client(nc, getTLSConfig(t))
341 err = tc.Handshake()
342 ok(t, err)
343
344 conn = textproto.NewConn(tc)
345
346 ok(t, conn.PrintfLine("EHLO test-tls-started"))
347 _, resp, err = conn.ReadResponse(250)
348 ok(t, err)
349 if strings.Contains(resp, "STARTTLS\n") {
350 t.Errorf("STARTTLS advertised when already started")
351 }
352
353 return conn
354 }
355
356 func b64enc(s string) string {
357 return string(base64.StdEncoding.EncodeToString([]byte(s)))
358 }
359
360 func TestTLS(t *testing.T) {
361 l := runServer(t, &testServer{tlsConfig: getTLSConfig(t)})
362 defer l.Close()
363
364 setupTLSClient(t, l.Addr())
365 }
366
367 func TestAuthWithoutTLS(t *testing.T) {
368 l := runServer(t, &testServer{})
369 defer l.Close()
370
371 conn := createClient(t, l.Addr())
372 readCodeLine(t, conn, 220)
373
374 ok(t, conn.PrintfLine("EHLO test"))
375 _, resp, err := conn.ReadResponse(250)
376 ok(t, err)
377
378 if strings.Contains(resp, "AUTH") {
379 t.Errorf("AUTH should not be advertised over plaintext")
380 }
381 }
382
383 func TestAuth(t *testing.T) {
384 l := runServer(t, &testServer{
385 tlsConfig: getTLSConfig(t),
386 userAuth: &userAuth{
387 authz: "-authz-",
388 authc: "-authc-",
389 passwd: "goats",
390 },
391 })
392 defer l.Close()
393
394 conn := setupTLSClient(t, l.Addr())
395
396 runTableTest(t, conn, []requestResponse{
397 {"AUTH", 501, nil},
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
408 {"NOOP", 250, nil},
409 })
410 }
411
412 func TestRelayRequiresAuth(t *testing.T) {
413 l := runServer(t, &testServer{
414 domain: "example.com",
415 tlsConfig: getTLSConfig(t),
416 userAuth: &userAuth{
417 authz: "",
418 authc: "mailbox@example.com",
419 passwd: "test",
420 },
421 })
422 defer l.Close()
423
424 conn := setupTLSClient(t, l.Addr())
425
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},
432 })
433 }
434
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),
439 userAuth: &userAuth{
440 authz: "",
441 authc: "mailbox@example.com",
442 passwd: "test",
443 },
444 }
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},
450 })
451 return
452 }
453
454 func TestBasicRelay(t *testing.T) {
455 server, l, conn := setupRelayTest(t)
456 defer l.Close()
457
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)
463
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)
470 }},
471 })
472
473 if len(server.relayed) != 1 {
474 t.Errorf("Expected 1 relayed message, got %d", len(server.relayed))
475 }
476 }
477
478 func TestNoInternalRelays(t *testing.T) {
479 _, l, conn := setupRelayTest(t)
480 defer l.Close()
481
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},
487 })
488 }
489
490 func TestSendAsRelay(t *testing.T) {
491 server, l, conn := setupRelayTest(t)
492 defer l.Close()
493
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)
501
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)
508 }},
509 })
510
511 if len(server.relayed) != 1 {
512 t.Errorf("Expected 1 relayed message, got %d", len(server.relayed))
513 }
514
515 replaced := "source@example.com"
516 original := "mailbox@example.com"
517
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)
521 }
522
523 if len(en.RcptTo) != 1 {
524 t.Errorf("Expected 1 recipient, got %d", len(en.RcptTo))
525 }
526 if en.RcptTo[0].Address != "valid@dest.xyz" {
527 t.Errorf("Unexpected RcptTo %q", en.RcptTo[0].Address)
528 }
529
530 msg := string(en.Data)
531
532 if strings.Index(msg, original) != -1 {
533 t.Errorf("Should not find %q in message %q", original, msg)
534 }
535
536 if strings.Index(msg, "\nFrom: <source@example.com>\n") == -1 {
537 t.Errorf("Could not find From: header in message %q", msg)
538 }
539 }
540
541 func TestSendMultipleRelay(t *testing.T) {
542 server, l, conn := setupRelayTest(t)
543 defer l.Close()
544
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)
552
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)
559 }},
560 })
561
562 if len(server.relayed) != 1 {
563 t.Errorf("Expected 1 relayed message, got %d", len(server.relayed))
564 }
565
566 replaced := "source@example.com"
567 original := "mailbox@example.com"
568
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)
572 }
573
574 if len(en.RcptTo) != 2 {
575 t.Errorf("Expected 2 recipient, got %d", len(en.RcptTo))
576 }
577 if en.RcptTo[0].Address != "valid@dest.xyz" {
578 t.Errorf("Unexpected RcptTo %q", en.RcptTo[0].Address)
579 }
580
581 msg := string(en.Data)
582
583 if strings.Index(msg, original) != -1 {
584 t.Errorf("Should not find %q in message %q", original, msg)
585 }
586
587 if strings.Index(msg, "\nFrom: <source@example.com>\n") == -1 {
588 t.Errorf("Could not find From: header in message %q", msg)
589 }
590 }