This commit is contained in:
Andreas Knuth 2026-02-11 18:33:44 -06:00
parent 67c2440f4a
commit 9bb327eada
1 changed files with 116 additions and 59 deletions

View File

@ -1,9 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""
sync_dynamodb_to_sieve.py - Sync DynamoDB rules to Dovecot Sieve
"""
import boto3 import boto3
import os import os
import shutil
from pathlib import Path from pathlib import Path
import json import json
import time import time
@ -58,105 +56,165 @@ def generate_sieve(email, rules):
'', '',
msg, msg,
'.', '.',
';' # <--- HIER WAR DER FEHLER ';'
]) ])
else: else:
# Sicherheitshalber JSON dump für escaping von Anführungszeichen nutzen
safe_msg = json.dumps(msg, ensure_ascii=False) safe_msg = json.dumps(msg, ensure_ascii=False)
lines.append(f'vacation :days 1 :from "{email}" {safe_msg};') lines.append(f'vacation :days 1 :from "{email}" {safe_msg};')
return '\n'.join(lines) + '\n' 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(): def sync():
"""Sync all rules from DynamoDB to Sieve""" """Sync logic"""
# 1. DB Status abrufen
try:
response = table.scan() 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
for item in response.get('Items', []): # 2. Filesystem scannen
email = item['email_address'] base_path = Path(VMAIL_BASE)
domain = email.split('@')[1]
user = email.split('@')[0]
# Path: /var/mail/domain.de/user/.dovecot.sieve if not base_path.exists():
mailbox_dir = Path(VMAIL_BASE) / domain / user / 'home' print("Warnung: /var/mail existiert nicht.")
return
# Skip if mailbox doesn't exist # Iteriere durch Domains
if not mailbox_dir.exists(): for domain_dir in base_path.iterdir():
mailbox_dir.mkdir(exist_ok=True) if not domain_dir.is_dir(): continue
os.system(f'chown docker:docker {mailbox_dir}')
sieve_path = mailbox_dir / '.dovecot.sieve' # 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'
# Generate & write
script = generate_sieve(email, item) script = generate_sieve(email, item)
sieve_path.write_text(script) sieve_path.write_text(script)
# Compile
os.system(f'sievec {sieve_path}') os.system(f'sievec {sieve_path}')
# Copy to sieve dir # Ownership
sieve_dir = mailbox_dir / 'sieve' os.system(f'chown docker:docker {sieve_path}')
sieve_dir.mkdir(exist_ok=True)
# (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 = sieve_dir / 'default.sieve'
managed_script.write_text(script) managed_script.write_text(script)
os.system(f'sievec {managed_script}') os.system(f'sievec {managed_script}')
# Ownership
os.system(f'chown -R docker:docker {sieve_dir}') 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}') print(f'{email}')
def wait_for_dovecot(): # --- FALL B: User ist NICHT in DB (Deaktivieren) ---
"""Wartet, bis der Dovecot Auth Socket verfügbar ist.""" else:
# Der Pfad zum Socket, über den doveadm kommuniziert # Nur wenn der Home-Ordner existiert (wir legen keine Leichen für nicht-existente User an)
socket_path = '/var/run/dovecot/auth-userdb' 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...") print("⏳ Warte auf Dovecot Start...")
while not os.path.exists(socket_path): while not os.path.exists(socket_path):
print(f" ... Socket {socket_path} noch nicht da. Schlafe 5s.")
time.sleep(5) time.sleep(5)
print("✅ Dovecot ist bereit!") print("✅ Dovecot ist bereit!")
if __name__ == '__main__': if __name__ == '__main__':
# 1. Erst warten, bis Dovecot da ist, sonst hagelt es Fehler beim Start
wait_for_dovecot() wait_for_dovecot()
# Pfad zur Cron-Definition (nur der String, z.B. "*/5 * * * *")
CRON_FILE = '/etc/sieve-schedule' CRON_FILE = '/etc/sieve-schedule'
# Fallback, falls Datei fehlt
cron_string = "*/5 * * * *" cron_string = "*/5 * * * *"
if os.path.exists(CRON_FILE): if os.path.exists(CRON_FILE):
with open(CRON_FILE, 'r') as f: with open(CRON_FILE, 'r') as f:
# Kommentare entfernen und String holen
content = f.read().strip() content = f.read().strip()
if content and not content.startswith('#'): if content and not content.startswith('#'):
cron_string = content cron_string = content
print(f"DynamoDB Sieve Sync gestartet. Zeitplan: {cron_string}") print(f"DynamoDB Sieve Sync (Safe Mode) 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() sync()
# Iterator erstellen
base_time = datetime.now() base_time = datetime.now()
iter = croniter(cron_string, base_time) iter = croniter(cron_string, base_time)
while True: while True:
# Den nächsten Zeitpunkt berechnen
next_run = iter.get_next(datetime) next_run = iter.get_next(datetime)
now = datetime.now() now = datetime.now()
sleep_seconds = (next_run - now).total_seconds() sleep_seconds = (next_run - now).total_seconds()
if sleep_seconds > 0: if sleep_seconds > 0:
# Warten bis zum nächsten Slot
time.sleep(sleep_seconds) time.sleep(sleep_seconds)
try: try:
@ -164,5 +222,4 @@ if __name__ == '__main__':
sync() sync()
except Exception as e: except Exception as e:
print(f"Fehler beim Sync: {e}") print(f"Fehler beim Sync: {e}")
# Wichtig: Bei Fehler nicht abstürzen, sondern weitermachen
pass pass