From f9c8c4e6c7b8f637df4fa88feccf1fae62b6547a Mon Sep 17 00:00:00 2001 From: root Date: Sat, 10 Jan 2026 10:16:36 +0100 Subject: [PATCH] Initial commit: VMail Manager Tool mit Argon2id Support --- .gitignore | 17 ++++ config.ini.example | 7 ++ vmailtool.py | 222 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 246 insertions(+) create mode 100644 .gitignore create mode 100644 config.ini.example create mode 100755 vmailtool.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..22d8fb1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +# Python-Interne Dateien +__pycache__/ +*.py[cod] +*$py.class + +# Virtuelle Umgebung (Venv) +vmail-env/ +venv/ +env/ + +# Konfiguration (Enthält deine Passwörter!) +config.ini + +# System-Dateien +.DS_Store +.idea/ +.vscode/ diff --git a/config.ini.example b/config.ini.example new file mode 100644 index 0000000..503a2bb --- /dev/null +++ b/config.ini.example @@ -0,0 +1,7 @@ +[database] +host = localhost +user = vmail_admin +password = dein_sicheres_passwort +database = vmail +default_domain = example.com + diff --git a/vmailtool.py b/vmailtool.py new file mode 100755 index 0000000..c033867 --- /dev/null +++ b/vmailtool.py @@ -0,0 +1,222 @@ +#!/root/sql/vmail-env/bin/python3 +import mysql.connector +import configparser +import getpass +import sys +import socket +from argon2 import PasswordHasher, Type + +# Konfiguration laden +config = configparser.ConfigParser() +config.read('config.ini') +hostname = socket.getfqdn() + +DEFAULT_DOM = config.get('database', 'default_domain', fallback="") + +ph = PasswordHasher( + time_cost=3, + memory_cost=131072, + parallelism=4, + hash_len=32, + salt_len=16, + type=Type.ID +) + +def get_db_connection(): + try: + return mysql.connector.connect( + host=config['database']['host'], + user=config['database']['user'], + password=config['database']['password'], + database=config['database']['database'] + ) + except Exception as e: + print(f"Fehler: {e}") + sys.exit(1) + +def hash_pw(password): + return f"{{ARGON2ID}}{ph.hash(password)}" + +def get_domain_input(prompt="Domain"): + if DEFAULT_DOM: + val = input(f"{prompt} [{DEFAULT_DOM}]: ").strip() + return val if val else DEFAULT_DOM + return input(f"{prompt}: ").strip() + +# --- FUNKTIONEN --- + +def show_users(): + conn = get_db_connection() + cursor = conn.cursor() + cursor.execute("SELECT id, username, domain, enabled, quota FROM accounts ORDER BY id ASC") + rows = cursor.fetchall() + print(f"\n{'ID':<4} | {'Email Adresse':<35} | {'Status':<10} | {'Quota'}") + print("-" * 65) + for r in rows: + status = "AKTIV" if r[3] else "GESPERRT" + print(f"{r[0]:<4} | {r[1]+'@'+r[2]:<35} | {status:<10} | {r[4]} MB") + conn.close() + +def add_user(): + user = input("Username (vor dem @): ") + dom = get_domain_input() + pw = getpass.getpass("Passwort: ") + hashed = hash_pw(pw) + conn = get_db_connection() + cursor = conn.cursor() + try: + cursor.execute("INSERT INTO accounts (username, domain, password, quota, enabled) VALUES (%s, %s, %s, 1024, 1)", + (user, dom, hashed)) + conn.commit() + print(f"✔ User {user}@{dom} angelegt.") + except Exception as e: print(f"❌ Fehler: {e}") + conn.close() + +def del_user(): + email = input("E-Mail des zu LÖSCHENDEN Users: ") + if '@' not in email: return + u, d = email.split('@') + confirm = input(f"Account {email} wirklich löschen? (y/N): ") + if confirm.lower() != 'y': return + conn = get_db_connection() + cursor = conn.cursor() + cursor.execute("DELETE FROM accounts WHERE username = %s AND domain = %s", (u, d)) + conn.commit() + print("✔ User gelöscht.") + conn.close() + +def show_aliases(): + conn = get_db_connection() + cursor = conn.cursor() + cursor.execute("SELECT id, source_username, source_domain, destination_username, destination_domain FROM aliases ORDER BY id ASC") + rows = cursor.fetchall() + print(f"\n{'ID':<4} | {'Alias (Source)':<30} | {'Ziel (Destination)':<30}") + print("-" * 70) + for r in rows: + print(f"{r[0]:<4} | {r[1]+'@'+r[2]:<30} | {r[3]+'@'+r[4]:<30}") + conn.close() + +def add_alias(): + s_user = input("Alias Name (Source): ") + s_dom = get_domain_input("Alias Domain") + d_user = input("Ziel Username (Dest): ") + d_dom = get_domain_input("Ziel Domain") + conn = get_db_connection() + cursor = conn.cursor() + try: + cursor.execute("INSERT INTO aliases (source_username, source_domain, destination_username, destination_domain, enabled) VALUES (%s, %s, %s, %s, 1)", + (s_user, s_dom, d_user, d_dom)) + conn.commit() + print("✔ Alias angelegt.") + except Exception as e: print(f"❌ Fehler: {e}") + conn.close() + +def del_alias(): + alias_id = input("ID des zu löschenden Alias: ") + conn = get_db_connection() + cursor = conn.cursor() + cursor.execute("DELETE FROM aliases WHERE id = %s", (alias_id,)) + conn.commit() + print(f"✔ Alias ID {alias_id} gelöscht.") + conn.close() + +def show_domains(): + conn = get_db_connection() + cursor = conn.cursor() + cursor.execute("SELECT id, domain FROM domains ORDER BY id ASC") + rows = cursor.fetchall() + print(f"\n{'ID':<4} | {'Domain'}") + print("-" * 30) + for r in rows: + print(f"{r[0]:<4} | {r[1]}") + conn.close() + +def add_domain(): + dom = input("Neue Domain: ").strip() + if not dom: return + conn = get_db_connection() + cursor = conn.cursor() + try: + cursor.execute("INSERT INTO domains (domain) VALUES (%s)", (dom,)) + conn.commit() + print(f"✔ Domain {dom} registriert.") + except Exception as e: print(f"❌ Fehler: {e}") + conn.close() + +def del_domain(): + dom = input("Zu löschende Domain (Name eingeben): ").strip() + if not dom: return + confirm = input(f"Soll die Domain '{dom}' wirklich gelöscht werden? (y/N): ") + if confirm.lower() != 'y': return + + conn = get_db_connection() + cursor = conn.cursor() + try: + cursor.execute("DELETE FROM domains WHERE domain = %s", (dom,)) + conn.commit() + if cursor.rowcount > 0: + print(f"✔ Domain '{dom}' wurde gelöscht.") + else: + print("❌ Domain nicht gefunden.") + except mysql.connector.Error as e: + if e.errno == 1451: + print("❌ Fehler: Domain kann nicht gelöscht werden, da noch User oder Aliase damit verknüpft sind!") + else: + print(f"❌ Fehler: {e}") + conn.close() + +def change_pw(): + email = input("E-Mail für PW-Wechsel: ") + if '@' not in email: return + u, d = email.split('@') + pw = getpass.getpass("Neues Passwort: ") + hashed = hash_pw(pw) + conn = get_db_connection() + cursor = conn.cursor() + cursor.execute("UPDATE accounts SET password = %s WHERE username = %s AND domain = %s", (hashed, u, d)) + conn.commit() + print("✔ Passwort aktualisiert.") + conn.close() + +def toggle_status(): + email = input("E-Mail zum (De)aktivieren: ") + if '@' not in email: return + u, d = email.split('@') + conn = get_db_connection() + cursor = conn.cursor() + cursor.execute("UPDATE accounts SET enabled = NOT enabled WHERE username = %s AND domain = %s", (u, d)) + conn.commit() + print("✔ Status geändert.") + conn.close() + +# --- MAIN --- + +def menu(): + while True: + print("\n" + "="*60) + print(f" VMAIL MANAGER ({hostname})") + print("="*60) + print("1) Show Users 6) Del Alias") + print("2) Add User (1024MB) 7) Domains (Show/Add/Del)") + print("3) Del User 8) Change Password") + print("4) Show Aliases 9) Toggle Status (An/Aus)") + print("5) Add Alias 0) Beenden") + + cmd = input("\nAuswahl > ") + if cmd == '1': show_users() + elif cmd == '2': add_user() + elif cmd == '3': del_user() + elif cmd == '4': show_aliases() + elif cmd == '5': add_alias() + elif cmd == '6': del_alias() + elif cmd == '7': + show_domains() + sub_cmd = input("\n[A]dd Domain, [D]el Domain oder [Enter] zurück: ").lower() + if sub_cmd == 'a': add_domain() + elif sub_cmd == 'd': del_domain() + elif cmd == '8': change_pw() + elif cmd == '9': toggle_status() + elif cmd == '0': break + +if __name__ == "__main__": + menu()