diff --git a/mail-alarm.py b/mail-alarm.py new file mode 100644 index 0000000..023aae9 --- /dev/null +++ b/mail-alarm.py @@ -0,0 +1,464 @@ +#!/usr/bin/env python3 +""" +Mail-Alarm - IMAP IDLE E-Mail Überwachung mit Pushover Emergency Alerts +""" + +import os +import sys +import time +import signal +import logging +import configparser +import subprocess +from pathlib import Path + +# Konstanten +CONFIG_DIR = Path("/etc/mail-alarm") +CONFIG_FILE = CONFIG_DIR / "config.ini" +LOG_FILE = Path("/var/log/mail-alarm.log") +SERVICE_NAME = "mail-alarm" +SERVICE_FILE = Path(f"/etc/systemd/system/{SERVICE_NAME}.service") +SCRIPT_PATH = Path("/usr/local/bin/mail-alarm.py") + +# Logging Setup +def setup_logging(daemon_mode=False): + """Konfiguriert das Logging.""" + log_format = "%(asctime)s - %(levelname)s - %(message)s" + + if daemon_mode: + logging.basicConfig( + level=logging.INFO, + format=log_format, + handlers=[ + logging.FileHandler(LOG_FILE), + logging.StreamHandler() + ] + ) + else: + logging.basicConfig( + level=logging.INFO, + format=log_format, + handlers=[logging.StreamHandler()] + ) + +def check_root(): + """Prüft ob das Script als root läuft.""" + if os.geteuid() != 0: + print("❌ Dieses Script muss als root ausgeführt werden!") + print(" Bitte mit 'sudo python3 mail-alarm.py' starten.") + sys.exit(1) + +def install_dependencies(): + """Installiert Python-Abhängigkeiten via apt.""" + print("\n📦 Installiere Abhängigkeiten...") + packages = ["python3-imapclient", "python3-requests"] + + try: + subprocess.run( + ["apt-get", "update"], + check=True, + capture_output=True + ) + subprocess.run( + ["apt-get", "install", "-y"] + packages, + check=True, + capture_output=True + ) + print("✅ Abhängigkeiten installiert") + return True + except subprocess.CalledProcessError as e: + print(f"❌ Fehler bei Installation: {e}") + return False + +def get_user_input(prompt, default=None, password=False): + """Holt Benutzereingabe mit optionalem Default-Wert.""" + if default: + prompt = f"{prompt} [{default}]: " + else: + prompt = f"{prompt}: " + + if password: + import getpass + value = getpass.getpass(prompt) + else: + value = input(prompt) + + return value.strip() if value.strip() else default + +def create_config(): + """Erstellt die Konfigurationsdatei interaktiv.""" + print("\n⚙️ Konfiguration erstellen") + print("-" * 40) + + print("\n📧 IMAP Einstellungen:") + imap_server = get_user_input(" IMAP Server", "imap.jtl-software.de") + imap_port = get_user_input(" IMAP Port", "993") + imap_user = get_user_input(" Benutzername/E-Mail") + imap_pass = get_user_input(" Passwort", password=True) + imap_folder = get_user_input(" Ordner", "INBOX") + + print("\n📱 Pushover Einstellungen:") + pushover_token = get_user_input(" App Token") + pushover_user = get_user_input(" User Key") + pushover_sound = get_user_input(" Sound (leer für Default)", "") + + print("\n⏱️ Alarm Einstellungen:") + retry = get_user_input(" Retry Intervall (Sekunden)", "30") + expire = get_user_input(" Expire Zeit (Sekunden)", "3600") + + # Config erstellen + config = configparser.ConfigParser() + config["IMAP"] = { + "server": imap_server, + "port": imap_port, + "username": imap_user, + "password": imap_pass, + "folder": imap_folder + } + config["PUSHOVER"] = { + "token": pushover_token, + "user": pushover_user, + "sound": pushover_sound + } + config["ALARM"] = { + "retry": retry, + "expire": expire, + "title": "Rufbereitschaft", + "message": "ALARM: Neue E-Mail im Rufbereitschafts-Postfach!" + } + + # Verzeichnis erstellen und Config speichern + CONFIG_DIR.mkdir(parents=True, exist_ok=True) + + with open(CONFIG_FILE, "w") as f: + config.write(f) + + # Nur root darf die Config lesen (enthält Passwörter) + os.chmod(CONFIG_FILE, 0o600) + + print(f"\n✅ Konfiguration gespeichert in {CONFIG_FILE}") + return True + +def load_config(): + """Lädt die Konfiguration.""" + if not CONFIG_FILE.exists(): + return None + + config = configparser.ConfigParser() + config.read(CONFIG_FILE) + return config + +def create_systemd_service(): + """Erstellt den systemd Service.""" + service_content = f"""[Unit] +Description=Mail-Alarm IMAP Überwachung +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +ExecStart=/usr/bin/python3 {SCRIPT_PATH} --daemon +Restart=always +RestartSec=10 +StandardOutput=append:{LOG_FILE} +StandardError=append:{LOG_FILE} + +[Install] +WantedBy=multi-user.target +""" + + with open(SERVICE_FILE, "w") as f: + f.write(service_content) + + print(f"✅ Systemd Service erstellt: {SERVICE_FILE}") + +def install(): + """Führt die Installation durch.""" + print("\n" + "=" * 50) + print("📥 INSTALLATION") + print("=" * 50) + + # Abhängigkeiten installieren + if not install_dependencies(): + return False + + # Script kopieren + print(f"\n📄 Kopiere Script nach {SCRIPT_PATH}...") + current_script = Path(__file__).resolve() + subprocess.run(["cp", str(current_script), str(SCRIPT_PATH)], check=True) + os.chmod(SCRIPT_PATH, 0o755) + print("✅ Script kopiert") + + # Log-Datei erstellen + LOG_FILE.touch(exist_ok=True) + os.chmod(LOG_FILE, 0o644) + + # Konfiguration erstellen + if not create_config(): + return False + + # Systemd Service erstellen + print("\n🔧 Erstelle Systemd Service...") + create_systemd_service() + + # Service aktivieren und starten + print("\n🚀 Starte Service...") + subprocess.run(["systemctl", "daemon-reload"], check=True) + subprocess.run(["systemctl", "enable", SERVICE_NAME], check=True) + subprocess.run(["systemctl", "start", SERVICE_NAME], check=True) + + print("\n" + "=" * 50) + print("✅ INSTALLATION ABGESCHLOSSEN") + print("=" * 50) + print(f"\n📋 Nützliche Befehle:") + print(f" Status: systemctl status {SERVICE_NAME}") + print(f" Logs: journalctl -u {SERVICE_NAME} -f") + print(f" Log-Datei: tail -f {LOG_FILE}") + print(f" Stoppen: systemctl stop {SERVICE_NAME}") + print(f" Starten: systemctl start {SERVICE_NAME}") + + return True + +def uninstall(): + """Führt die Deinstallation durch.""" + print("\n" + "=" * 50) + print("📤 DEINSTALLATION") + print("=" * 50) + + # Service stoppen und deaktivieren + print("\n🛑 Stoppe Service...") + subprocess.run(["systemctl", "stop", SERVICE_NAME], capture_output=True) + subprocess.run(["systemctl", "disable", SERVICE_NAME], capture_output=True) + + # Service-Datei entfernen + if SERVICE_FILE.exists(): + SERVICE_FILE.unlink() + print("✅ Systemd Service entfernt") + + subprocess.run(["systemctl", "daemon-reload"], check=True) + + # Script entfernen + if SCRIPT_PATH.exists(): + SCRIPT_PATH.unlink() + print("✅ Script entfernt") + + # Nachfragen ob Config und Logs gelöscht werden sollen + delete_config = input("\n🗑️ Konfiguration löschen? (j/N): ").lower() == "j" + if delete_config and CONFIG_DIR.exists(): + import shutil + shutil.rmtree(CONFIG_DIR) + print("✅ Konfiguration gelöscht") + + delete_logs = input("🗑️ Log-Datei löschen? (j/N): ").lower() == "j" + if delete_logs and LOG_FILE.exists(): + LOG_FILE.unlink() + print("✅ Log-Datei gelöscht") + + print("\n" + "=" * 50) + print("✅ DEINSTALLATION ABGESCHLOSSEN") + print("=" * 50) + + return True + +def show_status(): + """Zeigt den Status des Services an.""" + print("\n" + "=" * 50) + print("📊 STATUS") + print("=" * 50) + + # Service Status + result = subprocess.run( + ["systemctl", "is-active", SERVICE_NAME], + capture_output=True, + text=True + ) + status = result.stdout.strip() + + if status == "active": + print(f"\n🟢 Service: Läuft") + else: + print(f"\n🔴 Service: {status}") + + # Config Status + if CONFIG_FILE.exists(): + print(f"✅ Konfiguration: {CONFIG_FILE}") + else: + print(f"❌ Konfiguration: Nicht vorhanden") + + # Log Status + if LOG_FILE.exists(): + size = LOG_FILE.stat().st_size + print(f"📄 Log-Datei: {LOG_FILE} ({size} Bytes)") + + # Letzte Log-Einträge + if LOG_FILE.exists(): + print(f"\n📋 Letzte Log-Einträge:") + print("-" * 40) + result = subprocess.run( + ["tail", "-5", str(LOG_FILE)], + capture_output=True, + text=True + ) + print(result.stdout if result.stdout else " (keine Einträge)") + +def send_pushover_alert(config, subject="", sender=""): + """Sendet einen Pushover Emergency Alert.""" + import requests + + message = config["ALARM"]["message"] + if subject or sender: + message = f"{message}\n\nVon: {sender}\nBetreff: {subject}" + + data = { + "token": config["PUSHOVER"]["token"], + "user": config["PUSHOVER"]["user"], + "message": message, + "title": config["ALARM"]["title"], + "priority": "2", + "retry": config["ALARM"]["retry"], + "expire": config["ALARM"]["expire"] + } + + if config["PUSHOVER"]["sound"]: + data["sound"] = config["PUSHOVER"]["sound"] + + try: + response = requests.post( + "https://api.pushover.net/1/messages.json", + data=data, + timeout=30 + ) + response.raise_for_status() + logging.info(f"Pushover Alert gesendet: {subject}") + return True + except Exception as e: + logging.error(f"Fehler beim Senden des Pushover Alerts: {e}") + return False + +def run_daemon(): + """Hauptloop für die IMAP IDLE Überwachung.""" + from imapclient import IMAPClient + + setup_logging(daemon_mode=True) + logging.info("Mail-Alarm Daemon gestartet") + + config = load_config() + if not config: + logging.error("Keine Konfiguration gefunden!") + sys.exit(1) + + # Signal Handler für sauberes Beenden + def signal_handler(signum, frame): + logging.info("Beende Daemon...") + sys.exit(0) + + signal.signal(signal.SIGTERM, signal_handler) + signal.signal(signal.SIGINT, signal_handler) + + while True: + try: + logging.info(f"Verbinde mit {config['IMAP']['server']}...") + + with IMAPClient( + host=config["IMAP"]["server"], + port=int(config["IMAP"]["port"]), + ssl=True + ) as client: + client.login( + config["IMAP"]["username"], + config["IMAP"]["password"] + ) + logging.info("IMAP Login erfolgreich") + + client.select_folder(config["IMAP"]["folder"]) + logging.info(f"Überwache Ordner: {config['IMAP']['folder']}") + + # Initiale Anzahl ungelesener Mails merken + unseen = client.search(["UNSEEN"]) + last_unseen_count = len(unseen) + logging.info(f"Aktuelle ungelesene Mails: {last_unseen_count}") + + while True: + # IMAP IDLE starten + client.idle() + + try: + # Warte auf Änderungen (max. 5 Minuten, dann Reconnect) + responses = client.idle_check(timeout=300) + client.idle_done() + + if responses: + # Prüfe auf neue ungelesene Mails + unseen = client.search(["UNSEEN"]) + current_unseen_count = len(unseen) + + if current_unseen_count > last_unseen_count: + # Neue Mail(s) eingetroffen! + new_count = current_unseen_count - last_unseen_count + logging.info(f"🚨 {new_count} neue E-Mail(s)!") + + # Hole Details der neuesten Mail + if unseen: + latest_uid = unseen[-1] + messages = client.fetch([latest_uid], ["ENVELOPE"]) + + if latest_uid in messages: + envelope = messages[latest_uid][b"ENVELOPE"] + subject = envelope.subject.decode() if envelope.subject else "(kein Betreff)" + sender = envelope.from_[0].mailbox.decode() + "@" + envelope.from_[0].host.decode() if envelope.from_ else "(unbekannt)" + + logging.info(f"Von: {sender}, Betreff: {subject}") + send_pushover_alert(config, subject, sender) + else: + send_pushover_alert(config) + else: + send_pushover_alert(config) + + last_unseen_count = current_unseen_count + + except Exception as e: + client.idle_done() + raise e + + except Exception as e: + logging.error(f"Verbindungsfehler: {e}") + logging.info("Warte 30 Sekunden vor erneutem Verbindungsversuch...") + time.sleep(30) + +def main_menu(): + """Zeigt das Hauptmenü an.""" + while True: + print("\n" + "=" * 50) + print("📬 MAIL-ALARM SETUP") + print("=" * 50) + print("\n 1) Installieren (Service einrichten & starten)") + print(" 2) Deinstallieren (Service stoppen & entfernen)") + print(" 3) Status anzeigen") + print(" 4) Beenden") + + choice = input("\nAuswahl [1-4]: ").strip() + + if choice == "1": + install() + elif choice == "2": + uninstall() + elif choice == "3": + show_status() + elif choice == "4": + print("\n👋 Auf Wiedersehen!") + sys.exit(0) + else: + print("❌ Ungültige Auswahl") + +def main(): + """Hauptfunktion.""" + if len(sys.argv) > 1 and sys.argv[1] == "--daemon": + # Daemon-Modus + run_daemon() + else: + # Interaktiver Modus + check_root() + main_menu() + +if __name__ == "__main__": + main() \ No newline at end of file