#!/usr/bin/env python3 """ sync_dynamodb_to_sieve.py - Sync DynamoDB rules to Dovecot Sieve """ import boto3 import os from pathlib import Path import json import time from datetime import datetime try: from croniter import croniter except ImportError: print("Bitte 'croniter' via pip installieren!") exit(1) # Config REGION = 'us-east-2' TABLE = 'email-rules' VMAIL_BASE = '/var/mail' dynamodb = boto3.resource('dynamodb', region_name=REGION) table = dynamodb.Table(TABLE) def generate_sieve(email, rules): """Generate Sieve script from DynamoDB rules""" lines = ['require ["copy","vacation","variables"];', ''] # Skip if already processed by worker lines.extend([ '# Skip if already processed by worker', 'if header :contains "X-SES-Worker-Processed" "" {', ' keep;', ' stop;', '}', '' ]) # Forwards forwards = rules.get('forwards', []) if forwards: lines.append('# rule:[forward]') for fwd in forwards: lines.append(f'redirect :copy "{fwd}";') lines.append('') # OOO if rules.get('ooo_active'): msg = rules.get('ooo_message', 'I am away') content_type = rules.get('ooo_content_type', 'text') lines.append('# rule:[reply]') if content_type == 'html': lines.extend([ f'vacation :days 1 :from "{email}" :mime text:', 'Content-Type: text/html; charset=utf-8', '', msg, '.', ';' # <--- HIER WAR DER FEHLER ]) else: # Sicherheitshalber JSON dump für escaping von Anführungszeichen nutzen safe_msg = json.dumps(msg, ensure_ascii=False) lines.append(f'vacation :days 1 :from "{email}" {safe_msg};') return '\n'.join(lines) + '\n' def sync(): """Sync all rules from DynamoDB to Sieve""" response = table.scan() for item in response.get('Items', []): email = item['email_address'] domain = email.split('@')[1] user = email.split('@')[0] # Path: /var/mail/domain.de/user/.dovecot.sieve mailbox_dir = Path(VMAIL_BASE) / domain / user # Skip if mailbox doesn't exist if not mailbox_dir.exists(): print(f'⚠ Skipped {email} (mailbox not found)') continue sieve_path = mailbox_dir / '.dovecot.sieve' # Generate & write script = generate_sieve(email, item) sieve_path.write_text(script) # Compile os.system(f'sievec {sieve_path}') # Copy to sieve dir sieve_dir = mailbox_dir / 'sieve' sieve_dir.mkdir(exist_ok=True) managed_script = sieve_dir / 'default.sieve' managed_script.write_text(script) os.system(f'sievec {managed_script}') # Ownership os.system(f'chown -R docker:docker {sieve_dir}') # Aktivieren mit doveadm sieve put os.system(f'doveadm sieve put -u {email} -a default {managed_script}') print(f'✓ {email}') def wait_for_dovecot(): """Wartet, bis der Dovecot Auth Socket verfügbar ist.""" # Der Pfad zum Socket, über den doveadm kommuniziert socket_path = '/var/run/dovecot/auth-userdb' print("⏳ Warte auf Dovecot Start...") while not os.path.exists(socket_path): print(f" ... Socket {socket_path} noch nicht da. Schlafe 5s.") time.sleep(5) print("✅ Dovecot ist bereit!") if __name__ == '__main__': # 1. Erst warten, bis Dovecot da ist, sonst hagelt es Fehler beim Start wait_for_dovecot() # Pfad zur Cron-Definition (nur der String, z.B. "*/5 * * * *") CRON_FILE = '/etc/sieve-schedule' # Fallback, falls Datei fehlt cron_string = "*/5 * * * *" if os.path.exists(CRON_FILE): with open(CRON_FILE, 'r') as f: # Kommentare entfernen und String holen content = f.read().strip() if content and not content.startswith('#'): cron_string = content print(f"DynamoDB Sieve Sync gestartet. Zeitplan: {cron_string}") print(f"AWS Region: {os.environ.get('AWS_DEFAULT_REGION', 'nicht gesetzt')}") # Debug Check # Initialer Lauf beim Start? (Optional, hier auskommentiert) sync() # Iterator erstellen base_time = datetime.now() iter = croniter(cron_string, base_time) while True: # Den nächsten Zeitpunkt berechnen next_run = iter.get_next(datetime) now = datetime.now() sleep_seconds = (next_run - now).total_seconds() if sleep_seconds > 0: # Warten bis zum nächsten Slot time.sleep(sleep_seconds) try: print(f"[{datetime.now()}] Starte Sync...") sync() except Exception as e: print(f"Fehler beim Sync: {e}") # Wichtig: Bei Fehler nicht abstürzen, sondern weitermachen pass