Move send-as handling out of the smtp package and into the core server.
[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, authc string) {
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 want, got := len(test.expect), len(actualLines); want != got {
304 t.Errorf("wrong numbber of lines, want %d, got %d", want, got)
305 continue
306 }
307
308 for i, line := range actualLines {
309 if want, got := test.expect[i], strings.TrimLeft(line, " "); want != got {
310 t.Errorf("want equal string %q, got %q", want, got)
311 }
312 }
313 }
314
315 }
316
317 func getTLSConfig(t *testing.T) *tls.Config {
318 cert, err := tls.LoadX509KeyPair("../testtls/domain.crt", "../testtls/domain.key")
319 if err != nil {
320 t.Fatal(err)
321 return nil
322 }
323 return &tls.Config{
324 ServerName: "localhost",
325 Certificates: []tls.Certificate{cert},
326 InsecureSkipVerify: true,
327 }
328 }
329
330 func setupTLSClient(t *testing.T, addr net.Addr) *textproto.Conn {
331 nc, err := net.Dial(addr.Network(), addr.String())
332 ok(t, err)
333
334 conn := textproto.NewConn(nc)
335 readCodeLine(t, conn, 220)
336
337 ok(t, conn.PrintfLine("EHLO test-tls"))
338 _, resp, err := conn.ReadResponse(250)
339 ok(t, err)
340 if !strings.Contains(resp, "STARTTLS\n") {
341 t.Errorf("STARTTLS not advertised")
342 }
343
344 ok(t, conn.PrintfLine("STARTTLS"))
345 readCodeLine(t, conn, 220)
346
347 tc := tls.Client(nc, getTLSConfig(t))
348 err = tc.Handshake()
349 ok(t, err)
350
351 conn = textproto.NewConn(tc)
352
353 ok(t, conn.PrintfLine("EHLO test-tls-started"))
354 _, resp, err = conn.ReadResponse(250)
355 ok(t, err)
356 if strings.Contains(resp, "STARTTLS\n") {
357 t.Errorf("STARTTLS advertised when already started")
358 }
359
360 return conn
361 }
362
363 func b64enc(s string) string {
364 return string(base64.StdEncoding.EncodeToString([]byte(s)))
365 }
366
367 func TestTLS(t *testing.T) {
368 l := runServer(t, &testServer{tlsConfig: getTLSConfig(t)})
369 defer l.Close()
370
371 setupTLSClient(t, l.Addr())
372 }
373
374 func TestAuthWithoutTLS(t *testing.T) {
375 l := runServer(t, &testServer{})
376 defer l.Close()
377
378 conn := createClient(t, l.Addr())
379 readCodeLine(t, conn, 220)
380
381 ok(t, conn.PrintfLine("EHLO test"))
382 _, resp, err := conn.ReadResponse(250)
383 ok(t, err)
384
385 if strings.Contains(resp, "AUTH") {
386 t.Errorf("AUTH should not be advertised over plaintext")
387 }
388 }
389
390 func TestAuth(t *testing.T) {
391 l := runServer(t, &testServer{
392 tlsConfig: getTLSConfig(t),
393 userAuth: &userAuth{
394 authz: "-authz-",
395 authc: "-authc-",
396 passwd: "goats",
397 },
398 })
399 defer l.Close()
400
401 conn := setupTLSClient(t, l.Addr())
402
403 runTableTest(t, conn, []requestResponse{
404 {"AUTH", 501, nil},
405 {"AUTH OAUTHBEARER", 504, nil},
406 {"AUTH PLAIN", 501, nil}, // Bad syntax, missing space.
407 {"AUTH PLAIN ", 334, nil},
408 {b64enc("abc\x00def\x00ghf"), 535, nil},
409 {"AUTH PLAIN ", 334, nil},
410 {b64enc("\x00"), 501, nil},
411 {"AUTH PLAIN ", 334, nil},
412 {"this isn't base 64", 501, nil},
413 {"AUTH PLAIN ", 334, nil},
414 {b64enc("-authz-\x00-authc-\x00goats"), 235, nil},
415 {"AUTH PLAIN ", 503, nil}, // Already authenticated.
416 {"NOOP", 250, nil},
417 })
418 }
419
420 func TestAuthNoInitialResponse(t *testing.T) {
421 l := runServer(t, &testServer{
422 tlsConfig: getTLSConfig(t),
423 userAuth: &userAuth{
424 authz: "",
425 authc: "user",
426 passwd: "longpassword",
427 },
428 })
429 defer l.Close()
430
431 conn := setupTLSClient(t, l.Addr())
432
433 runTableTest(t, conn, []requestResponse{
434 {"AUTH PLAIN " + b64enc("\x00user\x00longpassword"), 235, nil},
435 })
436 }
437
438 func TestRelayRequiresAuth(t *testing.T) {
439 l := runServer(t, &testServer{
440 domain: "example.com",
441 tlsConfig: getTLSConfig(t),
442 userAuth: &userAuth{
443 authz: "",
444 authc: "mailbox@example.com",
445 passwd: "test",
446 },
447 })
448 defer l.Close()
449
450 conn := setupTLSClient(t, l.Addr())
451
452 runTableTest(t, conn, []requestResponse{
453 {"MAIL FROM:<apples@example.com>", 550, nil},
454 {"MAIL FROM:<mailbox@example.com>", 550, nil},
455 {"AUTH PLAIN ", 334, nil},
456 {b64enc("\x00mailbox@example.com\x00test"), 235, nil},
457 {"MAIL FROM:<mailbox@example.com>", 250, nil},
458 })
459 }
460
461 func setupRelayTest(t *testing.T) (server *testServer, l net.Listener, conn *textproto.Conn) {
462 server = &testServer{
463 domain: "example.com",
464 tlsConfig: getTLSConfig(t),
465 userAuth: &userAuth{
466 authz: "",
467 authc: "mailbox@example.com",
468 passwd: "test",
469 },
470 }
471 l = runServer(t, server)
472 conn = setupTLSClient(t, l.Addr())
473 runTableTest(t, conn, []requestResponse{
474 {"AUTH PLAIN ", 334, nil},
475 {b64enc("\x00mailbox@example.com\x00test"), 235, nil},
476 })
477 return
478 }
479
480 func TestBasicRelay(t *testing.T) {
481 server, l, conn := setupRelayTest(t)
482 defer l.Close()
483
484 runTableTest(t, conn, []requestResponse{
485 {"MAIL FROM:<mailbox@example.com>", 250, nil},
486 {"RCPT TO:<dest@another.net>", 250, nil},
487 {"DATA", 354, func(t testing.TB, conn *textproto.Conn) {
488 readCodeLine(t, conn, 354)
489
490 ok(t, conn.PrintfLine("From: <mailbox@example.com>"))
491 ok(t, conn.PrintfLine("To: <dest@example.com>"))
492 ok(t, conn.PrintfLine("Subject: Basic relay\n"))
493 ok(t, conn.PrintfLine("This is a basic relay message"))
494 ok(t, conn.PrintfLine("."))
495 readCodeLine(t, conn, 250)
496 }},
497 })
498
499 if want, got := 1, len(server.relayed); want != got {
500 t.Errorf("Want %d relayed message, got %d", want, got)
501 }
502 }
503
504 func TestSendMultipleRelay(t *testing.T) {
505 server, l, conn := setupRelayTest(t)
506 defer l.Close()
507
508 runTableTest(t, conn, []requestResponse{
509 {"MAIL FROM:<mailbox@example.com>", 250, nil},
510 {"RCPT TO:<valid@dest.xyz>", 250, nil},
511 {"RCPT TO:<another@dest.org>", 250, nil},
512 {"DATA", 354, func(t testing.TB, conn *textproto.Conn) {
513 readCodeLine(t, conn, 354)
514
515 ok(t, conn.PrintfLine("To: Cindy <valid@dest.xyz>, Sam <another@dest.org>"))
516 ok(t, conn.PrintfLine("From: Finn <mailbox@example.com>"))
517 ok(t, conn.PrintfLine("Subject: Two destinations\n"))
518 ok(t, conn.PrintfLine("And 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 en := server.relayed[0]
529 if want, got := "mailbox@example.com", en.MailFrom.Address; want != got {
530 t.Errorf("Want mail to be from %q, got %q", want, got)
531 }
532
533 if want, got := 2, len(en.RcptTo); want != got {
534 t.Errorf("Want %d recipients, got %d", want, got)
535 }
536 if want, got := "valid@dest.xyz", en.RcptTo[0].Address; want != got {
537 t.Errorf("Unexpected RcptTo %q", got)
538 }
539
540 msg := string(en.Data)
541
542 if strings.Index(msg, "\nFrom: Finn <mailbox@example.com>\n") == -1 {
543 t.Errorf("Could not find From: header in message %q", msg)
544 }
545
546 if strings.Index(msg, "\nSubject: Two destinations\n") == -1 {
547 t.Errorf("Could not find Subject: header in message %q", msg)
548 }
549 }