diff --git a/.gitignore b/.gitignore index 5e2c2d7..449db95 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ defaults.local.json + +node_modules/ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..d60fe62 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,42 @@ +# CLAUDE.md + +Thunderbird-MailExtension „HPS Vorlagen & Signaturen" mit Gitea-Sync. + +## Workflow (WICHTIG) + +1. **Nach jeder Code-Änderung am Plugin immer die `.xpi` neu bauen** (siehe Build unten), + damit `templates-reply-hotel.xpi` aktuell ist. +2. **Sobald der User zufrieden ist ("happy"), committen** — Code-Änderung + neu gebaute + `.xpi` zusammen. Nicht ungefragt vorher committen; auf das OK des Users warten. + +## Build der .xpi + +Immer **ohne** `defaults.local.json` bauen — die Datei enthält den Gitea-Token und darf +nicht in der (öffentlich released) `.xpi` landen. `zip` ist nicht installiert, `7z` schon: + +```bash +rm -f templates-reply-hotel.xpi +7z a -tzip templates-reply-hotel.xpi . \ + -xr'!.git' -xr'!node_modules' -xr'!web-editor' -xr'!.claude' \ + -xr'!defaults.local.json' -xr'!*.xpi' -xr'!release.sh' -xr'!*.md' +``` + +`defaults.local.json` wird nur beim allerersten Start gelesen (`templates_options.js`, +`if (!config)`) und ist nur zum Vorkonfigurieren frischer Installationen gedacht. Updates +brauchen sie nicht — bestehende Installs behalten ihre Config in `storage.local`. + +## Auto-Update (self-hosted über Gitea) + +- `manifest.json` → `browser_specific_settings.gecko.update_url` zeigt auf `updates.json` + (raw auf `main`). Repo muss öffentlich bleiben, sonst 401 für den anonymen Updater. +- Neues Release veröffentlichen: `version` in `manifest.json` bumpen, `.xpi` neu bauen, dann + `GITEA_TOKEN=… ./release.sh` (hasht die xpi, aktualisiert `updates.json`, legt das Gitea- + Release an + lädt die xpi hoch). `release.sh` bricht ab, falls der Token in der xpi steckt. +- Danach `updates.json` + `manifest.json` committen & pushen. + +## Repo + +- Sync-Daten-Repo (Templates/Signaturen): `hps/email-vorlagen` auf `git.hotel-park-soltau.de`. +- Plugin-Source + Release-Host: `kendrick.bollens/hps-thunderbird-templates`. + **Muss public bleiben** — der Thunderbird-Auto-Updater greift anonym (ohne Token) auf + `updates.json` und die Release-`.xpi` zu. Privat = 401 = Auto-Updates kaputt. diff --git a/README.md b/README.md index 17620d5..81b9911 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,23 @@ Wenn eine `defaults.local.json` im Plugin-Root existiert und in die XPI eingebau Die Datei ist in `.gitignore` — Tokens landen nicht im Repository. +## Auto-Update (self-hosted über Gitea) + +Installierte Add-ons aktualisieren sich automatisch über `updates.json` in diesem Repo +(`manifest.json` → `browser_specific_settings.gecko.update_url`). + +> **⚠️ Dieses Repository muss public bleiben.** +> Der Thunderbird-Auto-Updater greift **anonym (ohne Token)** auf `updates.json` und die +> Release-`.xpi` zu. Ist das Repo privat, liefert Gitea `401` — die automatischen Updates +> funktionieren dann nicht mehr. + +Neue Version veröffentlichen: + +1. `version` in `manifest.json` hochzählen, `.xpi` **ohne** `defaults.local.json` neu bauen. +2. `GITEA_TOKEN=… ./release.sh` — hasht die `.xpi`, aktualisiert `updates.json`, legt das + Gitea-Release an und lädt die `.xpi` als Asset hoch. +3. `updates.json` + `manifest.json` committen & pushen. + ## Einrichtung 1. **Verbindung konfigurieren**: Einstellungen-Tab (⚙) → Server-URL, Repository, Token eingeben → Verbindung speichern (entfällt bei vorkonfigurierter XPI) diff --git a/lib/gitea-sync.js b/lib/gitea-sync.js index e81e8c0..45aceb7 100644 --- a/lib/gitea-sync.js +++ b/lib/gitea-sync.js @@ -250,6 +250,9 @@ class SyncManager { const folders = [SHARED_FOLDER]; if (this.department) folders.push(this.department); if (this.authorEmail) folders.push(`${USER_FOLDER}/${this.authorEmail}`); + // Signatur-Bausteine mitüberwachen, damit Footer-/Header-Änderungen + // auf dem Server den Hintergrund-Sync (pullSignatures) auslösen. + folders.push('signatures/footers', 'signatures/headers'); for (const folder of folders) { const files = await this.client.listDir(folder); @@ -461,25 +464,50 @@ class SyncManager { } /** - * Load footer for editing (returns HTML) + * Resolve the footer file path for a given scope. + * - 'shared' → signatures/footers/_default.html (gilt für alle Abteilungen) + * - 'department' → signatures/footers/{department}.html */ - async loadFooter() { + footerPathForScope(scope) { + if (scope === 'shared') return 'signatures/footers/_default.html'; + if (!this.department) throw new Error('Keine Abteilung ausgewählt'); + return `signatures/footers/${this.department}.html`; + } + + /** + * Load footer for editing (returns HTML). + * - scope 'shared'/'department': lädt genau diese Datei (kein Fallback, + * damit der Editor den echten Inhalt der gewählten Ebene zeigt). + * - ohne scope: Fallback-Kette (Abteilung → gemeinsam) für die + * automatische Anwendung an die Signatur. + */ + async loadFooter(scope) { if (!this.isConfigured) throw new Error('Sync nicht konfiguriert'); + if (scope === 'shared' || scope === 'department') { + const filepath = this.footerPathForScope(scope); + const fileData = await this.client.getFile(filepath); + const html = (fileData && fileData.content) ? GiteaClient.fromBase64(fileData.content) : ''; + return { success: true, html, scope }; + } + const footer = await this.pullFooter(); return { success: true, html: footer }; } /** - * Push footer for current department to signatures/footers/{department}.html + * Push footer to the chosen scope: + * - 'shared' → signatures/footers/_default.html + * - 'department' → signatures/footers/{department}.html */ - async pushFooter(html) { + async pushFooter(html, scope) { if (!this.isConfigured) throw new Error('Sync nicht konfiguriert'); if (!this.config.authorName) throw new Error('Bitte Name eintragen'); - if (!this.department) throw new Error('Keine Abteilung ausgewählt'); - const filepath = `signatures/footers/${this.department}.html`; - const commitMsg = `Signatur-Footer ${this.department} - von ${this.config.authorName}`; + const targetScope = (scope === 'shared') ? 'shared' : 'department'; + const filepath = this.footerPathForScope(targetScope); + const label = targetScope === 'shared' ? 'gemeinsam' : this.department; + const commitMsg = `Signatur-Footer ${label} - von ${this.config.authorName}`; const existing = await this.client.getFile(filepath); @@ -492,8 +520,10 @@ class SyncManager { await this.client.createFile(filepath, html, commitMsg); } - await browser.storage.local.set({ 'sig_footer_cache': html }); - return { success: true }; + // Refresh cache with the footer that actually applies (Abteilung gewinnt + // vor gemeinsam) — nicht zwingend das gerade gespeicherte html. + await this.pullFooter(); + return { success: true, scope: targetScope }; } /** @@ -544,15 +574,29 @@ class SyncManager { targetFile = headerMap[personalName] || null; } - if (!targetFile) continue; // No header file yet — user hasn't set up signature + let header = null; + if (targetFile) { + const fileData = await this.client.getFile(targetFile.path); + if (fileData) header = GiteaClient.fromBase64(fileData.content); + } - const fileData = await this.client.getFile(targetFile.path); - if (!fileData) continue; + // Fallback: kein Server-Header → lokalen Header aus der aktuellen + // Signatur verwenden, damit der Footer trotzdem angewandt wird. + if (header === null) { + const existing = identity.signature || ''; + const localHeader = SyncManager.extractHeader(existing); + // Leere, nie eingerichtete Konten nicht mit reinem Footer versehen + if (!localHeader.trim() && !existing.includes(SyncManager.FOOTER_SEPARATOR)) continue; + header = localHeader; + } - const header = GiteaClient.fromBase64(fileData.content); loadedHeaders[email] = header; const fullSig = SyncManager.combineSignature(header, footer); + // Nur schreiben, wenn sich etwas ändert (vermeidet unnötige Writes + // bei jedem Hintergrund-Sync) + if (fullSig === identity.signature) continue; + await browser.identities.update(identity.id, { signature: fullSig, signatureIsPlainText: false, @@ -854,10 +898,10 @@ browser.runtime.onMessage.addListener(async (msg, sender) => { return await syncManager.loadSignatureTemplate(); case 'loadFooter': - return await syncManager.loadFooter(); + return await syncManager.loadFooter(msg.scope); case 'pushFooter': - return await syncManager.pushFooter(msg.html); + return await syncManager.pushFooter(msg.html, msg.scope); case 'autoDetect': const config = await syncManager.autoDetect(); diff --git a/manifest.json b/manifest.json index ff4db73..f1bc6be 100644 --- a/manifest.json +++ b/manifest.json @@ -6,7 +6,8 @@ "browser_specific_settings": { "gecko": { "id": "it@hotel-park-soltau.de", - "strict_min_version": "109.0" + "strict_min_version": "109.0", + "update_url": "https://git.hotel-park-soltau.de/kendrick.bollens/hps-thunderbird-templates/raw/branch/main/updates.json" } }, "permissions": [ diff --git a/release.sh b/release.sh new file mode 100755 index 0000000..4faac18 --- /dev/null +++ b/release.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +# release.sh — publish a new version of the Thunderbird add-on to Gitea +# and update updates.json so installed clients auto-update. +# +# Prereqs: +# - manifest.json "version" already bumped to the new version +# - templates-reply-hotel.xpi rebuilt for that version, WITHOUT defaults.local.json +# (the token must not ship in a public release; updates don't need defaults anyway) +# - GITEA_TOKEN exported (a Gitea token with repo write access) +# +# Usage: GITEA_TOKEN=xxxx ./release.sh +set -euo pipefail + +OWNER="kendrick.bollens" +REPO="hps-thunderbird-templates" +BASE="https://git.hotel-park-soltau.de" +XPI="templates-reply-hotel.xpi" +ID="it@hotel-park-soltau.de" + +cd "$(dirname "$0")" + +# --- 0. Safety: never publish a build that bundles the local defaults/token --- +if grep -qa "defaults.local.json" "$XPI"; then + echo "ABORT: $XPI contains defaults.local.json (your Gitea token!)." >&2 + echo " Rebuild the .xpi without it before releasing." >&2 + exit 1 +fi + +VERSION=$(jq -r '.version' manifest.json) +MINVER=$(jq -r '.browser_specific_settings.gecko.strict_min_version' manifest.json) +TAG="v${VERSION}" +HASH=$(sha256sum "$XPI" | awk '{print $1}') +LINK="${BASE}/${OWNER}/${REPO}/releases/download/${TAG}/${XPI}" + +echo "Version : $VERSION" +echo "Tag : $TAG" +echo "SHA-256 : $HASH" + +# --- 1. Rewrite updates.json: prepend this version (idempotent) --- +[ -f updates.json ] || echo "{\"addons\":{\"${ID}\":{\"updates\":[]}}}" > updates.json +TMP=$(mktemp) +jq --arg id "$ID" --arg v "$VERSION" --arg link "$LINK" \ + --arg hash "sha256:$HASH" --arg min "$MINVER" ' + .addons[$id].updates = + ([{version:$v, update_link:$link, update_hash:$hash, + applications:{gecko:{strict_min_version:$min}}}] + + [ .addons[$id].updates[]? | select(.version != $v) ]) +' updates.json > "$TMP" && mv "$TMP" updates.json +echo "updates.json updated" + +# --- 2. Create the Gitea release + upload the .xpi asset --- +: "${GITEA_TOKEN:?Set GITEA_TOKEN to a Gitea token with repo write access}" +API="${BASE}/api/v1/repos/${OWNER}/${REPO}" +AUTH="Authorization: token ${GITEA_TOKEN}" + +echo "Creating release $TAG ..." +REL_ID=$(curl -fsS -X POST "${API}/releases" \ + -H "$AUTH" -H "Content-Type: application/json" \ + -d "{\"tag_name\":\"${TAG}\",\"target_commitish\":\"main\",\"name\":\"${TAG}\"}" \ + | jq -r '.id') + +echo "Uploading $XPI to release $REL_ID ..." +curl -fsS -X POST "${API}/releases/${REL_ID}/assets?name=${XPI}" \ + -H "$AUTH" -F "attachment=@${XPI};type=application/x-xpinstall" >/dev/null + +echo +echo "Done. Now commit & push the manifest:" +echo " git add updates.json manifest.json && git commit -m \"Release ${TAG}\" && git push" diff --git a/templates-reply-hotel.xpi b/templates-reply-hotel.xpi index 8409787..3a3f7b5 100644 Binary files a/templates-reply-hotel.xpi and b/templates-reply-hotel.xpi differ diff --git a/templates_options/templates_options.html b/templates_options/templates_options.html index bf45822..ccafcac 100644 --- a/templates_options/templates_options.html +++ b/templates_options/templates_options.html @@ -933,7 +933,13 @@