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