package mailfetch import ( "bufio" "bytes" "errors" "net" "strings" "testing" "time" ) // fakeConn is an in-memory net.Conn for protocol tests. type fakeConn struct { r *bytes.Reader w bytes.Buffer } func newFakeConn(script string) *fakeConn { return &fakeConn{r: bytes.NewReader([]byte(script))} } func (c *fakeConn) Read(p []byte) (int, error) { return c.r.Read(p) } func (c *fakeConn) Write(p []byte) (int, error) { return c.w.Write(p) } func (c *fakeConn) Close() error { return nil } func (c *fakeConn) LocalAddr() net.Addr { return dummyAddr{} } func (c *fakeConn) RemoteAddr() net.Addr { return dummyAddr{} } func (c *fakeConn) SetDeadline(time.Time) error { return nil } func (c *fakeConn) SetReadDeadline(time.Time) error { return nil } func (c *fakeConn) SetWriteDeadline(time.Time) error { return nil } type dummyAddr struct{} func (dummyAddr) Network() string { return "fake" } func (dummyAddr) String() string { return "fake" } func newFakeClient(script string) (*Client, *fakeConn) { fc := newFakeConn(script) return &Client{conn: fc, br: bufio.NewReader(fc), timeout: time.Second}, fc } func TestReadResponseSingleLiteral(t *testing.T) { script := "* 1 FETCH (UID 10 BODY[] {5}\r\nHELLO)\r\n" c, _ := newFakeClient(script) resp, err := c.readResponse() if err != nil { t.Fatalf("readResponse() error = %v", err) } if len(resp.literals) != 1 || string(resp.literals[0]) != "HELLO" { t.Fatalf("literals = %q, want [HELLO]", resp.literals) } if !strings.Contains(resp.line, "UID 10") || !strings.Contains(resp.line, ")") { t.Errorf("assembled line = %q, want UID and closing paren", resp.line) } } func TestReadResponseLiteralAtEnd(t *testing.T) { script := "* 1 FETCH (BODY[] {5}\r\nHELLO\r\n\r\n" c, _ := newFakeClient(script) resp, err := c.readResponse() if err != nil { t.Fatalf("readResponse() error = %v", err) } if len(resp.literals) != 1 || string(resp.literals[0]) != "HELLO" { t.Fatalf("literals = %q, want [HELLO]", resp.literals) } } func TestSearchSince(t *testing.T) { script := "* SEARCH 10 11 12\r\nA0001 OK SEARCH completed\r\n" c, fc := newFakeClient(script) uids, err := c.SearchSince(time.Date(2026, 1, 2, 0, 0, 0, 0, time.UTC)) if err != nil { t.Fatalf("SearchSince() error = %v", err) } want := []uint32{10, 11, 12} if len(uids) != len(want) { t.Fatalf("uids = %v, want %v", uids, want) } for i := range want { if uids[i] != want[i] { t.Errorf("uids[%d] = %d, want %d", i, uids[i], want[i]) } } if !strings.Contains(fc.w.String(), "UID SEARCH SINCE 02-Jan-2026") { t.Errorf("command sent = %q, want UID SEARCH SINCE 02-Jan-2026", fc.w.String()) } } func TestFetchParsesGmailIDs(t *testing.T) { script := "* 1 FETCH (UID 10 X-GM-MSGID 1234000000000000000 X-GM-THRID 999 BODY[] {5}\r\nHELLO)\r\n" + "A0001 OK FETCH completed\r\n" c, _ := newFakeClient(script) results, err := c.Fetch([]uint32{10}) if err != nil { t.Fatalf("Fetch() error = %v", err) } if len(results) != 1 { t.Fatalf("results = %d, want 1", len(results)) } if results[0].UID != 10 { t.Errorf("UID = %d, want 10", results[0].UID) } if results[0].GmailMsgID != "1234000000000000000" { t.Errorf("GmailMsgID = %q", results[0].GmailMsgID) } if results[0].GmailThreadID != "999" { t.Errorf("GmailThreadID = %q", results[0].GmailThreadID) } if string(results[0].Raw) != "HELLO" { t.Errorf("Raw = %q, want HELLO", results[0].Raw) } } func TestLoginFailureIsServerError(t *testing.T) { script := "* OK greeting\r\nA0001 NO [AUTHENTICATIONFAILED] Invalid credentials\r\n" c, _ := newFakeClient(script) err := c.Login("user@example.test", "wrong") if err == nil { t.Fatal("Login() error = nil, want failure") } var se *ServerError if !errors.As(err, &se) { t.Fatalf("Login() error = %T, want *ServerError", err) } if !strings.Contains(se.Response, "AUTHENTICATIONFAILED") { t.Errorf("ServerError.Response = %q", se.Response) } } func TestLiteralLen(t *testing.T) { cases := []struct { in string n int ok bool }{ {"* 1 FETCH (BODY[] {12}", 12, true}, {"* 1 FETCH (BODY[] {abc}", 0, false}, {"* 1 FETCH (BODY[])", 0, false}, {"* SEARCH 1 2", 0, false}, } for _, tc := range cases { n, ok := literalLen(tc.in) if n != tc.n || ok != tc.ok { t.Errorf("literalLen(%q) = (%d, %v), want (%d, %v)", tc.in, n, ok, tc.n, tc.ok) } } } func TestImapQuote(t *testing.T) { if got := imapQuote(`pa"ss\word`); got != `"pa\"ss\\word"` { t.Errorf("imapQuote() = %q", got) } } func TestUIDSet(t *testing.T) { if got := uidSet([]uint32{1, 2, 3}); got != "1,2,3" { t.Errorf("uidSet() = %q, want 1,2,3", got) } }