Use internal compose modal for mail actions
This commit is contained in:
parent
e8029a177c
commit
29fd3f5cae
3 changed files with 229 additions and 24 deletions
|
|
@ -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, `<!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-10">
|
||||
<script src="/static/app.js?v=20260713-10" defer></script></head>
|
||||
<link rel="stylesheet" href="/static/style.css?v=20260713-11">
|
||||
<script src="/static/app.js?v=20260713-11" defer></script></head>
|
||||
<body class="ol2013">
|
||||
<header class="ribbon">
|
||||
<button class="app-menu-button" type="button" aria-label="Mail-Graveyard-Menue" aria-expanded="false" data-backstage-toggle><span class="mail-logo" aria-hidden="true"></span></button>
|
||||
|
|
@ -121,9 +166,9 @@ func renderShell(w http.ResponseWriter, r *http.Request, active, readingPane str
|
|||
fmt.Fprintf(w, `<!doctype html><html lang="de"><head><meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Mail-Graveyard</title>
|
||||
<link rel="stylesheet" href="/static/style.css?v=20260713-10">
|
||||
<link rel="stylesheet" href="/static/style.css?v=20260713-11">
|
||||
<script src="/static/htmx.min.js"></script>
|
||||
<script src="/static/app.js?v=20260713-10" defer></script></head>
|
||||
<script src="/static/app.js?v=20260713-11" defer></script></head>
|
||||
<body class="ol2013">
|
||||
<header class="ribbon">
|
||||
<button class="app-menu-button" type="button" aria-label="Mail-Graveyard-Menue" aria-expanded="false" data-backstage-toggle><span class="mail-logo" aria-hidden="true"></span></button>
|
||||
|
|
@ -697,9 +742,9 @@ func renderAccountsPage(w http.ResponseWriter, r *http.Request, accounts []Accou
|
|||
fmt.Fprintf(w, `<!doctype html><html lang="de"><head><meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Konten-Verwaltung - Mail-Graveyard</title>
|
||||
<link rel="stylesheet" href="/static/style.css?v=20260713-10">
|
||||
<link rel="stylesheet" href="/static/style.css?v=20260713-11">
|
||||
<script src="/static/htmx.min.js"></script>
|
||||
<script src="/static/app.js?v=20260713-10" defer></script></head>
|
||||
<script src="/static/app.js?v=20260713-11" defer></script></head>
|
||||
<body class="ol2013">
|
||||
<header class="ribbon">
|
||||
<button class="app-menu-button" type="button" aria-label="Mail-Graveyard-Menue" aria-expanded="false" data-backstage-toggle><span class="mail-logo" aria-hidden="true"></span></button>
|
||||
|
|
@ -743,9 +788,9 @@ func renderArchivesPage(w http.ResponseWriter, r *http.Request, archives []strin
|
|||
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-10">
|
||||
<link rel="stylesheet" href="/static/style.css?v=20260713-11">
|
||||
<script src="/static/htmx.min.js"></script>
|
||||
<script src="/static/app.js?v=20260713-10" defer></script></head>
|
||||
<script src="/static/app.js?v=20260713-11" defer></script></head>
|
||||
<body class="ol2013">
|
||||
<header class="ribbon">
|
||||
<button class="app-menu-button" type="button" aria-label="Mail-Graveyard-Menue" aria-expanded="false" data-backstage-toggle><span class="mail-logo" aria-hidden="true"></span></button>
|
||||
|
|
@ -791,9 +836,9 @@ func renderUsersPage(w http.ResponseWriter, r *http.Request, users []AppUser, ed
|
|||
fmt.Fprintf(w, `<!doctype html><html lang="de"><head><meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Benutzerverwaltung - Mail-Graveyard</title>
|
||||
<link rel="stylesheet" href="/static/style.css?v=20260713-10">
|
||||
<link rel="stylesheet" href="/static/style.css?v=20260713-11">
|
||||
<script src="/static/htmx.min.js"></script>
|
||||
<script src="/static/app.js?v=20260713-10" defer></script></head>
|
||||
<script src="/static/app.js?v=20260713-11" defer></script></head>
|
||||
<body class="ol2013">
|
||||
<header class="ribbon">
|
||||
<button class="app-menu-button" type="button" aria-label="Mail-Graveyard-Menue" aria-expanded="false" data-backstage-toggle><span class="mail-logo" aria-hidden="true"></span></button>
|
||||
|
|
@ -865,9 +910,9 @@ func renderTransferPage(w http.ResponseWriter, r *http.Request, mode transferMod
|
|||
fmt.Fprintf(w, `<!doctype html><html lang="de"><head><meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>%s - Mail-Graveyard</title>
|
||||
<link rel="stylesheet" href="/static/style.css?v=20260713-10">
|
||||
<link rel="stylesheet" href="/static/style.css?v=20260713-11">
|
||||
<script src="/static/htmx.min.js"></script>
|
||||
<script src="/static/app.js?v=20260713-10" defer></script></head>
|
||||
<script src="/static/app.js?v=20260713-11" defer></script></head>
|
||||
<body class="ol2013">
|
||||
<header class="ribbon">
|
||||
<button class="app-menu-button" type="button" aria-label="Mail-Graveyard-Menue" aria-expanded="false" data-backstage-toggle><span class="mail-logo" aria-hidden="true"></span></button>
|
||||
|
|
@ -1317,9 +1362,9 @@ func renderSourceEmailBoxPage(w http.ResponseWriter, r *http.Request, accounts [
|
|||
fmt.Fprintf(w, `<!doctype html><html lang="de"><head><meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Quell-Postfach - Mail-Graveyard</title>
|
||||
<link rel="stylesheet" href="/static/style.css?v=20260713-10">
|
||||
<link rel="stylesheet" href="/static/style.css?v=20260713-11">
|
||||
<script src="/static/htmx.min.js"></script>
|
||||
<script src="/static/app.js?v=20260713-10" defer></script></head>
|
||||
<script src="/static/app.js?v=20260713-11" defer></script></head>
|
||||
<body class="ol2013">
|
||||
<header class="ribbon">
|
||||
<button class="app-menu-button" type="button" aria-label="Mail-Graveyard-Menue" aria-expanded="false" data-backstage-toggle><span class="mail-logo" aria-hidden="true"></span></button>
|
||||
|
|
@ -1361,9 +1406,9 @@ func renderTargetEmailBoxPage(w http.ResponseWriter, r *http.Request, accounts [
|
|||
fmt.Fprintf(w, `<!doctype html><html lang="de"><head><meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Ziel-Postfach - Mail-Graveyard</title>
|
||||
<link rel="stylesheet" href="/static/style.css?v=20260713-10">
|
||||
<link rel="stylesheet" href="/static/style.css?v=20260713-11">
|
||||
<script src="/static/htmx.min.js"></script>
|
||||
<script src="/static/app.js?v=20260713-10" defer></script></head>
|
||||
<script src="/static/app.js?v=20260713-11" defer></script></head>
|
||||
<body class="ol2013">
|
||||
<header class="ribbon">
|
||||
<button class="app-menu-button" type="button" aria-label="Mail-Graveyard-Menue" aria-expanded="false" data-backstage-toggle><span class="mail-logo" aria-hidden="true"></span></button>
|
||||
|
|
|
|||
85
frontend-js/dist/app.js
vendored
85
frontend-js/dist/app.js
vendored
|
|
@ -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 = `
|
||||
<form class="mail-compose-box">
|
||||
<div class="mail-compose-head">
|
||||
<h2>Mail verfassen</h2>
|
||||
<button class="mail-compose-close" type="button" aria-label="Schliessen" data-compose-close>×</button>
|
||||
</div>
|
||||
<div class="mail-compose-fields">
|
||||
<label>An<input name="to" type="text" autocomplete="email" required placeholder="empfaenger@example.com"></label>
|
||||
<label>Betreff<input name="subject" type="text" placeholder="Betreff"></label>
|
||||
<label>Nachricht<textarea name="body" rows="14"></textarea></label>
|
||||
</div>
|
||||
<div class="mail-compose-status" aria-live="polite"></div>
|
||||
<div class="mail-compose-actions">
|
||||
<button class="btn secondary" type="button" data-compose-close>Abbrechen</button>
|
||||
<button class="btn" type="submit">Senden</button>
|
||||
</div>
|
||||
</form>
|
||||
`;
|
||||
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) => {
|
||||
|
|
|
|||
91
frontend-js/dist/style.css
vendored
91
frontend-js/dist/style.css
vendored
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue