update passwd
This commit is contained in:
parent
9905481e26
commit
212341744c
|
|
@ -0,0 +1,354 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Dovecot Passwd Manager
|
||||
|
||||
Verwaltet Benutzerkonten in der Dovecot passwd-Datei basierend auf den konfigurierten
|
||||
E-Mail-Domains und Benutzernamen. Das Script liest die gleichen Umgebungsvariablen wie
|
||||
der s3_email_downloader.py und kann separat ausgeführt werden.
|
||||
|
||||
Nutzung:
|
||||
python3 dovecot_passwd_manager.py # Nur Überprüfung, keine Änderungen
|
||||
python3 dovecot_passwd_manager.py update # Aktualisiert die passwd-Datei
|
||||
python3 dovecot_passwd_manager.py force # Erzwingt Aktualisierung auch ohne Änderungen
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import logging
|
||||
import datetime
|
||||
import subprocess
|
||||
import filecmp
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from tempfile import NamedTemporaryFile
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# .env-Datei laden
|
||||
load_dotenv()
|
||||
|
||||
# Logging konfigurieren
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
handlers=[
|
||||
logging.FileHandler('dovecot_passwd_manager.log'),
|
||||
logging.StreamHandler()
|
||||
]
|
||||
)
|
||||
logger = logging.getLogger("dovecot-passwd-manager")
|
||||
|
||||
# Konfiguration
|
||||
MAIL_DIR = os.environ.get('MAIL_DIR', './mail')
|
||||
DOVECOT_PASSWD_FILE = os.environ.get('DOVECOT_PASSWD_FILE', './config/passwd')
|
||||
DOVECOT_PASSWD_BACKUP = os.environ.get('DOVECOT_PASSWD_BACKUP', './config/passwd.bak')
|
||||
DOVECOT_CONTAINER = os.environ.get('DOVECOT_CONTAINER', 'dovecot')
|
||||
DEFAULT_PASSWORD_PATTERN = os.environ.get('DEFAULT_PASSWORD_PATTERN', '{domain}{year}!')
|
||||
UID = os.environ.get('MAIL_UID', '1001')
|
||||
GID = os.environ.get('MAIL_GID', '1000')
|
||||
AWS_REGION = os.environ.get('AWS_REGION', 'us-east-2')
|
||||
|
||||
# Domains-Konfiguration
|
||||
DOMAINS_CONFIG_FILE = os.environ.get('DOMAINS_CONFIG_FILE', 'domains_config.json')
|
||||
|
||||
def load_domains_config():
|
||||
"""Lädt die Domain-Konfiguration aus einer JSON-Datei oder aus Umgebungsvariablen"""
|
||||
domains = {}
|
||||
|
||||
# Versuchen, Konfiguration aus JSON-Datei zu laden
|
||||
config_file = Path(DOMAINS_CONFIG_FILE)
|
||||
if config_file.exists():
|
||||
try:
|
||||
with open(config_file, 'r') as f:
|
||||
domains = json.load(f)
|
||||
logger.info(f"Domain-Konfiguration aus {DOMAINS_CONFIG_FILE} geladen")
|
||||
return domains
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Laden der Domain-Konfiguration aus {DOMAINS_CONFIG_FILE}: {str(e)}")
|
||||
|
||||
# Fallback: Konfiguration aus Umgebungsvariablen
|
||||
domain_index = 1
|
||||
while True:
|
||||
domain_key = f"DOMAIN_{domain_index}"
|
||||
domain_name = os.environ.get(domain_key)
|
||||
|
||||
if not domain_name:
|
||||
break # Keine weitere Domain-Definition gefunden
|
||||
|
||||
bucket = os.environ.get(f"{domain_key}_BUCKET", "")
|
||||
prefix = os.environ.get(f"{domain_key}_PREFIX", "emails/")
|
||||
usernames = os.environ.get(f"{domain_key}_USERNAMES", "")
|
||||
region = os.environ.get(f"{domain_key}_REGION", AWS_REGION)
|
||||
|
||||
if domain_name and usernames:
|
||||
domains[domain_name] = {
|
||||
"bucket": bucket,
|
||||
"prefix": prefix,
|
||||
"usernames": usernames.split(','),
|
||||
"region": region
|
||||
}
|
||||
logger.info(f"Domain {domain_name} aus Umgebungsvariablen konfiguriert")
|
||||
else:
|
||||
logger.warning(f"Unvollständige Konfiguration für {domain_name}, wird übersprungen")
|
||||
|
||||
domain_index += 1
|
||||
|
||||
# Fallback für Abwärtskompatibilität
|
||||
if not domains:
|
||||
old_domain = os.environ.get('VALID_DOMAIN', '')
|
||||
old_bucket = os.environ.get('S3_BUCKET', '')
|
||||
old_prefix = os.environ.get('EMAIL_PREFIX', 'emails/')
|
||||
old_usernames = os.environ.get('VALID_USERNAMES', '')
|
||||
|
||||
if old_domain and old_usernames:
|
||||
domains[old_domain] = {
|
||||
"bucket": old_bucket,
|
||||
"prefix": old_prefix,
|
||||
"usernames": old_usernames.split(','),
|
||||
"region": AWS_REGION
|
||||
}
|
||||
logger.info(f"Alte Konfiguration für Domain {old_domain} geladen")
|
||||
|
||||
return domains
|
||||
|
||||
def generate_password(domain):
|
||||
"""Generiert ein Passwort nach dem Muster {domain}{year}!"""
|
||||
current_year = datetime.datetime.now().year
|
||||
domain_prefix = domain.split('.')[0] # Nur den ersten Teil der Domain verwenden
|
||||
return DEFAULT_PASSWORD_PATTERN.replace('{domain}', domain_prefix).replace('{year}', str(current_year))
|
||||
|
||||
def get_required_emails(domains_config):
|
||||
"""
|
||||
Ermittelt eine Liste aller E-Mail-Adressen, die in der passwd-Datei
|
||||
vorhanden sein sollten, basierend auf der Domain-Konfiguration.
|
||||
"""
|
||||
required_emails = []
|
||||
|
||||
for domain, config in domains_config.items():
|
||||
for username in config["usernames"]:
|
||||
email = f"{username}@{domain}"
|
||||
required_emails.append(email)
|
||||
|
||||
return sorted(required_emails)
|
||||
|
||||
def get_existing_emails(passwd_file):
|
||||
"""
|
||||
Liest die bestehende passwd-Datei und gibt eine Liste aller
|
||||
bereits konfigurierten E-Mail-Adressen zurück.
|
||||
"""
|
||||
existing_emails = []
|
||||
|
||||
if os.path.exists(passwd_file):
|
||||
try:
|
||||
with open(passwd_file, 'r') as f:
|
||||
for line in f:
|
||||
if line.strip() and ':' in line:
|
||||
email = line.strip().split(':')[0]
|
||||
existing_emails.append(email)
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Lesen der passwd-Datei: {str(e)}")
|
||||
|
||||
return sorted(existing_emails)
|
||||
|
||||
def read_existing_entries(passwd_file):
|
||||
"""
|
||||
Liest alle bestehenden Einträge aus der passwd-Datei und
|
||||
gibt ein Dictionary mit E-Mail-Adressen als Schlüssel zurück.
|
||||
"""
|
||||
existing_entries = {}
|
||||
|
||||
if os.path.exists(passwd_file):
|
||||
try:
|
||||
with open(passwd_file, 'r') as f:
|
||||
for line in f:
|
||||
if line.strip() and ':' in line:
|
||||
parts = line.strip().split(':')
|
||||
email = parts[0]
|
||||
existing_entries[email] = line.strip()
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Lesen der passwd-Datei: {str(e)}")
|
||||
|
||||
return existing_entries
|
||||
|
||||
def update_dovecot_passwd(domains_config, force=False):
|
||||
"""
|
||||
Aktualisiert die Dovecot-Passwortdatei basierend auf der Domain-Konfiguration.
|
||||
Erstellt eine neue temporäre Datei und vergleicht sie mit der vorhandenen,
|
||||
um festzustellen, ob ein Reload von Dovecot erforderlich ist.
|
||||
|
||||
Returns:
|
||||
bool: True, wenn Änderungen vorgenommen wurden, sonst False
|
||||
"""
|
||||
logger.info("Überprüfe Dovecot-Passwortdatei...")
|
||||
|
||||
# Prüfen, ob Aktualisierung überhaupt notwendig ist
|
||||
required_emails = get_required_emails(domains_config)
|
||||
existing_emails = get_existing_emails(DOVECOT_PASSWD_FILE)
|
||||
|
||||
# Prüfen auf fehlende oder überflüssige E-Mail-Adressen
|
||||
missing_emails = [email for email in required_emails if email not in existing_emails]
|
||||
surplus_emails = [email for email in existing_emails if email not in required_emails]
|
||||
|
||||
if not missing_emails and not surplus_emails and not force:
|
||||
logger.info("Alle erforderlichen E-Mail-Adressen sind bereits konfiguriert, keine Änderung notwendig")
|
||||
return False
|
||||
|
||||
if missing_emails:
|
||||
logger.info(f"Fehlende E-Mail-Adressen: {', '.join(missing_emails)}")
|
||||
|
||||
if surplus_emails:
|
||||
logger.info(f"Überflüssige E-Mail-Adressen: {', '.join(surplus_emails)}")
|
||||
|
||||
logger.info("Aktualisiere Dovecot-Passwortdatei...")
|
||||
|
||||
# Temporäre Datei für die neue Passwd erstellen
|
||||
temp_passwd = NamedTemporaryFile(delete=False, mode='w')
|
||||
temp_passwd_path = temp_passwd.name
|
||||
|
||||
try:
|
||||
# Bestehende Einträge einlesen
|
||||
existing_entries = read_existing_entries(DOVECOT_PASSWD_FILE)
|
||||
|
||||
# Neue Einträge generieren
|
||||
new_entries = {}
|
||||
for domain, config in domains_config.items():
|
||||
domain_password = generate_password(domain)
|
||||
for username in config["usernames"]:
|
||||
email = f"{username}@{domain}"
|
||||
# Wenn der Eintrag bereits existiert, verwenden wir diesen (damit Passwörter erhalten bleiben)
|
||||
if email in existing_entries:
|
||||
new_entries[email] = existing_entries[email]
|
||||
else:
|
||||
# Format: email:{PLAIN}password:uid:gid::/var/mail/domain/username:/bin/false
|
||||
new_entry = f"{email}:{{PLAIN}}{domain_password}:{UID}:{GID}::/var/mail/{domain}/{username}:/bin/false"
|
||||
new_entries[email] = new_entry
|
||||
logger.info(f"Neuer E-Mail-Account erstellt: {email} mit Standardpasswort")
|
||||
|
||||
# Sortierte Einträge in die temporäre Datei schreiben
|
||||
for email in sorted(new_entries.keys()):
|
||||
temp_passwd.write(f"{new_entries[email]}\n")
|
||||
|
||||
temp_passwd.close()
|
||||
|
||||
# Prüfen, ob Änderungen vorgenommen wurden
|
||||
if os.path.exists(DOVECOT_PASSWD_FILE) and filecmp.cmp(temp_passwd_path, DOVECOT_PASSWD_FILE):
|
||||
logger.info("Keine inhaltlichen Änderungen an der passwd-Datei erforderlich")
|
||||
os.unlink(temp_passwd_path)
|
||||
return False
|
||||
|
||||
# Sicherungskopie erstellen
|
||||
if os.path.exists(DOVECOT_PASSWD_FILE):
|
||||
try:
|
||||
shutil.copy2(DOVECOT_PASSWD_FILE, DOVECOT_PASSWD_BACKUP)
|
||||
logger.info(f"Sicherungskopie erstellt: {DOVECOT_PASSWD_BACKUP}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Konnte keine Sicherungskopie erstellen: {str(e)}")
|
||||
|
||||
# Neue Datei aktivieren
|
||||
try:
|
||||
# Stellen Sie sicher, dass das Verzeichnis existiert
|
||||
passwd_dir = os.path.dirname(DOVECOT_PASSWD_FILE)
|
||||
if passwd_dir and not os.path.exists(passwd_dir):
|
||||
os.makedirs(passwd_dir, exist_ok=True)
|
||||
|
||||
shutil.move(temp_passwd_path, DOVECOT_PASSWD_FILE)
|
||||
# Berechtigungen setzen
|
||||
os.chmod(DOVECOT_PASSWD_FILE, 0o600) # Nur Besitzer darf lesen/schreiben
|
||||
logger.info(f"Neue passwd-Datei aktiviert: {DOVECOT_PASSWD_FILE}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Aktivieren der neuen passwd-Datei: {str(e)}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei der Aktualisierung der passwd-Datei: {str(e)}")
|
||||
if os.path.exists(temp_passwd_path):
|
||||
os.unlink(temp_passwd_path)
|
||||
return False
|
||||
|
||||
def reload_dovecot():
|
||||
"""
|
||||
Führt einen Reload von Dovecot durch, um die neue Passwortdatei zu aktivieren,
|
||||
ohne den Container neu starten zu müssen.
|
||||
"""
|
||||
try:
|
||||
logger.info(f"Führe Dovecot-Reload durch...")
|
||||
result = subprocess.run(
|
||||
["docker", "exec", DOVECOT_CONTAINER, "doveadm", "reload"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
logger.info("Dovecot-Reload erfolgreich durchgeführt")
|
||||
return True
|
||||
else:
|
||||
logger.error(f"Dovecot-Reload fehlgeschlagen: {result.stderr}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Dovecot-Reload: {str(e)}")
|
||||
return False
|
||||
|
||||
def main():
|
||||
"""Hauptfunktion"""
|
||||
logger.info("Dovecot Passwd Manager gestartet")
|
||||
|
||||
# Kommandozeilenargumente prüfen
|
||||
update_mode = False
|
||||
force_mode = False
|
||||
|
||||
if len(sys.argv) > 1:
|
||||
if sys.argv[1].lower() == "update":
|
||||
update_mode = True
|
||||
elif sys.argv[1].lower() == "force":
|
||||
update_mode = True
|
||||
force_mode = True
|
||||
|
||||
try:
|
||||
# Domain-Konfigurationen laden
|
||||
domains_config = load_domains_config()
|
||||
|
||||
if not domains_config:
|
||||
logger.error("Keine Domain-Konfigurationen gefunden. Bitte konfigurieren Sie mindestens eine Domain.")
|
||||
return
|
||||
|
||||
logger.info(f"Folgende Domains werden verarbeitet: {', '.join(domains_config.keys())}")
|
||||
|
||||
# Im Nur-Prüfungs-Modus zeigen wir einfach die erforderlichen Änderungen an
|
||||
if not update_mode:
|
||||
required_emails = get_required_emails(domains_config)
|
||||
existing_emails = get_existing_emails(DOVECOT_PASSWD_FILE)
|
||||
|
||||
missing_emails = [email for email in required_emails if email not in existing_emails]
|
||||
surplus_emails = [email for email in existing_emails if email not in required_emails]
|
||||
|
||||
if not missing_emails and not surplus_emails:
|
||||
logger.info("Alle erforderlichen E-Mail-Adressen sind bereits konfiguriert")
|
||||
else:
|
||||
if missing_emails:
|
||||
logger.info(f"Fehlende E-Mail-Adressen: {', '.join(missing_emails)}")
|
||||
|
||||
if surplus_emails:
|
||||
logger.info(f"Überflüssige E-Mail-Adressen: {', '.join(surplus_emails)}")
|
||||
|
||||
logger.info("Verwenden Sie 'update' als Parameter, um die Änderungen anzuwenden")
|
||||
|
||||
return
|
||||
|
||||
# Im Update-Modus führen wir die Änderungen durch
|
||||
changes_made = update_dovecot_passwd(domains_config, force=force_mode)
|
||||
|
||||
if changes_made:
|
||||
reload_dovecot()
|
||||
else:
|
||||
logger.info("Keine Änderungen vorgenommen, Dovecot-Reload nicht erforderlich")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler: {str(e)}")
|
||||
import traceback
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Reference in New Issue