Implement AUTH PLAIN authentication extensions in SMTP.
[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 _, message, err := conn.ReadCodeLine(code)
32 if err != nil {
33 t.Errorf("%s ReadCodeLine error: %v", _fl(1), 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 blockList []string
67 tlsConfig *tls.Config
68 *userAuth
69 }
70
71 func (s *testServer) Name() string {
72 return "Test-Server"
73 }
74
75 func (s *testServer) TLSConfig() *tls.Config {
76 return s.tlsConfig
77 }
78
79 func (s *testServer) VerifyAddress(addr mail.Address) ReplyLine {
80 for _, block := range s.blockList {
81 if strings.ToLower(block) == addr.Address {
82 return ReplyBadMailbox
83 }
84 }
85 return ReplyOK
86 }
87
88 func (s *testServer) Authenticate(authz, authc, passwd string) bool {
89 return s.userAuth.authz == authz &&
90 s.userAuth.authc == authc &&
91 s.userAuth.passwd == passwd
92 }
93
94 func createClient(t *testing.T, addr net.Addr) *textproto.Conn {
95 conn, err := textproto.Dial(addr.Network(), addr.String())
96 if err != nil {
97 t.Fatal(err)
98 return nil
99 }
100 return conn
101 }
102
103 type requestResponse struct {
104 request string
105 responseCode int
106 handler func(testing.TB, *textproto.Conn)
107 }
108
109 func runTableTest(t testing.TB, conn *textproto.Conn, seq []requestResponse) {
110 for i, rr := range seq {
111 t.Logf("%s case %d", _fl(1), i)
112 ok(t, conn.PrintfLine(rr.request))
113 if rr.handler != nil {
114 rr.handler(t, conn)
115 } else {
116 readCodeLine(t, conn, rr.responseCode)
117 }
118 }
119 }
120
121 // RFC 5321 ยง D.1
122 func TestScenarioTypical(t *testing.T) {
123 s := testServer{
124 blockList: []string{"Green@foo.com"},
125 }
126 l := runServer(t, &s)
127 defer l.Close()
128
129 conn := createClient(t, l.Addr())
130
131 message := readCodeLine(t, conn, 220)
132 if !strings.HasPrefix(message, s.Name()) {
133 t.Errorf("Greeting does not have server name, got %q", message)
134 }
135
136 greet := "greeting.TestScenarioTypical"
137 ok(t, conn.PrintfLine("EHLO "+greet))
138
139 _, message, err := conn.ReadResponse(250)
140 ok(t, err)
141 if !strings.Contains(message, greet) {
142 t.Errorf("EHLO response does not contain greeting, got %q", message)
143 }
144
145 ok(t, conn.PrintfLine("MAIL FROM:<Smith@bar.com>"))
146 readCodeLine(t, conn, 250)
147
148 ok(t, conn.PrintfLine("RCPT TO:<Jones@foo.com>"))
149 readCodeLine(t, conn, 250)
150
151 ok(t, conn.PrintfLine("RCPT TO:<Green@foo.com>"))
152 readCodeLine(t, conn, 550)
153
154 ok(t, conn.PrintfLine("RCPT TO:<Brown@foo.com>"))
155 readCodeLine(t, conn, 250)
156
157 ok(t, conn.PrintfLine("DATA"))
158 readCodeLine(t, conn, 354)
159
160 ok(t, conn.PrintfLine("Blah blah blah..."))
161 ok(t, conn.PrintfLine("...etc. etc. etc."))
162 ok(t, conn.PrintfLine("."))
163 readCodeLine(t, conn, 250)
164
165 ok(t, conn.PrintfLine("QUIT"))
166 readCodeLine(t, conn, 221)
167 }
168
169 func TestVerifyAddress(t *testing.T) {
170 s := testServer{
171 blockList: []string{"banned@test.mail"},
172 }
173 l := runServer(t, &s)
174 defer l.Close()
175
176 conn := createClient(t, l.Addr())
177 readCodeLine(t, conn, 220)
178
179 runTableTest(t, conn, []requestResponse{
180 {"EHLO test", 0, func(t testing.TB, conn *textproto.Conn) { conn.ReadResponse(250) }},
181 {"VRFY banned@test.mail", 252, nil},
182 {"VRFY allowed@test.mail", 252, nil},
183 {"MAIL FROM:<sender@example.com>", 250, nil},
184 {"RCPT TO:<banned@test.mail>", 550, nil},
185 {"QUIT", 221, nil},
186 })
187 }
188
189 func TestBadAddress(t *testing.T) {
190 l := runServer(t, &testServer{})
191 defer l.Close()
192
193 conn := createClient(t, l.Addr())
194 readCodeLine(t, conn, 220)
195
196 runTableTest(t, conn, []requestResponse{
197 {"EHLO test", 0, func(t testing.TB, conn *textproto.Conn) { conn.ReadResponse(250) }},
198 {"MAIL FROM:<sender>", 501, nil},
199 {"MAIL FROM:<sender@foo.com> SIZE=2163", 250, nil},
200 {"RCPT TO:<banned.net>", 501, nil},
201 {"QUIT", 221, nil},
202 })
203 }
204
205 func TestCaseSensitivty(t *testing.T) {
206 s := &testServer{}
207 s.blockList = []string{"reject@mail.com"}
208 l := runServer(t, s)
209 defer l.Close()
210
211 conn := createClient(t, l.Addr())
212 readCodeLine(t, conn, 220)
213
214 runTableTest(t, conn, []requestResponse{
215 {"nOoP", 250, nil},
216 {"ehLO test.TEST", 0, func(t testing.TB, conn *textproto.Conn) { conn.ReadResponse(250) }},
217 {"mail FROM:<sender@example.com>", 250, nil},
218 {"RcPT tO:<receive@mail.com>", 250, nil},
219 {"RCPT TO:<reject@MAIL.com>", 550, nil},
220 {"RCPT TO:<reject@mail.com>", 550, nil},
221 {"DATa", 0, func(t testing.TB, conn *textproto.Conn) {
222 readCodeLine(t, conn, 354)
223
224 ok(t, conn.PrintfLine("."))
225 readCodeLine(t, conn, 250)
226 }},
227 {"MAIL FR:", 501, nil},
228 {"QUiT", 221, nil},
229 })
230 }
231
232 func TestGetReceivedInfo(t *testing.T) {
233 conn := connection{
234 server: &testServer{},
235 remoteAddr: &net.IPAddr{net.IPv4(127, 0, 0, 1), ""},
236 }
237
238 now := time.Now()
239
240 const crlf = "\r\n"
241 const line1 = "Received: from remote.test. (localhost [127.0.0.1])" + crlf
242 const line2 = "by Test-Server (mailpopbox) with "
243 const msgId = "abcdef.hijk"
244 lineLast := now.Format(time.RFC1123Z) + crlf
245
246 type params struct {
247 ehlo string
248 esmtp bool
249 tls bool
250 address string
251 }
252
253 tests := []struct {
254 params params
255
256 expect []string
257 }{
258 {params{"remote.test.", true, false, "foo@bar.com"},
259 []string{line1,
260 line2 + "ESMTP id " + msgId + crlf,
261 "for <foo@bar.com>" + crlf,
262 "(using PLAINTEXT);" + crlf,
263 lineLast, ""}},
264 }
265
266 for _, test := range tests {
267 t.Logf("%#v", test.params)
268
269 conn.ehlo = test.params.ehlo
270 conn.esmtp = test.params.esmtp
271 //conn.tls = test.params.tls
272
273 envelope := Envelope{
274 RcptTo: []mail.Address{{"", test.params.address}},
275 Received: now,
276 ID: msgId,
277 }
278
279 actual := conn.getReceivedInfo(envelope)
280 actualLines := strings.SplitAfter(string(actual), crlf)
281
282 if len(actualLines) != len(test.expect) {
283 t.Errorf("wrong numbber of lines, expected %d, got %d", len(test.expect), len(actualLines))
284 continue
285 }
286
287 for i, line := range actualLines {
288 expect := test.expect[i]
289 if expect != strings.TrimLeft(line, " ") {
290 t.Errorf("Expected equal string %q, got %q", expect, line)
291 }
292 }
293 }
294
295 }
296
297 func getTLSConfig(t *testing.T) *tls.Config {
298 cert, err := tls.LoadX509KeyPair("../testtls/domain.crt", "../testtls/domain.key")
299 if err != nil {
300 t.Fatal(err)
301 return nil
302 }
303 return &tls.Config{
304 ServerName: "localhost",
305 Certificates: []tls.Certificate{cert},
306 InsecureSkipVerify: true,
307 }
308 }
309
310 func setupTLSClient(t *testing.T, addr net.Addr) *textproto.Conn {
311 nc, err := net.Dial(addr.Network(), addr.String())
312 ok(t, err)
313
314 conn := textproto.NewConn(nc)
315 readCodeLine(t, conn, 220)
316
317 ok(t, conn.PrintfLine("EHLO test-tls"))
318 _, resp, err := conn.ReadResponse(250)
319 ok(t, err)
320 if !strings.Contains(resp, "STARTTLS\n") {
321 t.Errorf("STARTTLS not advertised")
322 }
323
324 ok(t, conn.PrintfLine("STARTTLS"))
325 readCodeLine(t, conn, 220)
326
327 tc := tls.Client(nc, getTLSConfig(t))
328 err = tc.Handshake()
329 ok(t, err)
330
331 conn = textproto.NewConn(tc)
332
333 ok(t, conn.PrintfLine("EHLO test-tls-started"))
334 _, resp, err = conn.ReadResponse(250)
335 ok(t, err)
336 if strings.Contains(resp, "STARTTLS\n") {
337 t.Errorf("STARTTLS advertised when already started")
338 }
339
340 return conn
341 }
342
343 func TestTLS(t *testing.T) {
344 l := runServer(t, &testServer{tlsConfig: getTLSConfig(t)})
345 defer l.Close()
346
347 setupTLSClient(t, l.Addr())
348 }
349
350 func TestAuthWithoutTLS(t *testing.T) {
351 l := runServer(t, &testServer{})
352 defer l.Close()
353
354 conn := createClient(t, l.Addr())
355 readCodeLine(t, conn, 220)
356
357 ok(t, conn.PrintfLine("EHLO test"))
358 _, resp, err := conn.ReadResponse(250)
359 ok(t, err)
360
361 if strings.Contains(resp, "AUTH") {
362 t.Errorf("AUTH should not be advertised over plaintext")
363 }
364 }
365
366 func TestAuth(t *testing.T) {
367 l := runServer(t, &testServer{
368 tlsConfig: getTLSConfig(t),
369 userAuth: &userAuth{
370 authz: "-authz-",
371 authc: "-authc-",
372 passwd: "goats",
373 },
374 })
375 defer l.Close()
376
377 conn := setupTLSClient(t, l.Addr())
378
379 b64enc := func(s string) string {
380 return string(base64.StdEncoding.EncodeToString([]byte(s)))
381 }
382
383 runTableTest(t, conn, []requestResponse{
384 {"AUTH", 501, nil},
385 {"AUTH OAUTHBEARER", 504, nil},
386 {"AUTH PLAIN", 334, nil},
387 {b64enc("abc\x00def\x00ghf"), 535, nil},
388 {"AUTH PLAIN", 334, nil},
389 {b64enc("\x00"), 501, nil},
390 {"AUTH PLAIN", 334, nil},
391 {"this isn't base 64", 501, nil},
392 {"AUTH PLAIN", 334, nil},
393 {b64enc("-authz-\x00-authc-\x00goats"), 250, nil},
394 {"AUTH PLAIN", 503, nil}, // already authenticated
395 {"NOOP", 250, nil},
396 })
397 }