]>
src.bluestatic.org Git - mailpopbox.git/blob - smtp/conn_test.go
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
25 func _fl(depth
int) string {
26 _
, file
, line
, _
:= runtime
.Caller(depth
+ 1)
27 return fmt
.Sprintf("[%s:%d]", filepath
.Base(file
), line
)
30 func ok(t testing
.TB
, err error
) {
32 t
.Errorf("%s unexpected error: %v", _fl(1), err
)
36 func readCodeLine(t testing
.TB
, conn
*textproto
.Conn
, code
int) string {
37 actual
, message
, err
:= conn
.ReadCodeLine(code
)
39 t
.Errorf("%s ReadCodeLine error, expected %d, got %d: %v", _fl(1), code
, actual
, err
)
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")
55 conn
, err
:= l
.Accept()
59 go AcceptConnection(conn
, server
, zap
.NewNop())
66 type userAuth
struct {
67 authz
, authc
, passwd
string
70 type testServer
struct {
79 func (s
*testServer
) Name() string {
83 func (s
*testServer
) TLSConfig() *tls
.Config
{
87 func (s
*testServer
) VerifyAddress(addr mail
.Address
) ReplyLine
{
88 if DomainForAddress(addr
) != s
.domain
{
89 return ReplyBadMailbox
91 for _
, block
:= range s
.blockList
{
92 if strings
.ToLower(block
) == addr
.Address
{
93 return ReplyBadMailbox
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
105 func (s
*testServer
) RelayMessage(en Envelope
) {
106 s
.relayed
= append(s
.relayed
, en
)
109 func createClient(t
*testing
.T
, addr net
.Addr
) *textproto
.Conn
{
110 conn
, err
:= textproto
.Dial(addr
.Network(), addr
.String())
118 type requestResponse
struct {
121 handler
func(testing
.TB
, *textproto
.Conn
)
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 {
130 readCodeLine(t
, conn
, rr
.responseCode
)
133 t
.Logf("%s case %d", _fl(1), i
)
139 func TestScenarioTypical(t
*testing
.T
) {
142 blockList
: []string{"Green@foo.com"},
144 l
:= runServer(t
, &s
)
147 conn
:= createClient(t
, l
.Addr())
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
)
154 greet
:= "greeting.TestScenarioTypical"
155 ok(t
, conn
.PrintfLine("EHLO "+greet
))
157 _
, message
, err
:= conn
.ReadResponse(250)
159 if !strings
.Contains(message
, greet
) {
160 t
.Errorf("EHLO response does not contain greeting, got %q", message
)
163 ok(t
, conn
.PrintfLine("MAIL FROM:<Smith@bar.com>"))
164 readCodeLine(t
, conn
, 250)
166 ok(t
, conn
.PrintfLine("RCPT TO:<Jones@foo.com>"))
167 readCodeLine(t
, conn
, 250)
169 ok(t
, conn
.PrintfLine("RCPT TO:<Green@foo.com>"))
170 readCodeLine(t
, conn
, 550)
172 ok(t
, conn
.PrintfLine("RCPT TO:<Brown@foo.com>"))
173 readCodeLine(t
, conn
, 250)
175 ok(t
, conn
.PrintfLine("DATA"))
176 readCodeLine(t
, conn
, 354)
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)
183 ok(t
, conn
.PrintfLine("QUIT"))
184 readCodeLine(t
, conn
, 221)
187 func TestVerifyAddress(t
*testing
.T
) {
190 blockList
: []string{"banned@test.mail"},
192 l
:= runServer(t
, &s
)
195 conn
:= createClient(t
, l
.Addr())
196 readCodeLine(t
, conn
, 220)
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},
208 func TestBadAddress(t
*testing
.T
) {
209 l
:= runServer(t
, &testServer
{})
212 conn
:= createClient(t
, l
.Addr())
213 readCodeLine(t
, conn
, 220)
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},
224 func TestCaseSensitivty(t
*testing
.T
) {
227 blockList
: []string{"reject@mail.com"},
232 conn
:= createClient(t
, l
.Addr())
233 readCodeLine(t
, conn
, 220)
235 runTableTest(t
, conn
, []requestResponse
{
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)
245 ok(t
, conn
.PrintfLine("."))
246 readCodeLine(t
, conn
, 250)
248 {"MAIL FR:", 501, nil},
253 func TestGetReceivedInfo(t
*testing
.T
) {
255 server
: &testServer
{},
256 remoteAddr
: &net
.IPAddr
{net
.IPv4(127, 0, 0, 1), ""},
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
279 {params
{"remote.test.", true, false, "foo@bar.com"},
281 line2
+ "ESMTP id " + msgId
+ crlf
,
282 "for <foo@bar.com>" + crlf
,
283 "(using PLAINTEXT);" + crlf
,
287 for _
, test
:= range tests
{
288 t
.Logf("%#v", test
.params
)
290 conn
.ehlo
= test
.params
.ehlo
291 conn
.esmtp
= test
.params
.esmtp
292 //conn.tls = test.params.tls
294 envelope
:= Envelope
{
295 RcptTo
: []mail
.Address
{{"", test
.params
.address
}},
300 actual
:= conn
.getReceivedInfo(envelope
)
301 actualLines
:= strings
.SplitAfter(string(actual
), crlf
)
303 if len(actualLines
) != len(test
.expect
) {
304 t
.Errorf("wrong numbber of lines, expected %d, got %d", len(test
.expect
), len(actualLines
))
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
)
318 func getTLSConfig(t
*testing
.T
) *tls
.Config
{
319 cert
, err
:= tls
.LoadX509KeyPair("../testtls/domain.crt", "../testtls/domain.key")
325 ServerName
: "localhost",
326 Certificates
: []tls
.Certificate
{cert
},
327 InsecureSkipVerify
: true,
331 func setupTLSClient(t
*testing
.T
, addr net
.Addr
) *textproto
.Conn
{
332 nc
, err
:= net
.Dial(addr
.Network(), addr
.String())
335 conn
:= textproto
.NewConn(nc
)
336 readCodeLine(t
, conn
, 220)
338 ok(t
, conn
.PrintfLine("EHLO test-tls"))
339 _
, resp
, err
:= conn
.ReadResponse(250)
341 if !strings
.Contains(resp
, "STARTTLS\n") {
342 t
.Errorf("STARTTLS not advertised")
345 ok(t
, conn
.PrintfLine("STARTTLS"))
346 readCodeLine(t
, conn
, 220)
348 tc
:= tls
.Client(nc
, getTLSConfig(t
))
352 conn
= textproto
.NewConn(tc
)
354 ok(t
, conn
.PrintfLine("EHLO test-tls-started"))
355 _
, resp
, err
= conn
.ReadResponse(250)
357 if strings
.Contains(resp
, "STARTTLS\n") {
358 t
.Errorf("STARTTLS advertised when already started")
364 func b64enc(s
string) string {
365 return string(base64
.StdEncoding
.EncodeToString([]byte(s
)))
368 func TestTLS(t
*testing
.T
) {
369 l
:= runServer(t
, &testServer
{tlsConfig
: getTLSConfig(t
)})
372 setupTLSClient(t
, l
.Addr())
375 func TestAuthWithoutTLS(t
*testing
.T
) {
376 l
:= runServer(t
, &testServer
{})
379 conn
:= createClient(t
, l
.Addr())
380 readCodeLine(t
, conn
, 220)
382 ok(t
, conn
.PrintfLine("EHLO test"))
383 _
, resp
, err
:= conn
.ReadResponse(250)
386 if strings
.Contains(resp
, "AUTH") {
387 t
.Errorf("AUTH should not be advertised over plaintext")
391 func TestAuth(t
*testing
.T
) {
392 l
:= runServer(t
, &testServer
{
393 tlsConfig
: getTLSConfig(t
),
402 conn
:= setupTLSClient(t
, l
.Addr())
404 runTableTest(t
, conn
, []requestResponse
{
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.
421 func TestAuthNoInitialResponse(t
*testing
.T
) {
422 l
:= runServer(t
, &testServer
{
423 tlsConfig
: getTLSConfig(t
),
427 passwd
: "longpassword",
432 conn
:= setupTLSClient(t
, l
.Addr())
434 runTableTest(t
, conn
, []requestResponse
{
435 {"AUTH PLAIN " + b64enc("\x00user\x00longpassword"), 250, nil},
439 func TestRelayRequiresAuth(t
*testing
.T
) {
440 l
:= runServer(t
, &testServer
{
441 domain
: "example.com",
442 tlsConfig
: getTLSConfig(t
),
445 authc
: "mailbox@example.com",
451 conn
:= setupTLSClient(t
, l
.Addr())
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},
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
),
468 authc
: "mailbox@example.com",
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},
481 func TestBasicRelay(t
*testing
.T
) {
482 server
, l
, conn
:= setupRelayTest(t
)
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)
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)
500 if len(server
.relayed
) != 1 {
501 t
.Errorf("Expected 1 relayed message, got %d", len(server
.relayed
))
505 func TestSendAsRelay(t
*testing
.T
) {
506 server
, l
, conn
:= setupRelayTest(t
)
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)
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)
524 if len(server
.relayed
) != 1 {
525 t
.Fatalf("Expected 1 relayed message, got %d", len(server
.relayed
))
528 replaced
:= "source@example.com"
529 original
:= "mailbox@example.com"
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
)
536 if len(en
.RcptTo
) != 1 {
537 t
.Errorf("Expected 1 recipient, got %d", len(en
.RcptTo
))
539 if en
.RcptTo
[0].Address
!= "valid@dest.xyz" {
540 t
.Errorf("Unexpected RcptTo %q", en
.RcptTo
[0].Address
)
543 msg
:= string(en
.Data
)
545 if strings
.Index(msg
, original
) != -1 {
546 t
.Errorf("Should not find %q in message %q", original
, msg
)
549 if strings
.Index(msg
, "\nFrom: <source@example.com>\n") == -1 {
550 t
.Errorf("Could not find From: header in message %q", msg
)
553 if strings
.Index(msg
, "\nSubject: Send-as relay \n") == -1 {
554 t
.Errorf("Could not find modified Subject: header in message %q", msg
)
558 func TestSendMultipleRelay(t
*testing
.T
) {
559 server
, l
, conn
:= setupRelayTest(t
)
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)
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)
578 if len(server
.relayed
) != 1 {
579 t
.Fatalf("Expected 1 relayed message, got %d", len(server
.relayed
))
582 replaced
:= "source@example.com"
583 original
:= "mailbox@example.com"
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
)
590 if len(en
.RcptTo
) != 2 {
591 t
.Errorf("Expected 2 recipient, got %d", len(en
.RcptTo
))
593 if en
.RcptTo
[0].Address
!= "valid@dest.xyz" {
594 t
.Errorf("Unexpected RcptTo %q", en
.RcptTo
[0].Address
)
597 msg
:= string(en
.Data
)
599 if strings
.Index(msg
, original
) != -1 {
600 t
.Errorf("Should not find %q in message %q", original
, msg
)
603 if strings
.Index(msg
, "\nFrom: Finn <source@example.com>\n") == -1 {
604 t
.Errorf("Could not find From: header in message %q", msg
)
607 if strings
.Index(msg
, "\nSubject: Two destinations \n") == -1 {
608 t
.Errorf("Could not find modified Subject: header in message %q", msg
)