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
@ -41,7 +39,7 @@ def generate_sieve(email, rules):
if forwards: if forwards:
lines.append('# rule:[forward]') lines.append('# rule:[forward]')
for fwd in forwards: for fwd in forwards:
lines.append(f'redirect :copy "{fwd}";') lines.append(f'redirect :copy "{fwd}";')
lines.append('') lines.append('')
# OOO # OOO
@ -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 sync(): def deactivate_sieve(email, mailbox_home):
"""Sync all rules from DynamoDB to Sieve""" """
response = table.scan() SICHERHEITS-VARIANTE:
Überschreibt das Sieve-Skript mit einem leeren 'keep;',
anstatt Dateien zu löschen.
"""
for item in response.get('Items', []): # Pfad zur aktiven Datei
email = item['email_address'] sieve_path = mailbox_home / '.dovecot.sieve'
domain = email.split('@')[1]
user = email.split('@')[0]
# Path: /var/mail/domain.de/user/.dovecot.sieve # Inhalt: Nur "keep;" -> Mail behalten, nichts tun.
mailbox_dir = Path(VMAIL_BASE) / domain / user / 'home' safe_content = (
'# Script deactivated by DynamoDB Sync (User not in DB)\n'
'keep;\n'
)
# Skip if mailbox doesn't exist # Prüfen, ob wir überhaupt etwas tun müssen (um unnötige Schreibvorgänge zu meiden)
if not mailbox_dir.exists(): # Wenn der Inhalt schon "keep;" ist, brechen wir ab.
mailbox_dir.mkdir(exist_ok=True) if sieve_path.exists() and not sieve_path.is_symlink():
os.system(f'chown docker:docker {mailbox_dir}') try:
current_content = sieve_path.read_text()
if "Script deactivated" in current_content:
return # Ist schon deaktiviert
except:
pass
sieve_path = mailbox_dir / '.dovecot.sieve' # 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
# Generate & write try:
script = generate_sieve(email, item) sieve_path.write_text(safe_content)
sieve_path.write_text(script)
# Compile # Kompilieren (wichtig, damit Dovecot die Änderung sofort sieht)
os.system(f'sievec {sieve_path}') os.system(f'sievec {sieve_path}')
# Copy to sieve dir # Ownership sicherstellen
sieve_dir = mailbox_dir / 'sieve' os.system(f'chown docker:docker {sieve_path}')
sieve_dir.mkdir(exist_ok=True)
managed_script = sieve_dir / 'default.sieve'
managed_script.write_text(script)
os.system(f'sievec {managed_script}')
# Ownership print(f'{email} (Regeln deaktiviert/geleert)')
os.system(f'chown -R docker:docker {sieve_dir}')
# Aktivieren mit doveadm sieve put except Exception as e:
os.system(f'doveadm sieve put -u {email} -a default {managed_script}') 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)
print(f'{email}')
def wait_for_dovecot(): 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' 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