forked from mhale/smtpd
-
Notifications
You must be signed in to change notification settings - Fork 0
/
smtpd_test.go
1603 lines (1339 loc) · 57.4 KB
/
smtpd_test.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
package smtpd
import (
"bufio"
"bytes"
"context"
"crypto/hmac"
"crypto/md5"
"crypto/tls"
"encoding/base64"
"errors"
"fmt"
"io"
"net"
"os"
"reflect"
"regexp"
"strings"
"testing"
"time"
)
var cert = makeCertificate()
// Create a client to run commands with. Parse the banner for 220 response.
func newConn(t *testing.T, server *Server) net.Conn {
clientConn, serverConn := net.Pipe()
session := server.newSession(serverConn)
go session.serve()
banner, err := bufio.NewReader(clientConn).ReadString('\n')
if err != nil {
t.Fatalf("Failed to read banner from test server: %v", err)
}
if banner[0:3] != "220" {
t.Fatalf("Read incorrect banner from test server: %v", banner)
}
return clientConn
}
// Send a command and verify the 3 digit code from the response.
func cmdCode(t *testing.T, conn net.Conn, cmd string, code string) string {
fmt.Fprintf(conn, "%s\r\n", cmd)
resp, err := bufio.NewReader(conn).ReadString('\n')
if err != nil {
t.Fatalf("Failed to read response from test server: %v", err)
}
if resp[0:3] != code {
t.Errorf("Command \"%s\" response code is %s, want %s", cmd, resp[0:3], code)
}
return strings.TrimSpace(resp)
}
// Simple tests: connect, send command, then send QUIT.
// RFC 2821 section 4.1.4 specifies that these commands do not require a prior EHLO,
// only that clients should send one, so test without EHLO.
func TestSimpleCommands(t *testing.T) {
tests := []struct {
cmd string
code string
}{
{"NOOP", "250"},
{"RSET", "250"},
{"HELP", "502"},
{"VRFY", "502"},
{"EXPN", "502"},
{"TEST", "500"}, // Unsupported command
{"", "500"}, // Blank command
}
for _, tt := range tests {
conn := newConn(t, &Server{})
cmdCode(t, conn, tt.cmd, tt.code)
cmdCode(t, conn, "QUIT", "221")
conn.Close()
}
}
func TestCmdHELO(t *testing.T) {
conn := newConn(t, &Server{})
// Send HELO, expect greeting.
cmdCode(t, conn, "HELO host.example.com", "250")
// Verify that HELO resets the current transaction state like RSET.
// RFC 2821 section 4.1.4 says EHLO should cause a reset, so verify that HELO does it too.
cmdCode(t, conn, "MAIL FROM:<[email protected]>", "250")
cmdCode(t, conn, "RCPT TO:<[email protected]>", "250")
cmdCode(t, conn, "HELO host.example.com", "250")
cmdCode(t, conn, "DATA", "503")
cmdCode(t, conn, "QUIT", "221")
conn.Close()
}
func TestCmdEHLO(t *testing.T) {
conn := newConn(t, &Server{})
// Send EHLO, expect greeting.
cmdCode(t, conn, "EHLO host.example.com", "250")
// Verify that EHLO resets the current transaction state like RSET.
// See RFC 2821 section 4.1.4 for more detail.
cmdCode(t, conn, "MAIL FROM:<[email protected]>", "250")
cmdCode(t, conn, "RCPT TO:<[email protected]>", "250")
cmdCode(t, conn, "EHLO host.example.com", "250")
cmdCode(t, conn, "DATA", "503")
cmdCode(t, conn, "QUIT", "221")
conn.Close()
}
func TestCmdRSET(t *testing.T) {
conn := newConn(t, &Server{})
cmdCode(t, conn, "EHLO host.example.com", "250")
// Verify that RSET clears the current transaction state.
cmdCode(t, conn, "MAIL FROM:<[email protected]>", "250")
cmdCode(t, conn, "RCPT TO:<[email protected]>", "250")
cmdCode(t, conn, "RSET", "250")
cmdCode(t, conn, "DATA", "503")
cmdCode(t, conn, "QUIT", "221")
conn.Close()
}
func TestCmdMAIL(t *testing.T) {
conn := newConn(t, &Server{})
cmdCode(t, conn, "EHLO host.example.com", "250")
// MAIL with no FROM arg should return 501 syntax error
cmdCode(t, conn, "MAIL", "501")
// MAIL with empty FROM arg should return 501 syntax error
cmdCode(t, conn, "MAIL FROM:", "501")
cmdCode(t, conn, "MAIL FROM: ", "501")
cmdCode(t, conn, "MAIL FROM: ", "501")
// MAIL with DSN-style FROM arg should return 250 Ok
cmdCode(t, conn, "MAIL FROM:<>", "250")
// MAIL with valid FROM arg should return 250 Ok
cmdCode(t, conn, "MAIL FROM:<[email protected]>", "250")
// MAIL with seemingly valid but noncompliant FROM arg (single space after the colon) should be tolerated and should return 250 Ok
cmdCode(t, conn, "MAIL FROM: <[email protected]>", "250")
// MAIL with seemingly valid but noncompliant FROM arg (double space after the colon) should return 501 syntax error
cmdCode(t, conn, "MAIL FROM: <[email protected]>", "501")
// MAIL with valid SIZE parameter should return 250 Ok
cmdCode(t, conn, "MAIL FROM:<[email protected]> SIZE=1000", "250")
// MAIL with bad size parameter should return 501 syntax error
cmdCode(t, conn, "MAIL FROM:<[email protected]> SIZE", "501")
cmdCode(t, conn, "MAIL FROM:<[email protected]> SIZE=", "501")
cmdCode(t, conn, "MAIL FROM:<[email protected]> SIZE= ", "501")
cmdCode(t, conn, "MAIL FROM:<[email protected]> SIZE=foo", "501")
// TODO: MAIL with valid AUTH parameter should return 250 Ok
// TODO: MAIL with invalid AUTH parameter must return 501 syntax error
cmdCode(t, conn, "QUIT", "221")
conn.Close()
}
func TestCmdMAILMaxSize(t *testing.T) {
maxSize := 10 + time.Now().Minute()
conn := newConn(t, &Server{MaxSize: maxSize})
cmdCode(t, conn, "EHLO host.example.com", "250")
// MAIL with no size parameter should return 250 Ok
cmdCode(t, conn, "MAIL FROM:<[email protected]>", "250")
// MAIL with bad size parameter should return 501 syntax error
cmdCode(t, conn, "MAIL FROM:<[email protected]> SIZE", "501")
cmdCode(t, conn, "MAIL FROM:<[email protected]> SIZE=", "501")
cmdCode(t, conn, "MAIL FROM:<[email protected]> SIZE= ", "501")
cmdCode(t, conn, "MAIL FROM:<[email protected]> SIZE=foo", "501")
// MAIL with size parameter zero should return 250 Ok
cmdCode(t, conn, "MAIL FROM:<[email protected]> SIZE=0", "250")
// MAIL below the maximum size should return 250 Ok
cmdCode(t, conn, fmt.Sprintf("MAIL FROM:<[email protected]> SIZE=%d", maxSize-1), "250")
// MAIL matching the maximum size should return 250 Ok
cmdCode(t, conn, fmt.Sprintf("MAIL FROM:<[email protected]> SIZE=%d", maxSize), "250")
// MAIL above the maximum size should return a maximum size exceeded error.
cmdCode(t, conn, fmt.Sprintf("MAIL FROM:<[email protected]> SIZE=%d", maxSize+1), "552")
// Clients should send either RSET or QUIT after receiving 552 (RFC 1870 section 6.2).
cmdCode(t, conn, "QUIT", "221")
conn.Close()
}
func TestCmdRCPT(t *testing.T) {
conn := newConn(t, &Server{})
cmdCode(t, conn, "EHLO host.example.com", "250")
// RCPT without prior MAIL should return 503 bad sequence
cmdCode(t, conn, "RCPT", "503")
cmdCode(t, conn, "MAIL FROM:<[email protected]>", "250")
// RCPT with no TO arg should return 501 syntax error
cmdCode(t, conn, "RCPT", "501")
// RCPT with empty TO arg should return 501 syntax error
cmdCode(t, conn, "RCPT TO:", "501")
cmdCode(t, conn, "RCPT TO: ", "501")
cmdCode(t, conn, "RCPT TO: ", "501")
// RCPT with valid TO arg should return 250 Ok
cmdCode(t, conn, "RCPT TO:<[email protected]>", "250")
// Up to 100 valid recipients should return 250 Ok
for i := 2; i < 101; i++ {
cmdCode(t, conn, fmt.Sprintf("RCPT TO:<recipient%[email protected]>", i), "250")
}
// 101st valid recipient with valid TO arg should return 452 too many recipients
cmdCode(t, conn, "RCPT TO:<[email protected]>", "452")
// RCPT with valid TO arg and prior DSN-style FROM arg should return 250 Ok
cmdCode(t, conn, "RSET", "250")
cmdCode(t, conn, "MAIL FROM:<>", "250")
cmdCode(t, conn, "RCPT TO:<[email protected]>", "250")
// RCPT with seemingly valid but noncompliant TO arg (single space after the colon) should be tolerated and should return 250 Ok
cmdCode(t, conn, "RSET", "250")
cmdCode(t, conn, "MAIL FROM:<>", "250")
cmdCode(t, conn, "RCPT TO: <[email protected]>", "250")
// RCPT with seemingly valid but noncompliant TO arg (double space after the colon) should return 501 syntax error
cmdCode(t, conn, "RSET", "250")
cmdCode(t, conn, "MAIL FROM:<>", "250")
cmdCode(t, conn, "RCPT TO: <[email protected]>", "501")
cmdCode(t, conn, "QUIT", "221")
conn.Close()
}
func TestCmdDATA(t *testing.T) {
conn := newConn(t, &Server{})
cmdCode(t, conn, "EHLO host.example.com", "250")
// DATA without prior MAIL & RCPT should return 503 bad sequence
cmdCode(t, conn, "DATA", "503")
cmdCode(t, conn, "RSET", "250")
// DATA without prior RCPT should return 503 bad sequence
cmdCode(t, conn, "MAIL FROM:<[email protected]>", "250")
cmdCode(t, conn, "DATA", "503")
cmdCode(t, conn, "RSET", "250")
// Test a full mail transaction.
cmdCode(t, conn, "MAIL FROM:<[email protected]>", "250")
cmdCode(t, conn, "RCPT TO:<[email protected]>", "250")
cmdCode(t, conn, "DATA", "354")
cmdCode(t, conn, "Test message.\r\n.", "250")
// Test a full mail transaction with a bad last recipient.
cmdCode(t, conn, "MAIL FROM:<[email protected]>", "250")
cmdCode(t, conn, "RCPT TO:<[email protected]>", "250")
cmdCode(t, conn, "RCPT TO:", "501")
cmdCode(t, conn, "DATA", "354")
cmdCode(t, conn, "Test message.\r\n.", "250")
cmdCode(t, conn, "QUIT", "221")
conn.Close()
}
func TestCmdDATAWithMaxSize(t *testing.T) {
// "Test message.\r\n." is 15 bytes after trailing period is removed.
conn := newConn(t, &Server{MaxSize: 15})
cmdCode(t, conn, "EHLO host.example.com", "250")
// Messages below the maximum size should return 250 Ok
cmdCode(t, conn, "MAIL FROM:<[email protected]>", "250")
cmdCode(t, conn, "RCPT TO:<[email protected]>", "250")
cmdCode(t, conn, "DATA", "354")
cmdCode(t, conn, "Test message\r\n.", "250")
// Messages matching the maximum size should return 250 Ok
cmdCode(t, conn, "MAIL FROM:<[email protected]>", "250")
cmdCode(t, conn, "RCPT TO:<[email protected]>", "250")
cmdCode(t, conn, "DATA", "354")
cmdCode(t, conn, "Test message.\r\n.", "250")
// Messages above the maximum size should return a maximum size exceeded error.
cmdCode(t, conn, "MAIL FROM:<[email protected]>", "250")
cmdCode(t, conn, "RCPT TO:<[email protected]>", "250")
cmdCode(t, conn, "DATA", "354")
cmdCode(t, conn, "Test message that is too long.\r\n.", "552")
// Clients should send either RSET or QUIT after receiving 552 (RFC 1870 section 6.2).
cmdCode(t, conn, "RSET", "250")
// Messages above the maximum size should return a maximum size exceeded error.
cmdCode(t, conn, "MAIL FROM:<[email protected]>", "250")
cmdCode(t, conn, "RCPT TO:<[email protected]>", "250")
cmdCode(t, conn, "DATA", "354")
cmdCode(t, conn, "Test message.\r\nSecond line that is too long.\r\n.", "552")
// Clients should send either RSET or QUIT after receiving 552 (RFC 1870 section 6.2).
cmdCode(t, conn, "QUIT", "221")
conn.Close()
}
type mockHandler struct {
handlerCalled int
}
func (m *mockHandler) handler(err error) func(a net.Addr, f string, t []string, d []byte, s *Session) error {
return func(a net.Addr, f string, t []string, d []byte, s *Session) error {
m.handlerCalled++
return err
}
}
func TestCmdDATAWithHandler(t *testing.T) {
m := mockHandler{}
conn := newConn(t, &Server{Handler: m.handler(nil)})
cmdCode(t, conn, "EHLO host.example.com", "250")
cmdCode(t, conn, "MAIL FROM:<[email protected]>", "250")
cmdCode(t, conn, "RCPT TO:<[email protected]>", "250")
cmdCode(t, conn, "DATA", "354")
cmdCode(t, conn, "Test message.\r\n.", "250")
cmdCode(t, conn, "QUIT", "221")
conn.Close()
if m.handlerCalled != 1 {
t.Errorf("MailHandler called %d times, want one call", m.handlerCalled)
}
}
func TestCmdDATAWithHandlerError(t *testing.T) {
m := mockHandler{}
conn := newConn(t, &Server{Handler: m.handler(errors.New("Handler error"))})
cmdCode(t, conn, "EHLO host.example.com", "250")
cmdCode(t, conn, "MAIL FROM:<[email protected]>", "250")
cmdCode(t, conn, "RCPT TO:<[email protected]>", "250")
cmdCode(t, conn, "DATA", "354")
cmdCode(t, conn, "Test message.\r\n.", "451")
cmdCode(t, conn, "QUIT", "221")
conn.Close()
if m.handlerCalled != 1 {
t.Errorf("MailHandler called %d times, want one call", m.handlerCalled)
}
}
func TestCmdSTARTTLS(t *testing.T) {
conn := newConn(t, &Server{})
cmdCode(t, conn, "EHLO host.example.com", "250")
// By default, TLS is not configured, so STARTTLS should return 502 not implemented.
cmdCode(t, conn, "STARTTLS", "502")
// Parameters are not allowed (RFC 3207 section 4).
cmdCode(t, conn, "STARTTLS FOO", "501")
cmdCode(t, conn, "QUIT", "221")
conn.Close()
}
func TestCmdSTARTTLSFailure(t *testing.T) {
// Deliberately misconfigure TLS to force a handshake failure.
server := &Server{TLSConfig: &tls.Config{}}
conn := newConn(t, server)
cmdCode(t, conn, "EHLO host.example.com", "250")
// When TLS is configured, STARTTLS should return 220 Ready to start TLS.
cmdCode(t, conn, "STARTTLS", "220")
// A failed TLS handshake should return 403 TLS handshake failed
tlsConn := tls.Client(conn, &tls.Config{InsecureSkipVerify: true})
err := tlsConn.Handshake()
if err != nil {
reader := bufio.NewReader(conn)
resp, readErr := reader.ReadString('\n')
if readErr != nil {
t.Fatalf("Failed to read response after failed TLS handshake: %v", err)
}
if resp[0:3] != "403" {
t.Errorf("Failed TLS handshake response code is %s, want 403", resp[0:3])
}
} else {
t.Error("TLS handshake succeeded with empty tls.Config, want failure")
}
cmdCode(t, conn, "QUIT", "221")
tlsConn.Close()
}
// Utility function to make a valid TLS certificate for use by the server.
func makeCertificate() tls.Certificate {
const certPEM = `
-----BEGIN CERTIFICATE-----
MIID9DCCAtygAwIBAgIJAIX/1sxuqZKrMA0GCSqGSIb3DQEBCwUAMFkxCzAJBgNV
BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX
aWRnaXRzIFB0eSBMdGQxEjAQBgNVBAMTCWxvY2FsaG9zdDAeFw0xNzA1MDYxNDIy
MjVaFw0yNzA1MDQxNDIyMjVaMFkxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpTb21l
LVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxEjAQBgNV
BAMTCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALO4
XVY5Kw9eNblqBenC03Wz6qemLFw8zLDNrehvjYuJPn5WVwvzLNP+3S02iqQD+Y1k
vszqDIZLQdjWLiEZdtxfemyIr+RePIMclnceGYFx3Zgg5qeyvOWlJLM41ZU8YZb/
zGj3RtXzuOZ5vePSLGS1nudjrKSBs7shRY8bYjkOqFujsSVnEK7s3Kb2Sf/rO+7N
RZ1df3hhyKtyq4Pb5eC1mtQqcRjRSZdTxva8kO4vRQbvGgjLUakvBVrrnwbww5a4
2wKbQPKIClEbSLyKQ62zR8gW1rPwBdokd8u9+rLbcmr7l0OuAsSn5Xi9x6VxXTNE
bgCa1KVoE4bpoGG+KQsCAwEAAaOBvjCBuzAdBgNVHQ4EFgQUILso/fozIhaoyi05
XNSWzP/ck+4wgYsGA1UdIwSBgzCBgIAUILso/fozIhaoyi05XNSWzP/ck+6hXaRb
MFkxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJ
bnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxEjAQBgNVBAMTCWxvY2FsaG9zdIIJAIX/
1sxuqZKrMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAIbzsvTZb8LA
JqyaTttsMMA1szf4WBX88lVWbIk91k0nlTa0BiU/UocKrU6c9PySwJ6FOFJpgpdH
z/kmJ+S+d4pvgqBzWbKMoMrNlMt6vL+H8Mbf/l/CN91eNM+gJZu2HgBIFGW1y4Wy
gOzjEm9bw15Hgqqs0P4CSy7jcelWA285DJ7IG1qdPGhAKxT4/UuDin8L/u2oeYWH
3DwTDO4kAUnKetcmNQFSX3Ge50uQypl8viYgFJ2axOfZ3imjQZrs7M1Og6Wnj/SD
F414wVQibsZyZp8cqwR/OinvxloPkPVnf163jPRtftuqezEY8Nyj83O5u5sC1Azs
X/Gm54QNk6w=
-----END CERTIFICATE-----`
const keyPEM = `
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAs7hdVjkrD141uWoF6cLTdbPqp6YsXDzMsM2t6G+Ni4k+flZX
C/Ms0/7dLTaKpAP5jWS+zOoMhktB2NYuIRl23F96bIiv5F48gxyWdx4ZgXHdmCDm
p7K85aUkszjVlTxhlv/MaPdG1fO45nm949IsZLWe52OspIGzuyFFjxtiOQ6oW6Ox
JWcQruzcpvZJ/+s77s1FnV1/eGHIq3Krg9vl4LWa1CpxGNFJl1PG9ryQ7i9FBu8a
CMtRqS8FWuufBvDDlrjbAptA8ogKURtIvIpDrbNHyBbWs/AF2iR3y736sttyavuX
Q64CxKfleL3HpXFdM0RuAJrUpWgThumgYb4pCwIDAQABAoIBAHzvYntJPKTvUhu2
F6w8kvHVBABNpbLtVUJniUj3G4fv/bCn5tVY1EX/e9QtgU2psbbYXUdoQRKuiHTr
15+M6zMhcKK4lsYDuL9QhU0DcKmq9WgHHzFfMK/YEN5CWT/ofNMSuhASLn0Xc+dM
pHQWrGPKWk/y25Z0z/P7mjZ0y+BrJOKlxV53A2AWpj4JtjX2YO6s/eiraFX+RNlv
GyWzeQ7Gynm2TD9VXhS+m40VVBmmbbeZYDlziDoWWNe9r26A+C8K65gZtjKdarMd
0LN89jJvI1pUxcIuvZJnumWUenZ7JhfBGpkfAwLB+MogUo9ekAHv1IZv/m3uWq9f
Zml2dZECgYEA2OCI8kkLRa3+IodqQNFrb/uZ16YouQ71B7nBgAxls9nuhyELKO7d
fzf1snPx6cbaCQKTyxrlYvck4gz8P09R7nVYwJuTmP0+QIgeCCc3Y9A2dyExaC6I
uKkFzJEqIVZNLvdjBRWQs5AiD1w58oto+wOvbagAQM483WiJ/qFaHCMCgYEA1CPo
zwI6pCn39RSYffK25HXM1q3i8ypkYdNsG6IVqS2FqHqj8XJSnDvLeIm7W1Rtw+uM
QdZ5O6PH31XgolG6LrFkW9vtfH+QnXQA2AnZQEfn034YZubhcexLqAkS9r0FUUZp
a1WI2jSxBBeB+to6MdNABuQOL3NHjPUidUKnOfkCgYA+HvKbE7ka2F+23DrfHh08
EkFat8lqWJJvCBIY73QiNAZSxnA/5UukqQ7DctqUL9U8R3S19JpH4qq55SZLrBi3
yP0HDokUhVVTfqm7hCAlgvpW3TcdtFaNLjzu/5WlvuaU0V+XkTnFdT+MTsp6YtxL
Kh8RtdF8vpZIhS0htm3tKQKBgQDQXoUp79KRtPdsrtIpw+GI/Xw50Yp9tkHrJLOn
YMlN5vzFw9CMM/KYqtLsjryMtJ0sN40IjhV+UxzbbYq7ZPMvMeaVo6vdAZ+WSH8b
tHDEBtzai5yEVntSXvrhDiimWnuCnVqmptlJG0BT+JMfRoKqtgjJu++DBARfm9hA
vTtsYQKBgE1ttTzd3HJoIhBBSvSMbyDWTED6jecKvsVypb7QeDxZCbIwCkoK9zn1
twPDHLBcUNhHJx6JWTR6BxI5DZoIA1tcKHtdO5smjLWNSKhXTsKWee2aNkZJkNIW
TDHSaTMOxVUEzpx84xClf561BTiTgzQy2MULpg3AK0Cv9l0+Yrvz
-----END RSA PRIVATE KEY-----`
cert, _ := tls.X509KeyPair([]byte(certPEM), []byte(keyPEM))
return cert
}
func TestCmdSTARTTLSSuccess(t *testing.T) {
// Configure a valid TLS certificate so the handshake will succeed.
server := &Server{TLSConfig: &tls.Config{Certificates: []tls.Certificate{cert}}}
conn := newConn(t, server)
cmdCode(t, conn, "EHLO host.example.com", "250")
// When TLS is configured, STARTTLS should return 220 Ready to start TLS.
cmdCode(t, conn, "STARTTLS", "220")
// A successful TLS handshake shouldn't return anything, it should wait for EHLO.
tlsConn := tls.Client(conn, &tls.Config{InsecureSkipVerify: true})
err := tlsConn.Handshake()
if err != nil {
t.Errorf("Failed to perform TLS handshake")
}
// The subsequent EHLO should be successful.
cmdCode(t, tlsConn, "EHLO host.example.com", "250")
// When TLS is already in use, STARTTLS should return 503 bad sequence.
cmdCode(t, tlsConn, "STARTTLS", "503")
cmdCode(t, tlsConn, "QUIT", "221")
tlsConn.Close()
}
func TestCmdSTARTTLSRequired(t *testing.T) {
tests := []struct {
cmd string
codeBefore string
codeAfter string
}{
{"EHLO host.example.com", "250", "250"},
{"NOOP", "250", "250"},
{"MAIL FROM:<[email protected]>", "530", "250"},
{"RCPT TO:<[email protected]>", "530", "250"},
{"RSET", "530", "250"}, // Reset before DATA to avoid having to actually send a message.
{"DATA", "530", "503"},
{"HELP", "502", "502"},
{"VRFY", "502", "502"},
{"EXPN", "502", "502"},
{"TEST", "500", "500"}, // Unsupported command
{"", "500", "500"}, // Blank command
{"AUTH", "530", "502"}, // AuthHandler not configured
}
// If TLS is not configured, the TLSRequired setting is ignored, so it must be configured for this test.
server := &Server{TLSConfig: &tls.Config{Certificates: []tls.Certificate{cert}}, TLSRequired: true}
conn := newConn(t, server)
// If TLS is required, but not in use, reject every command except NOOP, EHLO, STARTTLS, or QUIT as per RFC 3207 section 4.
for _, tt := range tests {
cmdCode(t, conn, tt.cmd, tt.codeBefore)
}
// Switch to using TLS.
cmdCode(t, conn, "STARTTLS", "220")
// A successful TLS handshake shouldn't return anything, it should wait for EHLO.
tlsConn := tls.Client(conn, &tls.Config{InsecureSkipVerify: true})
err := tlsConn.Handshake()
if err != nil {
t.Errorf("Failed to perform TLS handshake")
}
// The subsequent EHLO should be successful.
cmdCode(t, tlsConn, "EHLO host.example.com", "250")
// If TLS is required, and is in use, every command should work normally.
for _, tt := range tests {
cmdCode(t, tlsConn, tt.cmd, tt.codeAfter)
}
cmdCode(t, tlsConn, "QUIT", "221")
tlsConn.Close()
}
func TestMakeHeaders(t *testing.T) {
now := time.Now().Format("Mon, _2 Jan 2006 15:04:05 -0700 (MST)")
valid := "Received: from clientName (clientHost [clientIP])\r\n" +
" by serverName (smtpd) with SMTP\r\n" +
" for <[email protected]>; " +
fmt.Sprintf("%s\r\n", now)
srv := &Server{Appname: "smtpd", Hostname: "serverName"}
s := &Session{srv: srv, RemoteIP: "clientIP", RemoteHost: "clientHost", RemoteName: "clientName"}
headers := s.makeHeaders([]string{"[email protected]"})
if string(headers) != valid {
t.Errorf("makeHeaders() returned\n%v, want\n%v", string(headers), valid)
}
}
// Test parsing of commands into verbs and arguments.
func TestParseLine(t *testing.T) {
tests := []struct {
line string
verb string
args string
}{
{"EHLO host.example.com", "EHLO", "host.example.com"},
{"MAIL FROM:<[email protected]>", "MAIL", "FROM:<[email protected]>"},
{"RCPT TO:<[email protected]>", "RCPT", "TO:<[email protected]>"},
{"QUIT", "QUIT", ""},
}
s := &Session{}
for _, tt := range tests {
verb, args := s.parseLine(tt.line)
if verb != tt.verb || args != tt.args {
t.Errorf("ParseLine(%v) returned %v, %v, want %v, %v", tt.line, verb, args, tt.verb, tt.args)
}
}
}
// Test reading of complete lines from the socket.
func TestReadLine(t *testing.T) {
var buf bytes.Buffer
s := &Session{}
s.srv = &Server{}
s.br = bufio.NewReader(&buf)
// Ensure readLine() returns an EOF error on an empty buffer.
_, err := s.readLine()
if err != io.EOF {
t.Errorf("readLine() on empty buffer returned err: %v, want EOF", err)
}
// Ensure trailing <CRLF> is stripped.
line := "FOO BAR BAZ\r\n"
cmd := "FOO BAR BAZ"
buf.Write([]byte(line))
output, err := s.readLine()
if err != nil {
t.Errorf("readLine(%v) returned err: %v", line, err)
} else if output != cmd {
t.Errorf("readLine(%v) returned %v, want %v", line, output, cmd)
}
}
// Test reading of message data, including dot stuffing (see RFC 5321 section 4.5.2).
func TestReadData(t *testing.T) {
tests := []struct {
lines string
data string
}{
// Single line message.
{"Test message.\r\n.\r\n", "Test message.\r\n"},
// Single line message with leading period removed.
{".Test message.\r\n.\r\n", "Test message.\r\n"},
// Multiple line message.
{"Line 1.\r\nLine 2.\r\nLine 3.\r\n.\r\n", "Line 1.\r\nLine 2.\r\nLine 3.\r\n"},
// Multiple line message with leading period removed.
{"Line 1.\r\n.Line 2.\r\nLine 3.\r\n.\r\n", "Line 1.\r\nLine 2.\r\nLine 3.\r\n"},
// Multiple line message with one leading period removed.
{"Line 1.\r\n..Line 2.\r\nLine 3.\r\n.\r\n", "Line 1.\r\n.Line 2.\r\nLine 3.\r\n"},
}
var buf bytes.Buffer
s := &Session{}
s.srv = &Server{}
s.br = bufio.NewReader(&buf)
// Ensure readData() returns an EOF error on an empty buffer.
_, err := s.readData()
if err != io.EOF {
t.Errorf("readData() on empty buffer returned err: %v, want EOF", err)
}
for _, tt := range tests {
buf.Write([]byte(tt.lines))
data, err := s.readData()
if err != nil {
t.Errorf("readData(%v) returned err: %v", tt.lines, err)
} else if string(data) != tt.data {
t.Errorf("readData(%v) returned %v, want %v", tt.lines, string(data), tt.data)
}
}
}
// Test reading of message data with maximum size set (see RFC 1870 section 6.3).
func TestReadDataWithMaxSize(t *testing.T) {
tests := []struct {
lines string
maxSize int
err error
}{
// Maximum size of zero (the default) should not return an error.
{"Test message.\r\n.\r\n", 0, nil},
// Messages below the maximum size should not return an error.
{"Test message.\r\n.\r\n", 16, nil},
// Messages matching the maximum size should not return an error.
{"Test message.\r\n.\r\n", 15, nil},
// Messages above the maximum size should return a maximum size exceeded error.
{"Test message.\r\n.\r\n", 14, maxSizeExceeded(14)},
}
var buf bytes.Buffer
s := &Session{}
s.br = bufio.NewReader(&buf)
for _, tt := range tests {
s.srv = &Server{MaxSize: tt.maxSize}
buf.Write([]byte(tt.lines))
_, err := s.readData()
if err != tt.err {
t.Errorf("readData(%v) returned err: %v", tt.lines, tt.err)
}
}
}
// Utility function for parsing extensions listed as service extensions in response to an EHLO command.
func parseExtensions(t *testing.T, greeting string) map[string]string {
extensions := make(map[string]string)
lines := strings.Split(greeting, "\n")
if len(lines) > 1 {
iLast := len(lines) - 1
for i, line := range lines {
prefix := line[0:4]
// All but the last extension code prefix should be "250-".
if i != iLast && prefix != "250-" {
t.Errorf("Extension code prefix is %s, want '250-'", prefix)
}
// The last extension code prefix should be "250 ".
if i == iLast && prefix != "250 " {
t.Errorf("Extension code prefix is %s, want '250 '", prefix)
}
// Skip greeting line.
if i == 0 {
continue
}
// Add line as extension.
line = strings.TrimSpace(line[4:]) // Strip code prefix and trailing \r\n
if idx := strings.Index(line, " "); idx != -1 {
extensions[line[:idx]] = line[idx+1:]
} else {
extensions[line] = ""
}
}
}
return extensions
}
// Handler function for validating authentication credentials.
// The secret parameter is passed as nil for LOGIN and PLAIN authentication mechanisms.
func authHandler(remoteAddr net.Addr, mechanism string, username []byte, password []byte, shared []byte) (bool, error) {
return string(username) == "valid", nil
}
// Test the extensions listed in response to an EHLO command.
func TestMakeEHLOResponse(t *testing.T) {
s := &Session{}
s.srv = &Server{}
// Greeting should be returned without trailing newlines.
greeting := s.makeEHLOResponse()
if len(greeting) != len(strings.TrimSpace(greeting)) {
t.Errorf("EHLO greeting string has leading or trailing whitespace")
}
// By default, TLS is not configured, so STARTTLS should not appear.
extensions := parseExtensions(t, s.makeEHLOResponse())
if _, ok := extensions["STARTTLS"]; ok {
t.Errorf("STARTTLS appears in the extension list when TLS is not configured")
}
// If TLS is configured, but not already in use, STARTTLS should appear.
s.srv.TLSConfig = &tls.Config{}
extensions = parseExtensions(t, s.makeEHLOResponse())
if _, ok := extensions["STARTTLS"]; !ok {
t.Errorf("STARTTLS does not appear in the extension list when TLS is configured")
}
// If TLS is already used on the connection, STARTTLS should not appear.
s.TLS = true
extensions = parseExtensions(t, s.makeEHLOResponse())
if _, ok := extensions["STARTTLS"]; ok {
t.Errorf("STARTTLS appears in the extension list when TLS is already in use")
}
// Verify default SIZE extension is zero.
s.srv = &Server{}
extensions = parseExtensions(t, s.makeEHLOResponse())
if _, ok := extensions["SIZE"]; !ok {
t.Errorf("SIZE does not appear in the extension list")
} else if extensions["SIZE"] != "0" {
t.Errorf("SIZE appears in the extension list with incorrect parameter %s, want %s", extensions["SIZE"], "0")
}
// Verify configured maximum message size is listed correctly.
// Any integer will suffice, as long as it's not hardcoded.
maxSize := 10 + time.Now().Minute()
maxSizeStr := fmt.Sprintf("%d", maxSize)
s.srv = &Server{MaxSize: maxSize}
extensions = parseExtensions(t, s.makeEHLOResponse())
if _, ok := extensions["SIZE"]; !ok {
t.Errorf("SIZE does not appear in the extension list")
} else if extensions["SIZE"] != maxSizeStr {
t.Errorf("SIZE appears in the extension list with incorrect parameter %s, want %s", extensions["SIZE"], maxSizeStr)
}
// With no authentication handler configured, AUTH should not be advertised.
s.srv = &Server{}
extensions = parseExtensions(t, s.makeEHLOResponse())
if _, ok := extensions["AUTH"]; ok {
t.Errorf("AUTH appears in the extension list when no AuthHandler is specified")
}
// With an authentication handler configured, AUTH should be advertised.
s.srv = &Server{AuthHandler: authHandler}
extensions = parseExtensions(t, s.makeEHLOResponse())
if _, ok := extensions["AUTH"]; !ok {
t.Errorf("AUTH does not appear in the extension list when an AuthHandler is specified")
}
reLogin := regexp.MustCompile(`\bLOGIN\b`)
rePlain := regexp.MustCompile(`\bPLAIN\b`)
// RFC 4954 specifies that, without TLS in use, plaintext authentication mechanisms must not be advertised.
s.TLS = false
extensions = parseExtensions(t, s.makeEHLOResponse())
if reLogin.MatchString(extensions["AUTH"]) {
t.Errorf("AUTH mechanism LOGIN appears in the extension list when an AuthHandler is specified and TLS is not in use")
}
if rePlain.MatchString(extensions["AUTH"]) {
t.Errorf("AUTH mechanism PLAIN appears in the extension list when an AuthHandler is specified and TLS is not in use")
}
// RFC 4954 specifies that, with TLS in use, plaintext authentication mechanisms can be advertised.
s.TLS = true
extensions = parseExtensions(t, s.makeEHLOResponse())
if !reLogin.MatchString(extensions["AUTH"]) {
t.Errorf("AUTH mechanism LOGIN does not appear in the extension list when an AuthHandler is specified and TLS is in use")
}
if !rePlain.MatchString(extensions["AUTH"]) {
t.Errorf("AUTH mechanism PLAIN does not appear in the extension list when an AuthHandler is specified and TLS is in use")
}
}
func createTmpFile(content string) (file *os.File, err error) {
file, err = os.CreateTemp("", "")
if err != nil {
return
}
_, err = file.Write([]byte(content))
if err != nil {
return
}
err = file.Close()
return
}
func createTLSFiles() (
certFile *os.File,
keyFile *os.File,
passphrase string,
err error,
) {
const certPEM = `-----BEGIN CERTIFICATE-----
MIIDRzCCAi+gAwIBAgIJAKtg4oViVwv4MA0GCSqGSIb3DQEBCwUAMBQxEjAQBgNV
BAMMCWxvY2FsaG9zdDAgFw0xODA0MjAxMzMxNTBaGA8yMDg2MDUwODEzMzE1MFow
FDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
CgKCAQEA8h7vl0gUquis5jRtcnETyD+8WITZO0s53aIzp0Y+9HXiHW6FGJjbOZjM
IvozNVni+83QWKumRTgeSzIIW2j4V8iFMSNrvWmhmCKloesXS1aY6H979e01Ve8J
WAJFRe6vZJd6gC6Z/P+ELU3ie4Vtr1GYfkV7nZ6VFp5/V/5nxGFag5TUlpP5hcoS
9r2kvXofosVwe3x3udT8SEbv5eBD4bKeVyJs/RLbxSuiU1358Y1cDdVuHjcvfm3c
ajhheQ4vX9WXsk7LGGhnf1SrrPN/y+IDTXfvoHn+nJh4vMAB4yzQdE1V1N1AB8RA
0yBVJ6dwxRrSg4BFrNWhj3gfsvrA7wIDAQABo4GZMIGWMB0GA1UdDgQWBBQ4/ncp
befFuKH1hoYkPqLwuRrPRjAfBgNVHSMEGDAWgBQ4/ncpbefFuKH1hoYkPqLwuRrP
RjAJBgNVHRMEAjAAMBEGCWCGSAGG+EIBAQQEAwIGQDALBgNVHQ8EBAMCBaAwEwYD
VR0lBAwwCgYIKwYBBQUHAwEwFAYDVR0RBA0wC4IJbG9jYWxob3N0MA0GCSqGSIb3
DQEBCwUAA4IBAQBJBetEXiEIzKAEpXGX87j6aUON51Fdf6BiLMCghuGKyhnaOG32
4KJhtvVoS3ZUKPylh9c2VdItYlhWp76zd7YKk+3xUOixWeTMQHIvCvRGTyFibOPT
mApwp2pEnJCe4vjUrBaRhiyI+xnB70cWVF2qeernlLUeJA1mfYyQLz+v06ebDWOL
c/hPVQFB94lEdiyjGO7RZfIe8KwcK48g7iv0LQU4+c9MoWM2ZsVM1AL2tHzokSeA
u64gDTW4K0Tzx1ab7KmOFXYUjbz/xWuReMt33EwDXAErKCjbVt2T55Qx8UoKzSh1
tY0KDHdnYOzgsm2HIj2xcJqbeylYQvckNnoC
-----END CERTIFICATE-----`
const keyPEM = `-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: AES-256-CBC,C16BF8745B2CDB53AC2B1D7609893AA0
O13z7Yq7butaJmMfg9wRis9YnIDPsp4coYI6Ud+JGcP7iXoy95QMhovKWx25o1ol
tvUTsrsG27fHGf9qG02KizApIVtO9c1e0swCWzFrKRQX0JDiZDmilb9xosBNNst1
BOzOTRZEwFGSOCKZRBfSXyqC93TvLJ3DO9IUnKIeGt7upipvg29b/Dur/fyCy2WV
bLHXwUTDBm7j49yfoEyGkDjoB2QO9wgcgbacbnQJQ25fTFUwZpZJEJv6o1tRhoYM
ZMOhC9x1URmdHKN1+z2y5BrB6oNpParfeAMEvs/9FE6jJwYUR28Ql6Mhphfvr9W2
5Gxd3J65Ao9Vi2I5j5X6aBuNjyhXN3ScLjPG4lVZm9RU/uTPEt81pig/d5nSAjvF
Nfc08NuG3cnMyJSE/xScJ4D+GtX8U969wO4oKPCR4E/NFyXPR730ppupDFG6hzPD
PDmiszDtU438JAZ8AuFa1LkbyFnEW6KVD4h7VRr8YDjirCqnkgjNSI6dFY0NQ8H7
SyexB0lrceX6HZc+oNdAtkX3tYdzY3ExzUM5lSF1dkldnRbApLbqc4uuNIVXhXFM
dJnoPdKAzM6i+2EeVUxWNdafKDxnjVSHIHzHfIFJLQ4GS5rnz9keRFdyDjQL07tT
Lu9pPOmsadDXp7oSa81RgoCUfNZeR4jKpCk2BOft0L6ZSqwYFLcQHLIfJaGfn902
TUOTxHt0KzEUYeYSrXC2a6cyvXAd1YI7lOgy60qG89VHyCc2v5Bs4c4FNUDC/+Dj
4ZwogaAbSNkLaE0q3sYQRPdxSqLftyX0KitAgE7oGtdzBfe1cdBoozw3U67NEMMT
6qvk5j7RepPRSrapHtK5pMMdg5XpKFWcOXZ26VHVrDCj4JKdjVb4iyiQi94VveV0
w9+KcOtyrM7/jbQlCWnXpsIkP8VA/RIgh7CBn/h4oF1sO8ywP25OGQ7VWAVq1R9D
8bl8GzIdR9PZpFyOxuIac4rPa8tkDeoXKs4cxoao7H/OZO9o9aTB7CJMTL9yv0Kb
ntWuYxQchE6syoGsOgdGyZhaw4JeFkasDUP5beyNY+278NkzgGTOIMMTXIX46woP
ehzHKGHXVGf7ZiSFF+zAHMXZRSwNVMkOYwlIoRg1IbvIRbAXqAR6xXQTCVzNG0SU
cskojycBca1Cz3hDVIKYZd9beDhprVdr2a4K2nft2g2xRNjKPopsaqXx+VPibFUx
X7542eQ3eAlhkWUuXvt0q5a9WJdjJp9ODA0/d0akF6JQlEHIAyLfoUKB1HYwgUGG
6uRm651FDAab9U4cVC5PY1hfv/QwzpkNDkzgJAZ5SMOfZhq7IdBcqGd3lzPmq2FP
Vy1LVZIl3eM+9uJx5TLsBHH6NhMwtNhFCNa/5ksodQYlTvR8IrrgWlYg4EL69vjS
yt6HhhEN3lFCWvrQXQMp93UklbTlpVt6qcDXiC7HYbs3+EINargRd5Z+xL5i5vkN
f9k7s0xqhloWNPZcyOXMrox8L81WOY+sP4mVlGcfDRLdEJ8X2ofJpOAcwYCnjsKd
uEGsi+l2fTj/F+eZLE6sYoMprgJrbfeqtRWFguUgTn7s5hfU0tZ46al5d0vz8fWK
-----END RSA PRIVATE KEY-----`
passphrase = "test"
certFile, err = createTmpFile(certPEM)
if err != nil {
return
}
keyFile, err = createTmpFile(keyPEM)
return
}
func TestConfigureTLSWithPassphrase(t *testing.T) {
certFile, keyFile, passphrase, err := createTLSFiles()
if err != nil {
t.Errorf("Unexpected TLS files creation error: %s", err)
return
}
defer func() {
os.Remove(certFile.Name())
os.Remove(keyFile.Name())
}()
srv := &Server{}
err = srv.ConfigureTLSWithPassphrase(
certFile.Name(),
keyFile.Name(),
passphrase,
)
if err != nil {
t.Errorf("Unexpected error: %s", err)
}
if srv.TLSConfig == nil {
t.Errorf("Unexpected empty TLS config.")
}
}
func TestAuthMechs(t *testing.T) {
s := Session{}
s.srv = &Server{}
// Validate that non-TLS (default) configuration does not allow plaintext authentication mechanisms.
correct := map[string]bool{"LOGIN": false, "PLAIN": false, "CRAM-MD5": true}
mechs := s.authMechs()
if !reflect.DeepEqual(mechs, correct) {
t.Errorf("authMechs() returned %v, want %v", mechs, correct)
}
// Validate that TLS configuration allows plaintext authentication mechanisms.
correct = map[string]bool{"LOGIN": true, "PLAIN": true, "CRAM-MD5": true}
s.TLS = true
mechs = s.authMechs()
if !reflect.DeepEqual(mechs, correct) {
t.Errorf("authMechs() returned %v, want %v", mechs, correct)
}
// Validate that overridden values take precedence over RFC compliance when not using TLS.
correct = map[string]bool{"LOGIN": true, "PLAIN": true, "CRAM-MD5": false}
s.TLS = false
s.srv.AuthMechs = map[string]bool{"LOGIN": true, "PLAIN": true, "CRAM-MD5": false}
mechs = s.authMechs()
if !reflect.DeepEqual(mechs, correct) {
t.Errorf("authMechs() returned %v, want %v", mechs, correct)
}
// Validate that overridden values take precedence over RFC compliance when using TLS.
correct = map[string]bool{"LOGIN": false, "PLAIN": false, "CRAM-MD5": true}
s.TLS = true
s.srv.AuthMechs = map[string]bool{"LOGIN": false, "PLAIN": false, "CRAM-MD5": true}
mechs = s.authMechs()
if !reflect.DeepEqual(mechs, correct) {
t.Errorf("authMechs() returned %v, want %v", mechs, correct)
}
// Validate ability to explicitly disallow all mechanisms.
correct = map[string]bool{"LOGIN": false, "PLAIN": false, "CRAM-MD5": false}
s.srv.AuthMechs = map[string]bool{"LOGIN": false, "PLAIN": false, "CRAM-MD5": false}
mechs = s.authMechs()
if !reflect.DeepEqual(mechs, correct) {
t.Errorf("authMechs() returned %v, want %v", mechs, correct)
}
// Validate ability to explicitly allow all mechanisms.
correct = map[string]bool{"LOGIN": true, "PLAIN": true, "CRAM-MD5": true}
s.srv.AuthMechs = map[string]bool{"LOGIN": true, "PLAIN": true, "CRAM-MD5": true}
mechs = s.authMechs()
if !reflect.DeepEqual(mechs, correct) {
t.Errorf("authMechs() returned %v, want %v", mechs, correct)
}
}
func TestCmdAUTH(t *testing.T) {
server := &Server{}
conn := newConn(t, server)
cmdCode(t, conn, "EHLO host.example.com", "250")
// By default no authentication handler is configured, so AUTH should return 502 not implemented.
cmdCode(t, conn, "AUTH", "502")
cmdCode(t, conn, "QUIT", "221")
conn.Close()
}
func TestCmdAUTHOptional(t *testing.T) {
server := &Server{AuthHandler: authHandler}
conn := newConn(t, server)
cmdCode(t, conn, "EHLO host.example.com", "250")
// AUTH without mechanism parameter must return 501 syntax error.
cmdCode(t, conn, "AUTH", "501")
// AUTH with a supported mechanism should return 334.
cmdCode(t, conn, "AUTH CRAM-MD5", "334")
// AUTH must support cancellation with '*' and return 501 syntax error.
cmdCode(t, conn, "*", "501")
// AUTH with an unsupported mechanism should return 504 unrecognized type.
cmdCode(t, conn, "AUTH FOO", "504")
// The LOGIN and PLAIN mechanisms require a TLS connection, and are disabled by default.
cmdCode(t, conn, "AUTH LOGIN", "504")
cmdCode(t, conn, "AUTH PLAIN", "504")
// AUTH attempt during a mail transaction must return 503 bad sequence.
cmdCode(t, conn, "MAIL FROM:<[email protected]>", "250")
cmdCode(t, conn, "AUTH CRAM-MD5", "503")
cmdCode(t, conn, "RCPT TO:<[email protected]>", "250")