#!/usr/bin/env python3 import boto3 import os import shutil 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, '.', ';' ]) else: safe_msg = json.dumps(msg, ensure_ascii=False) lines.append(f'vacation :days 1 :from "{email}" {safe_msg};') return '\n'.join(lines) + '\n' def deactivate_sieve(email, mailbox_home): """ SICHERHEITS-VARIANTE: Überschreibt das Sieve-Skript mit einem leeren 'keep;', anstatt Dateien zu löschen. """ # Pfad zur aktiven Datei sieve_path = mailbox_home / '.dovecot.sieve' # Inhalt: Nur "keep;" -> Mail behalten, nichts tun. safe_content = ( '# Script deactivated by DynamoDB Sync (User not in DB)\n' 'keep;\n' ) # Prüfen, ob wir überhaupt etwas tun müssen (um unnötige Schreibvorgänge zu meiden) # Wenn der Inhalt schon "keep;" ist, brechen wir ab. if sieve_path.exists() and not sieve_path.is_symlink(): try: current_content = sieve_path.read_text() if "Script deactivated" in current_content: return # Ist schon deaktiviert except: pass # Datei sicher schreiben (überschreibt auch Symlinks, wenn os.open genutzt wird, # aber pathlib write_text folgt symlinks oder überschreibt file). # Um sicher zu gehen, dass wir keinen Symlink auf eine Systemdatei überschreiben: if sieve_path.is_symlink(): try: os.unlink(sieve_path) # Link entfernen except OSError: pass try: sieve_path.write_text(safe_content) # Kompilieren (wichtig, damit Dovecot die Änderung sofort sieht) os.system(f'sievec {sieve_path}') # Ownership sicherstellen os.system(f'chown docker:docker {sieve_path}') print(f'⚪ {email} (Regeln deaktiviert/geleert)') except Exception as e: print(f"Fehler beim Deaktivieren von {email}: {e}") def sync(): """Sync logic""" # 1. DB Status abrufen try: response = table.scan() db_users = {item['email_address']: item for item in response.get('Items', [])} except Exception as e: print(f"FATAL: Konnte DynamoDB nicht lesen ({e}). Breche ab, um keine Regeln zu löschen.") return # 2. Filesystem scannen base_path = Path(VMAIL_BASE) if not base_path.exists(): print("Warnung: /var/mail existiert nicht.") return # Iteriere durch Domains for domain_dir in base_path.iterdir(): if not domain_dir.is_dir(): continue # Iteriere durch User for user_dir in domain_dir.iterdir(): if not user_dir.is_dir(): continue user = user_dir.name domain = domain_dir.name email = f"{user}@{domain}" # WICHTIG: Wir arbeiten NUR im 'home' Unterordner # Die Mails liegen in user_dir/cur etc. -> Die fassen wir nicht an. mailbox_home = user_dir / 'home' # --- FALL A: User ist in der DB (Update) --- if email in db_users: item = db_users[email] if not mailbox_home.exists(): mailbox_home.mkdir(exist_ok=True) os.system(f'chown docker:docker {mailbox_home}') sieve_path = mailbox_home / '.dovecot.sieve' script = generate_sieve(email, item) sieve_path.write_text(script) os.system(f'sievec {sieve_path}') # Ownership os.system(f'chown docker:docker {sieve_path}') # (Optional) Auch in den sieve/ Ordner spiegeln für Roundcube Kompatibilität sieve_dir = mailbox_home / 'sieve' if sieve_dir.exists(): managed_script = sieve_dir / 'default.sieve' managed_script.write_text(script) os.system(f'sievec {managed_script}') os.system(f'chown -R docker:docker {sieve_dir}') print(f'✓ {email}') # --- FALL B: User ist NICHT in DB (Deaktivieren) --- else: # Nur wenn der Home-Ordner existiert (wir legen keine Leichen für nicht-existente User an) if mailbox_home.exists(): deactivate_sieve(email, mailbox_home) def wait_for_dovecot(): socket_path = '/var/run/dovecot/auth-userdb' print("⏳ Warte auf Dovecot Start...") while not os.path.exists(socket_path): time.sleep(5) print("✅ Dovecot ist bereit!") if __name__ == '__main__': wait_for_dovecot() CRON_FILE = '/etc/sieve-schedule' cron_string = "*/5 * * * *" if os.path.exists(CRON_FILE): with open(CRON_FILE, 'r') as f: content = f.read().strip() if content and not content.startswith('#'): cron_string = content print(f"DynamoDB Sieve Sync (Safe Mode) gestartet. Zeitplan: {cron_string}") sync() base_time = datetime.now() iter = croniter(cron_string, base_time) while True: next_run = iter.get_next(datetime) now = datetime.now() sleep_seconds = (next_run - now).total_seconds() if sleep_seconds > 0: time.sleep(sleep_seconds) try: print(f"[{datetime.now()}] Starte Sync...") sync() except Exception as e: print(f"Fehler beim Sync: {e}") pass