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 responseOK(t testing.TB, conn *textproto.Conn) string {
37 line, err := conn.ReadLine()
39 t.Errorf("%s responseOK: %v", _fl(1), err)
41 if !strings.HasPrefix(line, "+OK") {
42 t.Errorf("%s expected +OK, got %q", _fl(1), line)
47 func responseERR(t testing.TB, conn *textproto.Conn) string {
48 line, err := conn.ReadLine()
50 t.Errorf("%s responseERR: %v", _fl(1), err)
52 if !strings.HasPrefix(line, "-ERR") {
53 t.Errorf("%s expected -ERR, got %q", _fl(1), line)
58 func runServer(t *testing.T, po PostOffice) net.Listener {
59 l, err := net.Listen("tcp", "localhost:0")
67 conn, err := l.Accept()
71 go AcceptConnection(conn, po, zap.NewNop())
77 type testServer struct {
82 func (s *testServer) Name() string {
86 func (s *testServer) OpenMailbox(user, pass string) (Mailbox, error) {
87 if s.user == user && s.pass == pass {
90 return nil, fmt.Errorf("bad username/pass")
93 type testMailbox struct {
94 msgs map[int]*testMessage
97 type MessageList []Message
99 func (l MessageList) Len() int {
102 func (l MessageList) Less(i, j int) bool {
103 return l[i].ID() < l[j].ID()
105 func (l MessageList) Swap(i, j int) {
106 l[i], l[j] = l[j], l[i]
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])
114 sort.Sort(MessageList(msgs))
118 func (mb *testMailbox) GetMessage(id int) Message {
119 if msg, ok := mb.msgs[id]; ok {
125 func (mb *testMailbox) Retrieve(msg Message) (io.ReadCloser, error) {
126 r := strings.NewReader(msg.(*testMessage).body)
127 return ioutil.NopCloser(r), nil
130 func (mb *testMailbox) Delete(msg Message) error {
131 msg.(*testMessage).deleted = true
135 func (mb *testMailbox) Close() error {
139 func (mb *testMailbox) Reset() {
140 for _, msg := range mb.msgs {
145 type testMessage struct {
152 func (m *testMessage) UniqueID() string {
153 return fmt.Sprintf("%p", m)
156 func (m *testMessage) ID() int {
159 func (m *testMessage) Size() int {
162 func (m *testMessage) Deleted() bool {
166 func newTestServer() *testServer {
171 msgs: make(map[int]*testMessage),
177 func TestExampleSession(t *testing.T) {
182 s.mb.msgs[1] = &testMessage{1, 120, false, ""}
183 s.mb.msgs[2] = &testMessage{2, 200, false, ""}
185 conn, err := textproto.Dial(l.Addr().Network(), l.Addr().String())
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)
193 ok(t, conn.PrintfLine("USER u"))
196 ok(t, conn.PrintfLine("PASS p"))
199 ok(t, conn.PrintfLine("STAT"))
200 if want, got := "+OK 2 320", responseOK(t, conn); want != got {
201 t.Errorf("STAT want %q, got %q", want, got)
204 ok(t, conn.PrintfLine("LIST"))
206 lines, err := conn.ReadDotLines()
208 if want, got := 2, len(lines); want != got {
209 t.Errorf("LIST want %d lines, got %d", want, got)
211 if want, got := "1 120", lines[0]; want != got {
212 t.Errorf("LIST line 0 want %q, got %q", want, got)
214 if want, got := "2 200", lines[1]; want != got {
215 t.Errorf("LIST line 1 expected %q, got %q", want, got)
218 ok(t, conn.PrintfLine("QUIT"))
222 type requestResponse struct {
224 expecter func(testing.TB, *textproto.Conn) string
227 func expectOKResponse(predicate func(string) bool) func(testing.TB, *textproto.Conn) string {
228 return func(t testing.TB, conn *textproto.Conn) string {
229 line := responseOK(t, conn)
230 if !predicate(line) {
231 t.Errorf("%s Predicate failed, got %q", _fl(1), line)
237 func clientServerTest(t *testing.T, s *testServer, sequence []requestResponse) {
241 conn, err := textproto.Dial(l.Addr().Network(), l.Addr().String())
246 for _, pair := range sequence {
247 ok(t, conn.PrintfLine(pair.command))
248 pair.expecter(t, conn)
250 t.Logf("command %q", pair.command)
255 func TestAuthStates(t *testing.T) {
256 clientServerTest(t, newTestServer(), []requestResponse{
257 {"STAT", responseERR},
258 {"NOOP", responseOK},
259 {"USER bad", responseOK},
260 {"PASS bad", responseERR},
261 {"USER", responseERR},
262 {"USER x", responseOK},
263 {"PASS", responseERR},
264 {"LIST", responseERR},
265 {"USER u", responseOK},
266 {"PASS bad", responseERR},
267 {"STAT", responseERR},
268 {"PASS p", responseOK},
269 {"QUIT", responseOK},
273 func TestDeleted(t *testing.T) {
275 s.mb.msgs[1] = &testMessage{1, 999, false, ""}
276 s.mb.msgs[2] = &testMessage{2, 10, false, ""}
278 clientServerTest(t, s, []requestResponse{
279 {"USER u", responseOK},
280 {"PASS p", responseOK},
281 {"STAT", expectOKResponse(func(s string) bool {
282 return s == "+OK 2 1009"
284 {"DELE 1", responseOK},
285 {"RETR 1", responseERR},
286 {"DELE 1", responseERR},
287 {"STAT", expectOKResponse(func(s string) bool {
288 return s == "+OK 1 10"
290 {"RSET", responseOK},
291 {"STAT", expectOKResponse(func(s string) bool {
292 return s == "+OK 2 1009"
294 {"QUIT", responseOK},
298 func TestCaseSensitivty(t *testing.T) {
300 s.mb.msgs[999] = &testMessage{999, 1, false, "a"}
302 clientServerTest(t, s, []requestResponse{
303 {"user u", responseOK},
304 {"PasS p", responseOK},
305 {"sTaT", responseOK},
306 {"retr 1", responseERR},
307 {"dele 999", responseOK},
308 {"QUIT", responseOK},
312 func TestRetr(t *testing.T) {
314 s.mb.msgs[1] = &testMessage{1, 5, false, "hello"}
315 s.mb.msgs[2] = &testMessage{2, 69, false, "this\r\nis a\r\n.\r\ntest"}
317 clientServerTest(t, s, []requestResponse{
318 {"USER u", responseOK},
319 {"PASS p", responseOK},
320 {"STAT", responseOK},
321 {"RETR 1", func(t testing.TB, tp *textproto.Conn) string {
327 resp, err := tp.ReadDotLines()
333 want := []string{"hello"}
334 if !reflect.DeepEqual(resp, want) {
335 t.Errorf("Want %v, got %v", want, resp)
340 {"RETR 2", func(t testing.TB, tp *textproto.Conn) string {
346 resp, err := tp.ReadDotLines()
352 want := []string{"this", "is a", ".", "test"}
353 if !reflect.DeepEqual(resp, want) {
354 t.Errorf("Want %v, got %v", want, resp)
359 {"QUIT", responseOK},
363 func TestUidl(t *testing.T) {
365 s.mb.msgs[1] = &testMessage{1, 3, false, "abc"}
366 s.mb.msgs[2] = &testMessage{2, 1, true, "Z"}
367 s.mb.msgs[3] = &testMessage{3, 4, false, "test"}
369 clientServerTest(t, s, []requestResponse{
370 {"USER u", responseOK},
371 {"PASS p", responseOK},
372 {"UIDL", func(t testing.TB, tp *textproto.Conn) string {
378 resp, err := tp.ReadDotLines()
385 fmt.Sprintf("1 %p", s.mb.msgs[1]),
386 fmt.Sprintf("3 %p", s.mb.msgs[3]),
388 if !reflect.DeepEqual(resp, want) {
389 t.Errorf("Want %v, got %v", want, resp)
394 {"QUIT", responseOK},
398 func TestDele(t *testing.T) {
400 s.mb.msgs[1] = &testMessage{1, 3, false, "abc"}
401 s.mb.msgs[2] = &testMessage{2, 1, false, "d"}
403 clientServerTest(t, s, []requestResponse{
404 {"USER u", responseOK},
405 {"PASS p", responseOK},
406 {"STAT", expectOKResponse(func(s string) bool {
407 return s == "+OK 2 4"
409 {"DELE 1", responseOK},
410 {"STAT", expectOKResponse(func(s string) bool {
411 return s == "+OK 1 1"
413 {"RSET", responseOK},
414 {"STAT", expectOKResponse(func(s string) bool {
415 return s == "+OK 2 4"
417 {"QUIT", responseOK},
420 if s.mb.msgs[1].Deleted() || s.mb.msgs[2].Deleted() {
421 t.Errorf("RSET should not delete a message")
424 clientServerTest(t, s, []requestResponse{
425 {"USER u", responseOK},
426 {"PASS p", responseOK},
427 {"STAT", expectOKResponse(func(s string) bool {
428 return s == "+OK 2 4"
430 {"DELE 1", responseOK},
431 {"STAT", expectOKResponse(func(s string) bool {
432 return s == "+OK 1 1"
434 {"QUIT", responseOK},
437 if !s.mb.msgs[1].Deleted() {
438 t.Errorf("DELE did not work")
440 if s.mb.msgs[2].Deleted() {
441 t.Errorf("DELE the wrong message")
445 func TestCapa(t *testing.T) {
448 capaTest := func(t testing.TB, tp *textproto.Conn) string {
454 resp, err := tp.ReadDotLines()
466 caps := map[string]int{
470 for _, line := range resp {
471 if val, ok := caps[line]; ok {
472 if val == capNeeded {
475 t.Errorf("unxpected capa value %q", line)
481 for c, val := range caps {
483 t.Errorf("unexpected capa value for %q: %d", c, val)
489 clientServerTest(t, s, []requestResponse{
491 {"USER u", responseOK},
493 {"PASS p", responseOK},
495 {"QUIT", responseOK},