Add verified snapshots and harden archive identity
This commit is contained in:
parent
c6de214ac8
commit
68b9cee880
9 changed files with 1372 additions and 52 deletions
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue