2403 lines
92 KiB
Go
2403 lines
92 KiB
Go
package backend
|
|
|
|
import (
|
|
"bytes"
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
"html"
|
|
"io"
|
|
"net/http"
|
|
"net/mail"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
func RegisterRoutes(mux *http.ServeMux) {
|
|
mux.HandleFunc("/", homeHandler)
|
|
mux.HandleFunc("/login", loginHandler)
|
|
mux.HandleFunc("/logout", logoutHandler)
|
|
mux.HandleFunc("/password/forgot", forgotPasswordHandler)
|
|
mux.HandleFunc("/password/reset", resetPasswordHandler)
|
|
mux.HandleFunc("/users", usersHandler)
|
|
mux.HandleFunc("/users/save", userSaveHandler)
|
|
mux.HandleFunc("/users/toggle-active", userToggleActiveHandler)
|
|
mux.HandleFunc("/users/delete", userDeleteHandler)
|
|
|
|
// Umzug (Konten pflegen + starten/beobachten)
|
|
mux.HandleFunc("/accounts", accountsHandler) // TODO Codex (02/07)
|
|
mux.HandleFunc("/accounts/save", accountSaveHandler) // TODO Codex
|
|
mux.HandleFunc("/accounts/delete", accountDeleteHandler) // TODO Codex
|
|
mux.HandleFunc("/accounts/test", accountTestHandler) // TODO Codex: Quelle+Ziel-Login pruefen
|
|
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("/transfer/side", transferSideHandler)
|
|
mux.HandleFunc("/transfer/messages", transferMessagesHandler)
|
|
mux.HandleFunc("/transfer/message", transferMessageHandler)
|
|
mux.HandleFunc("/boxes/source", emailBoxHandler)
|
|
mux.HandleFunc("/boxes/source/messages", sourceMailboxMessagesHandler)
|
|
mux.HandleFunc("/boxes/source/message", sourceMailboxMessageHandler)
|
|
mux.HandleFunc("/boxes/target/messages", targetMailboxMessagesHandler)
|
|
mux.HandleFunc("/boxes/target/message", targetMailboxMessageHandler)
|
|
mux.HandleFunc("/boxes/target", emailBoxHandler)
|
|
mux.HandleFunc("/migrate/run", migrateRunHandler) // TODO Codex: RunMigration im Hintergrund
|
|
mux.HandleFunc("/migrate/status", migrateStatusHandler) // TODO Codex: jobs-Fortschritt (HTMX-Poll)
|
|
|
|
// Viewer (lokale mbox betrachten + weiterleiten)
|
|
mux.HandleFunc("/view", viewerHandler)
|
|
mux.HandleFunc("/view/source/select", sourceSelectHandler)
|
|
mux.HandleFunc("/view/message", messageHandler)
|
|
mux.HandleFunc("/view/target", targetMessageHandler)
|
|
mux.HandleFunc("/view/target/select", targetSelectHandler)
|
|
mux.HandleFunc("/view/manual-copy", manualCopyHandler)
|
|
mux.HandleFunc("/view/forward", forwardHandler)
|
|
}
|
|
|
|
func homeHandler(w http.ResponseWriter, r *http.Request) {
|
|
renderStartPage(w, r)
|
|
}
|
|
|
|
func renderStartPage(w http.ResponseWriter, r *http.Request) {
|
|
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>Mail-Graveyard</title>
|
|
<link rel="stylesheet" href="/static/style.css?v=20260713-6">
|
|
<script src="/static/app.js?v=20260713-6" 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">
|
|
%s
|
|
%s
|
|
</nav>
|
|
%s
|
|
</header>
|
|
<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="/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>
|
|
<a class="backstage-item" href="/users">Benutzerverwaltung</a>
|
|
<a class="backstage-item" href="/logout">Logout</a>
|
|
</aside>
|
|
<button class="backstage-shade" type="button" aria-label="Menue schliessen" data-backstage-close></button>
|
|
<main class="start-page">
|
|
<section class="welcome-panel">
|
|
<div class="welcome-kicker">Willkommen</div>
|
|
<h1>Mail-Graveyard</h1>
|
|
<p>Angemeldet als %s. Waehle eine Email-Box zum Sichten, starte einen Mail-Transfer oder pflege Benutzer und Postfaecher.</p>
|
|
<div class="welcome-actions">
|
|
<a class="btn" href="/view">Archiv-Postfach oeffnen</a>
|
|
<a class="btn secondary" href="/transfer/postfach-archiv">Mail-Transfer</a>
|
|
<a class="btn secondary" href="/users">Einstellungen</a>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
<footer class="statusbar"><span>bereit</span></footer>
|
|
</body></html>`, renderRibbonEmailBoxMenu("home"), renderRibbonTransferMenu(""), renderRibbonActions(false, user), html.EscapeString(userDisplayLabel(user)))
|
|
}
|
|
|
|
// renderShell ist das Grundgeruest im Outlook-2013-Look:
|
|
//
|
|
// [ Ribbon: Email-Box | Mail-Transfer | Einstellungen | Mail-Graveyard ]
|
|
// [ Archiv-mbox | Nachrichtenliste | Quelle/Target ]
|
|
func renderShell(w http.ResponseWriter, r *http.Request, active, readingPane 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>Mail-Graveyard</title>
|
|
<link rel="stylesheet" href="/static/style.css?v=20260713-6">
|
|
<script src="/static/htmx.min.js"></script>
|
|
<script src="/static/app.js?v=20260713-6" 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">
|
|
%s
|
|
%s
|
|
</nav>
|
|
%s
|
|
</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="/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>
|
|
<a class="backstage-item transfer-sub" href="/transfer/postfach-archiv">Postfach <--> Archiv-MBox</a>
|
|
<a class="backstage-item transfer-sub" href="/transfer/archiv-postfach">Archiv-MBox <--> Postfach</a>
|
|
<a class="backstage-item transfer-sub" href="/transfer/postfach-postfach">Postfach <--> Postfach</a>
|
|
<a class="backstage-item transfer-sub" href="/transfer/archiv-archiv">Archiv-MBox <--> Archiv-MBox</a>
|
|
<a class="backstage-item" href="/users">Benutzerverwaltung</a>
|
|
<a class="backstage-item" href="/logout">Logout</a>
|
|
</aside>
|
|
<button class="backstage-shade" type="button" aria-label="Menue schliessen" data-backstage-close></button>
|
|
<div class="three-pane">
|
|
<aside class="pane pane-tree" id="tree"
|
|
hx-get="/view" hx-trigger="load" hx-target="#tree" hx-swap="innerHTML">
|
|
<div class="pane-head">Archiv-Mailbox</div>
|
|
</aside>
|
|
<div class="pane-resizer" data-resize-index="0"></div>
|
|
<section class="pane pane-list" id="list">
|
|
<div class="pane-head">Nachrichten</div>
|
|
</section>
|
|
<div class="pane-resizer" data-resize-index="1"></div>
|
|
<section class="pane pane-read" id="read">
|
|
%s
|
|
</section>
|
|
</div>
|
|
<footer class="statusbar"><span id="mig-status"
|
|
hx-get="/migrate/status" hx-trigger="every 3s" hx-swap="innerHTML">bereit</span></footer>
|
|
</body></html>`, renderRibbonEmailBoxMenu("archive"), renderRibbonTransferMenu(""), renderRibbonActions(false, user), renderInitialReadPane(readingPane))
|
|
}
|
|
|
|
func accountsHandler(w http.ResponseWriter, r *http.Request) {
|
|
if !requireManager(w, r) {
|
|
return
|
|
}
|
|
accounts, err := ListAccounts()
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
archives, err := ListArchiveMailboxes()
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
editName := strings.TrimSpace(r.URL.Query().Get("edit"))
|
|
var edit Account
|
|
if editName != "" {
|
|
edit, err = GetAccount(editName)
|
|
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
}
|
|
renderAccountsPage(w, r, accounts, archives, edit, r.URL.Query().Get("msg"), r.URL.Query().Get("err"))
|
|
}
|
|
|
|
func accountSaveHandler(w http.ResponseWriter, r *http.Request) {
|
|
if !requireManager(w, r) {
|
|
return
|
|
}
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
if err := r.ParseForm(); err != nil {
|
|
redirectAccounts(w, r, "", err.Error())
|
|
return
|
|
}
|
|
a := accountFromForm(r)
|
|
if a.Name == "" || a.SrcHost == "" || a.SrcUser == "" || a.DstHost == "" || a.DstUser == "" {
|
|
redirectAccounts(w, r, a.Name, "Name, Quelle und Ziel sind Pflichtfelder.")
|
|
return
|
|
}
|
|
if existing, err := GetAccount(a.Name); err == nil {
|
|
if a.SrcPass == "" {
|
|
a.SrcPass = existing.SrcPass
|
|
}
|
|
if a.DstPass == "" {
|
|
a.DstPass = existing.DstPass
|
|
}
|
|
}
|
|
if err := SaveAccount(a); err != nil {
|
|
redirectAccounts(w, r, a.Name, err.Error())
|
|
return
|
|
}
|
|
redirectAccounts(w, r, a.Name, "Postfach gespeichert.")
|
|
}
|
|
|
|
func accountDeleteHandler(w http.ResponseWriter, r *http.Request) {
|
|
if !requireManager(w, r) {
|
|
return
|
|
}
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
name := strings.TrimSpace(r.FormValue("name"))
|
|
if name == "" {
|
|
redirectAccounts(w, r, "", "Kein Postfach gewaehlt.")
|
|
return
|
|
}
|
|
if err := DeleteAccount(name); err != nil {
|
|
redirectAccounts(w, r, name, err.Error())
|
|
return
|
|
}
|
|
redirectAccounts(w, r, "", "Postfach geloescht.")
|
|
}
|
|
|
|
func accountTestHandler(w http.ResponseWriter, r *http.Request) {
|
|
if !requireManager(w, r) {
|
|
return
|
|
}
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
name := strings.TrimSpace(r.FormValue("name"))
|
|
if name == "" {
|
|
redirectAccounts(w, r, "", "Kein Postfach gewaehlt.")
|
|
return
|
|
}
|
|
if err := CheckAccount(name); err != nil {
|
|
redirectAccounts(w, r, name, "Test fehlgeschlagen: "+err.Error())
|
|
return
|
|
}
|
|
redirectAccounts(w, r, name, "Quelle und Ziel erfolgreich getestet.")
|
|
}
|
|
|
|
func archivesHandler(w http.ResponseWriter, r *http.Request) {
|
|
if !requireManager(w, r) {
|
|
return
|
|
}
|
|
accounts, err := ListAccounts()
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
archives, err := ListArchiveMailboxes()
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
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) {
|
|
if !requireManager(w, r) {
|
|
return
|
|
}
|
|
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
|
|
}
|
|
archives, err := ListArchiveMailboxes()
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
renderTransferPage(w, r, transferModeBySlug(mode), accounts, archives)
|
|
}
|
|
|
|
func emailBoxHandler(w http.ResponseWriter, r *http.Request) {
|
|
accounts, err := ListAccounts()
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
kind := strings.TrimPrefix(r.URL.Path, "/boxes/")
|
|
switch kind {
|
|
case "source":
|
|
if r.Header.Get("HX-Request") == "true" {
|
|
selected := selectedSourceAccount(accounts, r.URL.Query().Get("source"))
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
fmt.Fprint(w, renderEmailBoxSourcePane(accounts, selected.Name))
|
|
fmt.Fprintf(w, `<div id="list" hx-swap-oob="innerHTML">%s</div>`, renderInitialSourceMailboxList(selected))
|
|
fmt.Fprintf(w, `<div id="read" hx-swap-oob="innerHTML">%s</div>`, renderInitialSourceMailboxRead(selected))
|
|
return
|
|
}
|
|
renderSourceEmailBoxPage(w, r, accounts, r.URL.Query().Get("source"))
|
|
case "target":
|
|
if r.Header.Get("HX-Request") == "true" {
|
|
selected := selectedTargetAccount(accounts, r.URL.Query().Get("target"))
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
fmt.Fprint(w, renderEmailBoxTargetPane(accounts, selected.Name))
|
|
fmt.Fprintf(w, `<div id="list" hx-swap-oob="innerHTML">%s</div>`, renderInitialTargetMailboxList(selected))
|
|
fmt.Fprintf(w, `<div id="read" hx-swap-oob="innerHTML">%s</div>`, renderInitialTargetMailboxRead(selected))
|
|
return
|
|
}
|
|
renderTargetEmailBoxPage(w, r, accounts, r.URL.Query().Get("target"))
|
|
default:
|
|
http.Redirect(w, r, "/view", http.StatusSeeOther)
|
|
}
|
|
}
|
|
|
|
func sourceMailboxMessagesHandler(w http.ResponseWriter, r *http.Request) {
|
|
accountName := strings.TrimSpace(r.URL.Query().Get("source"))
|
|
folder := strings.TrimSpace(r.URL.Query().Get("folder"))
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
if accountName == "" || folder == "" {
|
|
fmt.Fprint(w, `<div class="pane-head">Nachrichten</div><div class="ef-empty">Quell-Postfach und Ordner waehlen.</div>`)
|
|
return
|
|
}
|
|
account, err := GetAccount(accountName)
|
|
if err != nil {
|
|
fmt.Fprintf(w, `<div class="pane-head">Nachrichten</div><div class="ef-empty">%s</div>`, html.EscapeString(err.Error()))
|
|
return
|
|
}
|
|
messages, err := fetchSourceFolderMessages(account, folder)
|
|
if err != nil {
|
|
fmt.Fprintf(w, `<div class="pane-head">Nachrichten</div><div class="ef-empty">%s</div>`, html.EscapeString(err.Error()))
|
|
return
|
|
}
|
|
renderSourceMailboxMessageList(w, account, folder, messages)
|
|
fmt.Fprintf(w, `<div id="read" hx-swap-oob="innerHTML">%s</div>`, renderSourceMailboxRead(account, folder, 0, messages))
|
|
}
|
|
|
|
func sourceMailboxMessageHandler(w http.ResponseWriter, r *http.Request) {
|
|
accountName := strings.TrimSpace(r.URL.Query().Get("source"))
|
|
folder := strings.TrimSpace(r.URL.Query().Get("folder"))
|
|
index, err := strconv.Atoi(r.URL.Query().Get("index"))
|
|
if err != nil {
|
|
http.Error(w, "bad index", http.StatusBadRequest)
|
|
return
|
|
}
|
|
account, err := GetAccount(accountName)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
messages, err := fetchSourceFolderMessages(account, folder)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
fmt.Fprint(w, renderSourceMailboxRead(account, folder, index, messages))
|
|
}
|
|
|
|
func targetMailboxMessagesHandler(w http.ResponseWriter, r *http.Request) {
|
|
accountName := strings.TrimSpace(r.URL.Query().Get("target"))
|
|
folder := strings.TrimSpace(r.URL.Query().Get("folder"))
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
if accountName == "" || folder == "" {
|
|
fmt.Fprint(w, `<div class="pane-head">Nachrichten</div><div class="ef-empty">Ziel-Postfach und Ordner waehlen.</div>`)
|
|
return
|
|
}
|
|
account, err := GetAccount(accountName)
|
|
if err != nil {
|
|
fmt.Fprintf(w, `<div class="pane-head">Nachrichten</div><div class="ef-empty">%s</div>`, html.EscapeString(err.Error()))
|
|
return
|
|
}
|
|
messages, err := fetchTargetFolderMessages(account, folder)
|
|
if err != nil {
|
|
fmt.Fprintf(w, `<div class="pane-head">Nachrichten</div><div class="ef-empty">%s</div>`, html.EscapeString(err.Error()))
|
|
return
|
|
}
|
|
renderTargetMailboxMessageList(w, account, folder, messages)
|
|
fmt.Fprintf(w, `<div id="read" hx-swap-oob="innerHTML">%s</div>`, renderTargetMailboxRead(account, folder, 0, messages))
|
|
}
|
|
|
|
func targetMailboxMessageHandler(w http.ResponseWriter, r *http.Request) {
|
|
accountName := strings.TrimSpace(r.URL.Query().Get("target"))
|
|
folder := strings.TrimSpace(r.URL.Query().Get("folder"))
|
|
index, err := strconv.Atoi(r.URL.Query().Get("index"))
|
|
if err != nil {
|
|
http.Error(w, "bad index", http.StatusBadRequest)
|
|
return
|
|
}
|
|
account, err := GetAccount(accountName)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
messages, err := fetchTargetFolderMessages(account, folder)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
fmt.Fprint(w, renderTargetMailboxRead(account, folder, index, messages))
|
|
}
|
|
|
|
func archiveCreateHandler(w http.ResponseWriter, r *http.Request) {
|
|
if !requireManager(w, r) {
|
|
return
|
|
}
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
name, err := safeArchiveName(r.FormValue("archive_name"))
|
|
if err != nil {
|
|
redirectArchives(w, r, err.Error())
|
|
return
|
|
}
|
|
if err := os.MkdirAll(filepath.Join(Cfg.MboxRoot, name), 0o700); err != nil {
|
|
redirectArchives(w, r, err.Error())
|
|
return
|
|
}
|
|
if err := SaveArchiveMailbox(name); err != nil {
|
|
redirectArchives(w, r, err.Error())
|
|
return
|
|
}
|
|
redirectArchives(w, r, "Archiv-Mailbox angelegt.")
|
|
}
|
|
|
|
func archiveDeleteHandler(w http.ResponseWriter, r *http.Request) {
|
|
if !requireManager(w, r) {
|
|
return
|
|
}
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
name, err := safeArchiveName(r.FormValue("archive_name"))
|
|
if err != nil {
|
|
redirectArchives(w, r, err.Error())
|
|
return
|
|
}
|
|
if archiveInUse(name) {
|
|
redirectArchives(w, r, "Archiv-Mailbox ist noch einem Postfach zugeordnet.")
|
|
return
|
|
}
|
|
path := filepath.Join(Cfg.MboxRoot, name)
|
|
entries, err := os.ReadDir(path)
|
|
if os.IsNotExist(err) {
|
|
if err := DeleteArchiveMailbox(name); err != nil {
|
|
redirectArchives(w, r, err.Error())
|
|
return
|
|
}
|
|
redirectArchives(w, r, "Archiv-Mailbox entfernt.")
|
|
return
|
|
}
|
|
if err != nil {
|
|
redirectArchives(w, r, err.Error())
|
|
return
|
|
}
|
|
if len(entries) > 0 {
|
|
redirectArchives(w, r, "Archiv-Mailbox ist nicht leer und wurde nicht entfernt.")
|
|
return
|
|
}
|
|
if err := os.Remove(path); err != nil {
|
|
redirectArchives(w, r, err.Error())
|
|
return
|
|
}
|
|
if err := DeleteArchiveMailbox(name); err != nil {
|
|
redirectArchives(w, r, err.Error())
|
|
return
|
|
}
|
|
redirectArchives(w, r, "Leere Archiv-Mailbox entfernt.")
|
|
}
|
|
|
|
func migrateRunHandler(w http.ResponseWriter, r *http.Request) {
|
|
if !requireAdmin(w, r) {
|
|
return
|
|
}
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
name := strings.TrimSpace(r.FormValue("name"))
|
|
if name == "" {
|
|
name = "all"
|
|
}
|
|
go func() {
|
|
if err := RunMigration(name, false, 0); err != nil {
|
|
fmt.Printf("migration %s failed: %v\n", name, err)
|
|
}
|
|
}()
|
|
redirectAccounts(w, r, name, "Umzug gestartet.")
|
|
}
|
|
|
|
func migrateStatusHandler(w http.ResponseWriter, r *http.Request) {
|
|
type job struct {
|
|
Account string
|
|
Started string
|
|
Finished sql.NullString
|
|
Total int
|
|
Done int
|
|
Errors int
|
|
State string
|
|
}
|
|
var j job
|
|
err := DB.QueryRow(`SELECT a.name,j.started,j.finished,j.total,j.done,j.errors,j.state
|
|
FROM jobs j JOIN accounts a ON a.id=j.account_id
|
|
ORDER BY j.id DESC LIMIT 1`).Scan(&j.Account, &j.Started, &j.Finished, &j.Total, &j.Done, &j.Errors, &j.State)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
fmt.Fprint(w, "bereit")
|
|
return
|
|
}
|
|
if err != nil {
|
|
fmt.Fprintf(w, `<span class="bad">%s</span>`, html.EscapeString(err.Error()))
|
|
return
|
|
}
|
|
label := "laeuft"
|
|
if j.State != "" {
|
|
label = j.State
|
|
}
|
|
fmt.Fprintf(w, `%s: %s %d/%d Fehler %d`, html.EscapeString(j.Account), html.EscapeString(label), j.Done, j.Total, j.Errors)
|
|
}
|
|
|
|
func usersHandler(w http.ResponseWriter, r *http.Request) {
|
|
if !requireManager(w, r) {
|
|
return
|
|
}
|
|
users, err := ListAppUsers()
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
editName := strings.TrimSpace(r.URL.Query().Get("edit"))
|
|
var edit AppUser
|
|
if editName != "" {
|
|
edit, err = GetAppUser(editName)
|
|
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
}
|
|
renderUsersPage(w, r, users, edit, r.URL.Query().Get("msg"), r.URL.Query().Get("err"))
|
|
}
|
|
|
|
func userSaveHandler(w http.ResponseWriter, r *http.Request) {
|
|
if !requireManager(w, r) {
|
|
return
|
|
}
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
oldUsername := strings.TrimSpace(r.FormValue("old_username"))
|
|
username := strings.TrimSpace(r.FormValue("username"))
|
|
displayName := strings.TrimSpace(r.FormValue("display_name"))
|
|
password := r.FormValue("password")
|
|
role := strings.TrimSpace(r.FormValue("role"))
|
|
active := r.FormValue("active") == "1"
|
|
if username == "" {
|
|
redirectUsers(w, r, "", "Benutzername fehlt.")
|
|
return
|
|
}
|
|
lookupUsername := username
|
|
if oldUsername != "" {
|
|
lookupUsername = oldUsername
|
|
}
|
|
existing, existingErr := GetAppUser(lookupUsername)
|
|
if !IsAdmin(r) && existingErr == nil && existing.Role != roleUser {
|
|
redirectUsers(w, r, oldUsername, "Nur Admins duerfen Verwalter oder Admins bearbeiten.")
|
|
return
|
|
}
|
|
if !IsAdmin(r) {
|
|
role = roleUser
|
|
}
|
|
if errors.Is(existingErr, sql.ErrNoRows) && password == "" {
|
|
redirectUsers(w, r, username, "Passwort fuer neuen Benutzer fehlt.")
|
|
return
|
|
}
|
|
var hash string
|
|
if password != "" {
|
|
var err error
|
|
hash, err = HashPassword(password)
|
|
if err != nil {
|
|
redirectUsers(w, r, username, err.Error())
|
|
return
|
|
}
|
|
}
|
|
if err := UpdateAppUser(oldUsername, username, displayName, hash, role, active); err != nil {
|
|
redirectUsers(w, r, oldUsername, err.Error())
|
|
return
|
|
}
|
|
redirectUsers(w, r, username, "Benutzer gespeichert.")
|
|
}
|
|
|
|
func userDeleteHandler(w http.ResponseWriter, r *http.Request) {
|
|
if !requireManager(w, r) {
|
|
return
|
|
}
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
username := strings.TrimSpace(r.FormValue("username"))
|
|
if username == "" {
|
|
redirectUsers(w, r, "", "Kein Benutzer gewaehlt.")
|
|
return
|
|
}
|
|
current := CurrentUser(r)
|
|
if strings.EqualFold(username, current.Username) {
|
|
redirectUsers(w, r, username, "Eigenen Benutzer nicht loeschen.")
|
|
return
|
|
}
|
|
target, err := GetAppUser(username)
|
|
if err != nil {
|
|
redirectUsers(w, r, username, err.Error())
|
|
return
|
|
}
|
|
if !IsAdmin(r) && target.Role != roleUser {
|
|
redirectUsers(w, r, username, "Nur Admins duerfen Verwalter oder Admins loeschen.")
|
|
return
|
|
}
|
|
if err := DeleteAppUser(username); err != nil {
|
|
redirectUsers(w, r, username, err.Error())
|
|
return
|
|
}
|
|
redirectUsers(w, r, "", "Benutzer geloescht.")
|
|
}
|
|
|
|
func userToggleActiveHandler(w http.ResponseWriter, r *http.Request) {
|
|
if !requireManager(w, r) {
|
|
return
|
|
}
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
username := strings.TrimSpace(r.FormValue("username"))
|
|
active := r.FormValue("active") == "1"
|
|
if username == "" {
|
|
redirectUsers(w, r, "", "Kein Benutzer gewaehlt.")
|
|
return
|
|
}
|
|
current := CurrentUser(r)
|
|
if strings.EqualFold(username, current.Username) && !active {
|
|
redirectUsers(w, r, username, "Eigenen Benutzer nicht sperren.")
|
|
return
|
|
}
|
|
target, err := GetAppUser(username)
|
|
if err != nil {
|
|
redirectUsers(w, r, username, err.Error())
|
|
return
|
|
}
|
|
if !IsAdmin(r) && target.Role != roleUser {
|
|
redirectUsers(w, r, username, "Nur Admins duerfen Verwalter oder Admins sperren.")
|
|
return
|
|
}
|
|
if err := SaveAppUser(username, target.DisplayName, "", target.Role, active); err != nil {
|
|
redirectUsers(w, r, username, err.Error())
|
|
return
|
|
}
|
|
if !active {
|
|
_, _ = DB.Exec(`DELETE FROM app_sessions WHERE user_id=?`, target.ID)
|
|
redirectUsers(w, r, username, "Benutzer gesperrt.")
|
|
return
|
|
}
|
|
redirectUsers(w, r, username, "Benutzer aktiviert.")
|
|
}
|
|
|
|
func renderAccountsPage(w http.ResponseWriter, r *http.Request, accounts []Account, archives []string, edit Account, msg, errMsg string) {
|
|
user := CurrentUser(r)
|
|
if edit.Name == "" {
|
|
edit = Account{SrcPort: 993, SrcSecurity: "tls", SrcProto: "imap", DstPort: 993, DstSecurity: "tls", Active: true}
|
|
}
|
|
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>Konten-Verwaltung - Mail-Graveyard</title>
|
|
<link rel="stylesheet" href="/static/style.css?v=20260713-6">
|
|
<script src="/static/htmx.min.js"></script>
|
|
<script src="/static/app.js?v=20260713-6" 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">
|
|
%s
|
|
%s
|
|
</nav>
|
|
%s
|
|
</header>
|
|
<aside class="backstage-menu" aria-label="Mail-Graveyard-Menue">
|
|
<a class="backstage-item" href="/view">Archiv-Boxen</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>
|
|
<a class="backstage-item transfer-sub" href="/transfer/postfach-archiv">Postfach <--> Archiv-MBox</a>
|
|
<a class="backstage-item transfer-sub" href="/transfer/archiv-postfach">Archiv-MBox <--> Postfach</a>
|
|
<a class="backstage-item transfer-sub" href="/transfer/postfach-postfach">Postfach <--> Postfach</a>
|
|
<a class="backstage-item transfer-sub" href="/transfer/archiv-archiv">Archiv-MBox <--> Archiv-MBox</a>
|
|
<a class="backstage-item" href="/users">Benutzerverwaltung</a>
|
|
<a class="backstage-item" href="/logout">Logout</a>
|
|
</aside>
|
|
<button class="backstage-shade" type="button" aria-label="Menue schliessen" data-backstage-close></button>
|
|
<main class="accounts-page">`, renderRibbonEmailBoxMenu(""), renderRibbonTransferMenu(""), renderRibbonActions(false, user))
|
|
if msg != "" {
|
|
fmt.Fprintf(w, `<div class="notice ok">%s</div>`, html.EscapeString(msg))
|
|
}
|
|
if errMsg != "" {
|
|
fmt.Fprintf(w, `<div class="notice bad">%s</div>`, html.EscapeString(errMsg))
|
|
}
|
|
fmt.Fprint(w, `<section class="account-layout"><div class="account-list"><div class="pane-head">Eingetragene Postfaecher</div>`)
|
|
renderAccountsTable(w, accounts, edit.Name)
|
|
fmt.Fprint(w, `</div><div class="account-form-panel"><div class="pane-head">Postfach eintragen</div>`)
|
|
renderAccountForm(w, edit, archives)
|
|
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, 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=20260713-6">
|
|
<script src="/static/htmx.min.js"></script>
|
|
<script src="/static/app.js?v=20260713-6" 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">
|
|
%s
|
|
%s
|
|
</nav>
|
|
%s
|
|
</header>
|
|
<aside class="backstage-menu" aria-label="Mail-Graveyard-Menue">
|
|
<a class="backstage-item" href="/view">Archiv-Boxen</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>
|
|
<a class="backstage-item transfer-sub" href="/transfer/postfach-archiv">Postfach <--> Archiv-MBox</a>
|
|
<a class="backstage-item transfer-sub" href="/transfer/archiv-postfach">Archiv-MBox <--> Postfach</a>
|
|
<a class="backstage-item transfer-sub" href="/transfer/postfach-postfach">Postfach <--> Postfach</a>
|
|
<a class="backstage-item transfer-sub" href="/transfer/archiv-archiv">Archiv-MBox <--> Archiv-MBox</a>
|
|
<a class="backstage-item" href="/users">Benutzerverwaltung</a>
|
|
<a class="backstage-item" href="/logout">Logout</a>
|
|
</aside>
|
|
<button class="backstage-shade" type="button" aria-label="Menue schliessen" data-backstage-close></button>
|
|
<main class="accounts-page archive-page">`, renderRibbonEmailBoxMenu(""), renderRibbonTransferMenu(""), renderRibbonActions(false, user))
|
|
if msg != "" {
|
|
fmt.Fprintf(w, `<div class="notice ok">%s</div>`, html.EscapeString(msg))
|
|
}
|
|
if errMsg != "" {
|
|
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, 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>`)
|
|
}
|
|
|
|
func renderUsersPage(w http.ResponseWriter, r *http.Request, users []AppUser, edit AppUser, msg, errMsg string) {
|
|
if edit.Username == "" {
|
|
edit = AppUser{Role: roleUser, Active: true}
|
|
}
|
|
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>Benutzerverwaltung - Mail-Graveyard</title>
|
|
<link rel="stylesheet" href="/static/style.css?v=20260713-6">
|
|
<script src="/static/htmx.min.js"></script>
|
|
<script src="/static/app.js?v=20260713-6" 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">
|
|
%s
|
|
%s
|
|
</nav>
|
|
%s
|
|
</header>
|
|
<aside class="backstage-menu" aria-label="Mail-Graveyard-Menue">
|
|
<a class="backstage-item" href="/view">Archiv-Boxen</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>
|
|
<a class="backstage-item transfer-sub" href="/transfer/postfach-archiv">Postfach <--> Archiv-MBox</a>
|
|
<a class="backstage-item transfer-sub" href="/transfer/archiv-postfach">Archiv-MBox <--> Postfach</a>
|
|
<a class="backstage-item transfer-sub" href="/transfer/postfach-postfach">Postfach <--> Postfach</a>
|
|
<a class="backstage-item transfer-sub" href="/transfer/archiv-archiv">Archiv-MBox <--> Archiv-MBox</a>
|
|
<a class="backstage-item active" href="/users">Benutzerverwaltung</a>
|
|
<a class="backstage-item" href="/logout">Logout</a>
|
|
</aside>
|
|
<button class="backstage-shade" type="button" aria-label="Menue schliessen" data-backstage-close></button>
|
|
<main class="accounts-page users-page">`, renderRibbonEmailBoxMenu(""), renderRibbonTransferMenu(""), renderRibbonActions(true, CurrentUser(r)))
|
|
if msg != "" {
|
|
fmt.Fprintf(w, `<div class="notice ok">%s</div>`, html.EscapeString(msg))
|
|
}
|
|
if errMsg != "" {
|
|
fmt.Fprintf(w, `<div class="notice bad">%s</div>`, html.EscapeString(errMsg))
|
|
}
|
|
fmt.Fprint(w, `<section class="account-layout"><div class="account-list"><div class="pane-head">Benutzer</div>`)
|
|
renderUsersTable(w, users, edit.Username)
|
|
fmt.Fprint(w, `</div><div class="account-form-panel"><div class="pane-head">Benutzer eintragen</div>`)
|
|
renderUserForm(w, r, edit)
|
|
fmt.Fprint(w, `</div></section></main><footer class="statusbar"><span>angemeldet als `)
|
|
fmt.Fprint(w, html.EscapeString(userDisplayLabel(CurrentUser(r))+" / "+CurrentUser(r).Role))
|
|
fmt.Fprint(w, `</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"},
|
|
{Slug: "archiv-archiv", Title: "Archiv-MBox <--> Archiv-MBox", LeftTitle: "Archiv-MBox", RightTitle: "Archiv-MBox", LeftKind: "archive", RightKind: "archive"},
|
|
}
|
|
for _, mode := range modes {
|
|
if mode.Slug == slug {
|
|
return mode
|
|
}
|
|
}
|
|
return modes[0]
|
|
}
|
|
|
|
func renderTransferPage(w http.ResponseWriter, r *http.Request, mode transferMode, accounts []Account, archives []string) {
|
|
user := CurrentUser(r)
|
|
leftSelected := defaultTransferValue(mode.LeftKind, accounts, archives)
|
|
rightSelected := defaultTransferValue(mode.RightKind, accounts, archives)
|
|
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=20260713-6">
|
|
<script src="/static/htmx.min.js"></script>
|
|
<script src="/static/app.js?v=20260713-6" 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">
|
|
%s
|
|
%s
|
|
</nav>
|
|
%s
|
|
</header>
|
|
<aside class="backstage-menu" aria-label="Mail-Graveyard-Menue">
|
|
<a class="backstage-item" href="/view">Archiv-Boxen</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>
|
|
%s
|
|
<a class="backstage-item" href="/users">Benutzerverwaltung</a>
|
|
<a class="backstage-item" href="/logout">Logout</a>
|
|
</aside>
|
|
<button class="backstage-shade" type="button" aria-label="Menue schliessen" data-backstage-close></button>
|
|
<main class="transfer-workbench">
|
|
<aside class="pane pane-tree" id="left-folders">%s</aside>
|
|
<div class="pane-resizer" data-resize-index="0"></div>
|
|
<section class="pane pane-list" id="left-list">%s</section>
|
|
<div class="pane-resizer" data-resize-index="1"></div>
|
|
<section class="pane pane-read" id="transfer-preview">%s</section>
|
|
<div class="pane-resizer" data-resize-index="2"></div>
|
|
<section class="pane pane-list" id="right-list">%s</section>
|
|
<div class="pane-resizer" data-resize-index="3"></div>
|
|
<aside class="pane pane-tree" id="right-folders">%s</aside>
|
|
</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), renderRibbonEmailBoxMenu(""), renderRibbonTransferMenu(mode.Slug), renderRibbonActions(false, user), transferMenuItems(mode.Slug),
|
|
renderTransferFolderPane("left", mode.LeftKind, accounts, archives, leftSelected),
|
|
renderInitialTransferList("left", mode.LeftKind, accounts, archives, leftSelected),
|
|
renderInitialTransferPreview(mode.LeftKind, leftSelected, mode.RightKind, rightSelected),
|
|
renderInitialTransferList("right", mode.RightKind, accounts, archives, rightSelected),
|
|
renderTransferFolderPane("right", mode.RightKind, accounts, archives, rightSelected))
|
|
}
|
|
|
|
func transferSideHandler(w http.ResponseWriter, r *http.Request) {
|
|
side := transferSide(r.URL.Query().Get("side"))
|
|
kind := strings.TrimSpace(r.URL.Query().Get("kind"))
|
|
value := strings.TrimSpace(r.URL.Query().Get("value"))
|
|
accounts, err := ListAccounts()
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
archives, err := ListArchiveMailboxes()
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
fmt.Fprint(w, renderTransferFolderPane(side, kind, accounts, archives, value))
|
|
fmt.Fprintf(w, `<div id="%s-list" hx-swap-oob="innerHTML">%s</div>`, side, renderInitialTransferList(side, kind, accounts, archives, value))
|
|
fmt.Fprintf(w, `<div id="transfer-preview" hx-swap-oob="innerHTML">%s</div>`, renderInitialTransferPreview(kind, value, "", ""))
|
|
}
|
|
|
|
func transferMessagesHandler(w http.ResponseWriter, r *http.Request) {
|
|
side := transferSide(r.URL.Query().Get("side"))
|
|
kind := strings.TrimSpace(r.URL.Query().Get("kind"))
|
|
value := strings.TrimSpace(r.URL.Query().Get("value"))
|
|
folder := strings.TrimSpace(r.URL.Query().Get("folder"))
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
fmt.Fprint(w, renderTransferMessageList(side, kind, value, folder))
|
|
fmt.Fprintf(w, `<div id="transfer-preview" hx-swap-oob="innerHTML">%s</div>`, renderTransferPreview(kind, value, folder, 0))
|
|
}
|
|
|
|
func transferMessageHandler(w http.ResponseWriter, r *http.Request) {
|
|
kind := strings.TrimSpace(r.URL.Query().Get("kind"))
|
|
value := strings.TrimSpace(r.URL.Query().Get("value"))
|
|
folder := strings.TrimSpace(r.URL.Query().Get("folder"))
|
|
index, err := strconv.Atoi(r.URL.Query().Get("index"))
|
|
if err != nil {
|
|
http.Error(w, "bad index", http.StatusBadRequest)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
fmt.Fprint(w, renderTransferPreview(kind, value, folder, index))
|
|
}
|
|
|
|
func transferSide(value string) string {
|
|
if value == "right" {
|
|
return "right"
|
|
}
|
|
return "left"
|
|
}
|
|
|
|
func defaultTransferValue(kind string, accounts []Account, archives []string) string {
|
|
options := transferEntityOptions(kind, accounts, archives)
|
|
if len(options) == 0 {
|
|
return ""
|
|
}
|
|
return options[0].Value
|
|
}
|
|
|
|
func renderTransferFolderPane(side, kind string, accounts []Account, archives []string, selected string) string {
|
|
options := transferEntityOptions(kind, accounts, archives)
|
|
if selected == "" && len(options) > 0 {
|
|
selected = options[0].Value
|
|
}
|
|
var b strings.Builder
|
|
fmt.Fprintf(&b, `<div class="pane-head archive-head"><span>%s</span><form class="archive-select-form" hx-get="/transfer/side" hx-target="#%s-folders" hx-swap="innerHTML" hx-trigger="change from:#transfer-%s-select"><input type="hidden" name="side" value="%s"><input type="hidden" name="kind" value="%s"><select id="transfer-%s-select" class="archive-select" name="value">`,
|
|
html.EscapeString(transferKindLabel(kind)), side, side, side, html.EscapeString(kind), side)
|
|
if len(options) == 0 {
|
|
b.WriteString(`<option>Keine Eintraege</option>`)
|
|
} else {
|
|
for _, opt := range options {
|
|
attr := ""
|
|
if opt.Value == selected {
|
|
attr = ` selected`
|
|
}
|
|
fmt.Fprintf(&b, `<option value="%s"%s>%s</option>`, html.EscapeString(opt.Value), attr, html.EscapeString(opt.Label))
|
|
}
|
|
}
|
|
b.WriteString(`</select></form></div>`)
|
|
if len(options) == 0 {
|
|
b.WriteString(`<div class="ef-empty">Keine Eintraege.</div>`)
|
|
return b.String()
|
|
}
|
|
renderTransferFolders(&b, side, kind, selected)
|
|
return b.String()
|
|
}
|
|
|
|
type transferOption struct {
|
|
Value string
|
|
Label string
|
|
}
|
|
|
|
func transferEntityOptions(kind string, accounts []Account, archives []string) []transferOption {
|
|
switch kind {
|
|
case "archive":
|
|
out := make([]transferOption, 0, len(archives))
|
|
for _, archive := range archives {
|
|
out = append(out, transferOption{Value: archive, Label: archive})
|
|
}
|
|
return out
|
|
case "target":
|
|
targets := accountsByTarget(accounts)
|
|
out := make([]transferOption, 0, len(targets))
|
|
for _, a := range targets {
|
|
out = append(out, transferOption{Value: a.Name, Label: a.DstUser})
|
|
}
|
|
return out
|
|
default:
|
|
sources := accountsBySource(accounts)
|
|
out := make([]transferOption, 0, len(sources))
|
|
for _, a := range sources {
|
|
out = append(out, transferOption{Value: a.Name, Label: a.SrcUser})
|
|
}
|
|
return out
|
|
}
|
|
}
|
|
|
|
func transferKindLabel(kind string) string {
|
|
switch kind {
|
|
case "archive":
|
|
return "Archiv-MBox"
|
|
case "target":
|
|
return "Ziel-Postfach"
|
|
default:
|
|
return "Quell-Postfach"
|
|
}
|
|
}
|
|
|
|
func renderTransferFolders(b *strings.Builder, side, kind, value string) {
|
|
switch kind {
|
|
case "archive":
|
|
mboxes, _ := filepath.Glob(filepath.Join(Cfg.MboxRoot, value, "*.mbox"))
|
|
if len(mboxes) == 0 {
|
|
b.WriteString(`<div class="ef-empty">Keine Ordner.</div>`)
|
|
return
|
|
}
|
|
for _, folder := range archiveFolderInfos(mboxes) {
|
|
fmt.Fprintf(b, `<a class="tree-node folder" href="#" hx-get="/transfer/messages?side=%s&kind=%s&value=%s&folder=%s" hx-target="#%s-list" hx-swap="innerHTML"><span class="tree-label">%s</span><span class="tree-count">%d</span></a>`,
|
|
side, html.EscapeString(kind), urlEsc(value), urlEsc(folder.Name), side, html.EscapeString(folder.Label), folder.Count)
|
|
}
|
|
case "target":
|
|
account, err := GetAccount(value)
|
|
if err != nil {
|
|
fmt.Fprintf(b, `<div class="ef-empty">%s</div>`, html.EscapeString(err.Error()))
|
|
return
|
|
}
|
|
fmt.Fprintf(b, `<div class="tree-node account">%s</div>`, html.EscapeString(account.DstUser))
|
|
dst, err := OpenIMAPTarget(account)
|
|
if err != nil {
|
|
fmt.Fprintf(b, `<div class="ef-empty">Ordner konnten nicht geladen werden: %s</div>`, html.EscapeString(err.Error()))
|
|
return
|
|
}
|
|
defer dst.Close()
|
|
folders, err := dst.Folders()
|
|
if err != nil {
|
|
fmt.Fprintf(b, `<div class="ef-empty">Ordner konnten nicht geladen werden: %s</div>`, html.EscapeString(err.Error()))
|
|
return
|
|
}
|
|
for _, folder := range folders {
|
|
fmt.Fprintf(b, `<a class="tree-node folder" href="#" hx-get="/transfer/messages?side=%s&kind=%s&value=%s&folder=%s" hx-target="#%s-list" hx-swap="innerHTML"><span class="tree-label">%s</span><span class="tree-count">%s</span></a>`,
|
|
side, html.EscapeString(kind), urlEsc(value), urlEsc(folder.Name), side, html.EscapeString(folder.Name), html.EscapeString(targetFolderRole(folder)))
|
|
}
|
|
default:
|
|
account, err := GetAccount(value)
|
|
if err != nil {
|
|
fmt.Fprintf(b, `<div class="ef-empty">%s</div>`, html.EscapeString(err.Error()))
|
|
return
|
|
}
|
|
fmt.Fprintf(b, `<div class="tree-node account">%s</div>`, html.EscapeString(account.SrcUser))
|
|
if accountProto(account) == "pop3" {
|
|
fmt.Fprintf(b, `<a class="tree-node folder" href="#" hx-get="/transfer/messages?side=%s&kind=%s&value=%s&folder=INBOX" hx-target="#%s-list" hx-swap="innerHTML"><span class="tree-label">INBOX</span><span class="tree-count">POP3</span></a>`, side, html.EscapeString(kind), urlEsc(value), side)
|
|
return
|
|
}
|
|
src, err := OpenIMAPSource(account)
|
|
if err != nil {
|
|
fmt.Fprintf(b, `<div class="ef-empty">Ordner konnten nicht geladen werden: %s</div>`, html.EscapeString(err.Error()))
|
|
return
|
|
}
|
|
defer src.Close()
|
|
folders, err := src.Folders()
|
|
if err != nil {
|
|
fmt.Fprintf(b, `<div class="ef-empty">Ordner konnten nicht geladen werden: %s</div>`, html.EscapeString(err.Error()))
|
|
return
|
|
}
|
|
for _, folder := range folders {
|
|
fmt.Fprintf(b, `<a class="tree-node folder" href="#" hx-get="/transfer/messages?side=%s&kind=%s&value=%s&folder=%s" hx-target="#%s-list" hx-swap="innerHTML"><span class="tree-label">%s</span><span class="tree-count">%s</span></a>`,
|
|
side, html.EscapeString(kind), urlEsc(value), urlEsc(folder.Name), side, html.EscapeString(folder.Name), html.EscapeString(sourceFolderRole(folder)))
|
|
}
|
|
}
|
|
}
|
|
|
|
func renderInitialTransferList(side, kind string, accounts []Account, archives []string, selected string) string {
|
|
options := transferEntityOptions(kind, accounts, archives)
|
|
if selected == "" && len(options) > 0 {
|
|
selected = options[0].Value
|
|
}
|
|
if selected == "" {
|
|
return `<div class="pane-head">Nachrichten</div><div class="ef-empty">Postfach waehlen.</div>`
|
|
}
|
|
folder, err := firstTransferFolder(kind, selected)
|
|
if err != nil {
|
|
return fmt.Sprintf(`<div class="pane-head">Nachrichten</div><div class="ef-empty">%s</div>`, html.EscapeString(transferEmptyReason(kind, err)))
|
|
}
|
|
return renderTransferMessageList(side, kind, selected, folder)
|
|
}
|
|
|
|
func renderInitialTransferPreview(leftKind, leftValue, rightKind, rightValue string) string {
|
|
if leftValue != "" {
|
|
if folder, err := firstTransferFolder(leftKind, leftValue); err == nil {
|
|
return renderTransferPreview(leftKind, leftValue, folder, 0)
|
|
}
|
|
}
|
|
if rightValue != "" {
|
|
if folder, err := firstTransferFolder(rightKind, rightValue); err == nil {
|
|
return renderTransferPreview(rightKind, rightValue, folder, 0)
|
|
}
|
|
}
|
|
return renderEmptyPreviewPane("Nachricht links oder rechts waehlen.")
|
|
}
|
|
|
|
func transferEmptyReason(kind string, err error) string {
|
|
msg := err.Error()
|
|
if kind == "archive" && strings.Contains(msg, "Keine Ordner") {
|
|
return "Diese Archiv-MBox ist noch leer."
|
|
}
|
|
return msg
|
|
}
|
|
|
|
func firstTransferFolder(kind, value string) (string, error) {
|
|
switch kind {
|
|
case "archive":
|
|
mboxes, _ := filepath.Glob(filepath.Join(Cfg.MboxRoot, value, "*.mbox"))
|
|
infos := archiveFolderInfos(mboxes)
|
|
if len(infos) == 0 {
|
|
return "", fmt.Errorf("Keine Ordner.")
|
|
}
|
|
for _, folder := range infos {
|
|
if strings.EqualFold(folder.Name, "INBOX") {
|
|
return folder.Name, nil
|
|
}
|
|
}
|
|
return infos[0].Name, nil
|
|
case "target":
|
|
account, err := GetAccount(value)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return firstTargetFolder(account)
|
|
default:
|
|
account, err := GetAccount(value)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return firstSourceFolder(account)
|
|
}
|
|
}
|
|
|
|
func renderTransferMessageList(side, kind, value, folder string) string {
|
|
var b strings.Builder
|
|
b.WriteString(`<div class="pane-head">Nachrichten</div>`)
|
|
switch kind {
|
|
case "archive":
|
|
path, err := mboxPath(value, folder)
|
|
if err != nil {
|
|
fmt.Fprintf(&b, `<div class="ef-empty">%s</div>`, html.EscapeString(err.Error()))
|
|
return b.String()
|
|
}
|
|
entries, err := ReadMboxList(path)
|
|
if err != nil {
|
|
fmt.Fprintf(&b, `<div class="ef-empty">%s</div>`, html.EscapeString(err.Error()))
|
|
return b.String()
|
|
}
|
|
for _, entry := range entries {
|
|
subject := entry.Subject
|
|
if subject == "" {
|
|
subject = "(ohne Betreff)"
|
|
}
|
|
from := entry.From
|
|
if from == "" {
|
|
from = "(ohne Absender)"
|
|
}
|
|
active := ""
|
|
if entry.Index == 0 {
|
|
active = " active"
|
|
}
|
|
fmt.Fprintf(&b, `<a class="msg-row%s" href="#" hx-get="/transfer/message?side=%s&kind=%s&value=%s&folder=%s&index=%d" hx-target="#transfer-preview" hx-swap="innerHTML"><span class="msg-from">%s</span><span class="msg-subject">%s</span><span class="msg-date">%s</span></a>`,
|
|
active, side, html.EscapeString(kind), urlEsc(value), urlEsc(folder), entry.Index, html.EscapeString(from), html.EscapeString(subject), html.EscapeString(entry.Date))
|
|
}
|
|
if len(entries) == 0 {
|
|
b.WriteString(`<div class="ef-empty">Keine Nachrichten.</div>`)
|
|
}
|
|
case "target":
|
|
account, err := GetAccount(value)
|
|
if err != nil {
|
|
fmt.Fprintf(&b, `<div class="ef-empty">%s</div>`, html.EscapeString(err.Error()))
|
|
return b.String()
|
|
}
|
|
messages, err := fetchTargetFolderMessages(account, folder)
|
|
if err != nil {
|
|
fmt.Fprintf(&b, `<div class="ef-empty">%s</div>`, html.EscapeString(err.Error()))
|
|
return b.String()
|
|
}
|
|
renderTransferRawMessages(&b, side, kind, value, folder, messages)
|
|
default:
|
|
account, err := GetAccount(value)
|
|
if err != nil {
|
|
fmt.Fprintf(&b, `<div class="ef-empty">%s</div>`, html.EscapeString(err.Error()))
|
|
return b.String()
|
|
}
|
|
messages, err := fetchSourceFolderMessages(account, folder)
|
|
if err != nil {
|
|
fmt.Fprintf(&b, `<div class="ef-empty">%s</div>`, html.EscapeString(err.Error()))
|
|
return b.String()
|
|
}
|
|
renderTransferRawMessages(&b, side, kind, value, folder, messages)
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
func renderTransferRawMessages(b *strings.Builder, side, kind, value, folder string, messages []RawMessage) {
|
|
for i, raw := range messages {
|
|
subject := "(ohne Betreff)"
|
|
from := "(ohne Absender)"
|
|
date := ""
|
|
if msg, err := mail.ReadMessage(bytes.NewReader(raw.Body)); err == nil {
|
|
if v := decodeHeader(msg.Header.Get("Subject")); v != "" {
|
|
subject = v
|
|
}
|
|
if v := decodeHeader(msg.Header.Get("From")); v != "" {
|
|
from = v
|
|
}
|
|
date = decodeHeader(msg.Header.Get("Date"))
|
|
}
|
|
active := ""
|
|
if i == 0 {
|
|
active = " active"
|
|
}
|
|
fmt.Fprintf(b, `<a class="msg-row%s" href="#" hx-get="/transfer/message?side=%s&kind=%s&value=%s&folder=%s&index=%d" hx-target="#transfer-preview" hx-swap="innerHTML"><span class="msg-from">%s</span><span class="msg-subject">%s</span><span class="msg-date">%s</span></a>`,
|
|
active, side, html.EscapeString(kind), urlEsc(value), urlEsc(folder), i, html.EscapeString(from), html.EscapeString(subject), html.EscapeString(date))
|
|
}
|
|
if len(messages) == 0 {
|
|
b.WriteString(`<div class="ef-empty">Keine Nachrichten.</div>`)
|
|
}
|
|
}
|
|
|
|
func renderTransferPreview(kind, value, folder string, index int) string {
|
|
raw, label, err := transferRawMessage(kind, value, folder, index)
|
|
if err != nil {
|
|
return renderEmptyPreviewPane(err.Error())
|
|
}
|
|
msg, err := mail.ReadMessage(bytes.NewReader(raw))
|
|
var b strings.Builder
|
|
if err != nil {
|
|
renderMailboxReadPane(&b, "Vorschau", label, folder, index, "(unlesbar)", "", "", string(raw))
|
|
return b.String()
|
|
}
|
|
renderMailboxReadPane(&b, "Vorschau", label, folder, index,
|
|
decodeHeader(msg.Header.Get("Subject")),
|
|
decodeHeader(msg.Header.Get("From")),
|
|
decodeHeader(msg.Header.Get("Date")),
|
|
messageBody(raw),
|
|
)
|
|
return b.String()
|
|
}
|
|
|
|
func transferRawMessage(kind, value, folder string, index int) ([]byte, string, error) {
|
|
switch kind {
|
|
case "archive":
|
|
path, err := mboxPath(value, folder)
|
|
if err != nil {
|
|
return nil, value, err
|
|
}
|
|
raw, err := ReadMboxMessage(path, index)
|
|
return raw, value, err
|
|
case "target":
|
|
account, err := GetAccount(value)
|
|
if err != nil {
|
|
return nil, value, err
|
|
}
|
|
messages, err := fetchTargetFolderMessages(account, folder)
|
|
if err != nil {
|
|
return nil, account.DstUser, err
|
|
}
|
|
if index < 0 || index >= len(messages) {
|
|
return nil, account.DstUser, fmt.Errorf("Nachricht waehlen.")
|
|
}
|
|
return messages[index].Body, account.DstUser, nil
|
|
default:
|
|
account, err := GetAccount(value)
|
|
if err != nil {
|
|
return nil, value, err
|
|
}
|
|
messages, err := fetchSourceFolderMessages(account, folder)
|
|
if err != nil {
|
|
return nil, account.SrcUser, err
|
|
}
|
|
if index < 0 || index >= len(messages) {
|
|
return nil, account.SrcUser, fmt.Errorf("Nachricht waehlen.")
|
|
}
|
|
return messages[index].Body, account.SrcUser, nil
|
|
}
|
|
}
|
|
|
|
func renderSourceEmailBoxPage(w http.ResponseWriter, r *http.Request, accounts []Account, selected string) {
|
|
user := CurrentUser(r)
|
|
selectedAccount := selectedSourceAccount(accounts, selected)
|
|
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>Quell-Postfach - Mail-Graveyard</title>
|
|
<link rel="stylesheet" href="/static/style.css?v=20260713-6">
|
|
<script src="/static/htmx.min.js"></script>
|
|
<script src="/static/app.js?v=20260713-6" 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">
|
|
%s
|
|
%s
|
|
</nav>
|
|
%s
|
|
</header>
|
|
<aside class="backstage-menu" aria-label="Mail-Graveyard-Menue">
|
|
<a class="backstage-item" href="/view">Archiv-Boxen</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>
|
|
<a class="backstage-item transfer-sub" href="/transfer/postfach-archiv">Postfach <--> Archiv-MBox</a>
|
|
<a class="backstage-item transfer-sub" href="/transfer/archiv-postfach">Archiv-MBox <--> Postfach</a>
|
|
<a class="backstage-item transfer-sub" href="/transfer/postfach-postfach">Postfach <--> Postfach</a>
|
|
<a class="backstage-item transfer-sub" href="/transfer/archiv-archiv">Archiv-MBox <--> Archiv-MBox</a>
|
|
<a class="backstage-item" href="/users">Benutzerverwaltung</a>
|
|
<a class="backstage-item" href="/logout">Logout</a>
|
|
</aside>
|
|
<button class="backstage-shade" type="button" aria-label="Menue schliessen" data-backstage-close></button>
|
|
<div class="three-pane email-box-view">
|
|
<aside class="pane pane-tree" id="source-tree">%s</aside>
|
|
<div class="pane-resizer" data-resize-index="0"></div>
|
|
<section class="pane pane-list" id="list">%s</section>
|
|
<div class="pane-resizer" data-resize-index="1"></div>
|
|
<section class="pane pane-read" id="read">%s</section>
|
|
</div>
|
|
<footer class="statusbar"><span id="mig-status" hx-get="/migrate/status" hx-trigger="load,every 3s" hx-swap="innerHTML">bereit</span></footer></body></html>`,
|
|
renderRibbonEmailBoxMenu("source"), renderRibbonTransferMenu(""), renderRibbonActions(false, user), renderEmailBoxSourcePane(accounts, selectedAccount.Name), renderInitialSourceMailboxList(selectedAccount), renderInitialSourceMailboxRead(selectedAccount))
|
|
}
|
|
|
|
func renderTargetEmailBoxPage(w http.ResponseWriter, r *http.Request, accounts []Account, selected string) {
|
|
user := CurrentUser(r)
|
|
selectedAccount := selectedTargetAccount(accounts, selected)
|
|
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>Ziel-Postfach - Mail-Graveyard</title>
|
|
<link rel="stylesheet" href="/static/style.css?v=20260713-6">
|
|
<script src="/static/htmx.min.js"></script>
|
|
<script src="/static/app.js?v=20260713-6" 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">
|
|
%s
|
|
%s
|
|
</nav>
|
|
%s
|
|
</header>
|
|
<aside class="backstage-menu" aria-label="Mail-Graveyard-Menue">
|
|
<a class="backstage-item" href="/view">Archiv-Boxen</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>
|
|
<a class="backstage-item transfer-sub" href="/transfer/postfach-archiv">Postfach <--> Archiv-MBox</a>
|
|
<a class="backstage-item transfer-sub" href="/transfer/archiv-postfach">Archiv-MBox <--> Postfach</a>
|
|
<a class="backstage-item transfer-sub" href="/transfer/postfach-postfach">Postfach <--> Postfach</a>
|
|
<a class="backstage-item transfer-sub" href="/transfer/archiv-archiv">Archiv-MBox <--> Archiv-MBox</a>
|
|
<a class="backstage-item" href="/users">Benutzerverwaltung</a>
|
|
<a class="backstage-item" href="/logout">Logout</a>
|
|
</aside>
|
|
<button class="backstage-shade" type="button" aria-label="Menue schliessen" data-backstage-close></button>
|
|
<div class="three-pane email-box-view email-box-target-view">
|
|
<section class="pane pane-list" id="list">%s</section>
|
|
<div class="pane-resizer" data-resize-index="0"></div>
|
|
<section class="pane pane-read" id="read">%s</section>
|
|
<div class="pane-resizer" data-resize-index="1"></div>
|
|
<aside class="pane pane-tree" id="target-tree">%s</aside>
|
|
</div>
|
|
<footer class="statusbar"><span id="mig-status" hx-get="/migrate/status" hx-trigger="load,every 3s" hx-swap="innerHTML">bereit</span></footer></body></html>`,
|
|
renderRibbonEmailBoxMenu("target"), renderRibbonTransferMenu(""), renderRibbonActions(false, user), renderInitialTargetMailboxList(selectedAccount), renderInitialTargetMailboxRead(selectedAccount), renderEmailBoxTargetPane(accounts, selectedAccount.Name))
|
|
}
|
|
|
|
func renderEmailBoxSourcePane(accounts []Account, selected string) string {
|
|
var b strings.Builder
|
|
sourceAccounts := accountsBySource(accounts)
|
|
if len(sourceAccounts) == 0 {
|
|
b.WriteString(`<div class="pane-head archive-head"><span>Quell-Postfach</span></div><div class="ef-empty">Keine Quell-Postfaecher eingetragen.</div>`)
|
|
return b.String()
|
|
}
|
|
selectedAccount := selectedSourceAccount(accounts, selected)
|
|
b.WriteString(`<div class="pane-head archive-head"><span>Quell-Postfach</span><form class="archive-select-form" hx-get="/boxes/source" hx-target="#source-tree" hx-swap="innerHTML" hx-trigger="change from:#source-mailbox-select"><select id="source-mailbox-select" class="archive-select" name="source">`)
|
|
for _, a := range sourceAccounts {
|
|
label := fmt.Sprintf("%s (%s)", a.SrcUser, strings.ToUpper(accountProto(a)))
|
|
sel := ""
|
|
if a.Name == selectedAccount.Name {
|
|
sel = ` selected`
|
|
}
|
|
fmt.Fprintf(&b, `<option value="%s"%s>%s</option>`, html.EscapeString(a.Name), sel, html.EscapeString(label))
|
|
}
|
|
b.WriteString(`</select></form></div>`)
|
|
renderSourceFolders(&b, selectedAccount)
|
|
return b.String()
|
|
}
|
|
|
|
func selectedSourceAccount(accounts []Account, selected string) Account {
|
|
sourceAccounts := accountsBySource(accounts)
|
|
if len(sourceAccounts) == 0 {
|
|
return Account{}
|
|
}
|
|
if selected != "" {
|
|
for _, a := range sourceAccounts {
|
|
if a.Name == selected {
|
|
return a
|
|
}
|
|
}
|
|
}
|
|
return sourceAccounts[0]
|
|
}
|
|
|
|
func renderSourceFolders(b *strings.Builder, account Account) {
|
|
fmt.Fprintf(b, `<div class="tree-node account">%s</div>`, html.EscapeString(account.SrcUser))
|
|
if accountProto(account) == "pop3" {
|
|
b.WriteString(`<div class="tree-node folder"><span class="tree-label">INBOX</span><span class="tree-count">POP3</span></div>`)
|
|
return
|
|
}
|
|
src, err := OpenIMAPSource(account)
|
|
if err != nil {
|
|
fmt.Fprintf(b, `<div class="ef-empty">Ordner konnten nicht geladen werden: %s</div>`, html.EscapeString(err.Error()))
|
|
return
|
|
}
|
|
defer src.Close()
|
|
folders, err := src.Folders()
|
|
if err != nil {
|
|
fmt.Fprintf(b, `<div class="ef-empty">Ordner konnten nicht geladen werden: %s</div>`, html.EscapeString(err.Error()))
|
|
return
|
|
}
|
|
if len(folders) == 0 {
|
|
b.WriteString(`<div class="ef-empty">Keine Ordner in diesem Quell-Postfach.</div>`)
|
|
return
|
|
}
|
|
for _, folder := range folders {
|
|
fmt.Fprintf(b, `<a class="tree-node folder" href="#" title="%s" hx-get="/boxes/source/messages?source=%s&folder=%s" hx-target="#list" hx-swap="innerHTML"><span class="tree-label">%s</span><span class="tree-count">%s</span></a>`,
|
|
html.EscapeString(strings.Join(folder.Attrs, " ")), urlEsc(account.Name), urlEsc(folder.Name), html.EscapeString(folder.Name), html.EscapeString(sourceFolderRole(folder)))
|
|
}
|
|
}
|
|
|
|
func sourceFolderRole(folder Folder) string {
|
|
for _, attr := range folder.Attrs {
|
|
switch strings.ToLower(attr) {
|
|
case `\sent`:
|
|
return "Gesendet"
|
|
case `\drafts`:
|
|
return "Entwurf"
|
|
case `\trash`:
|
|
return "Papierkorb"
|
|
case `\junk`:
|
|
return "Spam"
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func fetchSourceFolderMessages(account Account, folder string) ([]RawMessage, error) {
|
|
if accountProto(account) == "pop3" {
|
|
src, err := OpenPOP3Source(account)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer src.Close()
|
|
return src.Fetch(folder)
|
|
}
|
|
src, err := OpenIMAPSource(account)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer src.Close()
|
|
return src.Fetch(folder)
|
|
}
|
|
|
|
func renderInitialSourceMailboxList(account Account) string {
|
|
if account.Name == "" {
|
|
return `<div class="pane-head">Nachrichten</div><div class="ef-empty">Quell-Postfach waehlen.</div>`
|
|
}
|
|
folder, err := firstSourceFolder(account)
|
|
if err != nil {
|
|
return fmt.Sprintf(`<div class="pane-head">Nachrichten</div><div class="ef-empty">%s</div>`, html.EscapeString(err.Error()))
|
|
}
|
|
messages, err := fetchSourceFolderMessages(account, folder)
|
|
if err != nil {
|
|
return fmt.Sprintf(`<div class="pane-head">Nachrichten</div><div class="ef-empty">%s</div>`, html.EscapeString(err.Error()))
|
|
}
|
|
var b strings.Builder
|
|
renderSourceMailboxMessageList(&b, account, folder, messages)
|
|
return b.String()
|
|
}
|
|
|
|
func renderInitialSourceMailboxRead(account Account) string {
|
|
if account.Name == "" {
|
|
return `<div class="ef-empty">Nachricht waehlen.</div>`
|
|
}
|
|
folder, err := firstSourceFolder(account)
|
|
if err != nil {
|
|
return `<div class="ef-empty">Nachricht waehlen.</div>`
|
|
}
|
|
messages, err := fetchSourceFolderMessages(account, folder)
|
|
if err != nil {
|
|
return `<div class="ef-empty">Nachricht waehlen.</div>`
|
|
}
|
|
return renderSourceMailboxRead(account, folder, 0, messages)
|
|
}
|
|
|
|
func firstSourceFolder(account Account) (string, error) {
|
|
if accountProto(account) == "pop3" {
|
|
return "INBOX", nil
|
|
}
|
|
src, err := OpenIMAPSource(account)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer src.Close()
|
|
folders, err := src.Folders()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
for _, folder := range folders {
|
|
if strings.EqualFold(folder.Name, "INBOX") {
|
|
return folder.Name, nil
|
|
}
|
|
}
|
|
if len(folders) == 0 {
|
|
return "", fmt.Errorf("Keine Ordner in diesem Quell-Postfach.")
|
|
}
|
|
return folders[0].Name, nil
|
|
}
|
|
|
|
func renderSourceMailboxMessageList(w io.Writer, account Account, folder string, messages []RawMessage) {
|
|
fmt.Fprint(w, `<div class="pane-head">Nachrichten</div>`)
|
|
for i, raw := range messages {
|
|
subject := "(ohne Betreff)"
|
|
from := "(ohne Absender)"
|
|
date := ""
|
|
if msg, err := mail.ReadMessage(bytes.NewReader(raw.Body)); err == nil {
|
|
if v := decodeHeader(msg.Header.Get("Subject")); v != "" {
|
|
subject = v
|
|
}
|
|
if v := decodeHeader(msg.Header.Get("From")); v != "" {
|
|
from = v
|
|
}
|
|
date = decodeHeader(msg.Header.Get("Date"))
|
|
}
|
|
active := ""
|
|
if i == 0 {
|
|
active = " active"
|
|
}
|
|
fmt.Fprintf(w, `<a class="msg-row%s" href="#" draggable="true" data-source="%s" data-folder="%s" data-index="%d" hx-get="/boxes/source/message?source=%s&folder=%s&index=%d" hx-target="#read" hx-swap="innerHTML"><span class="msg-from">%s</span><span class="msg-subject">%s</span><span class="msg-date">%s</span></a>`,
|
|
active,
|
|
html.EscapeString(account.Name), html.EscapeString(folder), i,
|
|
urlEsc(account.Name), urlEsc(folder), i,
|
|
html.EscapeString(from), html.EscapeString(subject), html.EscapeString(date))
|
|
}
|
|
if len(messages) == 0 {
|
|
fmt.Fprint(w, `<div class="ef-empty">Keine Nachrichten in diesem Quell-Ordner.</div>`)
|
|
}
|
|
}
|
|
|
|
func renderSourceMailboxRead(account Account, folder string, index int, messages []RawMessage) string {
|
|
if index < 0 || index >= len(messages) {
|
|
return `<div class="ef-empty">Nachricht waehlen.</div>`
|
|
}
|
|
raw := messages[index].Body
|
|
msg, err := mail.ReadMessage(bytes.NewReader(raw))
|
|
var b strings.Builder
|
|
if err != nil {
|
|
renderMailboxReadPane(&b, "Quelle", account.Name, folder, index, "(unlesbar)", "", "", string(raw))
|
|
return b.String()
|
|
}
|
|
renderMailboxReadPane(&b, "Quelle", account.Name, folder, index,
|
|
decodeHeader(msg.Header.Get("Subject")),
|
|
decodeHeader(msg.Header.Get("From")),
|
|
decodeHeader(msg.Header.Get("Date")),
|
|
messageBody(raw),
|
|
)
|
|
return b.String()
|
|
}
|
|
|
|
func renderEmailBoxTargetPane(accounts []Account, selected string) string {
|
|
var b strings.Builder
|
|
targetAccounts := accountsByTarget(accounts)
|
|
if len(targetAccounts) == 0 {
|
|
b.WriteString(`<div class="pane-head archive-head"><span>Ziel-Postfach</span></div><div class="ef-empty">Keine Ziel-Postfaecher eingetragen.</div>`)
|
|
return b.String()
|
|
}
|
|
selectedAccount := selectedTargetAccount(accounts, selected)
|
|
b.WriteString(`<div class="pane-head archive-head"><span>Ziel-Postfach</span><form class="archive-select-form" hx-get="/boxes/target" hx-target="#target-tree" hx-swap="innerHTML" hx-trigger="change from:#target-mailbox-select"><select id="target-mailbox-select" class="archive-select" name="target">`)
|
|
for _, a := range targetAccounts {
|
|
sel := ""
|
|
if a.Name == selectedAccount.Name {
|
|
sel = ` selected`
|
|
}
|
|
fmt.Fprintf(&b, `<option value="%s"%s>%s</option>`, html.EscapeString(a.Name), sel, html.EscapeString(a.DstUser))
|
|
}
|
|
b.WriteString(`</select></form></div>`)
|
|
renderTargetFolders(&b, selectedAccount)
|
|
return b.String()
|
|
}
|
|
|
|
func selectedTargetAccount(accounts []Account, selected string) Account {
|
|
targetAccounts := accountsByTarget(accounts)
|
|
if len(targetAccounts) == 0 {
|
|
return Account{}
|
|
}
|
|
if selected != "" {
|
|
for _, a := range targetAccounts {
|
|
if a.Name == selected {
|
|
return a
|
|
}
|
|
}
|
|
}
|
|
return targetAccounts[0]
|
|
}
|
|
|
|
func renderTargetFolders(b *strings.Builder, account Account) {
|
|
fmt.Fprintf(b, `<div class="tree-node account">%s</div>`, html.EscapeString(account.DstUser))
|
|
dst, err := OpenIMAPTarget(account)
|
|
if err != nil {
|
|
fmt.Fprintf(b, `<div class="ef-empty">Ordner konnten nicht geladen werden: %s</div>`, html.EscapeString(err.Error()))
|
|
return
|
|
}
|
|
defer dst.Close()
|
|
folders, err := dst.Folders()
|
|
if err != nil {
|
|
fmt.Fprintf(b, `<div class="ef-empty">Ordner konnten nicht geladen werden: %s</div>`, html.EscapeString(err.Error()))
|
|
return
|
|
}
|
|
if len(folders) == 0 {
|
|
b.WriteString(`<div class="ef-empty">Keine Ordner in diesem Ziel-Postfach.</div>`)
|
|
return
|
|
}
|
|
for _, folder := range folders {
|
|
fmt.Fprintf(b, `<a class="tree-node folder" href="#" title="%s" hx-get="/boxes/target/messages?target=%s&folder=%s" hx-target="#list" hx-swap="innerHTML"><span class="tree-label">%s</span><span class="tree-count">%s</span></a>`,
|
|
html.EscapeString(strings.Join(folder.Attrs, " ")), urlEsc(account.Name), urlEsc(folder.Name), html.EscapeString(folder.Name), html.EscapeString(targetFolderRole(folder)))
|
|
}
|
|
}
|
|
|
|
func targetFolderRole(folder TargetFolder) string {
|
|
for _, attr := range folder.Attrs {
|
|
switch strings.ToLower(attr) {
|
|
case `\sent`:
|
|
return "Gesendet"
|
|
case `\drafts`:
|
|
return "Entwurf"
|
|
case `\trash`:
|
|
return "Papierkorb"
|
|
case `\junk`:
|
|
return "Spam"
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func fetchTargetFolderMessages(account Account, folder string) ([]RawMessage, error) {
|
|
dst, err := OpenIMAPTarget(account)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer dst.Close()
|
|
return dst.Fetch(folder)
|
|
}
|
|
|
|
func renderInitialTargetMailboxList(account Account) string {
|
|
if account.Name == "" {
|
|
return `<div class="pane-head">Nachrichten</div><div class="ef-empty">Ziel-Postfach waehlen.</div>`
|
|
}
|
|
folder, err := firstTargetFolder(account)
|
|
if err != nil {
|
|
return fmt.Sprintf(`<div class="pane-head">Nachrichten</div><div class="ef-empty">%s</div>`, html.EscapeString(err.Error()))
|
|
}
|
|
messages, err := fetchTargetFolderMessages(account, folder)
|
|
if err != nil {
|
|
return fmt.Sprintf(`<div class="pane-head">Nachrichten</div><div class="ef-empty">%s</div>`, html.EscapeString(err.Error()))
|
|
}
|
|
var b strings.Builder
|
|
renderTargetMailboxMessageList(&b, account, folder, messages)
|
|
return b.String()
|
|
}
|
|
|
|
func renderInitialTargetMailboxRead(account Account) string {
|
|
if account.Name == "" {
|
|
return `<div class="ef-empty">Nachricht waehlen.</div>`
|
|
}
|
|
folder, err := firstTargetFolder(account)
|
|
if err != nil {
|
|
return `<div class="ef-empty">Nachricht waehlen.</div>`
|
|
}
|
|
messages, err := fetchTargetFolderMessages(account, folder)
|
|
if err != nil {
|
|
return `<div class="ef-empty">Nachricht waehlen.</div>`
|
|
}
|
|
return renderTargetMailboxRead(account, folder, 0, messages)
|
|
}
|
|
|
|
func firstTargetFolder(account Account) (string, error) {
|
|
dst, err := OpenIMAPTarget(account)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer dst.Close()
|
|
folders, err := dst.Folders()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
for _, folder := range folders {
|
|
if strings.EqualFold(folder.Name, "INBOX") {
|
|
return folder.Name, nil
|
|
}
|
|
}
|
|
if len(folders) == 0 {
|
|
return "", fmt.Errorf("Keine Ordner in diesem Ziel-Postfach.")
|
|
}
|
|
return folders[0].Name, nil
|
|
}
|
|
|
|
func renderTargetMailboxMessageList(w io.Writer, account Account, folder string, messages []RawMessage) {
|
|
fmt.Fprint(w, `<div class="pane-head">Nachrichten</div>`)
|
|
for i, raw := range messages {
|
|
subject := "(ohne Betreff)"
|
|
from := "(ohne Absender)"
|
|
date := ""
|
|
if msg, err := mail.ReadMessage(bytes.NewReader(raw.Body)); err == nil {
|
|
if v := decodeHeader(msg.Header.Get("Subject")); v != "" {
|
|
subject = v
|
|
}
|
|
if v := decodeHeader(msg.Header.Get("From")); v != "" {
|
|
from = v
|
|
}
|
|
date = decodeHeader(msg.Header.Get("Date"))
|
|
}
|
|
active := ""
|
|
if i == 0 {
|
|
active = " active"
|
|
}
|
|
fmt.Fprintf(w, `<a class="msg-row%s" href="#" draggable="true" data-target="%s" data-folder="%s" data-index="%d" hx-get="/boxes/target/message?target=%s&folder=%s&index=%d" hx-target="#read" hx-swap="innerHTML"><span class="msg-from">%s</span><span class="msg-subject">%s</span><span class="msg-date">%s</span></a>`,
|
|
active,
|
|
html.EscapeString(account.Name), html.EscapeString(folder), i,
|
|
urlEsc(account.Name), urlEsc(folder), i,
|
|
html.EscapeString(from), html.EscapeString(subject), html.EscapeString(date))
|
|
}
|
|
if len(messages) == 0 {
|
|
fmt.Fprint(w, `<div class="ef-empty">Keine Nachrichten in diesem Ziel-Ordner.</div>`)
|
|
}
|
|
}
|
|
|
|
func renderTargetMailboxRead(account Account, folder string, index int, messages []RawMessage) string {
|
|
if index < 0 || index >= len(messages) {
|
|
return `<div class="ef-empty">Nachricht waehlen.</div>`
|
|
}
|
|
raw := messages[index].Body
|
|
msg, err := mail.ReadMessage(bytes.NewReader(raw))
|
|
var b strings.Builder
|
|
if err != nil {
|
|
renderMailboxReadPane(&b, "Ziel", account.Name, folder, index, "(unlesbar)", "", "", string(raw))
|
|
return b.String()
|
|
}
|
|
renderMailboxReadPane(&b, "Ziel", account.Name, folder, index,
|
|
decodeHeader(msg.Header.Get("Subject")),
|
|
decodeHeader(msg.Header.Get("From")),
|
|
decodeHeader(msg.Header.Get("Date")),
|
|
messageBody(raw),
|
|
)
|
|
return b.String()
|
|
}
|
|
|
|
func renderMailboxReadPane(w io.Writer, title, account, folder string, index int, subject, from, date, body string) {
|
|
if subject == "" {
|
|
subject = "(ohne Betreff)"
|
|
}
|
|
fmt.Fprint(w, `<div class="pane-head mode-head"><span class="mode-label">Vorschau</span><span></span><button class="preview-popout" type="button" title="In modalem Fenster oeffnen" aria-label="In modalem Fenster oeffnen" data-open-mail-modal>⛶</button></div>`)
|
|
fmt.Fprintf(w, `<div class="read-body"><div class="read-head"><h1 class="read-subject">%s</h1><div class="read-meta">Von: %s<br>Datum: %s<br>Postfach: %s / %s / #%d</div></div><pre class="read-pre">%s</pre></div>`,
|
|
html.EscapeString(subject), html.EscapeString(from), html.EscapeString(date), html.EscapeString(account), html.EscapeString(folder), index+1, html.EscapeString(body))
|
|
}
|
|
|
|
func renderEmptyPreviewPane(message string) string {
|
|
return fmt.Sprintf(`<div class="pane-head mode-head"><span class="mode-label">Vorschau</span></div><div class="read-body"><div class="ef-empty">%s</div></div>`, html.EscapeString(message))
|
|
}
|
|
|
|
func accountsByTarget(accounts []Account) []Account {
|
|
seen := map[string]bool{}
|
|
out := make([]Account, 0, len(accounts))
|
|
for _, a := range accounts {
|
|
if strings.TrimSpace(a.DstUser) == "" || seen[a.DstUser] {
|
|
continue
|
|
}
|
|
seen[a.DstUser] = true
|
|
out = append(out, a)
|
|
}
|
|
for i := 0; i < len(out); i++ {
|
|
for j := i + 1; j < len(out); j++ {
|
|
if strings.ToLower(out[j].DstUser) < strings.ToLower(out[i].DstUser) {
|
|
out[i], out[j] = out[j], out[i]
|
|
}
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func renderEmailBoxTargetPaneLegacy(accounts []Account) string {
|
|
var b strings.Builder
|
|
b.WriteString(`<div class="pane-head archive-head"><span>Ziel-Postfach</span><form class="archive-select-form"><select class="archive-select" name="target">`)
|
|
targets := uniqueAccountValues(accounts, "target")
|
|
if len(targets) == 0 {
|
|
b.WriteString(`<option>Keine Ziele</option>`)
|
|
}
|
|
for _, target := range targets {
|
|
fmt.Fprintf(&b, `<option value="%s">%s</option>`, html.EscapeString(target), html.EscapeString(target))
|
|
}
|
|
b.WriteString(`</select></form></div>`)
|
|
if len(targets) == 0 {
|
|
b.WriteString(`<div class="ef-empty">Keine Ziel-Postfaecher eingetragen.</div>`)
|
|
return b.String()
|
|
}
|
|
for _, target := range targets {
|
|
fmt.Fprintf(&b, `<div class="tree-node account">%s</div>`, html.EscapeString(target))
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
func accountsBySource(accounts []Account) []Account {
|
|
out := append([]Account{}, accounts...)
|
|
for i := 0; i < len(out); i++ {
|
|
for j := i + 1; j < len(out); j++ {
|
|
left := strings.ToLower(out[i].SrcUser)
|
|
right := strings.ToLower(out[j].SrcUser)
|
|
if right < left {
|
|
out[i], out[j] = out[j], out[i]
|
|
}
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func accountProto(a Account) string {
|
|
if strings.TrimSpace(a.SrcProto) != "" {
|
|
return a.SrcProto
|
|
}
|
|
return "imap"
|
|
}
|
|
|
|
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"},
|
|
{Slug: "archiv-archiv", Title: "Archiv-MBox <--> Archiv-MBox"},
|
|
}
|
|
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 renderRibbonEmailBoxMenu(active string) string {
|
|
items := []transferMode{
|
|
{Slug: "archive", Title: "Archiv-Postfach"},
|
|
{Slug: "source", Title: "Quell-Postfach"},
|
|
{Slug: "target", Title: "Ziel-Postfach"},
|
|
}
|
|
paths := map[string]string{
|
|
"archive": "/view",
|
|
"source": "/boxes/source",
|
|
"target": "/boxes/target",
|
|
}
|
|
buttonClass := "tab ribbon-menu-button"
|
|
if active != "" {
|
|
if active == "home" {
|
|
buttonClass = "tab ribbon-menu-button"
|
|
} else {
|
|
buttonClass += " active"
|
|
}
|
|
}
|
|
var b strings.Builder
|
|
homeClass := "tab app-tab"
|
|
if active == "home" {
|
|
homeClass += " active"
|
|
}
|
|
fmt.Fprintf(&b, `<a class="%s" href="/">Mail-Graveyard</a>`, homeClass)
|
|
fmt.Fprintf(&b, `<div class="ribbon-menu"><button class="%s" type="button" aria-expanded="false" data-ribbon-menu-toggle>Email-Box</button><div class="ribbon-dropdown">`, buttonClass)
|
|
for _, item := range items {
|
|
class := "ribbon-dropdown-item"
|
|
if item.Slug == active {
|
|
class += " active"
|
|
}
|
|
fmt.Fprintf(&b, `<a class="%s" href="%s">%s</a>`, class, paths[item.Slug], html.EscapeString(item.Title))
|
|
}
|
|
b.WriteString(`</div></div>`)
|
|
return b.String()
|
|
}
|
|
|
|
func renderRibbonActions(settingsActive bool, user AppUser) string {
|
|
settingsClass := "ribbon-icon"
|
|
if settingsActive {
|
|
settingsClass += " active"
|
|
}
|
|
return fmt.Sprintf(`<div class="ribbon-spacer"></div><span class="ribbon-user" title="%s">%s</span><a class="%s" href="/users" title="Einstellungen" aria-label="Einstellungen">⚙</a><a class="ribbon-icon" href="/logout" title="Logout" aria-label="Logout">⏻</a>`,
|
|
html.EscapeString(user.Username), html.EscapeString(userDisplayName(user)), settingsClass)
|
|
}
|
|
|
|
func renderRibbonTransferMenu(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"},
|
|
{Slug: "archiv-archiv", Title: "Archiv-MBox <--> Archiv-MBox"},
|
|
}
|
|
buttonClass := "tab ribbon-menu-button"
|
|
if active != "" {
|
|
buttonClass += " active"
|
|
}
|
|
var b strings.Builder
|
|
fmt.Fprintf(&b, `<div class="ribbon-menu"><button class="%s" type="button" aria-expanded="false" data-ribbon-menu-toggle>Mail-Transfer</button><div class="ribbon-dropdown">`, buttonClass)
|
|
for _, item := range items {
|
|
class := "ribbon-dropdown-item"
|
|
if item.Slug == active {
|
|
class += " active"
|
|
}
|
|
fmt.Fprintf(&b, `<a class="%s" href="/transfer/%s">%s</a>`, class, item.Slug, html.EscapeString(item.Title))
|
|
}
|
|
b.WriteString(`</div></div>`)
|
|
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">`)
|
|
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>`)
|
|
return
|
|
}
|
|
fmt.Fprint(w, `<table class="account-table"><thead><tr><th>Name</th><th>Original</th><th>Archiv-mbox</th><th>Ziel</th><th>Status</th><th class="actions-col"></th></tr></thead><tbody>`)
|
|
for _, a := range accounts {
|
|
activeClass := ""
|
|
if a.Name == editName {
|
|
activeClass = ` class="selected"`
|
|
}
|
|
status := "aktiv"
|
|
if !a.Active {
|
|
status = "inaktiv"
|
|
}
|
|
fmt.Fprintf(w, `<tr%s><td><a href="/accounts?edit=%s">%s</a></td><td>%s</td><td>%s</td><td>%s</td><td>%s</td><td class="row-actions">
|
|
<form method="post" action="/accounts/test"><input type="hidden" name="name" value="%s"><button class="btn compact secondary" type="submit">Test</button></form>
|
|
<form method="post" action="/migrate/run"><input type="hidden" name="name" value="%s"><button class="btn compact secondary" type="submit">Umzug</button></form>
|
|
<form method="post" action="/accounts/delete"><input type="hidden" name="name" value="%s"><button class="btn compact secondary danger" type="submit">Loeschen</button></form>
|
|
</td></tr>`,
|
|
activeClass, urlQuery(a.Name), html.EscapeString(a.Name), html.EscapeString(a.SrcUser),
|
|
html.EscapeString(accountMboxDir(a)), html.EscapeString(a.DstUser), status,
|
|
html.EscapeString(a.Name), html.EscapeString(a.Name), html.EscapeString(a.Name))
|
|
}
|
|
fmt.Fprint(w, `</tbody></table>`)
|
|
}
|
|
|
|
func renderUsersTable(w http.ResponseWriter, users []AppUser, editName string) {
|
|
if len(users) == 0 {
|
|
fmt.Fprint(w, `<div class="ef-empty">Noch keine Benutzer eingetragen.</div>`)
|
|
return
|
|
}
|
|
fmt.Fprint(w, `<table class="account-table user-table"><thead><tr><th>Name</th><th>Login/E-Mail</th><th>Rolle</th><th>Status</th><th class="actions-col"></th></tr></thead><tbody>`)
|
|
for _, u := range users {
|
|
activeClass := ""
|
|
if u.Username == editName {
|
|
activeClass = ` class="selected"`
|
|
}
|
|
status := "aktiv"
|
|
if !u.Active {
|
|
status = "gesperrt"
|
|
}
|
|
nextActive := "0"
|
|
toggleLabel := "Sperren"
|
|
toggleClass := "btn compact secondary"
|
|
if !u.Active {
|
|
nextActive = "1"
|
|
toggleLabel = "Aktivieren"
|
|
}
|
|
editButton := fmt.Sprintf(`<a class="btn compact secondary" href="/users?edit=%s">Bearbeiten</a>`, urlQuery(u.Username))
|
|
toggleButton := fmt.Sprintf(`<form method="post" action="/users/toggle-active"><input type="hidden" name="username" value="%s"><input type="hidden" name="active" value="%s"><button class="%s" type="submit">%s</button></form>`,
|
|
html.EscapeString(u.Username), nextActive, toggleClass, toggleLabel)
|
|
deleteButton := fmt.Sprintf(`<form method="post" action="/users/delete"><input type="hidden" name="username" value="%s"><button class="btn compact secondary danger" type="submit">Loeschen</button></form>`, html.EscapeString(u.Username))
|
|
fmt.Fprintf(w, `<tr%s><td>%s</td><td>%s</td><td>%s</td><td>%s</td><td class="row-actions">%s%s%s</td></tr>`,
|
|
activeClass, html.EscapeString(userDisplayName(u)), html.EscapeString(u.Username), html.EscapeString(u.Role), status, editButton, toggleButton, deleteButton)
|
|
}
|
|
fmt.Fprint(w, `</tbody></table>`)
|
|
}
|
|
|
|
func renderUserForm(w http.ResponseWriter, r *http.Request, u AppUser) {
|
|
roleField := roleSelect(u.Role, IsAdmin(r))
|
|
if !IsAdmin(r) {
|
|
roleField = `<input type="hidden" name="role" value="user"><input class="input" value="user" disabled>`
|
|
}
|
|
fmt.Fprintf(w, `<form class="account-form user-form" method="post" action="/users/save">
|
|
<div class="form-section">
|
|
<input type="hidden" name="old_username" value="%s">
|
|
<label class="label">Login/E-Mail<input class="input" name="username" value="%s" required></label>
|
|
<label class="check-row"><input type="checkbox" name="active" value="1" %s> Aktiv</label>
|
|
</div>
|
|
<div class="form-columns">
|
|
<fieldset><legend>Zugang</legend>
|
|
<label class="label">Anzeigename<input class="input" name="display_name" value="%s" placeholder="z.B. Max Mustermann"></label>
|
|
<div class="form-hint">%s</div>
|
|
<label class="label">Passwort<input class="input" name="password" type="password" autocomplete="new-password" placeholder="%s"></label>
|
|
<label class="label">Rolle%s</label>
|
|
</fieldset>
|
|
</div>
|
|
<div class="form-actions">
|
|
<button class="btn" type="submit">Speichern</button>
|
|
<a class="btn secondary" href="/users">Neu</a>
|
|
</div>
|
|
</form>`,
|
|
html.EscapeString(u.Username), html.EscapeString(u.Username), checked(u.Active), html.EscapeString(u.DisplayName), passwordHint(u.Username), passwordPlaceholder(u.Username), roleField)
|
|
}
|
|
|
|
func userDisplayName(u AppUser) string {
|
|
name := strings.TrimSpace(u.DisplayName)
|
|
if name != "" {
|
|
return name
|
|
}
|
|
return u.Username
|
|
}
|
|
|
|
func userDisplayLabel(u AppUser) string {
|
|
name := strings.TrimSpace(u.DisplayName)
|
|
if name != "" {
|
|
return name + " <" + u.Username + ">"
|
|
}
|
|
return u.Username
|
|
}
|
|
|
|
func roleSelect(current string, admin bool) string {
|
|
if current == "" {
|
|
current = roleUser
|
|
}
|
|
values := []string{roleUser}
|
|
if admin {
|
|
values = []string{roleUser, roleManager, roleAdmin}
|
|
}
|
|
return selectHTML("role", current, values)
|
|
}
|
|
|
|
func passwordPlaceholder(username string) string {
|
|
if username == "" {
|
|
return "Pflicht fuer neuen Benutzer"
|
|
}
|
|
return "leer lassen = behalten"
|
|
}
|
|
|
|
func passwordHint(username string) string {
|
|
if username == "" {
|
|
return "Neuer Benutzer: Passwort eintragen."
|
|
}
|
|
return "Bearbeiten: Neues Passwort eintragen oder leer lassen, um das bestehende Passwort zu behalten."
|
|
}
|
|
|
|
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>Speicher</th><th>Mails</th><th>Dubletten</th><th></th></tr></thead><tbody>`)
|
|
for _, archive := range archives {
|
|
uses := archiveUses(archive, accounts)
|
|
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 == "" {
|
|
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>%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>`)
|
|
}
|
|
|
|
func renderArchiveForm(w http.ResponseWriter) {
|
|
fmt.Fprint(w, `<form class="archive-create-panel" method="post" action="/archives/create">
|
|
<label class="label">Neue Archiv-Mailbox<input class="input" name="archive_name" placeholder="z.B. kunde-altarchiv"></label>
|
|
<button class="btn" type="submit">Erstellen</button>
|
|
</form>
|
|
<div class="archive-help">
|
|
<div class="archive-help-title">Archivlogik</div>
|
|
<p>Archiv-mboxen sind eigenstaendige Mailboxen und nicht von einem Quellkonto abhaengig.</p>
|
|
<p>Sie koennen leer angelegt oder spaeter per Import mit mbox/PST-Inhalten gefuellt werden.</p>
|
|
<p>Entfernen ist nur moeglich, wenn die Mailbox keinem Konto zugeordnet und leer ist.</p>
|
|
</div>`)
|
|
}
|
|
|
|
func renderAccountForm(w http.ResponseWriter, a Account, archives []string) {
|
|
fmt.Fprintf(w, `<form class="account-form" method="post" action="/accounts/save">
|
|
<div class="form-section">
|
|
<label class="label">Kontoname<input class="input" name="name" value="%s" required></label>
|
|
<label class="check-row"><input type="checkbox" name="active" value="1" %s> Aktiv</label>
|
|
</div>
|
|
<div class="form-columns">
|
|
<fieldset><legend>Original</legend>
|
|
<label class="label">Server<input class="input" name="src_host" value="%s" required></label>
|
|
<div class="field-row">
|
|
<label class="label">Port<input class="input" name="src_port" type="number" min="1" max="65535" value="%d"></label>
|
|
<label class="label">Sicherheit%s</label>
|
|
</div>
|
|
<label class="label">Benutzer<input class="input" name="src_user" value="%s" required></label>
|
|
<label class="label">Passwort<input class="input" name="src_pass" type="password" autocomplete="new-password" placeholder="leer lassen = behalten"></label>
|
|
<label class="label">Protokoll%s</label>
|
|
<label class="check-row"><input type="checkbox" name="src_insecure" value="1" %s> Zertifikat nicht pruefen</label>
|
|
</fieldset>
|
|
<fieldset><legend>Archiv-mbox</legend>
|
|
<label class="label">Lokale Archiv-Mailbox%s</label>
|
|
</fieldset>
|
|
<fieldset><legend>Ziel</legend>
|
|
<label class="label">Server<input class="input" name="dst_host" value="%s" required></label>
|
|
<div class="field-row">
|
|
<label class="label">Port<input class="input" name="dst_port" type="number" min="1" max="65535" value="%d"></label>
|
|
<label class="label">Sicherheit%s</label>
|
|
</div>
|
|
<label class="label">Benutzer<input class="input" name="dst_user" value="%s" required></label>
|
|
<label class="label">Passwort<input class="input" name="dst_pass" type="password" autocomplete="new-password" placeholder="leer lassen = behalten"></label>
|
|
<label class="check-row"><input type="checkbox" name="dst_insecure" value="1" %s> Zertifikat nicht pruefen</label>
|
|
</fieldset>
|
|
</div>
|
|
<div class="form-actions">
|
|
<button class="btn" type="submit">Speichern</button>
|
|
<a class="btn secondary" href="/accounts">Neu</a>
|
|
</div>
|
|
</form>`,
|
|
html.EscapeString(a.Name), checked(a.Active),
|
|
html.EscapeString(a.SrcHost), a.SrcPort, securitySelect("src_security", a.SrcSecurity),
|
|
html.EscapeString(a.SrcUser), protoSelect(a.SrcProto), checked(a.SrcInsecure),
|
|
archiveSelect(accountMboxDir(a), archives),
|
|
html.EscapeString(a.DstHost), a.DstPort, securitySelect("dst_security", a.DstSecurity),
|
|
html.EscapeString(a.DstUser), checked(a.DstInsecure))
|
|
}
|
|
|
|
func accountFromForm(r *http.Request) Account {
|
|
return Account{
|
|
Name: strings.TrimSpace(r.FormValue("name")),
|
|
SrcHost: strings.TrimSpace(r.FormValue("src_host")),
|
|
SrcPort: parsePort(r.FormValue("src_port")),
|
|
SrcSecurity: strings.TrimSpace(r.FormValue("src_security")),
|
|
SrcInsecure: r.FormValue("src_insecure") == "1",
|
|
SrcUser: strings.TrimSpace(r.FormValue("src_user")),
|
|
SrcPass: r.FormValue("src_pass"),
|
|
SrcProto: strings.TrimSpace(r.FormValue("src_proto")),
|
|
DstHost: strings.TrimSpace(r.FormValue("dst_host")),
|
|
DstPort: parsePort(r.FormValue("dst_port")),
|
|
DstSecurity: strings.TrimSpace(r.FormValue("dst_security")),
|
|
DstInsecure: r.FormValue("dst_insecure") == "1",
|
|
DstUser: strings.TrimSpace(r.FormValue("dst_user")),
|
|
DstPass: r.FormValue("dst_pass"),
|
|
MboxDir: strings.TrimSpace(r.FormValue("mbox_dir")),
|
|
Active: r.FormValue("active") == "1",
|
|
}
|
|
}
|
|
|
|
func securitySelect(name, current string) string {
|
|
if current == "" {
|
|
current = "tls"
|
|
}
|
|
return selectHTML(name, current, []string{"tls", "starttls", "none"})
|
|
}
|
|
|
|
func protoSelect(current string) string {
|
|
if current == "" {
|
|
current = "imap"
|
|
}
|
|
return selectHTML("src_proto", current, []string{"imap", "pop3"})
|
|
}
|
|
|
|
func archiveSelect(current string, archives []string) string {
|
|
values := append([]string{}, archives...)
|
|
found := false
|
|
for _, archive := range values {
|
|
if archive == current {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if current != "" && !found {
|
|
values = append([]string{current}, values...)
|
|
}
|
|
if len(values) == 0 {
|
|
return `<select name="mbox_dir" disabled><option>erst Archiv-Mailbox erstellen</option></select>`
|
|
}
|
|
var b strings.Builder
|
|
b.WriteString(`<select name="mbox_dir">`)
|
|
for _, v := range values {
|
|
sel := ""
|
|
if v == current {
|
|
sel = ` selected`
|
|
}
|
|
fmt.Fprintf(&b, `<option value="%s"%s>%s</option>`, html.EscapeString(v), sel, html.EscapeString(v))
|
|
}
|
|
b.WriteString(`</select>`)
|
|
return b.String()
|
|
}
|
|
|
|
func selectHTML(name, current string, values []string) string {
|
|
var b strings.Builder
|
|
fmt.Fprintf(&b, `<select name="%s">`, name)
|
|
for _, v := range values {
|
|
sel := ""
|
|
if v == current {
|
|
sel = ` selected`
|
|
}
|
|
fmt.Fprintf(&b, `<option value="%s"%s>%s</option>`, v, sel, v)
|
|
}
|
|
b.WriteString(`</select>`)
|
|
return b.String()
|
|
}
|
|
|
|
func checked(v bool) string {
|
|
if v {
|
|
return "checked"
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func parsePort(value string) int {
|
|
port, _ := strconv.Atoi(strings.TrimSpace(value))
|
|
return port
|
|
}
|
|
|
|
func archiveUses(archive string, accounts []Account) string {
|
|
var uses []string
|
|
for _, a := range accounts {
|
|
if accountMboxDir(a) == archive {
|
|
uses = append(uses, a.Name)
|
|
}
|
|
}
|
|
return strings.Join(uses, ", ")
|
|
}
|
|
|
|
func archiveInUse(archive string) bool {
|
|
accounts, err := ListAccounts()
|
|
if err != nil {
|
|
return true
|
|
}
|
|
return archiveUses(archive, accounts) != ""
|
|
}
|
|
|
|
func safeArchiveName(value string) (string, error) {
|
|
name := strings.TrimSpace(value)
|
|
if name == "" {
|
|
return "", fmt.Errorf("Name der Archiv-Mailbox fehlt.")
|
|
}
|
|
if name == "." || name == ".." || strings.ContainsAny(name, `/\:`) || filepath.Clean(name) != name {
|
|
return "", fmt.Errorf("Ungueltiger Archiv-Mailbox-Name.")
|
|
}
|
|
return name, nil
|
|
}
|
|
|
|
func sortStrings(values []string) {
|
|
for i := 0; i < len(values); i++ {
|
|
for j := i + 1; j < len(values); j++ {
|
|
if strings.ToLower(values[j]) < strings.ToLower(values[i]) {
|
|
values[i], values[j] = values[j], values[i]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func redirectAccounts(w http.ResponseWriter, r *http.Request, edit, msg string) {
|
|
key := "msg"
|
|
if strings.Contains(strings.ToLower(msg), "fehl") || strings.Contains(strings.ToLower(msg), "pflicht") || strings.Contains(strings.ToLower(msg), "error") {
|
|
key = "err"
|
|
}
|
|
target := "/accounts?" + key + "=" + urlQuery(msg)
|
|
if edit != "" {
|
|
target += "&edit=" + urlQuery(edit)
|
|
}
|
|
http.Redirect(w, r, target, http.StatusSeeOther)
|
|
}
|
|
|
|
func redirectArchives(w http.ResponseWriter, r *http.Request, msg string) {
|
|
key := "msg"
|
|
if strings.Contains(strings.ToLower(msg), "fehl") || strings.Contains(strings.ToLower(msg), "ungueltig") || strings.Contains(strings.ToLower(msg), "nicht") || strings.Contains(strings.ToLower(msg), "error") {
|
|
key = "err"
|
|
}
|
|
http.Redirect(w, r, "/archives?"+key+"="+urlQuery(msg), http.StatusSeeOther)
|
|
}
|
|
|
|
func redirectUsers(w http.ResponseWriter, r *http.Request, edit, msg string) {
|
|
key := "msg"
|
|
if strings.Contains(strings.ToLower(msg), "fehl") || strings.Contains(strings.ToLower(msg), "pflicht") || strings.Contains(strings.ToLower(msg), "nicht") || strings.Contains(strings.ToLower(msg), "error") {
|
|
key = "err"
|
|
}
|
|
target := "/users?" + key + "=" + urlQuery(msg)
|
|
if edit != "" {
|
|
target += "&edit=" + urlQuery(edit)
|
|
}
|
|
http.Redirect(w, r, target, http.StatusSeeOther)
|
|
}
|
|
|
|
func urlQuery(value string) string {
|
|
return url.QueryEscape(value)
|
|
}
|