Merge branch 'master' into outbound
[mailpopbox.git] / smtp / conn_test.go
1 // mailpopbox
2 // Copyright 2020 Blue Static <https://www.bluestatic.org>
3 // This program is free software licensed under the GNU General Public License,
4 // version 3.0. The full text of the license can be found in LICENSE.txt.
5 // SPDX-License-Identifier: GPL-3.0-only
6
7 package smtp
8
9 import (
10 "crypto/tls"
11 "encoding/base64"
12 "fmt"
13 "net"
14 "net/mail"
15 "net/textproto"
16 "path/filepath"
17 "runtime"
18 "strings"
19 "testing"
20 "time"
21
22 "go.uber.org/zap"
23 )
24
25 func _fl(depth int) string {
26 _, file, line, _ := runtime.Caller(depth + 1)
27 return fmt.Sprintf("[%s:%d]", filepath.Base(file), line)
28 }
29
30 func ok(t testing.TB, err error) {
31 if err != nil {
32 t.Errorf("%s unexpected error: %v", _fl(1), err)
33 }
34 }
35
36 func readCodeLine(t testing.TB, conn *textproto.Conn, code int) string {
37 actual, message, err := conn.ReadCodeLine(code)
38 if err != nil {
39 t.Errorf("%s ReadCodeLine error, expected %d, got %d: %v", _fl(1), code, actual, err)
40 }
41 return message
42 }
43
44 // runServer creates a TCP socket, runs a listening server, and returns the connection.
45 // The server exits when the Conn is closed.
46 func runServer(t *testing.T, server Server) net.Listener {
47 l, err := net.Listen("tcp", "localhost:0")
48 if err != nil {
49 t.Fatal(err)
50 return nil
51 }
52
53 go func() {
54 for {
55 conn, err := l.Accept()
56 if err != nil {
57 return
58 }
59 go AcceptConnection(conn, server, zap.NewNop())
60 }
61 }()
62
63 return l
64 }
65
66 type userAuth struct {
67 authz, authc, passwd string
68 }
69
70 type testServer struct {
71 EmptyServerCallbacks
72 domain string
73 blockList []string
74 tlsConfig *tls.Config
75 *userAuth
76 relayed []Envelope
77 }
78
79 func (s *testServer) Name() string {
80 return "Test-Server"
81 }
82
83 func (s *testServer) TLSConfig() *tls.Config {
84 return s.tlsConfig
85 }
86
87 func (s *testServer) VerifyAddress(addr mail.Address) ReplyLine {
88 if DomainForAddress(addr) != s.domain {
89 return ReplyBadMailbox
90 }
91 for _, block := range s.blockList {
92 if strings.ToLower(block) == addr.Address {
93 return ReplyBadMailbox
94 }
95 }
96 return ReplyOK
97 }
98
99 func (s *testServer) Authenticate(authz, authc, passwd string) bool {
100 return s.userAuth.authz == authz &&
101 s.userAuth.authc == authc &&
102 s.userAuth.passwd == passwd
103 }
104
105 func (s *testServer) RelayMessage(en Envelope) {
106 s.relayed = append(s.relayed, en)
107 }
108
109 func createClient(t *testing.T, addr net.Addr) *textproto.Conn {
110 conn, err := textproto.Dial(addr.Network(), addr.String())
111 if err != nil {
112 t.Fatal(err)
113 return nil
114 }
115 return conn
116 }
117
118 type requestResponse struct {
119 request string
120 responseCode int
121 handler func(testing.TB, *textproto.Conn)
122 }
123
124 func runTableTest(t testing.TB, conn *textproto.Conn, seq []requestResponse) {
125 for i, rr := range seq {
126 ok(t, conn.PrintfLine(rr.request))
127 if rr.handler != nil {
128 rr.handler(t, conn)
129 } else {
130 readCodeLine(t, conn, rr.responseCode)
131 }
132 if t.Failed() {
133 t.Logf("%s case %d", _fl(1), i)
134 }
135 }
136 }
137
138 // RFC 5321 ยง D.1
139 func TestScenarioTypical(t *testing.T) {
140 s := testServer{
141 domain: "foo.com",
142 blockList: []string{"Green@foo.com"},
143 }
144 l := runServer(t, &s)
145 defer l.Close()
146
147 conn := createClient(t, l.Addr())
148
149 message := readCodeLine(t, conn, 220)
150 if !strings.HasPrefix(message, s.Name()) {
151 t.Errorf("Greeting does not have server name, got %q", message)
152 }
153
154 greet := "greeting.TestScenarioTypical"
155 ok(t, conn.PrintfLine("EHLO "+greet))
156
157 _, message, err := conn.ReadResponse(250)
158 ok(t, err)
159 if !strings.Contains(message, greet) {
160 t.Errorf("EHLO response does not contain greeting, got %q", message)
161 }
162
163 ok(t, conn.PrintfLine("MAIL FROM:<Smith@bar.com>"))
164 readCodeLine(t, conn, 250)
165
166 ok(t, conn.PrintfLine("RCPT TO:<Jones@foo.com>"))
167 readCodeLine(t, conn, 250)
168
169 ok(t, conn.PrintfLine("RCPT TO:<Green@foo.com>"))
170 readCodeLine(t, conn, 550)
171
172 ok(t, conn.PrintfLine("RCPT TO:<Brown@foo.com>"))
173 readCodeLine(t, conn, 250)
174
175 ok(t, conn.PrintfLine("DATA"))
176 readCodeLine(t, conn, 354)
177
178 ok(t, conn.PrintfLine("Blah blah blah..."))
179 ok(t, conn.PrintfLine("...etc. etc. etc."))
180 ok(t, conn.PrintfLine("."))
181 readCodeLine(t, conn, 250)
182
183 ok(t, conn.PrintfLine("QUIT"))
184 readCodeLine(t, conn, 221)
185 }
186
187 func TestVerifyAddress(t *testing.T) {
188 s := testServer{
189 domain: "test.mail",
190 blockList: []string{"banned@test.mail"},
191 }
192 l := runServer(t, &s)
193 defer l.Close()
194
195 conn := createClient(t, l.Addr())
196 readCodeLine(t, conn, 220)
197
198 runTableTest(t, conn, []requestResponse{
199 {"EHLO test", 0, func(t testing.TB, conn *textproto.Conn) { conn.ReadResponse(250) }},
200 {"VRFY banned@test.mail", 252, nil},
201 {"VRFY allowed@test.mail", 252, nil},
202 {"MAIL FROM:<sender@example.com>", 250, nil},
203 {"RCPT TO:<banned@test.mail>", 550, nil},
204 {"QUIT", 221, nil},
205 })
206 }
207
208 func TestBadAddress(t *testing.T) {
209 l := runServer(t, &testServer{})
210 defer l.Close()
211
212 conn := createClient(t, l.Addr())
213 readCodeLine(t, conn, 220)
214
215 runTableTest(t, conn, []requestResponse{
216 {"EHLO test", 0, func(t testing.TB, conn *textproto.Conn) { conn.ReadResponse(250) }},
217 {"MAIL FROM:<sender>", 501, nil},
218 {"MAIL FROM:<sender@foo.com> SIZE=2163", 250, nil},
219 {"RCPT TO:<banned.net>", 501, nil},
220 {"QUIT", 221, nil},
221 })
222 }
223
224 func TestCaseSensitivty(t *testing.T) {
225 s := &testServer{
226 domain: "mail.com",
227 blockList: []string{"reject@mail.com"},
228 }
229 l := runServer(t, s)
230 defer l.Close()
231
232 conn := createClient(t, l.Addr())
233 readCodeLine(t, conn, 220)
234
235 runTableTest(t, conn, []requestResponse{
236 {"nOoP", 250, nil},
237 {"ehLO test.TEST", 0, func(t testing.TB, conn *textproto.Conn) { conn.ReadResponse(250) }},
238 {"mail FROM:<sender@example.com>", 250, nil},
239 {"RcPT tO:<receive@mail.com>", 250, nil},
240 {"RCPT TO:<reject@MAIL.com>", 550, nil},
241 {"RCPT TO:<reject@mail.com>", 550, nil},
242 {"DATa", 0, func(t testing.TB, conn *textproto.Conn) {
243 readCodeLine(t, conn, 354)
244
245 ok(t, conn.PrintfLine("."))
246 readCodeLine(t, conn, 250)
247 }},
248 {"MAIL FR:", 501, nil},
249 {"QUiT", 221, nil},
250 })
251 }
252
253 func TestGetReceivedInfo(t *testing.T) {
254 conn := connection{
255 server: &testServer{},
256 remoteAddr: &net.IPAddr{net.IPv4(127, 0, 0, 1), ""},
257 }
258
259 now := time.Now()
260
261 const crlf = "\r\n"
262 const line1 = "Received: from remote.test. (localhost [127.0.0.1])" + crlf
263 const line2 = "by Test-Server (mailpopbox) with "
264 const msgId = "abcdef.hijk"
265 lineLast := now.Format(time.RFC1123Z) + crlf
266
267 type params struct {
268 ehlo string
269 esmtp bool
270 tls bool
271 address string
272 }
273
274 tests := []struct {
275 params params
276
277 expect []string
278 }{
279 {params{"remote.test.", true, false, "foo@bar.com"},
280 []string{line1,
281 line2 + "ESMTP id " + msgId + crlf,
282 "for <foo@bar.com>" + crlf,
283 "(using PLAINTEXT);" + crlf,
284 lineLast, ""}},
285 }
286
287 for _, test := range tests {
288 t.Logf("%#v", test.params)
289
290 conn.ehlo = test.params.ehlo
291 conn.esmtp = test.params.esmtp
292 //conn.tls = test.params.tls
293
294 envelope := Envelope{
295 RcptTo: []mail.Address{{"", test.params.address}},
296 Received: now,
297 ID: msgId,
298 }
299
300 actual := conn.getReceivedInfo(envelope)
301 actualLines := strings.SplitAfter(string(actual), crlf)
302
303 if len(actualLines) != len(test.expect) {
304 t.Errorf("wrong numbber of lines, expected %d, got %d", len(test.expect), len(actualLines))
305 continue
306 }
307
308 for i, line := range actualLines {
309 expect := test.expect[i]
310 if expect != strings.TrimLeft(line, " ") {
311 t.Errorf("Expected equal string %q, got %q", expect, line)
312 }
313 }
314 }
315
316 }
317
318 func getTLSConfig(t *testing.T) *tls.Config {
319 cert, err := tls.LoadX509KeyPair("../testtls/domain.crt", "../testtls/domain.key")
320 if err != nil {
321 t.Fatal(err)
322 return nil
323 }
324 return &tls.Config{
325 ServerName: "localhost",
326 Certificates: []tls.Certificate{cert},
327 InsecureSkipVerify: true,
328 }
329 }
330
331 func setupTLSClient(t *testing.T, addr net.Addr) *textproto.Conn {
332 nc, err := net.Dial(addr.Network(), addr.String())
333 ok(t, err)
334
335 conn := textproto.NewConn(nc)
336 readCodeLine(t, conn, 220)
337
338 ok(t, conn.PrintfLine("EHLO test-tls"))
339 _, resp, err := conn.ReadResponse(250)
340 ok(t, err)
341 if !strings.Contains(resp, "STARTTLS\n") {
342 t.Errorf("STARTTLS not advertised")
343 }
344
345 ok(t, conn.PrintfLine("STARTTLS"))
346 readCodeLine(t, conn, 220)
347
348 tc := tls.Client(nc, getTLSConfig(t))
349 err = tc.Handshake()
350 ok(t, err)
351
352 conn = textproto.NewConn(tc)
353
354 ok(t, conn.PrintfLine("EHLO test-tls-started"))
355 _, resp, err = conn.ReadResponse(250)
356 ok(t, err)
357 if strings.Contains(resp, "STARTTLS\n") {
358 t.Errorf("STARTTLS advertised when already started")
359 }
360
361 return conn
362 }
363
364 func b64enc(s string) string {
365 return string(base64.StdEncoding.EncodeToString([]byte(s)))
366 }
367
368 func TestTLS(t *testing.T) {
369 l := runServer(t, &testServer{tlsConfig: getTLSConfig(t)})
370 defer l.Close()
371
372 setupTLSClient(t, l.Addr())
373 }
374
375 func TestAuthWithoutTLS(t *testing.T) {
376 l := runServer(t, &testServer{})
377 defer l.Close()
378
379 conn := createClient(t, l.Addr())
380 readCodeLine(t, conn, 220)
381
382 ok(t, conn.PrintfLine("EHLO test"))
383 _, resp, err := conn.ReadResponse(250)
384 ok(t, err)
385
386 if strings.Contains(resp, "AUTH") {
387 t.Errorf("AUTH should not be advertised over plaintext")
388 }
389 }
390
391 func TestAuth(t *testing.T) {
392 l := runServer(t, &testServer{
393 tlsConfig: getTLSConfig(t),
394 userAuth: &userAuth{
395 authz: "-authz-",
396 authc: "-authc-",
397 passwd: "goats",
398 },
399 })
400 defer l.Close()
401
402 conn := setupTLSClient(t, l.Addr())
403
404 runTableTest(t, conn, []requestResponse{
405 {"AUTH", 501, nil},
406 {"AUTH OAUTHBEARER", 504, nil},
407 {"AUTH PLAIN", 501, nil}, // Bad syntax, missing space.
408 {"AUTH PLAIN ", 334, nil},
409 {b64enc("abc\x00def\x00ghf"), 535, nil},
410 {"AUTH PLAIN ", 334, nil},
411 {b64enc("\x00"), 501, nil},
412 {"AUTH PLAIN ", 334, nil},
413 {"this isn't base 64", 501, nil},
414 {"AUTH PLAIN ", 334, nil},
415 {b64enc("-authz-\x00-authc-\x00goats"), 250, nil},
416 {"AUTH PLAIN ", 503, nil}, // Already authenticated.
417 {"NOOP", 250, nil},
418 })
419 }
420
421 func TestAuthNoInitialResponse(t *testing.T) {
422 l := runServer(t, &testServer{
423 tlsConfig: getTLSConfig(t),
424 userAuth: &userAuth{
425 authz: "",
426 authc: "user",
427 passwd: "longpassword",
428 },
429 })
430 defer l.Close()
431
432 conn := setupTLSClient(t, l.Addr())
433
434 runTableTest(t, conn, []requestResponse{
435 {"AUTH PLAIN " + b64enc("\x00user\x00longpassword"), 250, nil},
436 })
437 }
438
439 func TestRelayRequiresAuth(t *testing.T) {
440 l := runServer(t, &testServer{
441 domain: "example.com",
442 tlsConfig: getTLSConfig(t),
443 userAuth: &userAuth{
444 authz: "",
445 authc: "mailbox@example.com",
446 passwd: "test",
447 },
448 })
449 defer l.Close()
450
451 conn := setupTLSClient(t, l.Addr())
452
453 runTableTest(t, conn, []requestResponse{
454 {"MAIL FROM:<apples@example.com>", 550, nil},
455 {"MAIL FROM:<mailbox@example.com>", 550, nil},
456 {"AUTH PLAIN ", 334, nil},
457 {b64enc("\x00mailbox@example.com\x00test"), 250, nil},
458 {"MAIL FROM:<mailbox@example.com>", 250, nil},
459 })
460 }
461
462 func setupRelayTest(t *testing.T) (server *testServer, l net.Listener, conn *textproto.Conn) {
463 server = &testServer{
464 domain: "example.com",
465 tlsConfig: getTLSConfig(t),
466 userAuth: &userAuth{
467 authz: "",
468 authc: "mailbox@example.com",
469 passwd: "test",
470 },
471 }
472 l = runServer(t, server)
473 conn = setupTLSClient(t, l.Addr())
474 runTableTest(t, conn, []requestResponse{
475 {"AUTH PLAIN ", 334, nil},
476 {b64enc("\x00mailbox@example.com\x00test"), 250, nil},
477 })
478 return
479 }
480
481 func TestBasicRelay(t *testing.T) {
482 server, l, conn := setupRelayTest(t)
483 defer l.Close()
484
485 runTableTest(t, conn, []requestResponse{
486 {"MAIL FROM:<mailbox@example.com>", 250, nil},
487 {"RCPT TO:<dest@another.net>", 250, nil},
488 {"DATA", 354, func(t testing.TB, conn *textproto.Conn) {
489 readCodeLine(t, conn, 354)
490
491 ok(t, conn.PrintfLine("From: <mailbox@example.com>"))
492 ok(t, conn.PrintfLine("To: <dest@example.com>"))
493 ok(t, conn.PrintfLine("Subject: Basic relay\n"))
494 ok(t, conn.PrintfLine("This is a basic relay message"))
495 ok(t, conn.PrintfLine("."))
496 readCodeLine(t, conn, 250)
497 }},
498 })
499
500 if len(server.relayed) != 1 {
501 t.Errorf("Expected 1 relayed message, got %d", len(server.relayed))
502 }
503 }
504
505 func TestSendAsRelay(t *testing.T) {
506 server, l, conn := setupRelayTest(t)
507 defer l.Close()
508
509 runTableTest(t, conn, []requestResponse{
510 {"MAIL FROM:<mailbox@example.com>", 250, nil},
511 {"RCPT TO:<valid@dest.xyz>", 250, nil},
512 {"DATA", 354, func(t testing.TB, conn *textproto.Conn) {
513 readCodeLine(t, conn, 354)
514
515 ok(t, conn.PrintfLine("From: <mailbox@example.com>"))
516 ok(t, conn.PrintfLine("To: <valid@dest.xyz>"))
517 ok(t, conn.PrintfLine("Subject: Send-as relay [sendas:source]\n"))
518 ok(t, conn.PrintfLine("We've switched the senders!"))
519 ok(t, conn.PrintfLine("."))
520 readCodeLine(t, conn, 250)
521 }},
522 })
523
524 if len(server.relayed) != 1 {
525 t.Fatalf("Expected 1 relayed message, got %d", len(server.relayed))
526 }
527
528 replaced := "source@example.com"
529 original := "mailbox@example.com"
530
531 en := server.relayed[0]
532 if en.MailFrom.Address != replaced {
533 t.Errorf("Expected mail to be from %q, got %q", replaced, en.MailFrom.Address)
534 }
535
536 if len(en.RcptTo) != 1 {
537 t.Errorf("Expected 1 recipient, got %d", len(en.RcptTo))
538 }
539 if en.RcptTo[0].Address != "valid@dest.xyz" {
540 t.Errorf("Unexpected RcptTo %q", en.RcptTo[0].Address)
541 }
542
543 msg := string(en.Data)
544
545 if strings.Index(msg, original) != -1 {
546 t.Errorf("Should not find %q in message %q", original, msg)
547 }
548
549 if strings.Index(msg, "\nFrom: <source@example.com>\n") == -1 {
550 t.Errorf("Could not find From: header in message %q", msg)
551 }
552
553 if strings.Index(msg, "\nSubject: Send-as relay \n") == -1 {
554 t.Errorf("Could not find modified Subject: header in message %q", msg)
555 }
556 }
557
558 func TestSendMultipleRelay(t *testing.T) {
559 server, l, conn := setupRelayTest(t)
560 defer l.Close()
561
562 runTableTest(t, conn, []requestResponse{
563 {"MAIL FROM:<mailbox@example.com>", 250, nil},
564 {"RCPT TO:<valid@dest.xyz>", 250, nil},
565 {"RCPT TO:<another@dest.org>", 250, nil},
566 {"DATA", 354, func(t testing.TB, conn *textproto.Conn) {
567 readCodeLine(t, conn, 354)
568
569 ok(t, conn.PrintfLine("To: Cindy <valid@dest.xyz>, Sam <another@dest.org>"))
570 ok(t, conn.PrintfLine("From: Finn <mailbox@example.com>"))
571 ok(t, conn.PrintfLine("Subject: Two destinations [sendas:source]\n"))
572 ok(t, conn.PrintfLine("And we've switched the senders!"))
573 ok(t, conn.PrintfLine("."))
574 readCodeLine(t, conn, 250)
575 }},
576 })
577
578 if len(server.relayed) != 1 {
579 t.Fatalf("Expected 1 relayed message, got %d", len(server.relayed))
580 }
581
582 replaced := "source@example.com"
583 original := "mailbox@example.com"
584
585 en := server.relayed[0]
586 if en.MailFrom.Address != replaced {
587 t.Errorf("Expected mail to be from %q, got %q", replaced, en.MailFrom.Address)
588 }
589
590 if len(en.RcptTo) != 2 {
591 t.Errorf("Expected 2 recipient, got %d", len(en.RcptTo))
592 }
593 if en.RcptTo[0].Address != "valid@dest.xyz" {
594 t.Errorf("Unexpected RcptTo %q", en.RcptTo[0].Address)
595 }
596
597 msg := string(en.Data)
598
599 if strings.Index(msg, original) != -1 {
600 t.Errorf("Should not find %q in message %q", original, msg)
601 }
602
603 if strings.Index(msg, "\nFrom: Finn <source@example.com>\n") == -1 {
604 t.Errorf("Could not find From: header in message %q", msg)
605 }
606
607 if strings.Index(msg, "\nSubject: Two destinations \n") == -1 {
608 t.Errorf("Could not find modified Subject: header in message %q", msg)
609 }
610 }