Files
stalwart-migrator/internal/backup/vandelay.go
T
jcoffey-dev 719a945d64 Initial commit: stalwart-migrator design and scaffolding
In-place upgrade tool for Stalwart Mail Server (0.15.5 -> latest) with
checkpointed rollback and post-migration validation. Design stage; see
ARCHITECTURE.md.
2026-08-22 18:17:17 -07:00

68 lines
2.5 KiB
Go

package backup
import (
"context"
"fmt"
"os/exec"
"path/filepath"
)
// VandelayOptions configures a per-account content export via Stalwart's
// own Vandelay import/export tool - the documented, backend-independent
// backup mechanism (ARCHITECTURE.md §4.2's belt-and-suspenders layer): each
// account's mail, calendars, contacts, Sieve scripts, and identities land in
// one self-contained SQLite archive.
type VandelayOptions struct {
BinaryPath string // defaults to "vandelay"
URL string // the source JMAP server
AuthBasic string // "user:app-password", per vandelay's own --auth-basic flag
OutDir string // one <account>.sqlite file per account goes here
}
// BuildVandelayImportArgs returns vandelay's argv for exporting one
// account's content into a self-contained SQLite archive, without the
// leading "vandelay" itself. "import" is vandelay's own verb for this - it
// names the direction relative to the archive file, not the live server;
// see ARCHITECTURE.md §4.2.
func BuildVandelayImportArgs(o VandelayOptions, accountName, outFile string) []string {
return []string{
"import", "jmap",
"--url", o.URL,
"--auth-basic", o.AuthBasic,
"--account-name", accountName,
outFile,
}
}
// ExportAccount runs one account's Vandelay export and returns the archive
// path it wrote.
func ExportAccount(ctx context.Context, o VandelayOptions, accountName string) (outFile string, err error) {
binary := o.BinaryPath
if binary == "" {
binary = "vandelay"
}
outFile = filepath.Join(o.OutDir, accountName+".sqlite")
cmd := exec.CommandContext(ctx, binary, BuildVandelayImportArgs(o, accountName, outFile)...)
out, err := cmd.CombinedOutput()
if err != nil {
return "", fmt.Errorf("backup: vandelay export of %s failed: %w (output: %s)", accountName, err, out)
}
return outFile, nil
}
// ExportAccounts exports every account in accounts, stopping at the first
// failure. A partial per-account backup set is reported precisely (how many
// succeeded, which one failed and why) rather than silently continuing past
// a failure that might indicate a systemic problem - bad credentials, an
// unreachable server - rather than a one-off.
func ExportAccounts(ctx context.Context, o VandelayOptions, accounts []string) (outFiles []string, err error) {
for _, acct := range accounts {
f, err := ExportAccount(ctx, o, acct)
if err != nil {
return outFiles, fmt.Errorf("backup: exported %d/%d accounts before failing: %w", len(outFiles), len(accounts), err)
}
outFiles = append(outFiles, f)
}
return outFiles, nil
}