#!/usr/bin/env python3 """ sync_dynamodb_to_sieve.py - Sync DynamoDB rules to Dovecot Sieve """ import boto3 import os from pathlib import Path import json # Config REGION = 'us-east-2' TABLE = 'email-rules' VMAIL_BASE = '/var/mail' dynamodb = boto3.resource('dynamodb', region_name=REGION) table = dynamodb.Table(TABLE) import json 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:', # HIER ENTFERNT: Die extra Leerzeile war falsch. # .join() macht bereits den nötigen Umbruch nach 'text:' 'Content-Type: text/html; charset=utf-8', '', msg, '.' ]) else: # WICHTIG: Escape double quotes in msg, sonst Syntax-Fehler bei "I'm here" # json.dumps erstellt einen String mit Anführungszeichen (z.B. "Text"), # wir brauchen aber nur den escaped Inhalt. safe_msg = json.dumps(msg, ensure_ascii=False) # json.dumps gibt '"msg"' zurück, wir nutzen das direkt lines.append(f'vacation :days 1 :from "{email}" {safe_msg};') # WICHTIG: Ein Sieve-Script (und speziell der Multi-Line Block) # muss zwingend mit einem Zeilenumbruch enden. return '\n'.join(lines) + '\n' def sync(): """Sync all rules from DynamoDB to Sieve""" response = table.scan() 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 # Skip if mailbox doesn't exist if not mailbox_dir.exists(): print(f'⚠ Skipped {email} (mailbox not found)') continue sieve_path = mailbox_dir / '.dovecot.sieve' # Generate & write script = generate_sieve(email, item) sieve_path.write_text(script) # Compile os.system(f'sievec {sieve_path}') # 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}') # Aktivieren mit doveadm sieve put os.system(f'doveadm sieve put -u {email} -a default {managed_script}') print(f'✓ {email}') if __name__ == '__main__': sync()