diff --git a/backend/00-router.go b/backend/00-router.go index 51995be..acb8512 100644 --- a/backend/00-router.go +++ b/backend/00-router.go @@ -61,6 +61,51 @@ func RegisterRoutes(mux *http.ServeMux) { mux.HandleFunc("/view/target/select", targetSelectHandler) mux.HandleFunc("/view/manual-copy", manualCopyHandler) mux.HandleFunc("/view/forward", forwardHandler) + mux.HandleFunc("/mail/send", mailSendHandler) +} + +func mailSendHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + if err := r.ParseForm(); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + to := splitRecipients(r.FormValue("to")) + subject := strings.TrimSpace(r.FormValue("subject")) + body := r.FormValue("body") + if len(to) == 0 { + http.Error(w, "Bitte mindestens einen Empfaenger eintragen.", http.StatusBadRequest) + return + } + if subject == "" { + subject = "(ohne Betreff)" + } + if err := SendPlainMail(to, subject, body); err != nil { + http.Error(w, err.Error(), http.StatusBadGateway) + return + } + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + fmt.Fprint(w, "gesendet") +} + +func splitRecipients(value string) []string { + fields := strings.FieldsFunc(value, func(r rune) bool { + return r == ',' || r == ';' || r == '\n' || r == '\r' || r == '\t' || r == ' ' + }) + recipients := make([]string, 0, len(fields)) + seen := make(map[string]bool, len(fields)) + for _, field := range fields { + addr := strings.TrimSpace(field) + if addr == "" || seen[strings.ToLower(addr)] { + continue + } + recipients = append(recipients, addr) + seen[strings.ToLower(addr)] = true + } + return recipients } func homeHandler(w http.ResponseWriter, r *http.Request) { @@ -73,8 +118,8 @@ func renderStartPage(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, ` Mail-Graveyard - - + +
@@ -121,9 +166,9 @@ func renderShell(w http.ResponseWriter, r *http.Request, active, readingPane str fmt.Fprintf(w, ` Mail-Graveyard - + - +
@@ -697,9 +742,9 @@ func renderAccountsPage(w http.ResponseWriter, r *http.Request, accounts []Accou fmt.Fprintf(w, ` Konten-Verwaltung - Mail-Graveyard - + - +
@@ -743,9 +788,9 @@ func renderArchivesPage(w http.ResponseWriter, r *http.Request, archives []strin fmt.Fprintf(w, ` Archiv-mbox Verwaltung - Mail-Graveyard - + - +
@@ -791,9 +836,9 @@ func renderUsersPage(w http.ResponseWriter, r *http.Request, users []AppUser, ed fmt.Fprintf(w, ` Benutzerverwaltung - Mail-Graveyard - + - +
@@ -865,9 +910,9 @@ func renderTransferPage(w http.ResponseWriter, r *http.Request, mode transferMod fmt.Fprintf(w, ` %s - Mail-Graveyard - + - +
@@ -1317,9 +1362,9 @@ func renderSourceEmailBoxPage(w http.ResponseWriter, r *http.Request, accounts [ fmt.Fprintf(w, ` Quell-Postfach - Mail-Graveyard - + - +
@@ -1361,9 +1406,9 @@ func renderTargetEmailBoxPage(w http.ResponseWriter, r *http.Request, accounts [ fmt.Fprintf(w, ` Ziel-Postfach - Mail-Graveyard - + - +
diff --git a/frontend-js/dist/app.js b/frontend-js/dist/app.js index e0b930c..10bfde2 100644 --- a/frontend-js/dist/app.js +++ b/frontend-js/dist/app.js @@ -42,11 +42,49 @@ return `${prefix}: ${clean || "(ohne Betreff)"}`; } - function openMailto(mail) { - const params = new URLSearchParams(); - if (mail.subject) params.set("subject", mail.subject); - if (mail.body) params.set("body", mail.body); - window.location.href = `mailto:${encodeURIComponent(mail.to || "")}?${params.toString()}`; + function ensureComposeModal() { + let modal = document.getElementById("mailComposeModal"); + if (modal) return modal; + modal = document.createElement("div"); + modal.className = "mail-compose-modal"; + modal.id = "mailComposeModal"; + modal.setAttribute("role", "dialog"); + modal.setAttribute("aria-modal", "true"); + modal.setAttribute("aria-label", "Mail verfassen"); + modal.innerHTML = ` +
+
+

Mail verfassen

+ +
+
+ + + +
+
+
+ + +
+
+ `; + document.body.appendChild(modal); + return modal; + } + + function closeComposeModal() { + document.getElementById("mailComposeModal")?.classList.remove("open"); + } + + function openCompose(mail) { + const modal = ensureComposeModal(); + modal.querySelector("[name='to']").value = mail.to || ""; + modal.querySelector("[name='subject']").value = mail.subject || ""; + modal.querySelector("[name='body']").value = mail.body || ""; + modal.querySelector(".mail-compose-status").textContent = ""; + modal.classList.add("open"); + setTimeout(() => modal.querySelector("[name='to']").focus(), 0); } function quoteBody(message) { @@ -85,7 +123,7 @@ }); document.addEventListener("click", (event) => { if (event.target.closest("[data-action-new]")) { - openMailto({ to: "", subject: "", body: "" }); + openCompose({ to: "", subject: "", body: "" }); } if (event.target.closest("[data-action-refresh]")) window.location.reload(); if (event.target.closest("[data-action-reply]")) { @@ -94,7 +132,7 @@ window.alert("Bitte zuerst eine Nachricht waehlen."); return; } - openMailto({ to: message.from, subject: prefixedSubject("Re", message.subject), body: quoteBody(message) }); + openCompose({ to: message.from, subject: prefixedSubject("Re", message.subject), body: quoteBody(message) }); } if (event.target.closest("[data-action-forward]")) { const message = selectedPreview(); @@ -102,7 +140,38 @@ window.alert("Bitte zuerst eine Nachricht waehlen."); return; } - openMailto({ to: "", subject: prefixedSubject("Fw", message.subject), body: forwardBody(message) }); + openCompose({ to: "", subject: prefixedSubject("Fw", message.subject), body: forwardBody(message) }); + } + if (event.target.closest("[data-compose-close]")) closeComposeModal(); + }); + document.addEventListener("click", (event) => { + if (event.target.id === "mailComposeModal") closeComposeModal(); + }); + document.addEventListener("keydown", (event) => { + if (event.key === "Escape") closeComposeModal(); + }); + document.addEventListener("submit", async (event) => { + const form = event.target.closest(".mail-compose-box"); + if (!form) return; + event.preventDefault(); + const status = form.querySelector(".mail-compose-status"); + const submit = form.querySelector("button[type='submit']"); + status.textContent = "Sende..."; + submit.disabled = true; + try { + const response = await fetch("/mail/send", { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8" }, + body: new URLSearchParams(new FormData(form)).toString() + }); + const text = await response.text(); + if (!response.ok) throw new Error(text || "Senden fehlgeschlagen."); + status.textContent = "Gesendet."; + setTimeout(closeComposeModal, 700); + } catch (error) { + status.textContent = error.message || "Senden fehlgeschlagen."; + } finally { + submit.disabled = false; } }); document.addEventListener("input", (event) => { diff --git a/frontend-js/dist/style.css b/frontend-js/dist/style.css index 802bc40..82c5ce0 100644 --- a/frontend-js/dist/style.css +++ b/frontend-js/dist/style.css @@ -540,6 +540,97 @@ a.tree-node { display: block; color: inherit; text-decoration: none; } overflow: auto; padding: 20px 24px; } +.mail-compose-modal { + position: fixed; + inset: 0; + z-index: 70; + display: none; + align-items: center; + justify-content: center; + padding: 24px; + background: rgba(0, 0, 0, .32); +} +.mail-compose-modal.open { display: flex; } +.mail-compose-box { + width: min(760px, calc(100vw - 48px)); + max-height: calc(100vh - 48px); + display: flex; + flex-direction: column; + background: #fff; + border: 1px solid var(--ol-border); + box-shadow: 0 20px 60px rgba(0, 0, 0, .28); +} +.mail-compose-head { + height: 42px; + display: flex; + align-items: center; + gap: 12px; + padding: 0 12px 0 16px; + color: #fff; + background: #2f343a; +} +.mail-compose-head h2 { + margin: 0; + font-size: 15px; + font-weight: 600; +} +.mail-compose-close { + margin-left: auto; + width: 30px; + height: 30px; + border: 0; + color: #fff; + background: transparent; + font-size: 22px; + line-height: 1; + cursor: pointer; +} +.mail-compose-close:hover { background: #454b53; } +.mail-compose-fields { + display: grid; + gap: 10px; + padding: 14px 16px; + overflow: auto; +} +.mail-compose-fields label { + display: grid; + grid-template-columns: 72px minmax(0, 1fr); + align-items: start; + gap: 10px; + color: var(--ol-muted); +} +.mail-compose-fields input, +.mail-compose-fields textarea { + width: 100%; + border: 1px solid #b8c4d0; + padding: 7px 8px; + color: var(--ol-text); + background: #fff; + font: inherit; +} +.mail-compose-fields textarea { + min-height: 260px; + resize: vertical; + font-family: Consolas, "Courier New", monospace; +} +.mail-compose-fields input:focus, +.mail-compose-fields textarea:focus { + outline: none; + border-color: var(--ol-blue); +} +.mail-compose-status { + min-height: 20px; + padding: 0 16px 8px 98px; + color: var(--ol-muted); +} +.mail-compose-actions { + display: flex; + justify-content: flex-end; + gap: 8px; + padding: 12px 16px; + border-top: 1px solid var(--ol-border); + background: #f7f9fb; +} .ef-empty { color: var(--ol-muted); padding: 24px; } .audit-note { padding: 10px 16px; border-bottom: 1px solid var(--ol-border); font-size: 12px; } .drop-zone {