Change the send-as functionality to use a string in the Subject.
[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 actual, message, err := conn.ReadCodeLine(code)
32 if err != nil {
33 t.Errorf("%s ReadCodeLine error, expected %d, got %d: %v", _fl(1), code, actual, 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 ok(t, conn.PrintfLine(rr.request))
121 if rr.handler != nil {
122 rr.handler(t, conn)
123 } else {
124 readCodeLine(t, conn, rr.responseCode)
125 }
126 if t.Failed() {
127 t.Logf("%s case %d", _fl(1), i)
128 }
129 }
130 }
131
132 // RFC 5321 ยง D.1
133 func TestScenarioTypical(t *testing.T) {
134 s := testServer{
135 domain: "foo.com",
136 blockList: []string{"Green@foo.com"},
137 }
138 l := runServer(t, &s)
139 defer l.Close()
140
141 conn := createClient(t, l.Addr())
142
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)
146 }
147
148 greet := "greeting.TestScenarioTypical"
149 ok(t, conn.PrintfLine("EHLO "+greet))
150
151 _, message, err := conn.ReadResponse(250)
152 ok(t, err)
153 if !strings.Contains(message, greet) {
154 t.Errorf("EHLO response does not contain greeting, got %q", message)
155 }
156
157 ok(t, conn.PrintfLine("MAIL FROM:<Smith@bar.com>"))
158 readCodeLine(t, conn, 250)
159
160 ok(t, conn.PrintfLine("RCPT TO:<Jones@foo.com>"))
161 readCodeLine(t, conn, 250)
162
163 ok(t, conn.PrintfLine("RCPT TO:<Green@foo.com>"))
164 readCodeLine(t, conn, 550)
165
166 ok(t, conn.PrintfLine("RCPT TO:<Brown@foo.com>"))
167 readCodeLine(t, conn, 250)
168
169 ok(t, conn.PrintfLine("DATA"))
170 readCodeLine(t, conn, 354)
171
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)
176
177 ok(t, conn.PrintfLine("QUIT"))
178 readCodeLine(t, conn, 221)
179 }
180
181 func TestVerifyAddress(t *testing.T) {
182 s := testServer{
183 domain: "test.mail",
184 blockList: []string{"banned@test.mail"},
185 }
186 l := runServer(t, &s)
187 defer l.Close()
188
189 conn := createClient(t, l.Addr())
190 readCodeLine(t, conn, 220)
191
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},
198 {"QUIT", 221, nil},
199 })
200 }
201
202 func TestBadAddress(t *testing.T) {
203 l := runServer(t, &testServer{})
204 defer l.Close()
205
206 conn := createClient(t, l.Addr())
207 readCodeLine(t, conn, 220)
208
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},
214 {"QUIT", 221, nil},
215 })
216 }
217
218 func TestCaseSensitivty(t *testing.T) {
219 s := &testServer{
220 domain: "mail.com",
221 blockList: []string{"reject@mail.com"},
222 }
223 l := runServer(t, s)
224 defer l.Close()
225
226 conn := createClient(t, l.Addr())
227 readCodeLine(t, conn, 220)
228
229 runTableTest(t, conn, []requestResponse{
230 {"nOoP", 250, nil},
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)
238
239 ok(t, conn.PrintfLine("."))
240 readCodeLine(t, conn, 250)
241 }},
242 {"MAIL FR:", 501, nil},
243 {"QUiT", 221, nil},
244 })
245 }
246
247 func TestGetReceivedInfo(t *testing.T) {
248 conn := connection{
249 server: &testServer{},
250 remoteAddr: &net.IPAddr{net.IPv4(127, 0, 0, 1), ""},
251 }
252
253 now := time.Now()
254
255 const crlf = "\r\n"
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
260
261 type params struct {
262 ehlo string
263 esmtp bool
264 tls bool
265 address string
266 }
267
268 tests := []struct {
269 params params
270
271 expect []string
272 }{
273 {params{"remote.test.", true, false, "foo@bar.com"},
274 []string{line1,
275 line2 + "ESMTP id " + msgId + crlf,
276 "for <foo@bar.com>" + crlf,
277 "(using PLAINTEXT);" + crlf,
278 lineLast, ""}},
279 }
280
281 for _, test := range tests {
282 t.Logf("%#v", test.params)
283
284 conn.ehlo = test.params.ehlo
285 conn.esmtp = test.params.esmtp
286 //conn.tls = test.params.tls
287
288 envelope := Envelope{
289 RcptTo: []mail.Address{{"", test.params.address}},
290 Received: now,
291 ID: msgId,
292 }
293
294 actual := conn.getReceivedInfo(envelope)
295 actualLines := strings.SplitAfter(string(actual), crlf)
296
297 if len(actualLines) != len(test.expect) {
298 t.Errorf("wrong numbber of lines, expected %d, got %d", len(test.expect), len(actualLines))
299 continue
300 }
301
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)
306 }
307 }
308 }
309
310 }
311
312 func getTLSConfig(t *testing.T) *tls.Config {
313 cert, err := tls.LoadX509KeyPair("../testtls/domain.crt", "../testtls/domain.key")
314 if err != nil {
315 t.Fatal(err)
316 return nil
317 }
318 return &tls.Config{
319 ServerName: "localhost",
320 Certificates: []tls.Certificate{cert},
321 InsecureSkipVerify: true,
322 }
323 }
324
325 func setupTLSClient(t *testing.T, addr net.Addr) *textproto.Conn {
326 nc, err := net.Dial(addr.Network(), addr.String())
327 ok(t, err)
328
329 conn := textproto.NewConn(nc)
330 readCodeLine(t, conn, 220)
331
332 ok(t, conn.PrintfLine("EHLO test-tls"))
333 _, resp, err := conn.ReadResponse(250)
334 ok(t, err)
335 if !strings.Contains(resp, "STARTTLS\n") {
336 t.Errorf("STARTTLS not advertised")
337 }
338
339 ok(t, conn.PrintfLine("STARTTLS"))
340 readCodeLine(t, conn, 220)
341
342 tc := tls.Client(nc, getTLSConfig(t))
343 err = tc.Handshake()
344 ok(t, err)
345
346 conn = textproto.NewConn(tc)
347
348 ok(t, conn.PrintfLine("EHLO test-tls-started"))
349 _, resp, err = conn.ReadResponse(250)
350 ok(t, err)
351 if strings.Contains(resp, "STARTTLS\n") {
352 t.Errorf("STARTTLS advertised when already started")
353 }
354
355 return conn
356 }
357
358 func b64enc(s string) string {
359 return string(base64.StdEncoding.EncodeToString([]byte(s)))
360 }
361
362 func TestTLS(t *testing.T) {
363 l := runServer(t, &testServer{tlsConfig: getTLSConfig(t)})
364 defer l.Close()
365
366 setupTLSClient(t, l.Addr())
367 }
368
369 func TestAuthWithoutTLS(t *testing.T) {
370 l := runServer(t, &testServer{})
371 defer l.Close()
372
373 conn := createClient(t, l.Addr())
374 readCodeLine(t, conn, 220)
375
376 ok(t, conn.PrintfLine("EHLO test"))
377 _, resp, err := conn.ReadResponse(250)
378 ok(t, err)
379
380 if strings.Contains(resp, "AUTH") {
381 t.Errorf("AUTH should not be advertised over plaintext")
382 }
383 }
384
385 func TestAuth(t *testing.T) {
386 l := runServer(t, &testServer{
387 tlsConfig: getTLSConfig(t),
388 userAuth: &userAuth{
389 authz: "-authz-",
390 authc: "-authc-",
391 passwd: "goats",
392 },
393 })
394 defer l.Close()
395
396 conn := setupTLSClient(t, l.Addr())
397
398 runTableTest(t, conn, []requestResponse{
399 {"AUTH", 501, nil},
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.
411 {"NOOP", 250, nil},
412 })
413 }
414
415 func TestAuthNoInitialResponse(t *testing.T) {
416 l := runServer(t, &testServer{
417 tlsConfig: getTLSConfig(t),
418 userAuth: &userAuth{
419 authz: "",
420 authc: "user",
421 passwd: "longpassword",
422 },
423 })
424 defer l.Close()
425
426 conn := setupTLSClient(t, l.Addr())
427
428 runTableTest(t, conn, []requestResponse{
429 {"AUTH PLAIN " + b64enc("\x00user\x00longpassword"), 250, nil},
430 })
431 }
432
433 func TestRelayRequiresAuth(t *testing.T) {
434 l := runServer(t, &testServer{
435 domain: "example.com",
436 tlsConfig: getTLSConfig(t),
437 userAuth: &userAuth{
438 authz: "",
439 authc: "mailbox@example.com",
440 passwd: "test",
441 },
442 })
443 defer l.Close()
444
445 conn := setupTLSClient(t, l.Addr())
446
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},
453 })
454 }
455
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),
460 userAuth: &userAuth{
461 authz: "",
462 authc: "mailbox@example.com",
463 passwd: "test",
464 },
465 }
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},
471 })
472 return
473 }
474
475 func TestBasicRelay(t *testing.T) {
476 server, l, conn := setupRelayTest(t)
477 defer l.Close()
478
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)
484
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)
491 }},
492 })
493
494 if len(server.relayed) != 1 {
495 t.Errorf("Expected 1 relayed message, got %d", len(server.relayed))
496 }
497 }
498
499 func TestSendAsRelay(t *testing.T) {
500 server, l, conn := setupRelayTest(t)
501 defer l.Close()
502
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)
508
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)
515 }},
516 })
517
518 if len(server.relayed) != 1 {
519 t.Fatalf("Expected 1 relayed message, got %d", len(server.relayed))
520 }
521
522 replaced := "source@example.com"
523 original := "mailbox@example.com"
524
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)
528 }
529
530 if len(en.RcptTo) != 1 {
531 t.Errorf("Expected 1 recipient, got %d", len(en.RcptTo))
532 }
533 if en.RcptTo[0].Address != "valid@dest.xyz" {
534 t.Errorf("Unexpected RcptTo %q", en.RcptTo[0].Address)
535 }
536
537 msg := string(en.Data)
538
539 if strings.Index(msg, original) != -1 {
540 t.Errorf("Should not find %q in message %q", original, msg)
541 }
542
543 if strings.Index(msg, "\nFrom: <source@example.com>\n") == -1 {
544 t.Errorf("Could not find From: header in message %q", msg)
545 }
546
547 if strings.Index(msg, "\nSubject: Send-as relay \n") == -1 {
548 t.Errorf("Could not find modified Subject: header in message %q", msg)
549 }
550 }
551
552 func TestSendMultipleRelay(t *testing.T) {
553 server, l, conn := setupRelayTest(t)
554 defer l.Close()
555
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)
562
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)
569 }},
570 })
571
572 if len(server.relayed) != 1 {
573 t.Fatalf("Expected 1 relayed message, got %d", len(server.relayed))
574 }
575
576 replaced := "source@example.com"
577 original := "mailbox@example.com"
578
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)
582 }
583
584 if len(en.RcptTo) != 2 {
585 t.Errorf("Expected 2 recipient, got %d", len(en.RcptTo))
586 }
587 if en.RcptTo[0].Address != "valid@dest.xyz" {
588 t.Errorf("Unexpected RcptTo %q", en.RcptTo[0].Address)
589 }
590
591 msg := string(en.Data)
592
593 if strings.Index(msg, original) != -1 {
594 t.Errorf("Should not find %q in message %q", original, msg)
595 }
596
597 if strings.Index(msg, "\nFrom: Finn <source@example.com>\n") == -1 {
598 t.Errorf("Could not find From: header in message %q", msg)
599 }
600
601 if strings.Index(msg, "\nSubject: Two destinations \n") == -1 {
602 t.Errorf("Could not find modified Subject: header in message %q", msg)
603 }
604 }