Add mail transfer submenu modes

This commit is contained in:
DonVoo 2026-07-12 23:29:09 +02:00
parent 6f7995e3f6
commit 6675e73b0f
2 changed files with 185 additions and 14 deletions

View file

@ -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, `<!doctype html><html lang="de"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Mail-Graveyard</title>
<link rel="stylesheet" href="/static/style.css?v=20260712-10">
<link rel="stylesheet" href="/static/style.css?v=20260712-11">
<script src="/static/htmx.min.js"></script>
<script src="/static/app.js?v=20260712-10" defer></script></head>
<script src="/static/app.js?v=20260712-11" defer></script></head>
<body class="ol2013">
<header class="ribbon">
<button class="app-menu-button" type="button" aria-label="Mail-Graveyard-Menue" aria-expanded="false" data-backstage-toggle><span class="mail-logo" aria-hidden="true"></span></button>
<nav class="ribbon-tabs">
<a class="tab %s" href="/view">Archiv-Boxen</a>
<a class="tab" href="/view">Mail-Transfer</a>
<a class="tab" href="/transfer/postfach-archiv">Mail-Transfer</a>
<a class="tab" href="/accounts">Einstellungen</a>
</nav>
<div class="ribbon-title">Mail-Graveyard</div>
@ -73,7 +75,10 @@ func renderShell(w http.ResponseWriter, active, readingPane string) {
<a class="backstage-item" href="/view">Import/Export</a>
<a class="backstage-item" href="/accounts">Konten-Verwaltung</a>
<a class="backstage-item" href="/archives">Archiv-mbox Verwaltung</a>
<a class="backstage-item" href="/view">Mail-Transfer</a>
<a class="backstage-item" href="/transfer/postfach-archiv">Mail-Transfer</a>
<a class="backstage-item transfer-sub" href="/transfer/postfach-archiv">Postfach &lt;--&gt; Archiv-MBox</a>
<a class="backstage-item transfer-sub" href="/transfer/archiv-postfach">Archiv-MBox &lt;--&gt; Postfach</a>
<a class="backstage-item transfer-sub" href="/transfer/postfach-postfach">Postfach &lt;--&gt; Postfach</a>
<a class="backstage-item" href="/accounts">Einstellungen</a>
</aside>
<button class="backstage-shade" type="button" aria-label="Menue schliessen" data-backstage-close></button>
@ -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, `<!doctype html><html lang="de"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Konten-Verwaltung - Mail-Graveyard</title>
<link rel="stylesheet" href="/static/style.css?v=20260712-10">
<link rel="stylesheet" href="/static/style.css?v=20260712-11">
<script src="/static/htmx.min.js"></script>
<script src="/static/app.js?v=20260712-10" defer></script></head>
<script src="/static/app.js?v=20260712-11" defer></script></head>
<body class="ol2013">
<header class="ribbon">
<button class="app-menu-button" type="button" aria-label="Mail-Graveyard-Menue" aria-expanded="false" data-backstage-toggle><span class="mail-logo" aria-hidden="true"></span></button>
<nav class="ribbon-tabs">
<a class="tab" href="/view">Archiv-Boxen</a>
<a class="tab" href="/view">Mail-Transfer</a>
<a class="tab" href="/transfer/postfach-archiv">Mail-Transfer</a>
<a class="tab" href="/accounts">Einstellungen</a>
</nav>
<div class="ribbon-title">Mail-Graveyard</div>
@ -311,7 +330,10 @@ func renderAccountsPage(w http.ResponseWriter, accounts []Account, edit Account,
<a class="backstage-item" href="/view">Import/Export</a>
<a class="backstage-item active" href="/accounts">Konten-Verwaltung</a>
<a class="backstage-item" href="/archives">Archiv-mbox Verwaltung</a>
<a class="backstage-item" href="/view">Mail-Transfer</a>
<a class="backstage-item" href="/transfer/postfach-archiv">Mail-Transfer</a>
<a class="backstage-item transfer-sub" href="/transfer/postfach-archiv">Postfach &lt;--&gt; Archiv-MBox</a>
<a class="backstage-item transfer-sub" href="/transfer/archiv-postfach">Archiv-MBox &lt;--&gt; Postfach</a>
<a class="backstage-item transfer-sub" href="/transfer/postfach-postfach">Postfach &lt;--&gt; Postfach</a>
<a class="backstage-item" href="/accounts">Einstellungen</a>
</aside>
<button class="backstage-shade" type="button" aria-label="Menue schliessen" data-backstage-close></button>
@ -334,15 +356,15 @@ func renderArchivesPage(w http.ResponseWriter, archives []string, accounts []Acc
fmt.Fprintf(w, `<!doctype html><html lang="de"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Archiv-mbox Verwaltung - Mail-Graveyard</title>
<link rel="stylesheet" href="/static/style.css?v=20260712-10">
<link rel="stylesheet" href="/static/style.css?v=20260712-11">
<script src="/static/htmx.min.js"></script>
<script src="/static/app.js?v=20260712-10" defer></script></head>
<script src="/static/app.js?v=20260712-11" defer></script></head>
<body class="ol2013">
<header class="ribbon">
<button class="app-menu-button" type="button" aria-label="Mail-Graveyard-Menue" aria-expanded="false" data-backstage-toggle><span class="mail-logo" aria-hidden="true"></span></button>
<nav class="ribbon-tabs">
<a class="tab" href="/view">Archiv-Boxen</a>
<a class="tab" href="/view">Mail-Transfer</a>
<a class="tab" href="/transfer/postfach-archiv">Mail-Transfer</a>
<a class="tab" href="/accounts">Einstellungen</a>
</nav>
<div class="ribbon-title">Mail-Graveyard</div>
@ -352,7 +374,10 @@ func renderArchivesPage(w http.ResponseWriter, archives []string, accounts []Acc
<a class="backstage-item" href="/view">Import/Export</a>
<a class="backstage-item" href="/accounts">Konten-Verwaltung</a>
<a class="backstage-item active" href="/archives">Archiv-mbox Verwaltung</a>
<a class="backstage-item" href="/view">Mail-Transfer</a>
<a class="backstage-item" href="/transfer/postfach-archiv">Mail-Transfer</a>
<a class="backstage-item transfer-sub" href="/transfer/postfach-archiv">Postfach &lt;--&gt; Archiv-MBox</a>
<a class="backstage-item transfer-sub" href="/transfer/archiv-postfach">Archiv-MBox &lt;--&gt; Postfach</a>
<a class="backstage-item transfer-sub" href="/transfer/postfach-postfach">Postfach &lt;--&gt; Postfach</a>
<a class="backstage-item" href="/accounts">Einstellungen</a>
</aside>
<button class="backstage-shade" type="button" aria-label="Menue schliessen" data-backstage-close></button>
@ -370,6 +395,133 @@ func renderArchivesPage(w http.ResponseWriter, archives []string, accounts []Acc
fmt.Fprint(w, `</div></section></main><footer class="statusbar"><span id="mig-status" hx-get="/migrate/status" hx-trigger="load,every 3s" hx-swap="innerHTML">bereit</span></footer></body></html>`)
}
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, `<!doctype html><html lang="de"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>%s - Mail-Graveyard</title>
<link rel="stylesheet" href="/static/style.css?v=20260712-11">
<script src="/static/htmx.min.js"></script>
<script src="/static/app.js?v=20260712-11" defer></script></head>
<body class="ol2013">
<header class="ribbon">
<button class="app-menu-button" type="button" aria-label="Mail-Graveyard-Menue" aria-expanded="false" data-backstage-toggle><span class="mail-logo" aria-hidden="true"></span></button>
<nav class="ribbon-tabs">
<a class="tab" href="/view">Archiv-Boxen</a>
<a class="tab active" href="/transfer/postfach-archiv">Mail-Transfer</a>
<a class="tab" href="/accounts">Einstellungen</a>
</nav>
<div class="ribbon-title">Mail-Graveyard</div>
</header>
<aside class="backstage-menu" aria-label="Mail-Graveyard-Menue">
<a class="backstage-item" href="/view">Archiv-Boxen</a>
<a class="backstage-item" href="/view">Import/Export</a>
<a class="backstage-item" href="/accounts">Konten-Verwaltung</a>
<a class="backstage-item" href="/archives">Archiv-mbox Verwaltung</a>
<a class="backstage-item active" href="/transfer/postfach-archiv">Mail-Transfer</a>
%s
<a class="backstage-item" href="/accounts">Einstellungen</a>
</aside>
<button class="backstage-shade" type="button" aria-label="Menue schliessen" data-backstage-close></button>
<main class="accounts-page transfer-page">
<section class="transfer-layout">
<div class="transfer-panel"><div class="pane-head">%s</div>%s</div>
<div class="transfer-panel"><div class="pane-head">%s</div>%s</div>
</section>
</main>
<footer class="statusbar"><span id="mig-status" hx-get="/migrate/status" hx-trigger="load,every 3s" hx-swap="innerHTML">bereit</span></footer></body></html>`,
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, `<a class="%s" href="/transfer/%s">%s</a>`, 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(`<div class="transfer-picker">`)
switch kind {
case "archive":
fmt.Fprintf(&b, `<label class="label">Archiv-MBox%s</label>`, transferSelect("archive", archives))
case "target":
fmt.Fprintf(&b, `<label class="label">Ziel-Postfach%s</label>`, transferSelect("target", uniqueAccountValues(accounts, "target")))
default:
fmt.Fprintf(&b, `<label class="label">Quell-Postfach%s</label>`, transferSelect("source", uniqueAccountValues(accounts, "source")))
}
b.WriteString(`</div>`)
return b.String()
}
func transferSelect(name string, values []string) string {
if len(values) == 0 {
return `<select disabled><option>Keine Eintraege</option></select>`
}
var b strings.Builder
fmt.Fprintf(&b, `<select name="%s">`, name)
for _, v := range values {
fmt.Fprintf(&b, `<option value="%s">%s</option>`, html.EscapeString(v), html.EscapeString(v))
}
b.WriteString(`</select>`)
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, `<div class="ef-empty">Noch keine Postfaecher eingetragen.</div>`)