Mail-Graveyard/backend/06-mbox_test.go
2026-07-15 00:14:54 +02:00

217 lines
5.3 KiB
Go

package backend
import (
"bytes"
"os"
"path/filepath"
"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: <zstd-test@example.com>\r\n" +
"From: Sender <sender@example.com>\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 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 <sender@example.com>\r\n" +
"Subject: " + subject + "\r\n" +
"Date: Tue, 14 Jul 2026 10:11:12 +0000\r\n" +
"\r\n" +
"Hello\r\n"),
}
}