Upgrade zap to the 1.0.0 release.
[mailpopbox.git] / pop3 / 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 pop3
8
9 import (
10 "fmt"
11 "io"
12 "io/ioutil"
13 "net"
14 "net/textproto"
15 "path/filepath"
16 "reflect"
17 "runtime"
18 "sort"
19 "strings"
20 "testing"
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 responseOK(t testing.TB, conn *textproto.Conn) string {
37 line, err := conn.ReadLine()
38 if err != nil {
39 t.Errorf("%s responseOK: %v", _fl(1), err)
40 }
41 if !strings.HasPrefix(line, "+OK") {
42 t.Errorf("%s expected +OK, got %q", _fl(1), line)
43 }
44 return line
45 }
46
47 func responseERR(t testing.TB, conn *textproto.Conn) string {
48 line, err := conn.ReadLine()
49 if err != nil {
50 t.Errorf("%s responseERR: %v", _fl(1), err)
51 }
52 if !strings.HasPrefix(line, "-ERR") {
53 t.Errorf("%s expected -ERR, got %q", _fl(1), line)
54 }
55 return line
56 }
57
58 func runServer(t *testing.T, po PostOffice) net.Listener {
59 l, err := net.Listen("tcp", "localhost:0")
60 if err != nil {
61 t.Fatal(err)
62 return nil
63 }
64
65 go func() {
66 for {
67 conn, err := l.Accept()
68 if err != nil {
69 return
70 }
71 go AcceptConnection(conn, po, zap.New(zap.NullEncoder()))
72 }
73 }()
74 return l
75 }
76
77 type testServer struct {
78 user, pass string
79 mb testMailbox
80 }
81
82 func (s *testServer) Name() string {
83 return "Test-Server"
84 }
85
86 func (s *testServer) OpenMailbox(user, pass string) (Mailbox, error) {
87 if s.user == user && s.pass == pass {
88 return &s.mb, nil
89 }
90 return nil, fmt.Errorf("bad username/pass")
91 }
92
93 type testMailbox struct {
94 msgs map[int]*testMessage
95 }
96
97 type MessageList []Message
98
99 func (l MessageList) Len() int {
100 return len(l)
101 }
102 func (l MessageList) Less(i, j int) bool {
103 return l[i].ID() < l[j].ID()
104 }
105 func (l MessageList) Swap(i, j int) {
106 l[i], l[j] = l[j], l[i]
107 }
108
109 func (mb *testMailbox) ListMessages() ([]Message, error) {
110 msgs := make([]Message, 0, len(mb.msgs))
111 for i, _ := range mb.msgs {
112 msgs = append(msgs, mb.msgs[i])
113 }
114 sort.Sort(MessageList(msgs))
115 return msgs, nil
116 }
117
118 func (mb *testMailbox) GetMessage(id int) Message {
119 if msg, ok := mb.msgs[id]; ok {
120 return msg
121 }
122 return nil
123 }
124
125 func (mb *testMailbox) Retrieve(msg Message) (io.ReadCloser, error) {
126 r := strings.NewReader(msg.(*testMessage).body)
127 return ioutil.NopCloser(r), nil
128 }
129
130 func (mb *testMailbox) Delete(msg Message) error {
131 msg.(*testMessage).deleted = true
132 return nil
133 }
134
135 func (mb *testMailbox) Close() error {
136 return nil
137 }
138
139 func (mb *testMailbox) Reset() {
140 for _, msg := range mb.msgs {
141 msg.deleted = false
142 }
143 }
144
145 type testMessage struct {
146 id int
147 size int
148 deleted bool
149 body string
150 }
151
152 func (m *testMessage) UniqueID() string {
153 return fmt.Sprintf("%p", m)
154 }
155
156 func (m *testMessage) ID() int {
157 return m.id
158 }
159 func (m *testMessage) Size() int {
160 return m.size
161 }
162 func (m *testMessage) Deleted() bool {
163 return m.deleted
164 }
165
166 func newTestServer() *testServer {
167 return &testServer{
168 user: "u",
169 pass: "p",
170 mb: testMailbox{
171 msgs: make(map[int]*testMessage),
172 },
173 }
174 }
175
176 // RFC 1939 ยง 10
177 func TestExampleSession(t *testing.T) {
178 s := newTestServer()
179 l := runServer(t, s)
180 defer l.Close()
181
182 s.mb.msgs[1] = &testMessage{1, 120, false, ""}
183 s.mb.msgs[2] = &testMessage{2, 200, false, ""}
184
185 conn, err := textproto.Dial(l.Addr().Network(), l.Addr().String())
186 ok(t, err)
187
188 line := responseOK(t, conn)
189 if !strings.Contains(line, s.Name()) {
190 t.Errorf("POP greeting did not include server name, got %q", line)
191 }
192
193 ok(t, conn.PrintfLine("USER u"))
194 responseOK(t, conn)
195
196 ok(t, conn.PrintfLine("PASS p"))
197 responseOK(t, conn)
198
199 ok(t, conn.PrintfLine("STAT"))
200 line = responseOK(t, conn)
201 expected := "+OK 2 320"
202 if line != expected {
203 t.Errorf("STAT expected %q, got %q", expected, line)
204 }
205
206 ok(t, conn.PrintfLine("LIST"))
207 responseOK(t, conn)
208 lines, err := conn.ReadDotLines()
209 ok(t, err)
210 if len(lines) != 2 {
211 t.Errorf("LIST expected 2 lines, got %d", len(lines))
212 }
213 expected = "1 120"
214 if lines[0] != expected {
215 t.Errorf("LIST line 0 expected %q, got %q", expected, lines[0])
216 }
217 expected = "2 200"
218 if lines[1] != expected {
219 t.Errorf("LIST line 1 expected %q, got %q", expected, lines[1])
220 }
221
222 ok(t, conn.PrintfLine("QUIT"))
223 responseOK(t, conn)
224 }
225
226 type requestResponse struct {
227 command string
228 expecter func(testing.TB, *textproto.Conn) string
229 }
230
231 func expectOKResponse(predicate func(string) bool) func(testing.TB, *textproto.Conn) string {
232 return func(t testing.TB, conn *textproto.Conn) string {
233 line := responseOK(t, conn)
234 if !predicate(line) {
235 t.Errorf("%s Predicate failed, got %q", _fl(1), line)
236 }
237 return line
238 }
239 }
240
241 func clientServerTest(t *testing.T, s *testServer, sequence []requestResponse) {
242 l := runServer(t, s)
243 defer l.Close()
244
245 conn, err := textproto.Dial(l.Addr().Network(), l.Addr().String())
246 ok(t, err)
247
248 responseOK(t, conn)
249
250 for _, pair := range sequence {
251 ok(t, conn.PrintfLine(pair.command))
252 pair.expecter(t, conn)
253 if t.Failed() {
254 t.Logf("command %q", pair.command)
255 }
256 }
257 }
258
259 func TestAuthStates(t *testing.T) {
260 clientServerTest(t, newTestServer(), []requestResponse{
261 {"STAT", responseERR},
262 {"NOOP", responseOK},
263 {"USER bad", responseOK},
264 {"PASS bad", responseERR},
265 {"USER", responseERR},
266 {"USER x", responseOK},
267 {"PASS", responseERR},
268 {"LIST", responseERR},
269 {"USER u", responseOK},
270 {"PASS bad", responseERR},
271 {"STAT", responseERR},
272 {"PASS p", responseOK},
273 {"QUIT", responseOK},
274 })
275 }
276
277 func TestDeleted(t *testing.T) {
278 s := newTestServer()
279 s.mb.msgs[1] = &testMessage{1, 999, false, ""}
280 s.mb.msgs[2] = &testMessage{2, 10, false, ""}
281
282 clientServerTest(t, s, []requestResponse{
283 {"USER u", responseOK},
284 {"PASS p", responseOK},
285 {"STAT", expectOKResponse(func(s string) bool {
286 return s == "+OK 2 1009"
287 })},
288 {"DELE 1", responseOK},
289 {"RETR 1", responseERR},
290 {"DELE 1", responseERR},
291 {"STAT", expectOKResponse(func(s string) bool {
292 return s == "+OK 1 10"
293 })},
294 {"RSET", responseOK},
295 {"STAT", expectOKResponse(func(s string) bool {
296 return s == "+OK 2 1009"
297 })},
298 {"QUIT", responseOK},
299 })
300 }
301
302 func TestCaseSensitivty(t *testing.T) {
303 s := newTestServer()
304 s.mb.msgs[999] = &testMessage{999, 1, false, "a"}
305
306 clientServerTest(t, s, []requestResponse{
307 {"user u", responseOK},
308 {"PasS p", responseOK},
309 {"sTaT", responseOK},
310 {"retr 1", responseERR},
311 {"dele 999", responseOK},
312 {"QUIT", responseOK},
313 })
314 }
315
316 func TestRetr(t *testing.T) {
317 s := newTestServer()
318 s.mb.msgs[1] = &testMessage{1, 5, false, "hello"}
319 s.mb.msgs[2] = &testMessage{2, 69, false, "this\r\nis a\r\n.\r\ntest"}
320
321 clientServerTest(t, s, []requestResponse{
322 {"USER u", responseOK},
323 {"PASS p", responseOK},
324 {"STAT", responseOK},
325 {"RETR 1", func(t testing.TB, tp *textproto.Conn) string {
326 responseOK(t, tp)
327 if t.Failed() {
328 return ""
329 }
330
331 resp, err := tp.ReadDotLines()
332 if err != nil {
333 t.Error(err)
334 return ""
335 }
336
337 expected := []string{"hello"}
338 if !reflect.DeepEqual(resp, expected) {
339 t.Errorf("Expected %v, got %v", expected, resp)
340 }
341
342 return ""
343 }},
344 {"RETR 2", func(t testing.TB, tp *textproto.Conn) string {
345 responseOK(t, tp)
346 if t.Failed() {
347 return ""
348 }
349
350 resp, err := tp.ReadDotLines()
351 if err != nil {
352 t.Error(err)
353 return ""
354 }
355
356 expected := []string{"this", "is a", ".", "test"}
357 if !reflect.DeepEqual(resp, expected) {
358 t.Errorf("Expected %v, got %v", expected, resp)
359 }
360
361 return ""
362 }},
363 {"QUIT", responseOK},
364 })
365 }
366
367 func TestUidl(t *testing.T) {
368 s := newTestServer()
369 s.mb.msgs[1] = &testMessage{1, 3, false, "abc"}
370 s.mb.msgs[2] = &testMessage{2, 1, true, "Z"}
371 s.mb.msgs[3] = &testMessage{3, 4, false, "test"}
372
373 clientServerTest(t, s, []requestResponse{
374 {"USER u", responseOK},
375 {"PASS p", responseOK},
376 {"UIDL", func(t testing.TB, tp *textproto.Conn) string {
377 responseOK(t, tp)
378 if t.Failed() {
379 return ""
380 }
381
382 resp, err := tp.ReadDotLines()
383 if err != nil {
384 t.Error(err)
385 return ""
386 }
387
388 expected := []string{
389 fmt.Sprintf("1 %p", s.mb.msgs[1]),
390 fmt.Sprintf("3 %p", s.mb.msgs[3]),
391 }
392 if !reflect.DeepEqual(resp, expected) {
393 t.Errorf("Expected %v, got %v", expected, resp)
394 }
395
396 return ""
397 }},
398 {"QUIT", responseOK},
399 })
400 }
401
402 func TestDele(t *testing.T) {
403 s := newTestServer()
404 s.mb.msgs[1] = &testMessage{1, 3, false, "abc"}
405 s.mb.msgs[2] = &testMessage{2, 1, false, "d"}
406
407 clientServerTest(t, s, []requestResponse{
408 {"USER u", responseOK},
409 {"PASS p", responseOK},
410 {"STAT", expectOKResponse(func(s string) bool {
411 return s == "+OK 2 4"
412 })},
413 {"DELE 1", responseOK},
414 {"STAT", expectOKResponse(func(s string) bool {
415 return s == "+OK 1 1"
416 })},
417 {"RSET", responseOK},
418 {"STAT", expectOKResponse(func(s string) bool {
419 return s == "+OK 2 4"
420 })},
421 {"QUIT", responseOK},
422 })
423
424 if s.mb.msgs[1].Deleted() || s.mb.msgs[2].Deleted() {
425 t.Errorf("RSET should not delete a message")
426 }
427
428 clientServerTest(t, s, []requestResponse{
429 {"USER u", responseOK},
430 {"PASS p", responseOK},
431 {"STAT", expectOKResponse(func(s string) bool {
432 return s == "+OK 2 4"
433 })},
434 {"DELE 1", responseOK},
435 {"STAT", expectOKResponse(func(s string) bool {
436 return s == "+OK 1 1"
437 })},
438 {"QUIT", responseOK},
439 })
440
441 if !s.mb.msgs[1].Deleted() {
442 t.Errorf("DELE did not work")
443 }
444 if s.mb.msgs[2].Deleted() {
445 t.Errorf("DELE the wrong message")
446 }
447 }
448
449 func TestCapa(t *testing.T) {
450 s := newTestServer()
451
452 capaTest := func(t testing.TB, tp *textproto.Conn) string {
453 responseOK(t, tp)
454 if t.Failed() {
455 return ""
456 }
457
458 resp, err := tp.ReadDotLines()
459 if err != nil {
460 t.Error(err)
461 return ""
462 }
463
464 const (
465 capNeeded = iota
466 capSeen
467 capOK
468 )
469
470 caps := map[string]int{
471 "USER": capNeeded,
472 "UIDL": capNeeded,
473 }
474 for _, line := range resp {
475 if val, ok := caps[line]; ok {
476 if val == capNeeded {
477 caps[line] = capOK
478 } else {
479 t.Errorf("unxpected capa value %q", line)
480 }
481 } else {
482 caps[line] = capSeen
483 }
484 }
485 for c, val := range caps {
486 if val != capOK {
487 t.Errorf("unexpected capa value for %q: %d", c, val)
488 }
489 }
490 return ""
491 }
492
493 clientServerTest(t, s, []requestResponse{
494 {"CAPA", capaTest},
495 {"USER u", responseOK},
496 {"CAPA", capaTest},
497 {"PASS p", responseOK},
498 {"CAPA", capaTest},
499 {"QUIT", responseOK},
500 })
501 }