Add verified snapshots and harden archive identity

This commit is contained in:
DonVoo 2026-07-17 00:37:02 +02:00
parent c6de214ac8
commit 68b9cee880
9 changed files with 1372 additions and 52 deletions

View file

@ -3,8 +3,10 @@ package backend
import (
"bytes"
"database/sql"
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"time"
@ -12,6 +14,39 @@ import (
"github.com/klauspost/compress/zstd"
)
func TestReindexPlainMboxStreamingMatchesReferenceParser(t *testing.T) {
var mbox bytes.Buffer
for i := 0; i < 257; i++ {
body := strings.Repeat(fmt.Sprintf("line-%03d From inside body\r\n", i), i%31+1)
mbox.Write(mboxRecord(RawMessage{
MessageID: fmt.Sprintf("stream-%03d@example.com", i),
Body: []byte(fmt.Sprintf(
"Message-ID: <stream-%03d@example.com>\r\nSubject: Stream %03d\r\n\r\n%s",
i, i, body,
)),
}))
}
path := filepath.Join(t.TempDir(), "streaming.mbox")
if err := os.WriteFile(path, mbox.Bytes(), 0o600); err != nil {
t.Fatal(err)
}
got, err := reindexPlainMbox(path)
if err != nil {
t.Fatal(err)
}
parts := splitMboxRecordsWithOffsets(mbox.Bytes())
if len(got) != len(parts) {
t.Fatalf("streaming entries=%d, reference parts=%d", len(got), len(parts))
}
for i, part := range parts {
want := indexEntryFromMboxRecord(part.Message, part.Offset, int64(part.Length), 0, int64(part.Length))
if got[i] != want {
t.Fatalf("entry %d differs:\n got %#v\n want %#v", i, got[i], want)
}
}
}
func TestZstdMboxRoundTripAndIndexRead(t *testing.T) {
oldCfg, oldDB := Cfg, DB
root := t.TempDir()
@ -325,6 +360,14 @@ func TestConnectDBConfiguresBusyTimeoutAndCopyStages(t *testing.T) {
Cfg, DB = oldCfg, oldDB
})
Cfg = Config{DBPath: filepath.Join(root, "mail-graveyard.db"), MboxRoot: filepath.Join(root, "backup")}
backupDB := filepath.Join(root, "PRE-existing-copy.db")
if err := os.WriteFile(backupDB, []byte("diagnostic copy"), 0o644); err != nil {
t.Fatal(err)
}
nonDB := filepath.Join(root, "keep-mode.txt")
if err := os.WriteFile(nonDB, []byte("not a database"), 0o644); err != nil {
t.Fatal(err)
}
if err := ConnectDB(true); err != nil {
t.Fatal(err)
}
@ -335,6 +378,24 @@ func TestConnectDBConfiguresBusyTimeoutAndCopyStages(t *testing.T) {
if timeout != 10000 {
t.Fatalf("busy_timeout=%d, want 10000", timeout)
}
if runtime.GOOS != "windows" {
for _, path := range []string{Cfg.DBPath, Cfg.DBPath + "-wal", Cfg.DBPath + "-shm", backupDB} {
info, err := os.Stat(path)
if err != nil {
t.Fatal(err)
}
if permissions := info.Mode().Perm(); permissions != 0o600 {
t.Fatalf("SQLite file %s permissions=%#o, want 0600", path, permissions)
}
}
info, err := os.Stat(nonDB)
if err != nil {
t.Fatal(err)
}
if permissions := info.Mode().Perm(); permissions != 0o644 {
t.Fatalf("non-DB file permissions changed to %#o", permissions)
}
}
if err := SaveAccount(Account{
Name: "stage-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", Active: true,
@ -379,6 +440,106 @@ func TestConnectDBConfiguresBusyTimeoutAndCopyStages(t *testing.T) {
}
}
func TestReplaceMboxIndexPreservesStoredMessageIDOnlyForSameBody(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")}
if err := ConnectDB(true); err != nil {
t.Fatal(err)
}
if err := SaveAccount(Account{
Name: "identity-restore", 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", Active: true,
}); err != nil {
t.Fatal(err)
}
account, err := GetAccount("identity-restore")
if err != nil {
t.Fatal(err)
}
stableHash := bodySHA256([]byte("same archived body"))
if err := ReplaceMboxIndex(account.ID, "INBOX", []MboxIndexEntry{{
MessageID: "stored@example.com> (added by postmaster)", BodySHA256: stableHash, FrameLen: 10, InnerLen: 10,
}}, 10); err != nil {
t.Fatal(err)
}
if err := ReplaceMboxIndex(account.ID, "INBOX", []MboxIndexEntry{{
MessageID: "parsed@example.com", BodySHA256: stableHash, FrameLen: 10, InnerLen: 10,
}}, 10); err != nil {
t.Fatal(err)
}
entry, err := GetMboxIndex(account.ID, "INBOX", 0)
if err != nil {
t.Fatal(err)
}
if entry.MessageID != "stored@example.com> (added by postmaster)" {
t.Fatalf("same body lost stored Message-ID: %q", entry.MessageID)
}
changedHash := bodySHA256([]byte("different archived body"))
if err := ReplaceMboxIndex(account.ID, "INBOX", []MboxIndexEntry{{
MessageID: "new-content@example.com", BodySHA256: changedHash, FrameLen: 11, InnerLen: 11,
}}, 11); err != nil {
t.Fatal(err)
}
entry, err = GetMboxIndex(account.ID, "INBOX", 0)
if err != nil {
t.Fatal(err)
}
if entry.MessageID != "new-content@example.com" {
t.Fatalf("different body inherited stale Message-ID: %q", entry.MessageID)
}
}
func TestPreciseLegacyMessageIDAliasesPreventLiveRecopy(t *testing.T) {
account, _, restore := setupSnapshotTest(t, "none")
defer restore()
tests := []struct {
header string
legacy string
}{
{
header: `<476BDFDB019289BE@mail21.bluewin.ch> (added by postmaster@bluewin.ch)`,
legacy: `476BDFDB019289BE@mail21.bluewin.ch> (added by postmaster@bluewin.ch)`,
},
{
header: `<22578196653698419217.BB8A0E8F48F3F290@dr-gold.de>+D271B1409C3C6028`,
legacy: `22578196653698419217.BB8A0E8F48F3F290@dr-gold.de>+D271B1409C3C6028`,
},
}
for i, tt := range tests {
folder := fmt.Sprintf("INBOX.%d", i)
body := []byte("Message-ID: " + tt.header + "\r\nSubject: malformed ID\r\n\r\noriginal body")
identity := identityForRawMessage(RawMessage{MessageID: messageID(body), Body: body})
if err := MarkIdentityCopied(account.ID, folder, MessageIdentity{MessageID: tt.legacy, BodySHA256: identity.BodySHA256}); err != nil {
t.Fatal(err)
}
state, err := GetCopyIdentityState(account.ID, folder, identity)
if err != nil {
t.Fatal(err)
}
if !state.MboxDone || !state.TargetDone {
t.Fatalf("fixture %d precise legacy alias was not recognized: %#v", i, state)
}
changedBody := []byte("Message-ID: " + tt.header + "\r\nSubject: malformed ID\r\n\r\ndifferent body")
changedIdentity := identityForRawMessage(RawMessage{MessageID: messageID(changedBody), Body: changedBody})
state, err = GetCopyIdentityState(account.ID, folder, changedIdentity)
if err != nil {
t.Fatal(err)
}
if state.MboxDone || state.TargetDone {
t.Fatalf("fixture %d legacy alias hid byte-different mail: %#v", i, state)
}
}
}
func TestIdentitySchemaUpgradePreservesLegacyAliases(t *testing.T) {
oldCfg, oldDB := Cfg, DB
root := t.TempDir()