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