254 lines
6.3 KiB
Go
254 lines
6.3 KiB
Go
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: <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 TestReadMboxMessagesBytesClonesBufferRecords(t *testing.T) {
|
|
mbox := []byte(strings.Join([]string{
|
|
"From one@example.com Tue Jul 14 10:00:00 2026",
|
|
"Message-ID: <one@example.com>",
|
|
"Subject: One",
|
|
"",
|
|
"short",
|
|
"From two@example.com Tue Jul 14 10:01:00 2026",
|
|
"Message-ID: <two@example.com>",
|
|
"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: <three@example.com>",
|
|
"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 <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"),
|
|
}
|
|
}
|