diff --git a/backend/00-router.go b/backend/00-router.go
index 09569f9..bed369c 100644
--- a/backend/00-router.go
+++ b/backend/00-router.go
@@ -132,8 +132,8 @@ func renderStartPage(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, `
Mail-Graveyard
-
-
+
+
%s
@@ -170,9 +170,9 @@ func renderShell(w http.ResponseWriter, r *http.Request, active, readingPane str
fmt.Fprintf(w, `
Mail-Graveyard
-
+
-
+
%s
@@ -752,9 +752,9 @@ func renderAccountsPage(w http.ResponseWriter, r *http.Request, accounts []Accou
fmt.Fprintf(w, `
Konten-Verwaltung - Mail-Graveyard
-
+
-
+
@@ -798,9 +798,9 @@ func renderArchivesPage(w http.ResponseWriter, r *http.Request, archives []strin
fmt.Fprintf(w, `
Archiv-mbox Verwaltung - Mail-Graveyard
-
+
-
+
@@ -846,9 +846,9 @@ func renderUsersPage(w http.ResponseWriter, r *http.Request, users []AppUser, ed
fmt.Fprintf(w, `
Benutzerverwaltung - Mail-Graveyard
-
+
-
+
@@ -920,9 +920,9 @@ func renderTransferPage(w http.ResponseWriter, r *http.Request, mode transferMod
fmt.Fprintf(w, `
%s - Mail-Graveyard
-
+
-
+
@@ -1381,9 +1381,9 @@ func renderSourceEmailBoxPage(w http.ResponseWriter, r *http.Request, accounts [
fmt.Fprintf(w, `
Quell-Postfach - Mail-Graveyard
-
+
-
+
@@ -1425,9 +1425,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 9cbd261..b7f3c84 100644
--- a/frontend-js/dist/app.js
+++ b/frontend-js/dist/app.js
@@ -129,6 +129,98 @@
applyListSearch(input.value);
}
+ function ensureContextMenu() {
+ let menu = document.getElementById("mailContextMenu");
+ if (menu) return menu;
+ menu = document.createElement("div");
+ menu.id = "mailContextMenu";
+ menu.className = "mail-context-menu";
+ menu.setAttribute("role", "menu");
+ menu.setAttribute("aria-label", "Nachrichten-Aktionen");
+ menu.innerHTML = `
+
+
+
+
+
+
+
+ `;
+ document.body.appendChild(menu);
+ return menu;
+ }
+
+ function closeContextMenu() {
+ const menu = document.getElementById("mailContextMenu");
+ if (menu) menu.classList.remove("open");
+ }
+
+ function openContextMenu(x, y, row) {
+ const menu = ensureContextMenu();
+ menu.dataset.rowContext = row ? "1" : "0";
+ const pad = 8;
+ menu.classList.add("open");
+ menu.style.left = "0px";
+ menu.style.top = "0px";
+ const rect = menu.getBoundingClientRect();
+ menu.style.left = `${Math.max(pad, Math.min(x, window.innerWidth - rect.width - pad))}px`;
+ menu.style.top = `${Math.max(pad, Math.min(y, window.innerHeight - rect.height - pad))}px`;
+ }
+
+ function rowMessage(row) {
+ if (!row) return null;
+ return {
+ subject: row.querySelector(".msg-subject")?.textContent?.trim() || "",
+ from: row.querySelector(".msg-from")?.textContent?.trim() || "",
+ date: row.querySelector(".msg-date")?.textContent?.trim() || "",
+ body: ""
+ };
+ }
+
+ function activeMessageFallback() {
+ return rowMessage(document.querySelector(".msg-row.active")) || selectedPreview();
+ }
+
+ function waitForPreview() {
+ return new Promise((resolve) => {
+ let done = false;
+ const finish = () => {
+ if (done) return;
+ done = true;
+ document.removeEventListener("htmx:afterSwap", finish);
+ resolve();
+ };
+ document.addEventListener("htmx:afterSwap", finish);
+ setTimeout(finish, 350);
+ });
+ }
+
+ async function activateContextRow() {
+ const row = document.querySelector(".msg-row.context-active");
+ if (!row) return null;
+ row.click();
+ await waitForPreview();
+ return row;
+ }
+
+ async function copyText(text) {
+ const value = text || "";
+ if (!value) return;
+ if (navigator.clipboard?.writeText) {
+ await navigator.clipboard.writeText(value);
+ return;
+ }
+ const area = document.createElement("textarea");
+ area.value = value;
+ area.style.position = "fixed";
+ area.style.left = "-9999px";
+ document.body.appendChild(area);
+ area.focus();
+ area.select();
+ document.execCommand("copy");
+ area.remove();
+ }
+
document.addEventListener("DOMContentLoaded", ensureActionBar);
function closeActionMenus() {
document.querySelectorAll(".action-menu.open").forEach((menu) => {
@@ -216,6 +308,63 @@
const input = event.target.closest("[data-action-search]");
if (input) runSearch(input);
});
+ document.addEventListener("contextmenu", (event) => {
+ const row = event.target.closest(".msg-row");
+ const readBody = event.target.closest(".read-body");
+ if (!row && !readBody) return;
+ event.preventDefault();
+ document.querySelectorAll(".msg-row.context-active").forEach((item) => item.classList.remove("context-active"));
+ if (row) {
+ row.classList.add("context-active");
+ row.closest(".pane")?.querySelectorAll(".msg-row.active").forEach((item) => item.classList.remove("active"));
+ row.classList.add("active");
+ }
+ openContextMenu(event.clientX, event.clientY, row);
+ });
+ document.addEventListener("click", async (event) => {
+ const menu = event.target.closest("#mailContextMenu");
+ if (!menu) {
+ closeContextMenu();
+ return;
+ }
+ const item = event.target.closest("[data-context-action]");
+ if (!item) return;
+ event.preventDefault();
+ const action = item.dataset.contextAction;
+ const contextRow = await activateContextRow();
+ closeContextMenu();
+ const rowMessageData = rowMessage(contextRow);
+ const previewMessage = selectedPreview();
+ const message = previewMessage || rowMessageData || activeMessageFallback();
+ if (!message && action !== "open") {
+ window.alert("Bitte zuerst eine Nachricht waehlen.");
+ return;
+ }
+ if (action === "open") {
+ const button = document.querySelector("#read [data-open-mail-modal], #transfer-preview [data-open-mail-modal], .pane-read [data-open-mail-modal]");
+ if (button) button.click();
+ return;
+ }
+ if (action === "reply") {
+ openCompose({ to: message.from, subject: prefixedSubject("Re", message.subject), body: quoteBody(message) });
+ return;
+ }
+ if (action === "forward") {
+ openCompose({ to: "", subject: prefixedSubject("Fw", message.subject), body: forwardBody(message) });
+ return;
+ }
+ if (action === "copy-subject") {
+ await copyText((rowMessageData || message).subject);
+ return;
+ }
+ if (action === "copy-from") {
+ await copyText((rowMessageData || message).from);
+ return;
+ }
+ if (action === "copy-body") {
+ await copyText(message.body);
+ }
+ });
})();
(function () {
diff --git a/frontend-js/dist/style.css b/frontend-js/dist/style.css
index ac6d5fb..9718bfc 100644
--- a/frontend-js/dist/style.css
+++ b/frontend-js/dist/style.css
@@ -238,6 +238,49 @@ body.ol2013 {
background: transparent;
border-color: transparent;
}
+.mail-context-menu {
+ position: fixed;
+ z-index: 80;
+ min-width: 220px;
+ display: none;
+ padding: 6px;
+ color: var(--ol-text);
+ background: #fff;
+ border: 1px solid var(--ol-border-dark);
+ box-shadow: 0 12px 34px rgba(20, 25, 35, .22);
+}
+.mail-context-menu.open { display: block; }
+.mail-context-menu button {
+ width: 100%;
+ height: 30px;
+ display: flex;
+ align-items: center;
+ gap: 9px;
+ padding: 0 10px;
+ border: 0;
+ color: var(--ol-text);
+ background: transparent;
+ font: inherit;
+ text-align: left;
+ cursor: pointer;
+}
+.mail-context-menu button:hover {
+ color: #fff;
+ background: var(--ol-blue);
+}
+.mail-context-menu button span {
+ width: 18px;
+ text-align: center;
+}
+.mail-context-menu .sep {
+ height: 1px;
+ margin: 5px 4px;
+ background: var(--ol-border);
+}
+.msg-row.context-active {
+ outline: 1px solid var(--ol-blue);
+ outline-offset: -1px;
+}
.action-menu {
position: relative;
height: 30px;