#!/usr/bin/env python3 """ Immo-Bot - simple UI, die den ganzen Ablauf automatisiert: 1. Kontaktformular auf immowelt (URL) mit Mock-Daten ausfuellen und - falls gewuenscht - automatisch absenden. 2. Posteingang (IMAP) beobachten, die Bestaetigungs-Mail finden und den "Formular ausfuellen"-Link extrahieren. 3. Diesen Link oeffnen und das Bewerbungsformular (Angular) ausfuellen. Es werden die bestehenden Scripte wiederverwendet: - autofill_kontakt.py -> Kontaktformular (React) - autofill.py -> Bewerbungsformular (Angular) Setup: pip install selenium (Chrome + chromedriver; Selenium 4 verwaltet den Driver selbst.) Start: python immo_bot.py Hinweis E-Mail: Fuer den Postfach-Zugriff wird IMAP mit App-Passwort genutzt. - Gmail: Host imap.gmail.com (App-Passwort in Google-Konto erzeugen) - Outlook: Host outlook.office365.com (persoenliche Outlook-Konten erlauben teils nur OAuth statt Passwort - dann Gmail nehmen oder die Mail an ein Gmail weiterleiten.) """ import os import re import time import email import imaplib import threading import tkinter as tk from tkinter import ttk, scrolledtext from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait # Bestehende Scripte wiederverwenden import autofill # Angular-Bewerbungsformular import autofill_kontakt as ak # React-Kontaktformular # --------------------------------------------------------------------------- # Schritt 1: Kontaktformular ausfuellen (+ optional absenden) # --------------------------------------------------------------------------- def fill_contact_form(driver, log, applicant_email=None): """Fuellt das immowelt-Kontaktformular. Nutzt Funktionen aus autofill_kontakt.""" if applicant_email: ak.MOCK["email"] = applicant_email # Mail = das Postfach, das wir beobachten ak.click_cookie_banner(driver) if not ak.visible(driver, 'input[name="email"]'): log("E-Mail-Feld nicht sichtbar -> versuche Kontakt-Button ...") ak.open_contact_form(driver) log("Warte auf Kontaktformular ...") for _ in range(25): if ak.visible(driver, 'input[name="email"]'): break time.sleep(1.0) if not ak.visible(driver, 'input[name="email"]'): log("!! Kein Kontaktformular gefunden.") return False log("Fuelle Kontaktformular ...") ak.choose_salutation(driver, ak.MOCK["salutation"]) ak.fill_by_name(driver, "firstName", ak.MOCK["firstName"]) ak.fill_by_name(driver, "lastName", ak.MOCK["lastName"]) ak.fill_by_name(driver, "email", ak.MOCK["email"]) ak.fill_phone(driver, ak.MOCK["phone"]) ak.fill_by_name(driver, "message", ak.MOCK["message"]) log(f" -> E-Mail im Formular: {ak.MOCK['email']}") return True def submit_contact_form(driver, log): """Klickt den Absenden-Button des Kontaktformulars.""" labels = ["Nachricht senden", "Anfrage senden", "Kontaktanfrage senden", "Anfrage absenden", "Absenden", "Senden", "Abschicken"] for lbl in labels: try: btns = driver.find_elements( By.XPATH, f"//button[contains(normalize-space(.), '{lbl}')]") for btn in btns: if btn.is_displayed() and btn.is_enabled(): driver.execute_script( "arguments[0].scrollIntoView({block:'center'});", btn) btn.click() log(f"Kontaktformular abgesendet (Button: '{lbl}').") return True except Exception: pass log("!! Kein Absenden-Button gefunden - bitte manuell pruefen.") return False # --------------------------------------------------------------------------- # Schritt 2: E-Mail beobachten und Formular-Link extrahieren # --------------------------------------------------------------------------- def _html_from_message(msg): """Holt den text/html-Teil einer E-Mail als String.""" parts = msg.walk() if msg.is_multipart() else [msg] for part in parts: if part.get_content_type() == "text/html": charset = part.get_content_charset() or "utf-8" try: return part.get_payload(decode=True).decode(charset, errors="replace") except Exception: return part.get_payload(decode=True).decode("utf-8", errors="replace") return "" def extract_form_link(html): """Sucht in der Mail den Link hinter 'Formular ausfuellen'.""" anchors = re.findall(r']*href="([^"]+)"[^>]*>(.*?)', html, re.I | re.S) exclude = ("withdraw", "widerruf", "abmeld", "unsub", "datenschutz", "impressum") candidates = [] for href, text in anchors: clean = re.sub(r"<[^>]+>", "", text) clean = re.sub(r" ?", " ", clean).strip().lower() if "formular" in clean and not any(x in href.lower() for x in exclude): candidates.append(href) # bevorzugt den echten Formular-Link for c in candidates: if "application/form" in c.lower(): return c if candidates: return candidates[0] # Fallback: irgendein application/form-Link im HTML m = re.search(r'https://[^"\s]*application/form[^"\s]*', html) return m.group(0) if m else None def watch_inbox_for_link(host, user, password, log, sender_hint="", timeout=300, poll=15): """Pollt den Posteingang, bis eine passende Mail mit Formular-Link kommt.""" log(f"Verbinde mit {host} als {user} ...") try: M = imaplib.IMAP4_SSL(host) M.login(user, password) except Exception as e: log(f"!! IMAP-Login fehlgeschlagen: {e}") return None deadline = time.time() + timeout seen_ids = set() try: while time.time() < deadline: M.select("INBOX") # ungelesene Mails zuerst; sonst die neuesten typ, data = M.search(None, "UNSEEN") ids = data[0].split() if typ == "OK" and data[0] else [] if not ids: typ, data = M.search(None, "ALL") ids = (data[0].split() if typ == "OK" and data[0] else [])[-10:] for mid in reversed(ids): if mid in seen_ids: continue seen_ids.add(mid) typ, msg_data = M.fetch(mid, "(RFC822)") if typ != "OK": continue msg = email.message_from_bytes(msg_data[0][1]) frm = str(msg.get("From", "")) if sender_hint and sender_hint.lower() not in frm.lower(): continue html = _html_from_message(msg) link = extract_form_link(html) if link: log(f"Formular-Link gefunden in Mail von: {frm}") return link remaining = int(deadline - time.time()) log(f"Noch keine passende Mail. Warte {poll}s (Rest {remaining}s) ...") time.sleep(poll) finally: try: M.logout() except Exception: pass log("!! Timeout - keine Mail mit Formular-Link gefunden.") return None # --------------------------------------------------------------------------- # Schritt 3: Bewerbungsformular (Angular) ausfuellen # --------------------------------------------------------------------------- def fill_application_form(driver, log): """Fuellt das Angular-Formular ueber die Funktionen aus autofill.py.""" wait = WebDriverWait(driver, 10) time.sleep(1.5) # Angular-Bootstrap log("Aktiviere zweiten Mieter ...") autofill.set_checkbox(driver, "formly_5_checkbox_secondTenant_5", True) time.sleep(0.5) log("Dropdowns ...") for fid, opt in autofill.SELECTS.items(): autofill.select_option(driver, wait, fid, opt) log("Textfelder ...") for fid, val in autofill.TEXT_FIELDS.items(): autofill.fill_text(driver, wait, fid, val) log("Zustimmungs-Checkboxen ...") for cid in autofill.CHECKBOXES: if cid == "formly_5_checkbox_secondTenant_5": continue autofill.set_checkbox(driver, cid, True) log("Bewerbungsformular ausgefuellt (nicht abgesendet).") return True # --------------------------------------------------------------------------- # UI # --------------------------------------------------------------------------- class ImmoBotUI: def __init__(self, root): self.root = root self.driver = None self.busy = False self.form_link = None root.title("Immo-Bot") root.geometry("720x640") pad = {"padx": 8, "pady": 4} frm = ttk.Frame(root, padding=12) frm.pack(fill="both", expand=True) # --- Eingaben --- ttk.Label(frm, text="immowelt Expose-/Kontakt-URL:").grid( row=0, column=0, sticky="w", **pad) self.url_var = tk.StringVar() ttk.Entry(frm, textvariable=self.url_var, width=70).grid( row=1, column=0, columnspan=3, sticky="we", **pad) ttk.Label(frm, text="Deine E-Mail (Formular + Postfach):").grid( row=2, column=0, sticky="w", **pad) self.email_var = tk.StringVar(value="abutepeli@outlook.de") ttk.Entry(frm, textvariable=self.email_var, width=40).grid( row=3, column=0, sticky="we", **pad) ttk.Label(frm, text="IMAP-Host:").grid(row=2, column=1, sticky="w", **pad) self.host_var = tk.StringVar(value="outlook.office365.com") ttk.Entry(frm, textvariable=self.host_var, width=28).grid( row=3, column=1, sticky="we", **pad) ttk.Label(frm, text="E-Mail App-Passwort:").grid( row=4, column=0, sticky="w", **pad) self.pw_var = tk.StringVar() ttk.Entry(frm, textvariable=self.pw_var, width=40, show="*").grid( row=5, column=0, sticky="we", **pad) ttk.Label(frm, text="Absender-Filter (optional):").grid( row=4, column=1, sticky="w", **pad) self.sender_var = tk.StringVar(value="muenchner-wohnen") ttk.Entry(frm, textvariable=self.sender_var, width=28).grid( row=5, column=1, sticky="we", **pad) self.submit_var = tk.BooleanVar(value=False) ttk.Checkbutton( frm, text="Kontaktformular automatisch ABSENDEN", variable=self.submit_var).grid(row=6, column=0, columnspan=2, sticky="w", **pad) # --- Buttons --- btns = ttk.Frame(frm) btns.grid(row=7, column=0, columnspan=3, sticky="we", pady=8) self.b_all = ttk.Button(btns, text="ALLES automatisch (1 -> 2 -> 3)", command=self.run_all) self.b_all.pack(side="left", padx=4) self.b1 = ttk.Button(btns, text="1. Kontakt ausfuellen", command=self.run_step1) self.b1.pack(side="left", padx=4) self.b2 = ttk.Button(btns, text="2. Mail -> Link", command=self.run_step2) self.b2.pack(side="left", padx=4) self.b3 = ttk.Button(btns, text="3. Bewerbung ausfuellen", command=self.run_step3) self.b3.pack(side="left", padx=4) # --- Log --- self.log_box = scrolledtext.ScrolledText(frm, height=18, wrap="word") self.log_box.grid(row=8, column=0, columnspan=3, sticky="nsew", **pad) frm.rowconfigure(8, weight=1) frm.columnconfigure(0, weight=1) self.log("Bereit. URL eingeben und einen Schritt starten.") # ---- Helfer ---- def log(self, msg): self.root.after(0, self._append, str(msg)) def _append(self, msg): self.log_box.insert("end", msg + "\n") self.log_box.see("end") def _run_bg(self, target): if self.busy: self.log("... laeuft bereits, bitte warten.") return self.busy = True def wrap(): try: target() except Exception as e: self.log(f"!! Fehler: {e}") finally: self.busy = False threading.Thread(target=wrap, daemon=True).start() def ensure_driver(self): if self.driver is None: remote = os.environ.get("SELENIUM_REMOTE_URL") opts = webdriver.ChromeOptions() if remote: # z.B. SELENIUM_REMOTE_URL=http://VPS-IP:4444 # Rendering live sichtbar ueber noVNC: http://VPS-IP:7900 (PW: secret) self.log(f"Verbinde mit Remote-Selenium: {remote}") self.driver = webdriver.Remote(command_executor=remote, options=opts) else: self.log("Starte lokales Chrome ...") self.driver = webdriver.Chrome(options=opts) return self.driver # ---- Aktionen ---- def run_step1(self): self._run_bg(self._step1) def _step1(self): url = self.url_var.get().strip() if not url: self.log("!! Bitte URL eingeben.") return d = self.ensure_driver() self.log(f"Oeffne {url}") d.get(url) time.sleep(2.0) if fill_contact_form(d, self.log, self.email_var.get().strip()): if self.submit_var.get(): submit_contact_form(d, self.log) else: self.log("Nicht abgesendet (Haken 'automatisch ABSENDEN' nicht gesetzt).") def run_step2(self): self._run_bg(self._step2) def _step2(self): link = watch_inbox_for_link( self.host_var.get().strip(), self.email_var.get().strip(), self.pw_var.get(), self.log, sender_hint=self.sender_var.get().strip(), ) if link: self.form_link = link self.log(f"LINK: {link}") def run_step3(self): self._run_bg(self._step3) def _step3(self): if not self.form_link: self.log("!! Kein Formular-Link vorhanden (erst Schritt 2 ausfuehren).") return d = self.ensure_driver() self.log(f"Oeffne Bewerbungsformular: {self.form_link}") d.get(self.form_link) fill_application_form(d, self.log) def run_all(self): self._run_bg(self._all) def _all(self): self._step1() if not self.submit_var.get(): self.log("Hinweis: Ohne Absenden kommt keine E-Mail. " "Setze den Haken 'automatisch ABSENDEN' fuer den Vollablauf.") return self._step2() if self.form_link: self._step3() self.log("== Ablauf beendet ==") def main(): root = tk.Tk() ImmoBotUI(root) root.mainloop() if __name__ == "__main__": main()