diff --git a/backend/00-router.go b/backend/00-router.go
index f6be991..15315e6 100644
--- a/backend/00-router.go
+++ b/backend/00-router.go
@@ -25,6 +25,8 @@ func RegisterRoutes(mux *http.ServeMux) {
mux.HandleFunc("/archives", archivesHandler)
mux.HandleFunc("/archives/create", archiveCreateHandler)
mux.HandleFunc("/archives/delete", archiveDeleteHandler)
+ mux.HandleFunc("/transfer", transferHandler)
+ mux.HandleFunc("/transfer/", transferHandler)
mux.HandleFunc("/migrate/run", migrateRunHandler) // TODO Codex: RunMigration im Hintergrund
mux.HandleFunc("/migrate/status", migrateStatusHandler) // TODO Codex: jobs-Fortschritt (HTMX-Poll)
@@ -55,15 +57,15 @@ func renderShell(w http.ResponseWriter, active, readingPane string) {
fmt.Fprintf(w, `
Mail-Graveyard
-
+
-
+
Mail-Graveyard
@@ -73,7 +75,10 @@ func renderShell(w http.ResponseWriter, active, readingPane string) {
Import/Export
Konten-Verwaltung
Archiv-mbox Verwaltung
- Mail-Transfer
+ Mail-Transfer
+ Postfach <--> Archiv-MBox
+ Archiv-MBox <--> Postfach
+ Postfach <--> Postfach
Einstellungen
@@ -186,6 +191,20 @@ func archivesHandler(w http.ResponseWriter, r *http.Request) {
renderArchivesPage(w, archiveMailboxes(accounts), accounts, r.URL.Query().Get("msg"), r.URL.Query().Get("err"))
}
+func transferHandler(w http.ResponseWriter, r *http.Request) {
+ mode := strings.TrimPrefix(r.URL.Path, "/transfer/")
+ if mode == "" || mode == "/transfer" {
+ http.Redirect(w, r, "/transfer/postfach-archiv", http.StatusSeeOther)
+ return
+ }
+ accounts, err := ListAccounts()
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ renderTransferPage(w, transferModeBySlug(mode), accounts, archiveMailboxes(accounts))
+}
+
func archiveCreateHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
@@ -293,15 +312,15 @@ func renderAccountsPage(w http.ResponseWriter, accounts []Account, edit Account,
fmt.Fprintf(w, `
Konten-Verwaltung - Mail-Graveyard
-
+
-
+
Mail-Graveyard
@@ -311,7 +330,10 @@ func renderAccountsPage(w http.ResponseWriter, accounts []Account, edit Account,
Import/Export
Konten-Verwaltung
Archiv-mbox Verwaltung
- Mail-Transfer
+ Mail-Transfer
+ Postfach <--> Archiv-MBox
+ Archiv-MBox <--> Postfach
+ Postfach <--> Postfach
Einstellungen
@@ -334,15 +356,15 @@ func renderArchivesPage(w http.ResponseWriter, archives []string, accounts []Acc
fmt.Fprintf(w, `
Archiv-mbox Verwaltung - Mail-Graveyard
-
+
-
+
Mail-Graveyard
@@ -352,7 +374,10 @@ func renderArchivesPage(w http.ResponseWriter, archives []string, accounts []Acc
Import/Export
Konten-Verwaltung
Archiv-mbox Verwaltung
- Mail-Transfer
+ Mail-Transfer
+ Postfach <--> Archiv-MBox
+ Archiv-MBox <--> Postfach
+ Postfach <--> Postfach
Einstellungen
@@ -370,6 +395,133 @@ func renderArchivesPage(w http.ResponseWriter, archives []string, accounts []Acc
fmt.Fprint(w, ``)
}
+type transferMode struct {
+ Slug string
+ Title string
+ LeftTitle string
+ RightTitle string
+ LeftKind string
+ RightKind string
+}
+
+func transferModeBySlug(slug string) transferMode {
+ modes := []transferMode{
+ {Slug: "postfach-archiv", Title: "Postfach <--> Archiv-MBox", LeftTitle: "Postfach", RightTitle: "Archiv-MBox", LeftKind: "source", RightKind: "archive"},
+ {Slug: "archiv-postfach", Title: "Archiv-MBox <--> Postfach", LeftTitle: "Archiv-MBox", RightTitle: "Postfach", LeftKind: "archive", RightKind: "target"},
+ {Slug: "postfach-postfach", Title: "Postfach <--> Postfach", LeftTitle: "Quell-Postfach", RightTitle: "Ziel-Postfach", LeftKind: "source", RightKind: "target"},
+ }
+ for _, mode := range modes {
+ if mode.Slug == slug {
+ return mode
+ }
+ }
+ return modes[0]
+}
+
+func renderTransferPage(w http.ResponseWriter, mode transferMode, accounts []Account, archives []string) {
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ fmt.Fprintf(w, `
+
+%s - Mail-Graveyard
+
+
+
+
+
+
+
+
+
+
+`,
+ html.EscapeString(mode.Title), transferMenuItems(mode.Slug),
+ html.EscapeString(mode.LeftTitle), renderTransferPicker(mode.LeftKind, accounts, archives),
+ html.EscapeString(mode.RightTitle), renderTransferPicker(mode.RightKind, accounts, archives))
+}
+
+func transferMenuItems(active string) string {
+ items := []transferMode{
+ {Slug: "postfach-archiv", Title: "Postfach <--> Archiv-MBox"},
+ {Slug: "archiv-postfach", Title: "Archiv-MBox <--> Postfach"},
+ {Slug: "postfach-postfach", Title: "Postfach <--> Postfach"},
+ }
+ var b strings.Builder
+ for _, item := range items {
+ class := "backstage-item transfer-sub"
+ if item.Slug == active {
+ class += " active"
+ }
+ fmt.Fprintf(&b, `%s`, class, item.Slug, html.EscapeString(item.Title))
+ }
+ return b.String()
+}
+
+func renderTransferPicker(kind string, accounts []Account, archives []string) string {
+ var b strings.Builder
+ b.WriteString(``)
+ switch kind {
+ case "archive":
+ fmt.Fprintf(&b, ``, transferSelect("archive", archives))
+ case "target":
+ fmt.Fprintf(&b, ``, transferSelect("target", uniqueAccountValues(accounts, "target")))
+ default:
+ fmt.Fprintf(&b, ``, transferSelect("source", uniqueAccountValues(accounts, "source")))
+ }
+ b.WriteString(`
`)
+ return b.String()
+}
+
+func transferSelect(name string, values []string) string {
+ if len(values) == 0 {
+ return ``
+ }
+ var b strings.Builder
+ fmt.Fprintf(&b, ``)
+ return b.String()
+}
+
+func uniqueAccountValues(accounts []Account, field string) []string {
+ seen := map[string]bool{}
+ var out []string
+ for _, a := range accounts {
+ value := a.SrcUser
+ if field == "target" {
+ value = a.DstUser
+ }
+ value = strings.TrimSpace(value)
+ if value == "" || seen[value] {
+ continue
+ }
+ seen[value] = true
+ out = append(out, value)
+ }
+ sortStrings(out)
+ return out
+}
+
func renderAccountsTable(w http.ResponseWriter, accounts []Account, editName string) {
if len(accounts) == 0 {
fmt.Fprint(w, `Noch keine Postfaecher eingetragen.
`)
diff --git a/frontend-js/dist/style.css b/frontend-js/dist/style.css
index d824577..18aa4d5 100644
--- a/frontend-js/dist/style.css
+++ b/frontend-js/dist/style.css
@@ -120,6 +120,10 @@ body.ol2013 {
color: var(--ol-blue);
border-left-color: #fff;
}
+.backstage-item.transfer-sub {
+ padding-left: 38px;
+ font-size: 12px;
+}
.backstage-shade {
position: fixed;
inset: 44px 0 22px 0;
@@ -351,18 +355,28 @@ a.tree-node { display: block; color: inherit; text-decoration: none; }
.archive-page {
background: #fff;
}
+.transfer-page {
+ background: #fff;
+}
.archive-page-layout {
display: grid;
grid-template-columns: minmax(520px, 1fr) minmax(360px, 460px);
min-height: 100%;
}
+.transfer-layout {
+ display: grid;
+ grid-template-columns: minmax(380px, 1fr) minmax(380px, 1fr);
+ min-height: 100%;
+}
.archive-list-panel,
-.archive-form-panel {
+.archive-form-panel,
+.transfer-panel {
min-width: 0;
border-right: 1px solid var(--ol-border);
background: #fff;
}
-.archive-form-panel {
+.archive-form-panel,
+.transfer-panel:last-child {
border-right: 0;
}
.account-table {
@@ -491,6 +505,10 @@ a.tree-node { display: block; color: inherit; text-decoration: none; }
color: var(--ol-blue-dark);
font-weight: 700;
}
+.transfer-picker {
+ padding: 14px;
+ border-bottom: 1px solid var(--ol-border);
+}
.muted { color: var(--ol-muted); }
/* Statusleiste */
@@ -527,6 +545,7 @@ a.tree-node { display: block; color: inherit; text-decoration: none; }
.searchbar { top: 30px; }
.account-layout,
.archive-page-layout,
+ .transfer-layout,
.form-columns,
.form-section {
grid-template-columns: 1fr;