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.
This commit is contained in:
2026-08-22 18:17:17 -07:00
commit 719a945d64
71 changed files with 6677 additions and 0 deletions
+101
View File
@@ -0,0 +1,101 @@
package validate
import (
"encoding/json"
"fmt"
"net"
"net/http"
"os"
"os/signal"
"strconv"
"strings"
"syscall"
"testing"
)
// TestMain lets this test binary also act as a fake Stalwart binary,
// mirroring internal/recovery's own TestMain - see that package's doc
// comment for why (the standard os/exec "helper process" technique). Beyond
// plain reachability, it also speaks just enough JMAP to serve
// stalwartapi.Client.AccountSnapshot (x:Account/query, x:Account/get,
// session discovery, Mailbox/get) for a single fixed fake account
// "[email protected]", so BootCheck's content-integrity comparison can be
// exercised against a real subprocess rather than mocked in-process. The
// mailbox message count it reports is configurable via
// STALWART_MIGRATOR_TEST_MAILBOX_COUNT (default 42), so tests can produce
// both a matching and a mismatching post-migration snapshot.
func TestMain(m *testing.M) {
if os.Getenv("STALWART_MIGRATOR_TEST_HELPER") == "1" {
runFakeStalwartServer()
return
}
os.Exit(m.Run())
}
func runFakeStalwartServer() {
port := os.Getenv("STALWART_MIGRATOR_TEST_PORT")
messageCount := 42
if v := os.Getenv("STALWART_MIGRATOR_TEST_MAILBOX_COUNT"); v != "" {
if n, err := strconv.Atoi(v); err == nil {
messageCount = n
}
}
ln, err := net.Listen("tcp", "127.0.0.1:"+port)
if err != nil {
fmt.Fprintln(os.Stderr, "fake stalwart: listen:", err)
os.Exit(1)
}
srv := &http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodGet && r.URL.Path == "/.well-known/jmap":
user, _, _ := r.BasicAuth()
if strings.Contains(user, "%") {
json.NewEncoder(w).Encode(map[string]any{
"apiUrl": "http://127.0.0.1:" + port + "/api",
"primaryAccounts": map[string]string{"urn:ietf:params:jmap:mail": "mail-alice"},
})
return
}
w.WriteHeader(http.StatusOK)
case r.Method == http.MethodPost && r.URL.Path == "/api":
var body map[string]any
json.NewDecoder(r.Body).Decode(&body)
methodCalls, _ := body["methodCalls"].([]any)
if len(methodCalls) == 0 {
w.WriteHeader(http.StatusBadRequest)
return
}
call := methodCalls[0].([]any)
name := call[0].(string)
switch name {
case "x:Account/query":
json.NewEncoder(w).Encode(map[string]any{"methodResponses": []any{
[]any{"x:Account/query", map[string]any{"ids": []string{"a1"}}, "q"},
}})
case "x:Account/get":
json.NewEncoder(w).Encode(map[string]any{"methodResponses": []any{
[]any{"x:Account/get", map[string]any{"list": []map[string]any{
{"id": "a1", "name": "[email protected]", "domainId": "example.com"},
}}, "g"},
}})
case "Mailbox/get":
json.NewEncoder(w).Encode(map[string]any{"methodResponses": []any{
[]any{"Mailbox/get", map[string]any{"list": []map[string]any{
{"name": "Inbox", "totalEmails": messageCount},
}}, "m"},
}})
default:
w.WriteHeader(http.StatusBadRequest)
}
default:
w.WriteHeader(http.StatusOK)
}
})}
go srv.Serve(ln)
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGTERM)
<-sigCh
os.Exit(0)
}