From b346d58beb5edbb1dd4be1a577e14ebe58704fce Mon Sep 17 00:00:00 2001 From: "Patrick Asmus (scriptos)" Date: Sun, 29 Mar 2026 00:00:03 +0100 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20Cleanup-Subcommand=20zum=20Entferne?= =?UTF-8?q?n=20doppelter=20Kalender=20hinzugef=C3=BCgt,=20Doku-Hinweis=20a?= =?UTF-8?q?uf=20v0.2.0=20pr=C3=A4zisiert?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/mcbdd/main.go | 78 +++++++++++++++++++++++++++++++++++++++++ docs/funktionsweise.md | 2 +- docs/troubleshooting.md | 17 +++++++++ 3 files changed, 96 insertions(+), 1 deletion(-) diff --git a/cmd/mcbdd/main.go b/cmd/mcbdd/main.go index ae24196..8b4c9a4 100644 --- a/cmd/mcbdd/main.go +++ b/cmd/mcbdd/main.go @@ -40,6 +40,13 @@ type Daemon struct { } func main() { + if len(os.Args) > 1 && os.Args[1] == "cleanup" { + if err := runCleanup(); err != nil { + slog.Error("cleanup failed", "err", err) + os.Exit(1) + } + return + } if err := run(); err != nil { slog.Error("fatal error", "err", err) os.Exit(1) @@ -175,6 +182,77 @@ func (d *Daemon) processUser(ctx context.Context, m mailcow.Mailbox) error { return nil } +func runCleanup() error { + if len(os.Args) < 3 { + fmt.Fprintf(os.Stderr, "Verwendung: %s cleanup \n", os.Args[0]) + fmt.Fprintf(os.Stderr, "\nEntfernt einen automatisch erstellten Geburtstagskalender aus allen Mailboxen.\n") + fmt.Fprintf(os.Stderr, "Nur Kalender, deren Einträge ausschließlich vom Daemon erstellt wurden, werden gelöscht.\n") + os.Exit(1) + } + oldCalendarName := os.Args[2] + + slog.Info("starting calendar cleanup", "calendarName", oldCalendarName) + + mailcowBase := os.Getenv("MAILCOW_BASE") + if mailcowBase == "" { + return fmt.Errorf("MAILCOW_BASE environment variable is not set") + } + mailcowAPIKey := os.Getenv("MAILCOW_APIKEY") + if mailcowAPIKey == "" { + return fmt.Errorf("MAILCOW_APIKEY environment variable is not set") + } + calendarName := os.Getenv("CALENDAR_NAME") + if calendarName == "" { + calendarName = "Birthdays" + } + + d := &Daemon{ + userTokens: make(map[string]string), + userTokensLock: &sync.RWMutex{}, + baseURL: mailcowBase, + stateFilepath: os.Getenv("STATEFILE"), + httpClient: &http.Client{Transport: buildTransport()}, + calendarName: calendarName, + } + if len(d.stateFilepath) == 0 { + d.stateFilepath = "state.json" + } + d.mailcowClient = mailcow.New(d.httpClient, mailcowBase, mailcowAPIKey) + + if err := d.loadState(); err != nil { + return fmt.Errorf("error loading state: %w", err) + } + + mb, err := d.mailcowClient.GetMailboxes(context.Background()) + if err != nil { + return fmt.Errorf("error fetching mailboxes: %w", err) + } + + processed, skipped := 0, 0 + for _, m := range mb { + if !m.IsActive() { + continue + } + d.userTokensLock.RLock() + pass, ok := d.userTokens[m.Username] + d.userTokensLock.RUnlock() + if !ok { + slog.Warn("no stored password for user, skipping", "user", m.Username) + skipped++ + continue + } + ctx := context.Background() + davclient := webdav.HTTPClientWithBasicAuth(d.httpClient, m.Username, pass) + if err := d.cleanupOldCalendar(ctx, davclient, m.Username, oldCalendarName); err != nil { + slog.Error("error cleaning up calendar", "user", m.Username, "err", err) + } + processed++ + } + + slog.Info("cleanup finished", "processed", processed, "skipped", skipped) + return nil +} + // buildTransport erstellt einen http.Transport. // Wenn MAILCOW_RESOLVE_HOST gesetzt ist (z. B. "nginx-mailcow"), wird der // tatsächliche TCP-Connect auf diesen Host umgeleitet, während TLS-SNI und diff --git a/docs/funktionsweise.md b/docs/funktionsweise.md index dde1f09..b183174 100644 --- a/docs/funktionsweise.md +++ b/docs/funktionsweise.md @@ -21,7 +21,7 @@ Der Mailcow Birthday Daemon synchronisiert automatisch Geburtstagskalender für - Die berechneten Ereignisse werden in einen Kalender synchronisiert, dessen Name über `CALENDAR_NAME` konfigurierbar ist (Standard: „Birthdays"). Der Anzeigename kann vom Benutzer in SOGo zusätzlich umbenannt werden. - Bei Änderung von `CALENDAR_NAME` wird der alte Kalender beim nächsten Start automatisch entfernt und ein neuer mit dem neuen Namen erstellt. Der alte Kalender wird dabei nur gelöscht, wenn er ausschließlich vom Daemon erstellte Einträge enthält – manuell angelegte Kalender mit gleichem Namen bleiben unangetastet. -> **Wichtig:** Damit die Umbenennung korrekt erkannt wird, muss der Daemon **mindestens einmal** mit dem neuen Code und dem **alten** Kalendernamen gelaufen sein, damit der Name im State-File gespeichert wird. Erst danach `CALENDAR_NAME` ändern und erneut starten. Wird der Name geändert, bevor der State aktualisiert wurde, kann der alte Kalender nicht automatisch entfernt werden und muss manuell gelöscht werden. +> **Wichtig (betrifft nur Updates von vor v0.2.0):** Damit die Umbenennung korrekt erkannt wird, muss der Daemon **mindestens einmal** mit dem neuen Code (ab v0.2.0) und dem **alten** Kalendernamen gelaufen sein, damit der Name im State-File gespeichert wird. Erst danach `CALENDAR_NAME` ändern und erneut starten. Wird der Name geändert, bevor der State aktualisiert wurde, kann der alte Kalender nicht automatisch entfernt werden. In diesem Fall kann der integrierte Cleanup-Befehl verwendet werden (siehe [Troubleshooting](troubleshooting.md#doppelte-kalender-nach-umbenennung-von-calendar_name)). ## Benachrichtigungen diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 5c61b76..b62b4be 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -15,6 +15,23 @@ Das gespeicherte App-Passwort für den betroffenen Benutzer ist ungültig (z. B. manuell in Mailcow gelöscht). Der Daemon verwirft das alte Passwort automatisch und erstellt beim nächsten Zyklus ein neues. +## Doppelte Kalender nach Umbenennung von `CALENDAR_NAME` + +Wurde `CALENDAR_NAME` geändert, bevor der alte Name im State-File gespeichert war (z. B. bei einem Update von vor v0.2.0), existieren pro Mailbox zwei Geburtstagskalender. Der integrierte Cleanup-Befehl entfernt den alten Kalender aus allen Mailboxen – aber nur, wenn alle enthaltenen Einträge vom Daemon erstellt wurden. Manuell angelegte Kalender mit gleichem Namen bleiben unangetastet. + +```bash +cd /opt/mailcow-dockerized +docker compose exec birthdaydaemon /mailcow-birthday-daemon cleanup +``` + +**Beispiel:** Der alte Kalender hieß `Birthdays` und wurde auf `Geburtstage` umgestellt: + +```bash +docker compose exec birthdaydaemon /mailcow-birthday-daemon cleanup Birthdays +``` + +> **Hinweis:** Der Daemon muss vorher mindestens einmal gelaufen sein, damit App-Passwörter im State-File vorhanden sind. Benutzer ohne gespeichertes Passwort werden übersprungen. + ## Kalender erscheint nicht in SOGo SOGo zeigt neue Kalender manchmal erst nach einem Neuladen der Seite (Strg+Shift+R) oder nach dem nächsten Login an. Der Kalender wird unter dem Namen erstellt, der in `CALENDAR_NAME` konfiguriert ist (Standard: `Birthdays`). From a321bb691781e192baee13276bd2b1e881db1e92 Mon Sep 17 00:00:00 2001 From: "Patrick Asmus (scriptos)" Date: Sun, 29 Mar 2026 00:03:52 +0100 Subject: [PATCH 2/4] =?UTF-8?q?fix(ci):=20GoReleaser=20gitea=5Furls=20expl?= =?UTF-8?q?izit=20konfiguriert=20=E2=80=93=20Release-URL-Fehler=20behoben?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/release.yaml | 2 +- .goreleaser.yaml | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.gitea/workflows/release.yaml b/.gitea/workflows/release.yaml index ed40e23..121ca28 100644 --- a/.gitea/workflows/release.yaml +++ b/.gitea/workflows/release.yaml @@ -32,6 +32,6 @@ jobs: args: release --clean env: GITEA_TOKEN: ${{ secrets.REGISTRY_TOKEN }} - GITEA_SERVER_URL: ${{ secrets.REGISTRY_URL }} + REGISTRY_URL: ${{ secrets.REGISTRY_URL }} REGISTRY_USER: ${{ secrets.REGISTRY_USER }} GORELEASER_FORCE_TOKEN: gitea diff --git a/.goreleaser.yaml b/.goreleaser.yaml index fcfae13..b724ad8 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -29,6 +29,10 @@ release: gitea: owner: "{{ .Env.REGISTRY_USER }}" name: mailcow-birthday-daemon + gitea_urls: + api: "{{ .Env.REGISTRY_URL }}/api/v1/" + download: "{{ .Env.REGISTRY_URL }}" + skip_tls_verify: false prerelease: auto changelog: sort: asc From cb8192640de33da5172396f59ac1d3198e458504 Mon Sep 17 00:00:00 2001 From: "Patrick Asmus (scriptos)" Date: Sun, 29 Mar 2026 00:23:40 +0100 Subject: [PATCH 3/4] =?UTF-8?q?fix(ci):=20gitea=5Furls=20auf=20Top-Level?= =?UTF-8?q?=20verschoben=20=E2=80=93=20YAML-Unmarshal-Fehler=20behoben?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .goreleaser.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.goreleaser.yaml b/.goreleaser.yaml index b724ad8..8897d6f 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -29,11 +29,11 @@ release: gitea: owner: "{{ .Env.REGISTRY_USER }}" name: mailcow-birthday-daemon - gitea_urls: - api: "{{ .Env.REGISTRY_URL }}/api/v1/" - download: "{{ .Env.REGISTRY_URL }}" - skip_tls_verify: false prerelease: auto +gitea_urls: + api: "{{ .Env.REGISTRY_URL }}/api/v1/" + download: "{{ .Env.REGISTRY_URL }}" + skip_tls_verify: false changelog: sort: asc filters: From 882fb6448d3097519562242ebf0785b4dd10a976 Mon Sep 17 00:00:00 2001 From: "Patrick Asmus (scriptos)" Date: Sun, 29 Mar 2026 00:52:07 +0100 Subject: [PATCH 4/4] =?UTF-8?q?docs:=20Beispielausgabe=20f=C3=BCr=20Cleanu?= =?UTF-8?q?p-Befehl=20erg=C3=A4nzt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/troubleshooting.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index b62b4be..f027c1f 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -30,6 +30,17 @@ docker compose exec birthdaydaemon /mailcow-birthday-daemon cleanup **Hinweis:** Der Daemon muss vorher mindestens einmal gelaufen sein, damit App-Passwörter im State-File vorhanden sind. Benutzer ohne gespeichertes Passwort werden übersprungen. ## Kalender erscheint nicht in SOGo