3 Commits

Author SHA1 Message Date
7448759732 Merge branch 'master' into syncintervall
All checks were successful
Build Test Docker Image / docker-test (pull_request) Successful in 1m20s
Run Tests / test (pull_request) Successful in 4m54s
2026-03-29 18:48:22 +00:00
a799975c1f Merge pull request 'feat: Dateibasierten Docker-Healthcheck hinzugefügt' (#14) from healtcheck into master
Some checks failed
Run Tests / test (push) Has been cancelled
Reviewed-on: #14
2026-03-29 18:48:13 +00:00
b36ae57d48 feat: Sync-Intervall über SYNC_INTERVAL konfigurierbar (Standard: 15m)
All checks were successful
Build Test Docker Image / docker-test (pull_request) Successful in 1m33s
Run Tests / test (pull_request) Successful in 4m57s
2026-03-29 20:46:56 +02:00
5 changed files with 44 additions and 10 deletions

View File

@@ -12,7 +12,7 @@ Ein einfacher Daemon, der automatisch einen Geburtstagskalender für jede Mailco
- Liest Geburtstage aus allen CardDAV-Adressbüchern jeder Mailbox
- Erstellt und synchronisiert automatisch einen Geburtstagskalender pro Benutzer
- Synchronisation alle **15 Minuten**
- Synchronisation standardmäßig alle **15 Minuten** (konfigurierbar über `SYNC_INTERVAL`)
- Läuft als Docker-Container direkt im Mailcow-Stack
## Schnellstart

View File

@@ -9,10 +9,17 @@ import (
"time"
)
// maxSyncAge ist die maximale Dauer seit dem letzten Sync-Lauf, bevor der
// Healthcheck den Daemon als unhealthy meldet. Da der Sync alle 15 Minuten
// läuft, erlauben wir 20 Minuten Toleranz.
const maxSyncAge = 20 * time.Minute
// maxSyncAge berechnet die maximale Dauer seit dem letzten Sync-Lauf, bevor
// der Healthcheck den Daemon als unhealthy meldet. Die Toleranz beträgt
// 5 Minuten über dem konfigurierten Sync-Intervall.
func maxSyncAge() time.Duration {
syncInterval, err := parseSyncInterval()
if err != nil {
// Fallback: 20 Minuten (15m Standard-Intervall + 5m Toleranz).
return 20 * time.Minute
}
return syncInterval + 5*time.Minute
}
// healthFile ist der Dateiname der Healthcheck-Statusdatei, die neben dem
// State-File abgelegt wird.
@@ -73,7 +80,7 @@ func runHealthcheck() error {
if s.LastError != "" {
return fmt.Errorf("last sync failed: %s", s.LastError)
}
if time.Since(s.LastSync) > maxSyncAge {
if time.Since(s.LastSync) > maxSyncAge() {
return fmt.Errorf("last sync too old: %s ago", time.Since(s.LastSync).Round(time.Second))
}
return nil

View File

@@ -37,6 +37,7 @@ type Daemon struct {
oldCalendarName string
notificationEnabled bool
notificationTrigger string
syncInterval time.Duration
health *healthState
}
@@ -85,6 +86,12 @@ func run() error {
calendarName = "Birthdays"
}
notificationEnabled := strings.EqualFold(os.Getenv("NOTIFICATION_ENABLED"), "true")
syncInterval, err := parseSyncInterval()
if err != nil {
return err
}
slog.Info("sync interval configured", "interval", syncInterval)
notificationTrigger := "PT8H"
if notificationEnabled {
notificationTime := os.Getenv("NOTIFICATION_TIME")
@@ -107,6 +114,7 @@ func run() error {
calendarName: calendarName,
notificationEnabled: notificationEnabled,
notificationTrigger: notificationTrigger,
syncInterval: syncInterval,
}
if len(d.stateFilepath) == 0 {
d.stateFilepath = "state.json"
@@ -131,7 +139,7 @@ func (d *Daemon) daemonLoop() {
if err != nil {
slog.Error("error while syncing birthdays", "err", err)
}
time.Sleep(time.Minute * 15)
time.Sleep(d.syncInterval)
}
}
@@ -266,6 +274,23 @@ func runCleanup() error {
return nil
}
// parseSyncInterval liest SYNC_INTERVAL aus der Umgebung und gibt die
// geparste Dauer zurück. Standard: 15m.
func parseSyncInterval() (time.Duration, error) {
raw := os.Getenv("SYNC_INTERVAL")
if raw == "" {
return 15 * time.Minute, nil
}
d, err := time.ParseDuration(raw)
if err != nil {
return 0, fmt.Errorf("invalid SYNC_INTERVAL %q: %w", raw, err)
}
if d < 1*time.Minute {
return 0, fmt.Errorf("SYNC_INTERVAL must be at least 1m, got %s", d)
}
return d, 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

View File

@@ -31,11 +31,11 @@ Der Mailcow Birthday Daemon synchronisiert automatisch Geburtstagskalender für
## Synchronisationsintervall
Der Synchronisationszyklus läuft alle **15 Minuten** automatisch.
Der Synchronisationszyklus läuft standardmäßig alle **15 Minuten** automatisch. Das Intervall kann über die Umgebungsvariable `SYNC_INTERVAL` angepasst werden (z. B. `SYNC_INTERVAL=30m`). Details zu den möglichen Werten finden sich in der [Umgebungsvariablen-Tabelle](schnellstart.md#umgebungsvariablen).
## Healthcheck
- Das Dockerfile enthält eine `HEALTHCHECK`-Anweisung, die den eingebauten Subcommand `healthcheck` nutzt es werden keine externen Tools wie `curl` oder `wget` benötigt und kein Port wird geöffnet.
- Nach jedem Sync-Lauf schreibt der Daemon eine kleine Statusdatei (`health.json`) neben das State-File. Der `healthcheck`-Subcommand liest diese Datei und prüft, ob der letzte Sync aktuell und fehlerfrei war.
- Docker zeigt den Status in `docker ps` als `(healthy)` oder `(unhealthy)` an.
- Der Healthcheck meldet **unhealthy**, wenn der letzte Sync-Lauf fehlgeschlagen ist oder länger als 20 Minuten zurückliegt. Während der Startphase (bevor der erste Sync abgeschlossen ist) gilt der Daemon als healthy.
- Der Healthcheck meldet **unhealthy**, wenn der letzte Sync-Lauf fehlgeschlagen ist oder länger als das konfigurierte Sync-Intervall plus 5 Minuten Toleranz zurückliegt. Während der Startphase (bevor der erste Sync abgeschlossen ist) gilt der Daemon als healthy.

View File

@@ -27,6 +27,7 @@ services:
# - MAILCOW_RESOLVE_HOST=nginx-mailcow
# - NOTIFICATION_ENABLED=true
# - NOTIFICATION_TIME=08:00
# - SYNC_INTERVAL=15m
volumes:
- birthdaydaemon:/data
@@ -62,6 +63,7 @@ docker compose pull birthdaydaemon && docker compose up -d --no-deps birthdaydae
| `NOTIFICATION_ENABLED` | Nein | `false` | Aktiviert Kalender-Benachrichtigungen (VALARM) für Geburtstags-Events (`true`/`false`) |
| `NOTIFICATION_TIME` | Nein | `08:00` | Uhrzeit der Benachrichtigung im Format `HH:MM` (nur wirksam wenn `NOTIFICATION_ENABLED=true`) |
| `STATEFILE` | Nein | `state.json` (im Container: `/data/state.json`) | Pfad zur Zustandsdatei, in der App-Passwörter und der aktuelle Kalendername gespeichert werden |
| `SYNC_INTERVAL` | Nein | `15m` | Intervall zwischen den Synchronisationsläufen im Go-Duration-Format (z. B. `10m`, `30m`, `1h`). Mindestwert: `1m`. |
## API-Key erstellen
@@ -78,7 +80,7 @@ cd /opt/mailcow-dockerized
docker compose logs -f birthdaydaemon
```
Nach dem Start synchronisiert der Daemon automatisch alle 15 Minuten die Geburtstagskalender für jede Mailbox.
Nach dem Start synchronisiert der Daemon automatisch alle 15 Minuten (konfigurierbar über `SYNC_INTERVAL`) die Geburtstagskalender für jede Mailbox.
> **Hinweis für bestehende Installationen:** Falls der Daemon die Mailcow-API wegen Hairpin-NAT nicht erreichen kann, muss lediglich `MAILCOW_RESOLVE_HOST=nginx-mailcow` als Umgebungsvariable ergänzt werden. Details siehe [Installationsabschnitt](#installation).