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..8897d6f 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -30,6 +30,10 @@ release: owner: "{{ .Env.REGISTRY_USER }}" name: mailcow-birthday-daemon prerelease: auto +gitea_urls: + api: "{{ .Env.REGISTRY_URL }}/api/v1/" + download: "{{ .Env.REGISTRY_URL }}" + skip_tls_verify: false changelog: sort: asc filters: 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..f027c1f 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -15,6 +15,34 @@ 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 +``` + +**Beispielausgabe:** + +``` +2026/03/28 23:46:22 INFO starting calendar cleanup calendarName=Birthdays +2026/03/28 23:46:22 INFO using internal resolve host for connections resolveHost=nginx-mailcow +2026/03/28 23:46:22 INFO removed old birthday calendar user=user1@example.com calendar=Birthdays +2026/03/28 23:46:23 INFO removed old birthday calendar user=user2@example.com calendar=Birthdays +2026/03/28 23:46:23 INFO removed old birthday calendar user=user3@example.com calendar=Birthdays +2026/03/28 23:46:24 INFO cleanup finished processed=5 skipped=0 +``` + +> **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`).