Add archive import export and dedup tools

This commit is contained in:
DonVoo 2026-07-13 02:39:09 +02:00
parent 8d2838aea7
commit d5576ddbde
4 changed files with 528 additions and 36 deletions

View file

@ -32,6 +32,10 @@ func RegisterRoutes(mux *http.ServeMux) {
mux.HandleFunc("/archives", archivesHandler)
mux.HandleFunc("/archives/create", archiveCreateHandler)
mux.HandleFunc("/archives/delete", archiveDeleteHandler)
mux.HandleFunc("/archives/dedup", archiveDedupHandler)
mux.HandleFunc("/import-export", importExportHandler)
mux.HandleFunc("/import-export/upload", archiveUploadHandler)
mux.HandleFunc("/import-export/download", archiveDownloadHandler)
mux.HandleFunc("/transfer", transferHandler)
mux.HandleFunc("/transfer/", transferHandler)
mux.HandleFunc("/boxes/source", emailBoxHandler)
@ -59,8 +63,8 @@ func renderStartPage(w http.ResponseWriter, r *http.Request) {
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-21">
<script src="/static/app.js?v=20260712-21" defer></script></head>
<link rel="stylesheet" href="/static/style.css?v=20260712-22">
<script src="/static/app.js?v=20260712-22" 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>
@ -73,7 +77,7 @@ func renderStartPage(w http.ResponseWriter, r *http.Request) {
<aside class="backstage-menu" aria-label="Mail-Graveyard-Menue">
<a class="backstage-item active" href="/">Start</a>
<a class="backstage-item" href="/view">Archiv-Boxen</a>
<a class="backstage-item" href="/view">Import/Export</a>
<a class="backstage-item" href="/import-export">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="/transfer/postfach-archiv">Mail-Transfer</a>
@ -107,9 +111,9 @@ func renderShell(w http.ResponseWriter, r *http.Request, active, readingPane str
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-21">
<link rel="stylesheet" href="/static/style.css?v=20260712-22">
<script src="/static/htmx.min.js"></script>
<script src="/static/app.js?v=20260712-21" defer></script></head>
<script src="/static/app.js?v=20260712-22" 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>
@ -121,7 +125,7 @@ func renderShell(w http.ResponseWriter, r *http.Request, active, readingPane str
</header>
<aside class="backstage-menu" aria-label="Mail-Graveyard-Menue">
<a class="backstage-item active" href="/view">Archiv-Boxen</a>
<a class="backstage-item" href="/view">Import/Export</a>
<a class="backstage-item" href="/import-export">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="/transfer/postfach-archiv">Mail-Transfer</a>
@ -263,7 +267,7 @@ func archivesHandler(w http.ResponseWriter, r *http.Request) {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
renderArchivesPage(w, r, archives, accounts, r.URL.Query().Get("msg"), r.URL.Query().Get("err"))
renderArchivesPage(w, r, archives, accounts, archiveStatsMap(archives), r.URL.Query().Get("msg"), r.URL.Query().Get("err"))
}
func transferHandler(w http.ResponseWriter, r *http.Request) {
@ -578,9 +582,9 @@ func renderAccountsPage(w http.ResponseWriter, r *http.Request, accounts []Accou
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-21">
<link rel="stylesheet" href="/static/style.css?v=20260712-22">
<script src="/static/htmx.min.js"></script>
<script src="/static/app.js?v=20260712-21" defer></script></head>
<script src="/static/app.js?v=20260712-22" 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>
@ -592,7 +596,7 @@ func renderAccountsPage(w http.ResponseWriter, r *http.Request, accounts []Accou
</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="/import-export">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="/transfer/postfach-archiv">Mail-Transfer</a>
@ -617,15 +621,15 @@ func renderAccountsPage(w http.ResponseWriter, r *http.Request, accounts []Accou
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>`)
}
func renderArchivesPage(w http.ResponseWriter, r *http.Request, archives []string, accounts []Account, msg, errMsg string) {
func renderArchivesPage(w http.ResponseWriter, r *http.Request, archives []string, accounts []Account, stats map[string]ArchiveStats, msg, errMsg string) {
user := CurrentUser(r)
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>Archiv-mbox Verwaltung - Mail-Graveyard</title>
<link rel="stylesheet" href="/static/style.css?v=20260712-21">
<link rel="stylesheet" href="/static/style.css?v=20260712-22">
<script src="/static/htmx.min.js"></script>
<script src="/static/app.js?v=20260712-21" defer></script></head>
<script src="/static/app.js?v=20260712-22" 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>
@ -637,7 +641,7 @@ func renderArchivesPage(w http.ResponseWriter, r *http.Request, archives []strin
</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="/import-export">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="/transfer/postfach-archiv">Mail-Transfer</a>
@ -656,7 +660,7 @@ func renderArchivesPage(w http.ResponseWriter, r *http.Request, archives []strin
fmt.Fprintf(w, `<div class="notice bad">%s</div>`, html.EscapeString(errMsg))
}
fmt.Fprint(w, `<section class="archive-page-layout"><div class="archive-list-panel"><div class="pane-head">Archiv-mboxen</div>`)
renderArchiveList(w, archives, accounts)
renderArchiveList(w, archives, accounts, stats)
fmt.Fprint(w, `</div><div class="archive-form-panel"><div class="pane-head">Archiv-mbox anlegen</div>`)
renderArchiveForm(w)
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>`)
@ -670,9 +674,9 @@ func renderUsersPage(w http.ResponseWriter, r *http.Request, users []AppUser, ed
fmt.Fprintf(w, `<!doctype html><html lang="de"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Benutzerverwaltung - Mail-Graveyard</title>
<link rel="stylesheet" href="/static/style.css?v=20260712-21">
<link rel="stylesheet" href="/static/style.css?v=20260712-22">
<script src="/static/htmx.min.js"></script>
<script src="/static/app.js?v=20260712-21" defer></script></head>
<script src="/static/app.js?v=20260712-22" 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>
@ -684,7 +688,7 @@ func renderUsersPage(w http.ResponseWriter, r *http.Request, users []AppUser, ed
</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="/import-export">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="/transfer/postfach-archiv">Mail-Transfer</a>
@ -740,9 +744,9 @@ func renderTransferPage(w http.ResponseWriter, r *http.Request, mode transferMod
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-21">
<link rel="stylesheet" href="/static/style.css?v=20260712-22">
<script src="/static/htmx.min.js"></script>
<script src="/static/app.js?v=20260712-21" defer></script></head>
<script src="/static/app.js?v=20260712-22" 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>
@ -754,7 +758,7 @@ func renderTransferPage(w http.ResponseWriter, r *http.Request, mode transferMod
</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="/import-export">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>
@ -781,9 +785,9 @@ func renderSourceEmailBoxPage(w http.ResponseWriter, r *http.Request, accounts [
fmt.Fprintf(w, `<!doctype html><html lang="de"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Quell-Postfach - Mail-Graveyard</title>
<link rel="stylesheet" href="/static/style.css?v=20260712-21">
<link rel="stylesheet" href="/static/style.css?v=20260712-22">
<script src="/static/htmx.min.js"></script>
<script src="/static/app.js?v=20260712-21" defer></script></head>
<script src="/static/app.js?v=20260712-22" 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>
@ -795,7 +799,7 @@ func renderSourceEmailBoxPage(w http.ResponseWriter, r *http.Request, accounts [
</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="/import-export">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="/transfer/postfach-archiv">Mail-Transfer</a>
@ -823,9 +827,9 @@ func renderTargetEmailBoxPage(w http.ResponseWriter, r *http.Request, accounts [
fmt.Fprintf(w, `<!doctype html><html lang="de"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Ziel-Postfach - Mail-Graveyard</title>
<link rel="stylesheet" href="/static/style.css?v=20260712-21">
<link rel="stylesheet" href="/static/style.css?v=20260712-22">
<script src="/static/htmx.min.js"></script>
<script src="/static/app.js?v=20260712-21" defer></script></head>
<script src="/static/app.js?v=20260712-22" 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>
@ -837,7 +841,7 @@ func renderTargetEmailBoxPage(w http.ResponseWriter, r *http.Request, accounts [
</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="/import-export">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="/transfer/postfach-archiv">Mail-Transfer</a>
@ -1011,6 +1015,37 @@ func renderRibbonTransferMenu(active string) string {
return b.String()
}
func renderBackstage(active string) string {
items := []struct {
Key string
Href string
Label string
}{
{"home", "/", "Start"},
{"archive", "/view", "Archiv-Boxen"},
{"import-export", "/import-export", "Import/Export"},
{"accounts", "/accounts", "Konten-Verwaltung"},
{"archives", "/archives", "Archiv-mbox Verwaltung"},
{"transfer", "/transfer/postfach-archiv", "Mail-Transfer"},
{"users", "/users", "Benutzerverwaltung"},
{"logout", "/logout", "Logout"},
}
var b strings.Builder
b.WriteString(`<aside class="backstage-menu" aria-label="Mail-Graveyard-Menue">`)
for _, item := range items {
class := "backstage-item"
if item.Key == active {
class += " active"
}
fmt.Fprintf(&b, `<a class="%s" href="%s">%s</a>`, class, item.Href, html.EscapeString(item.Label))
if item.Key == "transfer" {
b.WriteString(transferMenuItems(""))
}
}
b.WriteString(`</aside><button class="backstage-shade" type="button" aria-label="Menue schliessen" data-backstage-close></button>`)
return b.String()
}
func renderTransferPicker(kind string, accounts []Account, archives []string) string {
var b strings.Builder
b.WriteString(`<div class="transfer-picker">`)
@ -1185,20 +1220,26 @@ func passwordHint(username string) string {
return "Bearbeiten: Neues Passwort eintragen oder leer lassen, um das bestehende Passwort zu behalten."
}
func renderArchiveList(w http.ResponseWriter, archives []string, accounts []Account) {
func renderArchiveList(w http.ResponseWriter, archives []string, accounts []Account, stats map[string]ArchiveStats) {
if len(archives) == 0 {
fmt.Fprint(w, `<div class="ef-empty">Noch keine Archiv-Mailbox angelegt.</div>`)
return
}
fmt.Fprint(w, `<table class="archive-table"><thead><tr><th>Archiv-Mailbox</th><th>Zuordnung</th><th></th></tr></thead><tbody>`)
fmt.Fprint(w, `<table class="archive-table"><thead><tr><th>Archiv-Mailbox</th><th>Zuordnung</th><th>Speicher</th><th>Mails</th><th>Dubletten</th><th></th></tr></thead><tbody>`)
for _, archive := range archives {
uses := archiveUses(archive, accounts)
action := `<span class="muted">belegt</span>`
stat := stats[archive]
var actions strings.Builder
fmt.Fprintf(&actions, `<a class="btn compact secondary" href="/import-export/download?archive_name=%s">Download</a>`, urlQuery(archive))
if stat.Duplicates > 0 {
fmt.Fprintf(&actions, `<form method="post" action="/archives/dedup"><input type="hidden" name="archive_name" value="%s"><button class="btn compact secondary" type="submit">Dedup</button></form>`, html.EscapeString(archive))
}
if uses == "" {
action = fmt.Sprintf(`<form method="post" action="/archives/delete"><input type="hidden" name="archive_name" value="%s"><button class="btn compact secondary danger" type="submit">Entfernen</button></form>`, html.EscapeString(archive))
fmt.Fprintf(&actions, `<form method="post" action="/archives/delete"><input type="hidden" name="archive_name" value="%s"><button class="btn compact secondary danger" type="submit">Entfernen</button></form>`, html.EscapeString(archive))
uses = "frei"
}
fmt.Fprintf(w, `<tr><td>%s</td><td>%s</td><td class="archive-action">%s</td></tr>`, html.EscapeString(archive), html.EscapeString(uses), action)
fmt.Fprintf(w, `<tr><td>%s</td><td>%s</td><td>%s</td><td>%d</td><td>%d</td><td class="archive-action">%s</td></tr>`,
html.EscapeString(archive), html.EscapeString(uses), html.EscapeString(formatBytes(stat.Bytes)), stat.Messages, stat.Duplicates, actions.String())
}
fmt.Fprint(w, `</tbody></table>`)
}