From 9bb327eadaff11fc9faa34e28332a9785c3d8289 Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Wed, 11 Feb 2026 18:33:44 -0600 Subject: [PATCH] cleanup --- DMS/sync_dynamodb_to_sieve.py | 175 ++++++++++++++++++++++------------ 1 file changed, 116 insertions(+), 59 deletions(-) diff --git a/DMS/sync_dynamodb_to_sieve.py b/DMS/sync_dynamodb_to_sieve.py index bd122e4..f373f13 100644 --- a/DMS/sync_dynamodb_to_sieve.py +++ b/DMS/sync_dynamodb_to_sieve.py @@ -1,9 +1,7 @@ #!/usr/bin/env python3 -""" -sync_dynamodb_to_sieve.py - Sync DynamoDB rules to Dovecot Sieve -""" import boto3 import os +import shutil from pathlib import Path import json import time @@ -41,7 +39,7 @@ def generate_sieve(email, rules): if forwards: lines.append('# rule:[forward]') for fwd in forwards: - lines.append(f'redirect :copy "{fwd}";') + lines.append(f'redirect :copy "{fwd}";') lines.append('') # OOO @@ -58,105 +56,165 @@ def generate_sieve(email, rules): '', 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() +def deactivate_sieve(email, mailbox_home): + """ + SICHERHEITS-VARIANTE: + Überschreibt das Sieve-Skript mit einem leeren 'keep;', + anstatt Dateien zu löschen. + """ - 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 / 'home' - - # Skip if mailbox doesn't exist - if not mailbox_dir.exists(): - mailbox_dir.mkdir(exist_ok=True) - os.system(f'chown docker:docker {mailbox_dir}') - - sieve_path = mailbox_dir / '.dovecot.sieve' - - # Generate & write - script = generate_sieve(email, item) - sieve_path.write_text(script) + # 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' + ) - # Compile + # 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}") - # 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}') +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 - # Aktivieren mit doveadm sieve put - os.system(f'doveadm sieve put -u {email} -a default {managed_script}') + # 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) - 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) + print(f"DynamoDB Sieve Sync (Safe Mode) gestartet. Zeitplan: {cron_string}") + 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: @@ -164,5 +222,4 @@ if __name__ == '__main__': sync() except Exception as e: print(f"Fehler beim Sync: {e}") - # Wichtig: Bei Fehler nicht abstürzen, sondern weitermachen pass \ No newline at end of file