package backend import ( "bytes" "os" "path/filepath" "strings" "testing" "time" "github.com/klauspost/compress/zstd" ) func TestZstdMboxRoundTripAndIndexRead(t *testing.T) { oldCfg, oldDB := Cfg, DB root := t.TempDir() t.Cleanup(func() { if DB != nil { _ = DB.Close() } Cfg, DB = oldCfg, oldDB }) Cfg = Config{ DBPath: filepath.Join(root, "mail-graveyard.db"), MboxRoot: filepath.Join(root, "backup"), MboxCompression: "zstd", } if err := ConnectDB(true); err != nil { t.Fatal(err) } if err := SaveArchiveMailbox("archive"); err != nil { t.Fatal(err) } if err := SaveAccount(Account{ Name: "test-account", SrcHost: "source.example", SrcPort: 993, SrcSecurity: "tls", SrcUser: "source@example.com", SrcPass: "x", DstHost: "target.example", DstPort: 993, DstSecurity: "tls", DstUser: "target@example.com", DstPass: "x", MboxDir: "archive", Active: true, }); err != nil { t.Fatal(err) } account, err := GetAccount("test-account") if err != nil { t.Fatal(err) } msg := RawMessage{ MessageID: "zstd-test@example.com", InternalDate: time.Date(2026, 7, 14, 10, 11, 12, 0, time.UTC), Body: []byte("Message-ID: \r\n" + "From: Sender \r\n" + "Subject: Zstd Test\r\n" + "Date: Tue, 14 Jul 2026 10:11:12 +0000\r\n" + "\r\n" + "Hello\r\nFrom inside body\r\n"), } writer, err := NewMboxWriter(filepath.Join(Cfg.MboxRoot, "archive")) if err != nil { t.Fatal(err) } info, err := writer.Append("INBOX", msg) if err != nil { t.Fatal(err) } if err := SaveMboxIndex(MboxIndexEntry{ AccountID: account.ID, Folder: "INBOX", MessageID: msg.MessageID, Subject: info.Subject, From: info.From, Date: info.Date, FileOffset: info.FileOffset, FrameLen: info.FrameLen, InnerOffset: info.InnerOffset, InnerLen: info.InnerLen, }); err != nil { t.Fatal(err) } compressed, err := os.ReadFile(info.Path) if err != nil { t.Fatal(err) } dec, err := zstd.NewReader(nil) if err != nil { t.Fatal(err) } defer dec.Close() plain, err := dec.DecodeAll(compressed, nil) if err != nil { t.Fatal(err) } if want := mboxRecord(msg); !bytes.Equal(plain, want) { t.Fatalf("zstd round-trip differs: got %d bytes, want %d", len(plain), len(want)) } entries, err := ReadMboxList(info.Path) if err != nil { t.Fatal(err) } if len(entries) != 1 || entries[0].Index != 0 || entries[0].Subject != "Zstd Test" { t.Fatalf("unexpected index list: %#v", entries) } raw, err := ReadMboxMessage(info.Path, entries[0].Index) if err != nil { t.Fatal(err) } if !bytes.Contains(raw, []byte("Hello")) || bytes.Contains(raw, []byte(">From inside body")) { t.Fatalf("unexpected message body: %q", raw) } } func TestPlainMboxPartialIndexIsRebuiltBeforeList(t *testing.T) { oldCfg, oldDB := Cfg, DB root := t.TempDir() t.Cleanup(func() { if DB != nil { _ = DB.Close() } Cfg, DB = oldCfg, oldDB }) Cfg = Config{ DBPath: filepath.Join(root, "mail-graveyard.db"), MboxRoot: filepath.Join(root, "backup"), MboxCompression: "none", } if err := ConnectDB(true); err != nil { t.Fatal(err) } if err := SaveArchiveMailbox("archive"); err != nil { t.Fatal(err) } if err := SaveAccount(Account{ Name: "test-account", SrcHost: "source.example", SrcPort: 993, SrcSecurity: "tls", SrcUser: "source@example.com", SrcPass: "x", DstHost: "target.example", DstPort: 993, DstSecurity: "tls", DstUser: "target@example.com", DstPass: "x", MboxDir: "archive", Active: true, }); err != nil { t.Fatal(err) } account, err := GetAccount("test-account") if err != nil { t.Fatal(err) } writer, err := NewMboxWriter(filepath.Join(Cfg.MboxRoot, "archive")) if err != nil { t.Fatal(err) } first := testRawMessage("first@example.com", "First") firstInfo, err := writer.Append("INBOX", first) if err != nil { t.Fatal(err) } if err := SaveMboxIndex(MboxIndexEntry{ AccountID: account.ID, Folder: "INBOX", MessageID: first.MessageID, Subject: firstInfo.Subject, From: firstInfo.From, Date: firstInfo.Date, FileOffset: firstInfo.FileOffset, FrameLen: firstInfo.FrameLen, InnerOffset: firstInfo.InnerOffset, InnerLen: firstInfo.InnerLen, }); err != nil { t.Fatal(err) } if err := UpdateMboxIndexState(account.ID, "INBOX", firstInfo.FileOffset+firstInfo.FrameLen); err != nil { t.Fatal(err) } if _, err := writer.Append("INBOX", testRawMessage("second@example.com", "Second")); err != nil { t.Fatal(err) } entries, err := ReadMboxList(firstInfo.Path) if err != nil { t.Fatal(err) } if len(entries) != 2 { t.Fatalf("expected rebuilt full list with 2 entries, got %#v", entries) } if entries[0].Subject != "Second" || entries[1].Subject != "First" { t.Fatalf("unexpected entries after reindex: %#v", entries) } } func TestReadMboxMessagesBytesClonesBufferRecords(t *testing.T) { mbox := []byte(strings.Join([]string{ "From one@example.com Tue Jul 14 10:00:00 2026", "Message-ID: ", "Subject: One", "", "short", "From two@example.com Tue Jul 14 10:01:00 2026", "Message-ID: ", "Subject: Two", "", "this message is deliberately much longer than the first one", "and has another line", "From three@example.com Tue Jul 14 10:02:00 2026", "Message-ID: ", "Subject: Three", "", "tiny", "", }, "\n")) msgs := readMboxMessagesBytes(mbox) if len(msgs) != 3 { t.Fatalf("expected 3 messages, got %d", len(msgs)) } wants := []string{"Subject: One", "Subject: Two", "Subject: Three"} for i, want := range wants { if !bytes.Contains(msgs[i], []byte(want)) { t.Fatalf("message %d does not contain %q: %q", i, want, msgs[i]) } } if bytes.Contains(msgs[0], []byte("Subject: Two")) || bytes.Contains(msgs[1], []byte("Subject: Three")) { t.Fatalf("messages share buffer contents: %#q", msgs) } } func testRawMessage(id, subject string) RawMessage { return RawMessage{ MessageID: id, InternalDate: time.Date(2026, 7, 14, 10, 11, 12, 0, time.UTC), Body: []byte("Message-ID: <" + id + ">\r\n" + "From: Sender \r\n" + "Subject: " + subject + "\r\n" + "Date: Tue, 14 Jul 2026 10:11:12 +0000\r\n" + "\r\n" + "Hello\r\n"), } }