email-amazon/DMS/sync_dynamodb_to_sieve.py

225 lines
6.9 KiB
Python

#!/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