2026-03-25 22:03:39 +01:00
# -*- coding: utf-8 -*-
"""
AzA E - Mail - E - Mail Verwaltung für Arztpraxis
Mailbird - ähnliche Oberfläche mit IMAP / SMTP Support und KI - Features
"""
import os
import sys
import json
import tkinter as tk
from tkinter import ttk , messagebox , scrolledtext , simpledialog , filedialog
from datetime import datetime
import threading
import imaplib
import smtplib
import time
import email
from email . mime . text import MIMEText
from email . mime . multipart import MIMEMultipart
from email . header import decode_header
from openai import OpenAI
from dotenv import load_dotenv
from PIL import Image , ImageDraw , ImageTk
import tempfile
import wave
import io
# Konfigurationsdatei
CONFIG_FILENAME = " aza_email_config.json "
def _config_path ( ) :
return os . path . join ( os . path . dirname ( os . path . abspath ( __file__ ) ) , CONFIG_FILENAME )
def _get_account_password ( index : int ) - > str :
""" Lädt E-Mail-Passwort aus ENV (AZA_EMAIL_PASSWORD_0, _1, ...). """
return os . getenv ( f " AZA_EMAIL_PASSWORD_ { index } " , " " ) . strip ( )
def _strip_passwords ( accounts : list ) - > list :
""" Entfernt alle Passwort-Felder aus Account-Daten vor dem Speichern. """
cleaned = [ ]
for acc in accounts :
acc_copy = { k : v for k , v in acc . items ( ) if k not in ( " password " , " _password " ) }
cleaned . append ( acc_copy )
return cleaned
def _check_plaintext_migration ( accounts : list ) - > list :
""" Prüft ob Klartext-Passwörter in der Config vorhanden sind.
Gibt Liste der betroffenen E - Mail - Adressen zurück . """
affected = [ ]
for acc in accounts :
if acc . get ( " password " , " " ) . strip ( ) :
affected . append ( acc . get ( " email " , f " Konto { len ( affected ) } " ) )
return affected
def load_email_config ( ) :
""" Lädt E-Mail Konfiguration (Fenstergeometrie, Konten).
Passwörter werden aus ENV geladen , NICHT aus der JSON - Datei . """
try :
path = _config_path ( )
if os . path . isfile ( path ) :
with open ( path , " r " , encoding = " utf-8 " ) as f :
data = json . load ( f )
plaintext = _check_plaintext_migration ( data . get ( " accounts " , [ ] ) )
if plaintext :
print (
" SICHERHEITSWARNUNG: Klartext-Passwoerter in "
f " { CONFIG_FILENAME } gefunden! \n "
" Betroffene Konten: " + " , " . join ( plaintext ) + " \n "
" Bitte entfernen und stattdessen ENV-Variablen setzen: \n "
+ " \n " . join (
f " AZA_EMAIL_PASSWORD_ { i } "
for i in range ( len ( plaintext ) )
) + " \n "
" Die Passwoerter in der Datei werden IGNORIERT. " ,
file = sys . stderr ,
)
for i , acc in enumerate ( data . get ( " accounts " , [ ] ) ) :
acc . pop ( " password " , None )
env_pw = _get_account_password ( i )
if env_pw :
acc [ " _password " ] = env_pw
return data
except Exception :
pass
return { }
def save_email_config ( data ) :
""" Speichert E-Mail Konfiguration. Passwörter werden NIE geschrieben. """
try :
path = _config_path ( )
safe_data = dict ( data )
if " accounts " in safe_data :
safe_data [ " accounts " ] = _strip_passwords ( safe_data [ " accounts " ] )
with open ( path , " w " , encoding = " utf-8 " ) as f :
json . dump ( safe_data , f , indent = 2 , ensure_ascii = False )
except Exception :
pass
# ========== Textfeld-Schriftgröße mit ▲▼-Pfeilen ==========
_FONT_SIZE_SETTINGS_FILE = os . path . join ( os . path . dirname ( os . path . abspath ( __file__ ) ) , " aza_email_font_sizes.json " )
def _load_font_sizes ( ) :
try :
if os . path . isfile ( _FONT_SIZE_SETTINGS_FILE ) :
with open ( _FONT_SIZE_SETTINGS_FILE , " r " , encoding = " utf-8 " ) as f :
return json . load ( f )
except Exception :
pass
return { }
def _save_font_size ( key , size ) :
data = _load_font_sizes ( )
data [ key ] = size
try :
with open ( _FONT_SIZE_SETTINGS_FILE , " w " , encoding = " utf-8 " ) as f :
json . dump ( data , f , indent = 2 )
except Exception :
pass
def add_text_font_size_control ( parent_frame , text_widget , initial_size = 10 , label = " Aa " , bg_color = " #F5FCFF " , save_key = None ) :
""" ▲▼-Pfeile für Textfeld-Schriftgröße (5– 20pt), unauffällig im Hintergrund. """
if save_key :
saved = _load_font_sizes ( ) . get ( save_key )
if saved is not None :
initial_size = int ( saved )
_size = [ max ( 5 , min ( 20 , initial_size ) ) ]
_fg = " #8AAFC0 "
_fg_hover = " #1a4d6d "
cf = tk . Frame ( parent_frame , bg = bg_color , highlightthickness = 0 , bd = 0 )
cf . pack ( side = " right " , padx = 4 )
tk . Label ( cf , text = label , font = ( " Segoe UI " , 8 ) , bg = bg_color , fg = _fg ) . pack ( side = " left " , padx = ( 0 , 1 ) )
size_lbl = tk . Label ( cf , text = str ( _size [ 0 ] ) , font = ( " Segoe UI " , 8 ) , bg = bg_color , fg = _fg , width = 2 , anchor = " center " )
size_lbl . pack ( side = " left " )
def _apply ( ns ) :
ns = max ( 5 , min ( 20 , ns ) )
_size [ 0 ] = ns
size_lbl . configure ( text = str ( ns ) )
text_widget . configure ( font = ( " Segoe UI " , ns ) )
if save_key :
_save_font_size ( save_key , ns )
text_widget . configure ( font = ( " Segoe UI " , _size [ 0 ] ) )
btn_up = tk . Label ( cf , text = " \u25B2 " , font = ( " Segoe UI " , 7 ) , bg = bg_color , fg = _fg , cursor = " hand2 " , bd = 0 , highlightthickness = 0 )
btn_up . pack ( side = " left " , padx = ( 2 , 0 ) )
btn_down = tk . Label ( cf , text = " \u25BC " , font = ( " Segoe UI " , 7 ) , bg = bg_color , fg = _fg , cursor = " hand2 " , bd = 0 , highlightthickness = 0 )
btn_down . pack ( side = " left " )
btn_up . bind ( " <Button-1> " , lambda e : _apply ( _size [ 0 ] + 1 ) )
btn_down . bind ( " <Button-1> " , lambda e : _apply ( _size [ 0 ] - 1 ) )
for w in ( btn_up , btn_down ) :
w . bind ( " <Enter> " , lambda e , ww = w : ww . configure ( fg = _fg_hover ) )
w . bind ( " <Leave> " , lambda e , ww = w : ww . configure ( fg = _fg ) )
return _size
# ==========================================================
class AudioRecorder :
"""
Minimaler Recorder mit sounddevice für E - Mail - Diktat .
Speichert als 16 kHz mono WAV ( 16 - bit PCM ) .
"""
def __init__ ( self , samplerate = 16000 , channels = 1 ) :
self . samplerate = samplerate
self . channels = channels
self . _stream = None
self . _frames = [ ]
self . _recording = False
def start ( self ) :
try :
import sounddevice as sd
except Exception as e :
raise RuntimeError (
" Python-Paket ' sounddevice ' fehlt. \n \n "
" Installiere es mit: \n "
" pip install sounddevice \n \n "
f " Details: { e } "
)
self . _frames = [ ]
self . _recording = True
def callback ( indata , frames , time_info , status ) :
if status :
pass
if self . _recording :
self . _frames . append ( indata . copy ( ) )
self . _stream = sd . InputStream (
samplerate = self . samplerate ,
channels = self . channels ,
callback = callback ,
dtype = " float32 " ,
blocksize = 0 ,
)
self . _stream . start ( )
def stop_and_save_wav ( self ) - > str :
if not self . _stream :
raise RuntimeError ( " Recorder wurde nicht gestartet. " )
self . _recording = False
self . _stream . stop ( )
self . _stream . close ( )
self . _stream = None
if not self . _frames :
raise RuntimeError ( " Keine Audio-Daten aufgenommen (leer). " )
import numpy as np
audio = np . concatenate ( self . _frames , axis = 0 )
audio = np . clip ( audio , - 1.0 , 1.0 )
pcm16 = ( audio * 32767.0 ) . astype ( np . int16 )
fd , path = tempfile . mkstemp ( suffix = " .wav " , prefix = " email_rec_ " )
os . close ( fd )
with wave . open ( path , " wb " ) as wf :
wf . setnchannels ( self . channels )
wf . setsampwidth ( 2 )
wf . setframerate ( self . samplerate )
wf . writeframes ( pcm16 . tobytes ( ) )
return path
class EmailApp :
def __init__ ( self , root ) :
self . root = root
self . root . title ( " AzA E-Mail " )
self . root . geometry ( " 1200x700 " )
self . root . minsize ( 800 , 500 )
# Farben wie Mailbird (dunkles Theme)
self . bg_dark = " #2C3E50 "
self . bg_medium = " #34495E "
self . bg_light = " #ECF0F1 "
self . fg_light = " #FFFFFF "
self . fg_dark = " #2C3E50 "
self . accent_blue = " #3498DB "
self . accent_hover = " #2980B9 "
self . root . configure ( bg = self . bg_dark )
# E-Mail Konten und Nachrichten
self . accounts = [ ] # [{"name": "", "email": "", "imap_server": "", "imap_port": 993, ...}]
self . current_account = None
self . emails = [ ]
self . current_folder = " inbox "
self . selected_email = None # Aktuell ausgewählte E-Mail
# E-Mail Cache für schnelleres Laden
self . email_cache = { } # {folder_id: [emails...]}
self . cache_timestamp = { } # {folder_id: timestamp}
self . cache_max_age = 60 # Cache 60 Sekunden gültig
# Kontakte für Auto-Complete
self . contacts = set ( ) # Set von E-Mail-Adressen
self . _load_contacts ( )
# Unterschriften-Einstellungen
self . signature = " " # HTML/Text-Unterschrift
self . signature_auto_reply = False # Auto bei Antworten
self . signature_auto_new = True # Auto bei neuen E-Mails
# Schriftgrößen-Einstellungen
self . font_size_compose = 10 # Schriftgröße für Compose-Fenster
self . font_size_list = 10 # Schriftgröße für E-Mail-Liste
# Liste aller offenen Compose-Textwidgets für dynamische Updates
self . compose_text_widgets = [ ] # Liste von Text-Widgets
# Audio-Recorder für Diktat
self . recorder = AudioRecorder ( )
self . is_recording = False
2026-04-16 13:32:32 +02:00
# KI-Client initialisieren (Backend-Proxy oder lokal)
2026-03-25 22:03:39 +01:00
load_dotenv ( )
2026-04-16 13:32:32 +02:00
try :
from aza_ai_client import get_ai_client
self . client = get_ai_client ( )
except Exception :
api_key = os . getenv ( " OPENAI_API_KEY " , " " ) . strip ( )
self . client = OpenAI ( api_key = api_key ) if api_key else None
2026-03-25 22:03:39 +01:00
# Gespeicherte Geometrie und Konten laden
config = load_email_config ( )
saved_geom = config . get ( " geometry " , " " )
if saved_geom :
try :
self . root . geometry ( saved_geom )
except :
pass
self . accounts = config . get ( " accounts " , [ ] )
for i , acc in enumerate ( self . accounts ) :
if not acc . get ( " _password " ) :
env_pw = _get_account_password ( i )
if env_pw :
acc [ " _password " ] = env_pw
if self . accounts :
self . current_account = self . accounts [ 0 ]
# Unterschrift laden
self . signature = config . get ( " signature " , " " )
self . signature_auto_reply = config . get ( " signature_auto_reply " , False )
self . signature_auto_new = config . get ( " signature_auto_new " , True )
# Schriftgrößen laden
self . font_size_compose = config . get ( " font_size_compose " , 10 )
self . font_size_list = config . get ( " font_size_list " , 10 )
self . root . protocol ( " WM_DELETE_WINDOW " , self . _on_close )
self . _build_ui ( )
# E-Mails asynchron laden (verzögert, damit Fenster sofort erscheint)
if self . current_account :
# Zeige sofort Lade-Status
self . status_var . set ( " ⏳ Verbinde mit E-Mail-Server... " )
# Lade E-Mails nach 100ms (Fenster ist dann sichtbar)
self . root . after ( 100 , self . _fetch_emails_with_progress )
else :
self . status_var . set ( " Kein Konto konfiguriert. Bitte über ⚙ Konten hinzufügen. " )
def _on_close ( self ) :
""" Speichert Geometrie, Konten, Unterschrift und Schriftgrößen beim Schließen. """
config = load_email_config ( )
config [ " geometry " ] = self . root . geometry ( )
config [ " accounts " ] = self . accounts
config [ " signature " ] = self . signature
config [ " signature_auto_reply " ] = self . signature_auto_reply
config [ " signature_auto_new " ] = self . signature_auto_new
config [ " font_size_compose " ] = self . font_size_compose
config [ " font_size_list " ] = self . font_size_list
save_email_config ( config )
self . root . destroy ( )
def _build_ui ( self ) :
""" Erstellt die Mailbird-ähnliche Oberfläche. """
# Toolbar oben
toolbar = tk . Frame ( self . root , bg = self . bg_medium , height = 50 )
toolbar . pack ( fill = " x " , side = " top " )
toolbar . pack_propagate ( False )
# Toolbar Buttons
btn_frame = tk . Frame ( toolbar , bg = self . bg_medium )
btn_frame . pack ( side = " left " , padx = 10 , pady = 5 )
self . _create_toolbar_button ( btn_frame , " ✉ Neue E-Mail " , self . _compose_email ) . pack ( side = " left " , padx = 2 )
self . _create_toolbar_button ( btn_frame , " ↻ Aktualisieren " , self . _refresh ) . pack ( side = " left " , padx = 2 )
self . _create_toolbar_button ( btn_frame , " 🗑 Löschen " , self . _delete_selected ) . pack ( side = " left " , padx = 2 )
self . _create_toolbar_button ( btn_frame , " 🚫 Als Spam " , self . _mark_as_spam ) . pack ( side = " left " , padx = 2 )
# Rechte Seite der Toolbar: Schriftgrößen + KI-Funktionen & Einstellungen
btn_frame_right = tk . Frame ( toolbar , bg = self . bg_medium )
btn_frame_right . pack ( side = " right " , padx = 10 , pady = 5 )
# Schriftgrößen-Spinboxes
font_frame = tk . Frame ( toolbar , bg = self . bg_medium )
font_frame . pack ( side = " right " , padx = 5 )
# Spinbox für E-Mail-Liste
list_font_frame = tk . Frame ( font_frame , bg = " #E7F9FD " , relief = " solid " , bd = 1 )
list_font_frame . pack ( side = " left " , padx = 5 )
tk . Label ( list_font_frame , text = " Aa " , bg = " #E7F9FD " , fg = self . fg_dark ,
font = ( " Segoe UI " , 8 ) ) . pack ( side = " left " , padx = ( 5 , 2 ) )
list_spinbox = tk . Spinbox ( list_font_frame , from_ = 5 , to = 12 , width = 3 ,
bg = " #E7F9FD " , relief = " flat " , bd = 0 ,
font = ( " Segoe UI " , 9 ) , justify = " center " ,
textvariable = tk . IntVar ( value = self . font_size_list ) ,
command = self . _on_list_font_change )
list_spinbox . pack ( side = " left " , padx = ( 0 , 5 ) )
list_spinbox . delete ( 0 , " end " )
list_spinbox . insert ( 0 , str ( self . font_size_list ) )
list_spinbox . bind ( " <Return> " , lambda e : self . _on_list_font_change ( ) )
list_spinbox . bind ( " <FocusOut> " , lambda e : self . _on_list_font_change ( ) )
self . list_spinbox = list_spinbox
# Spinbox für Compose-Text
compose_font_frame = tk . Frame ( font_frame , bg = " #E7F9FD " , relief = " solid " , bd = 1 )
compose_font_frame . pack ( side = " left " , padx = 5 )
tk . Label ( compose_font_frame , text = " Aa " , bg = " #E7F9FD " , fg = self . fg_dark ,
font = ( " Segoe UI " , 8 ) ) . pack ( side = " left " , padx = ( 5 , 2 ) )
compose_spinbox = tk . Spinbox ( compose_font_frame , from_ = 5 , to = 12 , width = 3 ,
bg = " #E7F9FD " , relief = " flat " , bd = 0 ,
font = ( " Segoe UI " , 9 ) , justify = " center " ,
textvariable = tk . IntVar ( value = self . font_size_compose ) ,
command = self . _on_compose_font_change )
compose_spinbox . pack ( side = " left " , padx = ( 0 , 5 ) )
compose_spinbox . delete ( 0 , " end " )
compose_spinbox . insert ( 0 , str ( self . font_size_compose ) )
compose_spinbox . bind ( " <Return> " , lambda e : self . _on_compose_font_change ( ) )
compose_spinbox . bind ( " <FocusOut> " , lambda e : self . _on_compose_font_change ( ) )
self . compose_spinbox = compose_spinbox
self . _create_toolbar_button ( btn_frame_right , " ↩ Antworten " , self . _reply_email ) . pack ( side = " left " , padx = 2 )
self . _create_toolbar_button ( btn_frame_right , " 🗑 Löschen " , self . _delete_selected ) . pack ( side = " left " , padx = 2 )
self . _create_toolbar_button ( btn_frame_right , " 🚫 Als Spam " , self . _mark_as_spam ) . pack ( side = " left " , padx = 2 )
self . _create_toolbar_button ( btn_frame_right , " ✅ Kein Spam " , self . _mark_as_not_spam ) . pack ( side = " left " , padx = 2 )
self . _create_toolbar_button ( btn_frame_right , " 📝 KI Zusammenfassung " , self . _ai_summarize ) . pack ( side = " left " , padx = 2 )
self . _create_toolbar_button ( btn_frame_right , " 💡 KI Antworten " , self . _ai_reply_suggestions ) . pack ( side = " left " , padx = 2 )
self . _create_toolbar_button ( btn_frame_right , " 📋 Vorlagen " , self . _email_templates ) . pack ( side = " left " , padx = 2 )
self . _create_toolbar_button ( btn_frame_right , " ✍ Unterschrift " , self . _manage_signature ) . pack ( side = " left " , padx = 2 )
self . _create_toolbar_button ( btn_frame_right , " ⚙ Konten " , self . _manage_accounts ) . pack ( side = " left " , padx = 2 )
# Hauptbereich: 3-Spalten Layout wie Mailbird
main_paned = ttk . PanedWindow ( self . root , orient = " horizontal " )
main_paned . pack ( fill = " both " , expand = True )
# Linke Sidebar (Ordner)
left_frame = tk . Frame ( main_paned , bg = self . bg_dark , width = 200 )
self . _create_sidebar ( left_frame )
main_paned . add ( left_frame , weight = 0 )
# Mittlerer Bereich (E-Mail Liste)
middle_frame = tk . Frame ( main_paned , bg = self . bg_light , width = 350 )
self . _create_email_list ( middle_frame )
main_paned . add ( middle_frame , weight = 1 )
# Rechter Bereich (E-Mail Vorschau)
right_frame = tk . Frame ( main_paned , bg = " #FFFFFF " , width = 600 )
self . _create_preview ( right_frame )
main_paned . add ( right_frame , weight = 2 )
# Statusleiste unten
statusbar = tk . Frame ( self . root , bg = self . bg_medium , height = 25 )
statusbar . pack ( fill = " x " , side = " bottom " )
self . status_var = tk . StringVar ( value = " Bereit. " )
tk . Label ( statusbar , textvariable = self . status_var , bg = self . bg_medium ,
fg = self . fg_light , anchor = " w " , padx = 10 ) . pack ( fill = " x " )
def _create_toolbar_button ( self , parent , text , command ) :
""" Erstellt einen Toolbar-Button im Mailbird-Stil. """
btn = tk . Button (
parent , text = text , command = command ,
bg = self . accent_blue , fg = self . fg_light ,
relief = " flat " , padx = 12 , pady = 6 ,
font = ( " Segoe UI " , 9 ) ,
cursor = " hand2 "
)
btn . bind ( " <Enter> " , lambda e : btn . configure ( bg = self . accent_hover ) )
btn . bind ( " <Leave> " , lambda e : btn . configure ( bg = self . accent_blue ) )
return btn
def _on_list_font_change ( self ) :
""" Handler für Änderung der Listen-Schriftgröße. """
try :
new_size = int ( self . list_spinbox . get ( ) )
if 5 < = new_size < = 12 :
self . font_size_list = new_size
print ( f " DEBUG: Schriftgröße Liste geändert auf { new_size } " )
# Liste neu laden mit neuer Schriftgröße
self . _refresh_email_list ( )
# WICHTIG: Auch die E-Mail-Vorschau aktualisieren!
if hasattr ( self , ' preview_text ' ) :
try :
widget = self . preview_text
# ScrolledText ist ein Frame mit Text-Widget innen
# Wir müssen das interne Text-Widget finden
text_widget = None
for child in widget . winfo_children ( ) :
if isinstance ( child , tk . Text ) :
text_widget = child
break
# Wenn kein Child gefunden, ist preview_text selbst das Text-Widget
if text_widget is None :
text_widget = widget
# Font ändern
text_widget . configure ( font = ( " Segoe UI " , self . font_size_list ) )
text_widget . update_idletasks ( )
print ( f " DEBUG: ✓ Preview-Schriftgröße erfolgreich geändert zu { self . font_size_list } " )
except Exception as e :
print ( f " DEBUG: ✗ Fehler beim Ändern der Preview-Schrift: { e } " )
import traceback
traceback . print_exc ( )
# Sofort speichern
config = load_email_config ( )
config [ " font_size_list " ] = self . font_size_list
save_email_config ( config )
except ValueError as e :
print ( f " DEBUG: ValueError bei font change: { e } " )
except Exception as e :
print ( f " DEBUG: Unerwarteter Fehler: { e } " )
import traceback
traceback . print_exc ( )
def _on_compose_font_change ( self ) :
""" Handler für Änderung der Compose-Schriftgröße. """
try :
new_size = int ( self . compose_spinbox . get ( ) )
if 5 < = new_size < = 12 :
self . font_size_compose = new_size
# Sofort speichern
config = load_email_config ( )
config [ " font_size_compose " ] = self . font_size_compose
save_email_config ( config )
# WICHTIG: Aktualisiere ALLE offenen Compose-Fenster sofort!
for text_widget in self . compose_text_widgets [ : ] : # Kopie der Liste
try :
if text_widget . winfo_exists ( ) :
# Ändere Schriftgröße
text_widget . configure ( font = ( " Segoe UI " , self . font_size_compose ) )
else :
# Widget existiert nicht mehr, aus Liste entfernen
self . compose_text_widgets . remove ( text_widget )
except :
# Bei Fehler aus Liste entfernen
if text_widget in self . compose_text_widgets :
self . compose_text_widgets . remove ( text_widget )
except ValueError :
pass
def _create_sidebar ( self , parent ) :
""" Erstellt die linke Sidebar mit Ordnern. """
tk . Label ( parent , text = " Ordner " , bg = self . bg_dark , fg = self . fg_light ,
font = ( " Segoe UI " , 11 , " bold " ) , anchor = " w " , padx = 10 , pady = 10 ) . pack ( fill = " x " )
# Ordner-Liste
folders = [
( " 📥 Posteingang " , " inbox " , " INBOX " ) ,
( " 📤 Gesendet " , " sent " , " Sent " ) ,
( " 🚫 Spam " , " spam " , " Junk " ) ,
( " ✏ Entwürfe " , " drafts " , " Drafts " ) ,
( " 🗑 Papierkorb " , " trash " , " Trash " ) ,
]
self . folder_buttons = [ ]
self . folder_mapping = { } # folder_id -> IMAP folder name
for label , folder_id , imap_folder in folders :
self . folder_mapping [ folder_id ] = imap_folder
btn = tk . Button (
parent , text = label , anchor = " w " ,
bg = self . bg_dark , fg = self . fg_light ,
relief = " flat " , padx = 15 , pady = 10 ,
font = ( " Segoe UI " , 10 ) ,
cursor = " hand2 " ,
command = lambda f = folder_id : self . _select_folder ( f )
)
btn . pack ( fill = " x " )
btn . bind ( " <Enter> " , lambda e , b = btn : b . configure ( bg = self . bg_medium ) )
btn . bind ( " <Leave> " , lambda e , b = btn : b . configure ( bg = self . bg_dark ) )
self . folder_buttons . append ( ( btn , folder_id ) )
def _create_email_list ( self , parent ) :
""" Erstellt die mittlere E-Mail-Liste im Mailbird-Stil. """
# Header
header = tk . Frame ( parent , bg = " #FFFFFF " , height = 50 )
header . pack ( fill = " x " )
header . pack_propagate ( False )
self . email_list_header = tk . Label ( header , text = " Posteingang " , bg = " #FFFFFF " , fg = self . fg_dark ,
font = ( " Segoe UI " , 14 , " bold " ) , anchor = " w " , padx = 15 )
self . email_list_header . pack ( fill = " both " , expand = True )
# E-Mail Container mit Canvas für Scrolling
self . email_list_container = tk . Frame ( parent , bg = " #F5F5F5 " )
self . email_list_container . pack ( fill = " both " , expand = True )
self . email_canvas = tk . Canvas ( self . email_list_container , bg = " #F5F5F5 " , highlightthickness = 0 )
scrollbar = tk . Scrollbar ( self . email_list_container , orient = " vertical " , command = self . email_canvas . yview )
self . email_scrollable_frame = tk . Frame ( self . email_canvas , bg = " #F5F5F5 " )
self . email_scrollable_frame . bind (
" <Configure> " ,
lambda e : self . email_canvas . configure ( scrollregion = self . email_canvas . bbox ( " all " ) )
)
self . email_canvas_frame = self . email_canvas . create_window ( ( 0 , 0 ) , window = self . email_scrollable_frame , anchor = " nw " )
self . email_canvas . configure ( yscrollcommand = scrollbar . set )
self . email_canvas . pack ( side = " left " , fill = " both " , expand = True )
scrollbar . pack ( side = " right " , fill = " y " )
# Mausrad-Scrolling aktivieren - EINFACHE und ROBUSTE Methode!
def _on_mousewheel ( event ) :
# Windows
self . email_canvas . yview_scroll ( int ( - 1 * ( event . delta / 120 ) ) , " units " )
def _on_mousewheel_linux_up ( event ) :
# Linux scroll up
self . email_canvas . yview_scroll ( - 1 , " units " )
def _on_mousewheel_linux_down ( event ) :
# Linux scroll down
self . email_canvas . yview_scroll ( 1 , " units " )
# Bind für Canvas UND scrollable_frame UND root (damit es überall funktioniert!)
self . email_canvas . bind ( " <MouseWheel> " , _on_mousewheel )
self . email_canvas . bind ( " <Button-4> " , _on_mousewheel_linux_up )
self . email_canvas . bind ( " <Button-5> " , _on_mousewheel_linux_down )
self . email_scrollable_frame . bind ( " <MouseWheel> " , _on_mousewheel )
self . email_scrollable_frame . bind ( " <Button-4> " , _on_mousewheel_linux_up )
self . email_scrollable_frame . bind ( " <Button-5> " , _on_mousewheel_linux_down )
# Speichere Callbacks für späteres Binden neuer Widgets
self . _mousewheel_callback = _on_mousewheel
self . _mousewheel_linux_up_callback = _on_mousewheel_linux_up
self . _mousewheel_linux_down_callback = _on_mousewheel_linux_down
# Canvas-Größe anpassen, wenn Parent sich ändert
self . email_canvas . bind ( " <Configure> " , self . _on_canvas_configure )
self . email_item_frames = [ ]
def _on_canvas_configure ( self , event ) :
""" Passt die Canvas-Breite an. """
self . email_canvas . itemconfig ( self . email_canvas_frame , width = event . width )
def _get_initials ( self , name_or_email ) :
""" Extrahiert Initialen aus Name oder E-Mail. """
# Entferne E-Mail-Adresse in Klammern
name = name_or_email . split ( ' < ' ) [ 0 ] . strip ( )
if not name or ' @ ' in name :
# Falls nur E-Mail: erste 2 Buchstaben
email_part = name_or_email . split ( ' @ ' ) [ 0 ] if ' @ ' in name_or_email else name_or_email
return email_part [ : 2 ] . upper ( )
# Extrahiere Initialen aus Namen
parts = name . split ( )
if len ( parts ) > = 2 :
return ( parts [ 0 ] [ 0 ] + parts [ - 1 ] [ 0 ] ) . upper ( )
elif len ( parts ) == 1 and len ( parts [ 0 ] ) > = 2 :
return parts [ 0 ] [ : 2 ] . upper ( )
return " ?? "
def _get_color_from_name ( self , name_or_email ) :
""" Generiert eine konsistente Pastellfarbe basierend auf dem Namen. """
# Pastellfarben (heller und weniger gesättigt)
colors = [
" #A8D5BA " , # Pastellgrün
" #FFB3C6 " , # Pastellpink
" #C5A3D9 " , # Pastelllila
" #FFD4A3 " , # Pfirsich
" #A3C4F3 " , # Pastellblau
" #FFB5B5 " , # Hellrot
" #A3E4F0 " , # Hellcyan
" #E8F48C " , # Helllime
" #C4A57B " , # Hellbraun
" #B0BEC5 " , # Hellblaugrau
]
# Hash des Namens für konsistente Farbwahl
hash_value = sum ( ord ( c ) for c in name_or_email )
return colors [ hash_value % len ( colors ) ]
def _format_date ( self , date_str ) :
""" Formatiert Datum im Mailbird-Stil (11:05, Gestern, Montag, etc.). """
try :
from email . utils import parsedate_to_datetime
dt = parsedate_to_datetime ( date_str )
now = datetime . now ( dt . tzinfo )
# Differenz in Tagen
diff_days = ( now . date ( ) - dt . date ( ) ) . days
if diff_days == 0 :
# Heute: Uhrzeit anzeigen
return dt . strftime ( " % H: % M " )
elif diff_days == 1 :
return " Gestern "
elif diff_days < = 6 :
# Diese Woche: Wochentag
weekdays = [ " Montag " , " Dienstag " , " Mittwoch " , " Donnerstag " , " Freitag " , " Samstag " , " Sonntag " ]
return weekdays [ dt . weekday ( ) ]
else :
# Älter: Datum
return dt . strftime ( " %d . % m. % Y " )
except :
# Fallback
return date_str [ : 10 ] if len ( date_str ) > 10 else date_str
def _refresh_email_list ( self ) :
""" Aktualisiert die E-Mail-Liste im Mailbird-Stil. """
print ( f " DEBUG: _refresh_email_list aufgerufen. self.emails hat { len ( self . emails ) } Einträge " )
# Lösche alte E-Mail-Items (ALLE Kinder des Frames!)
for frame in self . email_item_frames :
frame . destroy ( )
self . email_item_frames = [ ]
# WICHTIG: Lösche auch alle anderen Widgets im scrollable_frame
for widget in self . email_scrollable_frame . winfo_children ( ) :
widget . destroy ( )
print ( f " DEBUG: Alle alten Widgets gelöscht " )
# Wenn keine E-Mails, zeige Lade-Indikator
if not self . emails :
print ( f " DEBUG: self.emails ist leer, zeige Lade-Indikator " )
loading_frame = tk . Frame ( self . email_scrollable_frame , bg = " #F5F5F5 " )
loading_frame . pack ( fill = " both " , expand = True , pady = 50 )
loading_label = tk . Label ( loading_frame , text = " ⏳ Lädt E-Mails... " ,
bg = " #F5F5F5 " , fg = " #666666 " ,
font = ( " Segoe UI " , 12 ) )
loading_label . pack ( )
self . email_item_frames . append ( loading_frame )
return
print ( f " DEBUG: Erstelle { len ( self . emails ) } E-Mail-Items... " )
for idx , email in enumerate ( self . emails ) :
self . _create_email_item ( email , idx )
# WICHTIG: Mausrad-Scrolling für alle neuen Items aktivieren!
def bind_mousewheel_recursive ( widget ) :
""" Bindet Mausrad-Events rekursiv für alle Kinder. """
if hasattr ( self , ' _mousewheel_callback ' ) :
widget . bind ( " <MouseWheel> " , self . _mousewheel_callback )
widget . bind ( " <Button-4> " , self . _mousewheel_linux_up_callback )
widget . bind ( " <Button-5> " , self . _mousewheel_linux_down_callback )
for child in widget . winfo_children ( ) :
bind_mousewheel_recursive ( child )
# Binde Mausrad für alle neuen Items
bind_mousewheel_recursive ( self . email_scrollable_frame )
print ( f " DEBUG: Mausrad-Scrolling für neue Items aktiviert " )
# WICHTIG: Canvas-Scrollregion nach Erstellen aller Items aktualisieren!
self . email_canvas . update_idletasks ( )
self . email_canvas . configure ( scrollregion = self . email_canvas . bbox ( " all " ) )
print ( f " DEBUG: Canvas-Scrollregion aktualisiert " )
print ( f " DEBUG: E-Mail-Items erstellt, zeige erste E-Mail in Vorschau " )
# WICHTIG: Zeige die erste E-Mail automatisch in der Vorschau
if len ( self . emails ) > 0 :
self . _show_email_preview ( 0 )
def _create_email_item ( self , email , idx ) :
""" Erstellt ein einzelnes E-Mail-Item im Mailbird-Stil. """
print ( f " DEBUG: Erstelle E-Mail-Item { idx } : Von= ' { email . get ( ' from ' , ' ' ) [ : 30 ] } ' , Betreff= ' { email . get ( ' subject ' , ' ' ) [ : 30 ] } ' " )
# Container für E-Mail
item_frame = tk . Frame ( self . email_scrollable_frame , bg = " #FFFFFF " , relief = " flat " , bd = 0 )
item_frame . pack ( fill = " x " , padx = 5 , pady = 2 )
print ( f " DEBUG: item_frame erstellt und gepackt für E-Mail { idx } " )
# Hover-Effekt
def on_enter ( e ) :
item_frame . configure ( bg = " #F0F0F0 " )
def on_leave ( e ) :
item_frame . configure ( bg = " #FFFFFF " )
def on_click ( e ) :
self . _show_email_preview ( idx )
item_frame . bind ( " <Enter> " , on_enter )
item_frame . bind ( " <Leave> " , on_leave )
item_frame . bind ( " <Button-1> " , on_click )
# Haupt-Container
main_container = tk . Frame ( item_frame , bg = " #FFFFFF " )
main_container . pack ( fill = " x " , padx = 10 , pady = 8 )
# Linker Bereich: Avatar-Kreis
left_frame = tk . Frame ( main_container , bg = " #FFFFFF " )
left_frame . pack ( side = " left " , padx = ( 0 , 10 ) )
# Erstelle perfekt runden Kreis mit PIL (Anti-Aliasing)
initials = self . _get_initials ( email [ ' from ' ] )
base_color = self . _get_color_from_name ( email [ ' from ' ] )
# Konvertiere Hex zu RGB für Opazität
def hex_to_rgb ( hex_color ) :
hex_color = hex_color . lstrip ( ' # ' )
return tuple ( int ( hex_color [ i : i + 2 ] , 16 ) for i in ( 0 , 2 , 4 ) )
def rgb_to_hex ( rgb ) :
return ' # {:02x} {:02x} {:02x} ' . format ( int ( rgb [ 0 ] ) , int ( rgb [ 1 ] ) , int ( rgb [ 2 ] ) )
# 70% Opazität durch Mischung mit Weiß
rgb = hex_to_rgb ( base_color )
white = ( 255 , 255 , 255 )
opacity = 0.7
blended_rgb = tuple ( int ( rgb [ i ] * opacity + white [ i ] * ( 1 - opacity ) ) for i in range ( 3 ) )
# Erstelle hochauflösendes Bild für perfekten Kreis
size = 80 # Höhere Auflösung für Anti-Aliasing
img = Image . new ( ' RGBA ' , ( size , size ) , ( 255 , 255 , 255 , 0 ) )
draw = ImageDraw . Draw ( img )
# Zeichne perfekt runden Kreis mit Anti-Aliasing
draw . ellipse ( [ 0 , 0 , size - 1 , size - 1 ] , fill = blended_rgb + ( 255 , ) , outline = None )
# Skaliere auf finale Größe (macht Kanten glatt)
final_size = 40
img = img . resize ( ( final_size , final_size ) , Image . Resampling . LANCZOS )
# Konvertiere zu PhotoImage
photo = ImageTk . PhotoImage ( img )
# Label für Avatar-Bild
avatar_label = tk . Label ( left_frame , image = photo , bg = " #FFFFFF " )
avatar_label . image = photo # Referenz behalten
avatar_label . pack ( )
# Text (Initialen) über dem Bild
initial_label = tk . Label ( left_frame , text = initials , bg = rgb_to_hex ( blended_rgb ) ,
fg = " #555555 " , font = ( " Segoe UI " , 11 , " bold " ) )
initial_label . place ( in_ = avatar_label , relx = 0.5 , rely = 0.5 , anchor = " center " )
avatar_label . bind ( " <Button-1> " , on_click )
initial_label . bind ( " <Button-1> " , on_click )
# Mittlerer Bereich: Name, Betreff, Vorschau
middle_frame = tk . Frame ( main_container , bg = " #FFFFFF " )
middle_frame . pack ( side = " left " , fill = " both " , expand = True )
# Name und Uhrzeit (obere Zeile)
top_row = tk . Frame ( middle_frame , bg = " #FFFFFF " )
top_row . pack ( fill = " x " )
# Name (fett) - verwendet font_size_list
name_only = email [ ' from ' ] . split ( ' < ' ) [ 0 ] . strip ( )
if not name_only :
name_only = email [ ' from ' ] . split ( ' @ ' ) [ 0 ]
name_label = tk . Label ( top_row , text = name_only , bg = " #FFFFFF " , fg = " #1F1F1F " ,
font = ( " Segoe UI " , self . font_size_list , " bold " ) , anchor = " w " )
name_label . pack ( side = " left " , fill = " x " , expand = True )
name_label . bind ( " <Button-1> " , on_click )
# Anhang-Symbol VOR dem Datum (rechts) - BILD-ICON statt Emoji
if email . get ( ' has_attachments ' , False ) :
try :
# Lade das Büroklammer-Bild
paperclip_path = os . path . join ( os . path . dirname ( os . path . abspath ( __file__ ) ) , " assets " , " paperclip.png " )
if os . path . exists ( paperclip_path ) :
from PIL import Image as PILImage
img_clip = PILImage . open ( paperclip_path )
# Größe basierend auf font_size
icon_size = self . font_size_list + 8
img_clip = img_clip . resize ( ( icon_size , icon_size ) , PILImage . Resampling . LANCZOS )
photo_clip = ImageTk . PhotoImage ( img_clip )
attachment_icon = tk . Label ( top_row , image = photo_clip , bg = " #FFFFFF " , cursor = " hand2 " )
attachment_icon . image = photo_clip # Referenz behalten!
attachment_icon . pack ( side = " right " , padx = ( 5 , 5 ) )
attachment_icon . bind ( " <Button-1> " , on_click )
else :
# Fallback auf Emoji
attachment_icon = tk . Label ( top_row , text = " 📎 " , bg = " #FFFFFF " , fg = " #555555 " ,
font = ( " Segoe UI " , self . font_size_list + 3 , " bold " ) )
attachment_icon . pack ( side = " right " , padx = ( 5 , 5 ) )
attachment_icon . bind ( " <Button-1> " , on_click )
except Exception as e :
print ( f " DEBUG: Büroklammer-Icon Fehler: { e } " )
# Fallback auf Emoji bei Fehler
attachment_icon = tk . Label ( top_row , text = " 📎 " , bg = " #FFFFFF " , fg = " #555555 " ,
font = ( " Segoe UI " , self . font_size_list + 3 , " bold " ) )
attachment_icon . pack ( side = " right " , padx = ( 5 , 5 ) )
attachment_icon . bind ( " <Button-1> " , on_click )
# Zeit/Datum (rechts) - verwendet font_size_list - 1
date_formatted = self . _format_date ( email [ ' date ' ] )
date_label = tk . Label ( top_row , text = date_formatted , bg = " #FFFFFF " , fg = " #666666 " ,
font = ( " Segoe UI " , max ( self . font_size_list - 1 , 5 ) ) , anchor = " e " )
date_label . pack ( side = " right " )
date_label . bind ( " <Button-1> " , on_click )
# Betreff (zweite Zeile) - verwendet font_size_list - 1
subject_text = email [ ' subject ' ]
if len ( subject_text ) > 50 :
subject_text = subject_text [ : 50 ] + " ... "
subject_label = tk . Label ( middle_frame , text = subject_text , bg = " #FFFFFF " , fg = " #333333 " ,
font = ( " Segoe UI " , max ( self . font_size_list - 1 , 5 ) ) , anchor = " w " )
subject_label . pack ( fill = " x " , pady = ( 2 , 0 ) )
subject_label . bind ( " <Button-1> " , on_click )
# Dünne Trennlinie
separator = tk . Frame ( self . email_scrollable_frame , bg = " #E0E0E0 " , height = 1 )
separator . pack ( fill = " x " , padx = 15 )
self . email_item_frames . append ( item_frame )
def _show_email_preview ( self , idx ) :
""" Zeigt die E-Mail-Vorschau an. """
if idx > = len ( self . emails ) :
return
email = self . emails [ idx ]
self . selected_email = email # Speichere aktuell ausgewählte E-Mail
# Vorschau aktualisieren
self . preview_subject . configure ( text = email [ " subject " ] )
self . preview_from . configure ( text = f " Von: { email [ ' from ' ] } " )
self . preview_date . configure ( text = f " Datum: { email [ ' date ' ] } " )
# Text ist jetzt immer "normal" (markierbar), nur Inhalt ändern
self . preview_text . delete ( " 1.0 " , " end " )
# Text formatieren (Visitenkarten-freundlich)
formatted_body = self . _format_email_body ( email [ " body " ] )
self . preview_text . insert ( " 1.0 " , formatted_body )
# NICHT mehr auf "disabled" setzen - bleibt "normal" für Markieren & Kopieren!
# Anhänge anzeigen (falls vorhanden)
for widget in self . preview_attachments_frame . winfo_children ( ) :
widget . destroy ( )
attachments = email . get ( " attachments " , [ ] )
if attachments :
tk . Label ( self . preview_attachments_frame , text = f " 📎 { len ( attachments ) } Anhang(e): " ,
bg = " #FFFFFF " , fg = self . fg_dark , font = ( " Segoe UI " , 10 , " bold " ) ,
anchor = " w " ) . pack ( fill = " x " , pady = ( 5 , 5 ) )
for att in attachments :
att_frame = tk . Frame ( self . preview_attachments_frame , bg = " #F0F0F0 " ,
relief = " solid " , bd = 1 )
att_frame . pack ( fill = " x " , pady = 2 )
# Icon basierend auf Dateityp
if att . get ( ' is_image ' , False ) :
icon = " 🖼️ "
elif att [ ' content_type ' ] . startswith ( ' application/pdf ' ) :
icon = " 📄 "
elif ' word ' in att [ ' content_type ' ] or att [ ' filename ' ] . endswith ( ' .docx ' ) :
icon = " 📝 "
elif ' excel ' in att [ ' content_type ' ] or att [ ' filename ' ] . endswith ( ' .xlsx ' ) :
icon = " 📊 "
else :
icon = " 📎 "
info_label = tk . Label ( att_frame ,
text = f " { icon } { att [ ' filename ' ] } ( { att [ ' size ' ] } ) " ,
bg = " #F0F0F0 " , fg = self . fg_dark ,
font = ( " Segoe UI " , 9 ) , anchor = " w " )
info_label . pack ( side = " left " , padx = 10 , pady = 5 , fill = " x " , expand = True )
# Bild-Vorschau anzeigen (falls Bild)
if att . get ( ' is_image ' , False ) :
try :
from PIL import Image
import io
# Bild aus Payload laden
img = Image . open ( io . BytesIO ( att [ ' payload ' ] ) )
# Thumbnail erstellen (max 200x150)
img . thumbnail ( ( 200 , 150 ) , Image . Resampling . LANCZOS )
photo = ImageTk . PhotoImage ( img )
# Bild in neuem Frame anzeigen
img_label = tk . Label ( att_frame , image = photo , bg = " #F0F0F0 " )
img_label . image = photo # Referenz behalten!
img_label . pack ( side = " left " , padx = 5 )
except :
pass # Bild konnte nicht geladen werden
download_btn = tk . Button ( att_frame , text = " 💾 Download " ,
command = lambda a = att : self . _download_attachment ( a ) ,
bg = " #3498DB " , fg = " #FFFFFF " ,
font = ( " Segoe UI " , 8 ) , relief = " flat " ,
padx = 10 , pady = 3 , cursor = " hand2 " )
download_btn . pack ( side = " right " , padx = 5 , pady = 2 )
def _create_preview ( self , parent ) :
""" Erstellt die rechte E-Mail-Vorschau. """
# Header
header_frame = tk . Frame ( parent , bg = " #F8F9FA " , height = 100 )
header_frame . pack ( fill = " x " )
header_frame . pack_propagate ( False )
info_frame = tk . Frame ( header_frame , bg = " #F8F9FA " )
info_frame . pack ( fill = " both " , expand = True , padx = 20 , pady = 15 )
self . preview_subject = tk . Label (
info_frame , text = " Betreff " , bg = " #F8F9FA " , fg = self . fg_dark ,
font = ( " Segoe UI " , 14 , " bold " ) , anchor = " w "
)
self . preview_subject . pack ( fill = " x " )
self . preview_from = tk . Label (
info_frame , text = " Von: " , bg = " #F8F9FA " , fg = " #7F8C8D " ,
font = ( " Segoe UI " , 9 ) , anchor = " w "
)
self . preview_from . pack ( fill = " x " , pady = ( 5 , 0 ) )
self . preview_date = tk . Label (
info_frame , text = " Datum: " , bg = " #F8F9FA " , fg = " #7F8C8D " ,
font = ( " Segoe UI " , 9 ) , anchor = " w "
)
self . preview_date . pack ( fill = " x " )
# Trennlinie
tk . Frame ( parent , bg = " #BDC3C7 " , height = 1 ) . pack ( fill = " x " )
# Anhänge-Bereich (UNTER dem Header, VOR dem Text)
self . preview_attachments_frame = tk . Frame ( parent , bg = " #FFFFFF " )
self . preview_attachments_frame . pack ( fill = " x " , padx = 20 , pady = ( 10 , 0 ) )
# E-Mail Inhalt - TEXT MARKIERBAR & KOPIERBAR!
content_frame = tk . Frame ( parent , bg = " #FFFFFF " )
content_frame . pack ( fill = " both " , expand = True , padx = 20 , pady = 20 )
preview_header = tk . Frame ( content_frame , bg = " #FFFFFF " )
preview_header . pack ( fill = " x " , anchor = " w " )
self . preview_text = scrolledtext . ScrolledText (
content_frame , wrap = " word " , bg = " #FFFFFF " , fg = self . fg_dark ,
font = ( " Segoe UI " , self . font_size_list ) , relief = " flat " , bd = 0 ,
state = " normal " ,
cursor = " arrow "
)
self . preview_text . pack ( fill = " both " , expand = True )
add_text_font_size_control ( preview_header , self . preview_text , initial_size = self . font_size_list , bg_color = " #FFFFFF " , save_key = " email_preview " )
# Nur Schreibschutz, aber Markieren & Kopieren erlauben
# Bindings für Kopieren (Strg+C funktioniert automatisch!)
self . preview_text . bind ( " <Button-1> " , lambda e : " break " ) # Verhindert Cursor-Setzen
self . preview_text . bind ( " <KeyPress> " , lambda e : " break " if e . keysym not in [ " c " , " C " , " a " , " A " ] and not e . state & 0x4 else None )
# Kontextmenü für Kopieren
preview_menu = tk . Menu ( self . preview_text , tearoff = 0 )
preview_menu . add_command ( label = " Kopieren " , command = lambda : self . _copy_preview_text ( ) ,
accelerator = " Strg+C " )
preview_menu . add_command ( label = " Alles markieren " , command = lambda : self . preview_text . tag_add ( " sel " , " 1.0 " , " end " ) ,
accelerator = " Strg+A " )
def show_preview_menu ( event ) :
try :
preview_menu . tk_popup ( event . x_root , event . y_root )
finally :
preview_menu . grab_release ( )
self . preview_text . bind ( " <Button-3> " , show_preview_menu ) # Rechtsklick
def _copy_preview_text ( self ) :
""" Kopiert den markierten Text aus der Vorschau. """
try :
selected_text = self . preview_text . get ( " sel.first " , " sel.last " )
self . root . clipboard_clear ( )
self . root . clipboard_append ( selected_text )
except :
pass # Nichts markiert
def _load_contacts ( self ) :
""" Lädt gespeicherte Kontakte aus Datei. """
try :
contacts_file = os . path . join ( os . path . dirname ( os . path . abspath ( __file__ ) ) , " aza_email_contacts.json " )
if os . path . isfile ( contacts_file ) :
with open ( contacts_file , " r " , encoding = " utf-8 " ) as f :
data = json . load ( f )
self . contacts = set ( data . get ( " contacts " , [ ] ) )
except :
self . contacts = set ( )
def _save_contacts ( self ) :
""" Speichert Kontakte in Datei. """
try :
contacts_file = os . path . join ( os . path . dirname ( os . path . abspath ( __file__ ) ) , " aza_email_contacts.json " )
with open ( contacts_file , " w " , encoding = " utf-8 " ) as f :
json . dump ( { " contacts " : list ( self . contacts ) } , f , indent = 2 , ensure_ascii = False )
except Exception as e :
print ( f " DEBUG: Fehler beim Speichern von Kontakten: { e } " )
def _add_contact ( self , email ) :
""" Fügt E-Mail-Adresse zu Kontakten hinzu. """
if email and " @ " in email :
email = email . strip ( ) . lower ( )
self . contacts . add ( email )
self . _save_contacts ( )
def _compose_email ( self ) :
""" Öffnet ein Fenster zum Verfassen einer neuen E-Mail. """
compose_win = tk . Toplevel ( self . root )
compose_win . title ( " Neue E-Mail " )
compose_win . geometry ( " 700x500 " )
compose_win . configure ( bg = " #FFFFFF " )
# Formular
form_frame = tk . Frame ( compose_win , bg = " #FFFFFF " , padx = 20 , pady = 20 )
form_frame . pack ( fill = " both " , expand = True )
# An
tk . Label ( form_frame , text = " An: " , bg = " #FFFFFF " , fg = self . fg_dark ,
font = ( " Segoe UI " , 10 ) ) . grid ( row = 0 , column = 0 , sticky = " w " , pady = 5 )
self . compose_to_entry = tk . Entry ( form_frame , font = ( " Segoe UI " , 10 ) , relief = " solid " , bd = 1 )
self . compose_to_entry . grid ( row = 0 , column = 1 , sticky = " ew " , pady = 5 )
# Auto-Complete Listbox für E-Mail-Adressen
autocomplete_listbox = None
autocomplete_window = None
def show_autocomplete ( event = None ) :
""" Zeigt Auto-Complete Vorschläge. """
nonlocal autocomplete_listbox , autocomplete_window
typed = self . compose_to_entry . get ( ) . strip ( ) . lower ( )
if len ( typed ) < 2 :
hide_autocomplete ( )
return
# Finde passende Kontakte
matches = [ c for c in self . contacts if typed in c ]
if not matches :
hide_autocomplete ( )
return
# Erstelle/Update Autocomplete-Fenster
if not autocomplete_window :
autocomplete_window = tk . Toplevel ( compose_win )
autocomplete_window . wm_overrideredirect ( True )
autocomplete_window . attributes ( ' -topmost ' , True )
autocomplete_listbox = tk . Listbox (
autocomplete_window ,
font = ( " Segoe UI " , 10 ) ,
height = min ( 8 , len ( matches ) ) , # Größer: 8 statt 5
width = 50 , # Breiter!
relief = " solid " ,
bd = 1
)
autocomplete_listbox . pack ( fill = " both " , expand = True )
def select_contact ( event = None ) :
try :
if autocomplete_listbox . curselection ( ) :
selected = autocomplete_listbox . get ( autocomplete_listbox . curselection ( ) [ 0 ] )
self . compose_to_entry . delete ( 0 , " end " )
self . compose_to_entry . insert ( 0 , selected )
self . compose_to_entry . icursor ( " end " )
hide_autocomplete ( )
self . compose_to_entry . focus_set ( )
except Exception as e :
print ( f " DEBUG: Fehler bei select_contact: { e } " )
def on_listbox_click ( event ) :
""" Bei Klick auf Item in Listbox. """
try :
# Finde geklicktes Item
index = autocomplete_listbox . nearest ( event . y )
autocomplete_listbox . selection_clear ( 0 , " end " )
autocomplete_listbox . selection_set ( index )
autocomplete_listbox . activate ( index )
# Warte kurz, dann wähle aus
compose_win . after ( 50 , select_contact )
except Exception as e :
print ( f " DEBUG: Fehler bei on_listbox_click: { e } " )
autocomplete_listbox . bind ( " <Button-1> " , on_listbox_click )
autocomplete_listbox . bind ( " <ButtonRelease-1> " , lambda e : select_contact ( ) )
autocomplete_listbox . bind ( " <Return> " , select_contact )
# Update Position und Größe
x = self . compose_to_entry . winfo_rootx ( )
y = self . compose_to_entry . winfo_rooty ( ) + self . compose_to_entry . winfo_height ( )
w = max ( self . compose_to_entry . winfo_width ( ) , 400 ) # Mindestens 400px breit!
h = min ( 200 , len ( matches ) * 25 + 10 ) # Höher: 25px pro Item
autocomplete_window . geometry ( f " { w } x { h } + { x } + { y } " )
# Update Liste
autocomplete_listbox . delete ( 0 , " end " )
for match in matches [ : 10 ] : # Max 10 Vorschläge
autocomplete_listbox . insert ( " end " , match )
def hide_autocomplete ( event = None ) :
""" Versteckt Auto-Complete. """
nonlocal autocomplete_window
if autocomplete_window :
try :
autocomplete_window . destroy ( )
except :
pass
autocomplete_window = None
def on_key_release ( event ) :
""" Bei Tastendruck Auto-Complete aktualisieren. """
if event . keysym in [ " Up " , " Down " , " Return " , " Escape " ] :
return
show_autocomplete ( )
def on_focus_out ( event ) :
""" Bei Fokus-Verlust Auto-Complete verstecken. """
# Verzögerung, damit Klick auf Listbox noch funktioniert
compose_win . after ( 200 , hide_autocomplete )
self . compose_to_entry . bind ( " <KeyRelease> " , on_key_release )
self . compose_to_entry . bind ( " <FocusOut> " , on_focus_out )
# Betreff
tk . Label ( form_frame , text = " Betreff: " , bg = " #FFFFFF " , fg = self . fg_dark ,
font = ( " Segoe UI " , 10 ) ) . grid ( row = 1 , column = 0 , sticky = " w " , pady = 5 )
self . compose_subject_entry = tk . Entry ( form_frame , font = ( " Segoe UI " , 10 ) , relief = " solid " , bd = 1 )
self . compose_subject_entry . grid ( row = 1 , column = 1 , sticky = " ew " , pady = 5 )
# Nachricht
tk . Label ( form_frame , text = " Nachricht: " , bg = " #FFFFFF " , fg = self . fg_dark ,
font = ( " Segoe UI " , 10 ) ) . grid ( row = 2 , column = 0 , sticky = " nw " , pady = 5 )
text_frame = tk . Frame ( form_frame , bg = " #FFFFFF " )
text_frame . grid ( row = 2 , column = 1 , sticky = " nsew " , pady = 5 )
compose_header = tk . Frame ( text_frame , bg = " #FFFFFF " )
compose_header . pack ( fill = " x " , anchor = " w " )
self . compose_body_text = scrolledtext . ScrolledText (
text_frame , wrap = " word " , font = ( " Segoe UI " , self . font_size_compose ) ,
relief = " solid " , bd = 1 , height = 15
)
self . compose_body_text . pack ( fill = " both " , expand = True )
add_text_font_size_control ( compose_header , self . compose_body_text , initial_size = self . font_size_compose , bg_color = " #FFFFFF " , save_key = " email_compose " )
# Unterschrift automatisch einfügen bei neuen E-Mails
if self . signature_auto_new and self . signature :
self . compose_body_text . insert ( " end " , " \n \n " + self . signature )
# WICHTIG: Widget zur Liste hinzufügen für dynamische Updates
self . compose_text_widgets . append ( self . compose_body_text )
# Beim Schließen des Fensters aus Liste entfernen
def on_close ( ) :
if self . compose_body_text in self . compose_text_widgets :
self . compose_text_widgets . remove ( self . compose_body_text )
compose_win . destroy ( )
form_frame . columnconfigure ( 1 , weight = 1 )
form_frame . rowconfigure ( 2 , weight = 1 )
# Buttons
btn_frame = tk . Frame ( compose_win , bg = " #FFFFFF " , pady = 10 )
btn_frame . pack ( fill = " x " , padx = 20 )
def insert_signature ( ) :
""" Fügt Unterschrift am Ende ein. """
if self . signature :
current_text = self . compose_body_text . get ( " 1.0 " , " end-1c " )
if not current_text . endswith ( self . signature ) :
self . compose_body_text . insert ( " end " , " \n \n " + self . signature )
else :
messagebox . showinfo ( " Keine Unterschrift " ,
" Bitte erstellen Sie zuerst eine Unterschrift über den Button ' ✍ Unterschrift ' . " ,
parent = compose_win )
def start_dictation ( ) :
""" Startet Diktat für E-Mail. """
try :
import sounddevice as sd
except :
messagebox . showerror ( " Fehler " , " sounddevice nicht installiert. \n \n Bitte installieren: \n pip install sounddevice " , parent = compose_win )
return
# Recorder als Instanz-Variable speichern
if not hasattr ( start_dictation , ' recorder ' ) :
start_dictation . recorder = None
start_dictation . recording = False
if not start_dictation . recording :
# Start recording
try :
print ( " DEBUG: Starte Aufnahme... " )
start_dictation . recorder = AudioRecorder ( )
start_dictation . recorder . start ( )
start_dictation . recording = True
dict_btn . config ( text = " ⏹ Stopp " , bg = " #E74C3C " )
self . status_var . set ( " 🎤 Aufnahme läuft... " )
print ( " DEBUG: Aufnahme gestartet " )
except Exception as e :
print ( f " DEBUG: Fehler beim Starten: { e } " )
messagebox . showerror ( " Fehler " , f " Aufnahme fehlgeschlagen: \n { str ( e ) } " , parent = compose_win )
else :
# Stop recording
print ( " DEBUG: Stoppe Aufnahme... " )
dict_btn . config ( state = " disabled " , text = " ⏳ Transkribiere... " )
self . status_var . set ( " ⏳ Transkribiere Audio... " )
def transcribe_worker ( ) :
try :
wav_path = start_dictation . recorder . stop_and_save_wav ( )
start_dictation . recording = False
print ( f " DEBUG: Audio gespeichert: { wav_path } " )
print ( f " DEBUG: Starte Transkription... " )
2026-04-16 13:32:32 +02:00
# Transkription über zentralen KI-Client
try :
from aza_ai_client import get_ai_client
_tclient = get_ai_client ( )
except Exception :
load_dotenv ( )
_tclient = OpenAI ( )
2026-03-25 22:03:39 +01:00
with open ( wav_path , " rb " ) as audio_file :
2026-04-16 13:32:32 +02:00
transcript = _tclient . audio . transcriptions . create (
2026-03-25 22:03:39 +01:00
model = " gpt-4o-mini-transcribe " ,
file = audio_file ,
language = " de "
)
text = transcript . text
print ( f " DEBUG: Transkription erfolgreich: { text [ : 50 ] } ... " )
# Text einfügen
compose_win . after ( 0 , lambda : self . compose_body_text . insert ( " insert " , text + " " ) )
# Cleanup
try :
os . remove ( wav_path )
except :
pass
compose_win . after ( 0 , lambda : self . status_var . set ( " ✓ Transkription eingefügt " ) )
compose_win . after ( 0 , lambda : dict_btn . config ( state = " normal " , text = " 🎤 Diktat starten " , bg = " #3498DB " ) )
except Exception as e :
error_msg = str ( e )
print ( f " DEBUG: FEHLER bei Transkription: { error_msg } " )
import traceback
traceback . print_exc ( )
compose_win . after ( 0 , lambda msg = error_msg : messagebox . showerror ( " Fehler " , f " Transkription fehlgeschlagen: \n { msg } " , parent = compose_win ) )
compose_win . after ( 0 , lambda : dict_btn . config ( state = " normal " , text = " 🎤 Diktat starten " , bg = " #3498DB " ) )
compose_win . after ( 0 , lambda : self . status_var . set ( " ❌ Transkription fehlgeschlagen " ) )
start_dictation . recording = False
threading . Thread ( target = transcribe_worker , daemon = True ) . start ( )
def ai_format ( ) :
""" KI formatiert den Text. """
text = self . compose_body_text . get ( " 1.0 " , " end-1c " ) . strip ( )
if not text :
messagebox . showinfo ( " Hinweis " , " Bitte geben Sie zuerst Text ein. " , parent = compose_win )
return
ai_btn . config ( state = " disabled " , text = " ⏳ KI formatiert... " )
def worker ( ) :
try :
2026-04-16 13:32:32 +02:00
try :
from aza_ai_client import get_ai_client
_fclient = get_ai_client ( )
except Exception :
load_dotenv ( )
_fclient = OpenAI ( )
response = _fclient . chat . completions . create (
2026-03-25 22:03:39 +01:00
model = " gpt-4o-mini " ,
messages = [ {
" role " : " system " ,
" content " : " Formatiere den folgenden E-Mail-Text professionell: Korrigiere Grammatik, Rechtschreibung und Interpunktion. Behalte den Inhalt bei, mache ihn aber gut lesbar und professionell. "
} , {
" role " : " user " ,
" content " : text
} ]
)
formatted = response . choices [ 0 ] . message . content
compose_win . after ( 0 , lambda : self . compose_body_text . delete ( " 1.0 " , " end " ) )
compose_win . after ( 0 , lambda : self . compose_body_text . insert ( " 1.0 " , formatted ) )
compose_win . after ( 0 , lambda : self . status_var . set ( " ✓ Text von KI formatiert " ) )
compose_win . after ( 0 , lambda : ai_btn . config ( state = " normal " , text = " 🤖 KI formatiert " ) )
except Exception as e :
error_msg = str ( e )
compose_win . after ( 0 , lambda msg = error_msg : messagebox . showerror ( " Fehler " , f " KI-Formatierung fehlgeschlagen: \n { msg } " , parent = compose_win ) )
compose_win . after ( 0 , lambda : ai_btn . config ( state = " normal " , text = " 🤖 KI formatiert " ) )
threading . Thread ( target = worker , daemon = True ) . start ( )
def change_font_size ( delta ) :
""" Ändert Schriftgröße. """
current_font = self . compose_body_text . cget ( " font " )
if isinstance ( current_font , str ) :
parts = current_font . split ( )
if len ( parts ) > = 2 :
try :
size = int ( parts [ 1 ] ) + delta
size = max ( 8 , min ( 24 , size ) ) # 8-24 Bereich
self . compose_body_text . configure ( font = ( parts [ 0 ] , size ) )
size_label . config ( text = f " { size } " )
except :
pass
# Linke Seite: Hauptbuttons
left_frame = tk . Frame ( btn_frame , bg = " #FFFFFF " )
left_frame . pack ( side = " left " , fill = " x " , expand = True )
send_btn = tk . Button (
left_frame , text = " 📨 Senden " , command = lambda : self . _send_email ( compose_win ) ,
bg = self . accent_blue , fg = self . fg_light ,
font = ( " Segoe UI " , 10 ) , relief = " flat " , padx = 20 , pady = 8 ,
cursor = " hand2 "
)
send_btn . pack ( side = " left " , padx = 5 )
# Speichere send_btn für später
self . _current_send_btn = send_btn
dict_btn = tk . Button (
left_frame , text = " 🎤 Diktat starten " , command = start_dictation ,
bg = " #3498DB " , fg = self . fg_light ,
font = ( " Segoe UI " , 10 ) , relief = " flat " , padx = 15 , pady = 8 ,
cursor = " hand2 "
)
dict_btn . pack ( side = " left " , padx = 5 )
ai_btn = tk . Button (
left_frame , text = " 🤖 KI formatiert " , command = ai_format ,
bg = " #9B59B6 " , fg = self . fg_light ,
font = ( " Segoe UI " , 10 ) , relief = " flat " , padx = 15 , pady = 8 ,
cursor = " hand2 "
)
ai_btn . pack ( side = " left " , padx = 5 )
sig_btn = tk . Button (
left_frame , text = " ✍ Unterschrift " , command = insert_signature ,
bg = " #27AE60 " , fg = self . fg_light ,
font = ( " Segoe UI " , 10 ) , relief = " flat " , padx = 15 , pady = 8 ,
cursor = " hand2 "
)
sig_btn . pack ( side = " left " , padx = 5 )
# Rechte Seite: Schriftgröße
right_frame = tk . Frame ( btn_frame , bg = " #FFFFFF " )
right_frame . pack ( side = " right " )
tk . Label ( right_frame , text = " Größe: " , bg = " #FFFFFF " , fg = self . fg_dark ,
font = ( " Segoe UI " , 9 ) ) . pack ( side = " left " , padx = 5 )
tk . Button ( right_frame , text = " − " , command = lambda : change_font_size ( - 1 ) ,
bg = " #ECF0F1 " , fg = self . fg_dark , font = ( " Segoe UI " , 10 , " bold " ) ,
relief = " flat " , width = 2 , cursor = " hand2 " ) . pack ( side = " left " , padx = 2 )
size_label = tk . Label ( right_frame , text = str ( self . font_size_compose ) , bg = " #FFFFFF " ,
fg = self . fg_dark , font = ( " Segoe UI " , 10 ) , width = 3 )
size_label . pack ( side = " left " , padx = 2 )
tk . Button ( right_frame , text = " + " , command = lambda : change_font_size ( + 1 ) ,
bg = " #ECF0F1 " , fg = self . fg_dark , font = ( " Segoe UI " , 10 , " bold " ) ,
relief = " flat " , width = 2 , cursor = " hand2 " ) . pack ( side = " left " , padx = 2 )
cancel_btn = tk . Button (
btn_frame , text = " Abbrechen " , command = on_close ,
bg = " #95A5A6 " , fg = self . fg_light ,
font = ( " Segoe UI " , 10 ) , relief = " flat " , padx = 20 , pady = 8 ,
cursor = " hand2 "
)
cancel_btn . pack ( side = " right " , padx = 5 )
# Auch beim X-Button das Widget entfernen
compose_win . protocol ( " WM_DELETE_WINDOW " , on_close )
def _send_email ( self , window ) :
""" Sendet E-Mail via SMTP. """
if not self . current_account :
messagebox . showerror ( " Fehler " , " Kein Konto konfiguriert. " , parent = window )
return
to_email = self . compose_to_entry . get ( ) . strip ( )
subject = self . compose_subject_entry . get ( ) . strip ( )
body = self . compose_body_text . get ( " 1.0 " , " end-1c " ) . strip ( )
if not to_email :
messagebox . showerror ( " Fehler " , " Empfänger ist erforderlich. " , parent = window )
return
# Betreff ist optional - wenn leer, verwende "(Kein Betreff)"
if not subject :
subject = " (Kein Betreff) "
print ( f " DEBUG: Starte E-Mail-Versand an { to_email } " )
# Disable Send-Button
if hasattr ( self , ' _current_send_btn ' ) :
self . _current_send_btn . config ( state = " disabled " , text = " 📨 Sende... " )
self . status_var . set ( " 📨 Sende E-Mail... " )
def worker ( ) :
try :
acc = self . current_account
smtp_server = acc . get ( " smtp_server " , " " )
smtp_port = acc . get ( " smtp_port " , 587 )
email_addr = acc . get ( " email " , " " )
password = acc . get ( " _password " , " " )
print ( f " DEBUG: SMTP Connect zu { smtp_server } : { smtp_port } " )
# SMTP Verbindung
server = smtplib . SMTP ( smtp_server , smtp_port , timeout = 30 )
server . set_debuglevel ( 1 ) # Debug-Ausgaben
server . starttls ( )
print ( f " DEBUG: SMTP Login als { email_addr } " )
server . login ( email_addr , password )
# E-Mail erstellen
msg = MIMEMultipart ( )
msg [ " From " ] = email_addr
msg [ " To " ] = to_email
msg [ " Subject " ] = subject
msg [ " Date " ] = email . utils . formatdate ( localtime = True )
msg . attach ( MIMEText ( body , " plain " , " utf-8 " ) )
print ( f " DEBUG: Sende E-Mail... " )
# Senden
server . send_message ( msg )
server . quit ( )
print ( f " DEBUG: ✓ E-Mail erfolgreich per SMTP gesendet! " )
# SOFORT Erfolgsmeldung und Fenster schließen (nicht auf IMAP warten!)
print ( f " DEBUG: Zeige Erfolgsmeldung und schließe Fenster... " )
self . root . after ( 0 , lambda : self . status_var . set ( " ✓ E-Mail gesendet " ) )
self . root . after ( 0 , lambda : messagebox . showinfo ( " Erfolg " , " E-Mail wurde gesendet! " , parent = window ) )
self . root . after ( 0 , window . destroy )
# E-Mail-Adresse zu Kontakten hinzufügen
self . _add_contact ( to_email )
# Nach erfolgreichem Senden: In Sent-Ordner kopieren (im Hintergrund)
try :
print ( f " DEBUG: Speichere E-Mail im Gesendet-Ordner... " )
imap_server = acc . get ( " imap_server " , " " )
imap_port = acc . get ( " imap_port " , 993 )
mail = imaplib . IMAP4_SSL ( imap_server , imap_port , timeout = 10 )
mail . login ( email_addr , password )
# Finde oder erstelle Sent-Ordner
sent_folder = self . _find_or_create_sent_folder ( mail )
if sent_folder :
# E-Mail mit aktuellem Datum speichern (timezone-aware!)
from datetime import datetime , timezone
now = datetime . now ( timezone . utc )
date_time = imaplib . Time2Internaldate ( now )
print ( f " DEBUG: Speichere in Ordner: ' { sent_folder } ' mit Datum: { date_time } " )
# WICHTIG: Ordner mit Leerzeichen/Punkten in Anführungszeichen!
if " " in sent_folder or " . " in sent_folder :
folder_to_use = f ' " { sent_folder } " '
else :
folder_to_use = sent_folder
# E-Mail in Sent-Ordner kopieren
result = mail . append (
folder_to_use ,
' \\ Seen ' ,
date_time ,
msg . as_bytes ( )
)
print ( f " DEBUG: IMAP append result: { result } " )
print ( f " DEBUG: ✓ E-Mail erfolgreich in { sent_folder } gespeichert " )
else :
print ( f " DEBUG: FEHLER - Kein Sent-Ordner gefunden oder erstellt! " )
print ( f " DEBUG: Schließe IMAP-Verbindung... " )
try :
mail . close ( )
mail . logout ( )
print ( f " DEBUG: IMAP-Logout erfolgreich " )
except :
# Logout kann hängen, ignorieren
print ( f " DEBUG: IMAP-Logout übersprungen " )
pass
except Exception as e :
print ( f " DEBUG: Fehler beim Speichern in Sent: { e } " )
# Fehler nicht anzeigen, da E-Mail bereits gesendet wurde
print ( f " DEBUG: Lösche Cache... " )
# Cache für Sent-Ordner und Inbox löschen
if " sent " in self . email_cache :
del self . email_cache [ " sent " ]
print ( f " DEBUG: Sent-Cache gelöscht " )
if " sent " in self . cache_timestamp :
del self . cache_timestamp [ " sent " ]
if " inbox " in self . email_cache :
del self . email_cache [ " inbox " ]
print ( f " DEBUG: Inbox-Cache gelöscht " )
if " inbox " in self . cache_timestamp :
del self . cache_timestamp [ " inbox " ]
# WICHTIG: Wenn Gesendet-Ordner aktuell geöffnet ist, neu laden!
if self . current_folder == " sent " :
print ( f " DEBUG: Gesendet-Ordner ist aktuell offen, lade neu... " )
self . root . after ( 1000 , self . _fetch_emails ) # Nach 1 Sekunde neu laden
# Wenn Posteingang aktuell geöffnet ist, neu laden!
if self . current_folder == " inbox " :
print ( f " DEBUG: Posteingang ist aktuell offen, lade neu... " )
self . root . after ( 1000 , self . _fetch_emails ) # Nach 1 Sekunde neu laden
print ( f " DEBUG: E-Mail-Versand komplett abgeschlossen! " )
except Exception as e :
error_msg = str ( e )
print ( f " DEBUG: SMTP Fehler: { error_msg } " )
import traceback
traceback . print_exc ( )
self . root . after ( 0 , lambda msg = error_msg : messagebox . showerror ( " SMTP Fehler " , f " Konnte E-Mail nicht senden: \n { msg } " , parent = window ) )
if hasattr ( self , ' _current_send_btn ' ) :
self . root . after ( 0 , lambda : self . _current_send_btn . config ( state = " normal " , text = " 📨 Senden " ) )
threading . Thread ( target = worker , daemon = True ) . start ( )
def _fetch_emails_with_progress ( self ) :
""" Lädt E-Mails mit Live-Fortschrittsanzeige. """
self . _fetch_emails ( )
def _find_or_create_sent_folder ( self , mail ) :
""" Findet oder erstellt den Sent-Ordner - AKTUELLEN, nicht migrierten! """
try :
print ( f " DEBUG: Suche Sent-Ordner... " )
# Liste alle Ordner
status , folders_list = mail . list ( )
if status != " OK " :
return None
available_folders = [ ]
for folder_line in folders_list :
try :
if isinstance ( folder_line , bytes ) :
folder_str = folder_line . decode ( ' utf-8 ' , errors = ' ignore ' )
else :
folder_str = str ( folder_line )
# Parse Ordnername (letzter String in Anführungszeichen)
import re
matches = re . findall ( r ' " ([^ " ]*) " ' , folder_str )
if matches and len ( matches ) > = 2 :
folder_name = matches [ - 1 ]
elif matches :
folder_name = matches [ 0 ]
else :
parts = folder_str . split ( )
folder_name = parts [ - 1 ] if parts else " "
if folder_name and folder_name != " . " :
available_folders . append ( folder_name )
except :
continue
print ( f " DEBUG: ALLE verfügbaren Ordner: \n { chr ( 10 ) . join ( available_folders ) } " )
# WICHTIG: Priorisiere AKTUELLE Ordner, nicht migrierte!
# Migrations-Ordner ZULETZT versuchen!
sent_variants = [
" INBOX.Sent " , # Standard IMAP
" Sent " , # Einfach
" Sent Items " , # Outlook-Style
" Sent Messages " , # Alternative
" [Gmail]/Sent Mail " , # Gmail
" [Gmail]/Gesendet " , # Gmail Deutsch
" Gesendet " , # Deutsch
" INBOX.migriert_von_servertown.Sent Messages " , # NUR als Fallback!
" INBOX.migriert_von_servertown.Sent " ,
]
# Suche exakten Match
for variant in sent_variants :
if variant in available_folders :
print ( f " DEBUG: ✓✓✓ Sent-Ordner gefunden (exakt): { variant } " )
return variant
# Suche mit Teilstring-Match (aber IGNORIERE migrierte Ordner!)
for available in available_folders :
if ( " sent " in available . lower ( ) or " gesendet " in available . lower ( ) ) and " migriert " not in available . lower ( ) :
print ( f " DEBUG: ✓✓✓ Sent-Ordner gefunden (Teilstring, nicht migriert): { available } " )
return available
# Fallback: Auch migrierte Ordner erlauben
for available in available_folders :
if " sent " in available . lower ( ) or " gesendet " in available . lower ( ) :
print ( f " DEBUG: ⚠ Sent-Ordner gefunden (migriert als Fallback): { available } " )
return available
# Nicht gefunden - erstelle neuen Sent-Ordner
print ( f " DEBUG: Kein Sent-Ordner gefunden, erstelle neuen... " )
try :
# Versuche "INBOX.Sent" zu erstellen
mail . create ( " INBOX.Sent " )
print ( " DEBUG: ✓✓✓ INBOX.Sent-Ordner neu erstellt " )
return " INBOX.Sent "
except Exception as create_err :
print ( f " DEBUG: Konnte INBOX.Sent nicht erstellen: { create_err } " )
try :
# Alternative: "Sent"
mail . create ( " Sent " )
print ( " DEBUG: ✓✓✓ Sent-Ordner neu erstellt " )
return " Sent "
except :
print ( " DEBUG: FEHLER - Konnte keinen Sent-Ordner erstellen " )
return None
except Exception as e :
print ( f " DEBUG: FEHLER bei _find_or_create_sent_folder: { e } " )
import traceback
traceback . print_exc ( )
return None
def _refresh ( self ) :
""" Aktualisiert die E-Mail-Liste (forciert Neu-Laden). """
# Cache für aktuellen Ordner löschen
if self . current_folder in self . email_cache :
del self . email_cache [ self . current_folder ]
if self . current_folder in self . cache_timestamp :
del self . cache_timestamp [ self . current_folder ]
self . _fetch_emails ( )
def _fetch_emails ( self ) :
""" Lädt E-Mails vom IMAP-Server mit Caching. """
if not self . current_account :
self . status_var . set ( " Kein Konto konfiguriert. Bitte über ⚙ Konten hinzufügen. " )
return
# IMAP Ordner-Name ermitteln
imap_folder = self . folder_mapping . get ( self . current_folder , " INBOX " )
folder_names = {
" inbox " : " Posteingang " ,
" sent " : " Gesendet " ,
" spam " : " Spam " ,
" drafts " : " Entwürfe " ,
" trash " : " Papierkorb "
}
folder_display = folder_names . get ( self . current_folder , " Ordner " )
# Prüfe Cache
import time
cache_key = self . current_folder
current_time = time . time ( )
if cache_key in self . email_cache and cache_key in self . cache_timestamp :
age = current_time - self . cache_timestamp [ cache_key ]
if age < self . cache_max_age :
# Cache verwenden
self . emails = self . email_cache [ cache_key ]
self . _refresh_email_list ( )
self . status_var . set ( f " { len ( self . emails ) } E-Mails in { folder_display } (aus Cache) " )
return
self . status_var . set ( f " Lade { folder_display } ... " )
def worker ( ) :
try :
acc = self . current_account
imap_server = acc . get ( " imap_server " , " " )
imap_port = acc . get ( " imap_port " , 993 )
email_addr = acc . get ( " email " , " " )
password = acc . get ( " _password " , " " )
if not all ( [ imap_server , email_addr , password ] ) :
self . root . after ( 0 , lambda : self . status_var . set ( " Konto-Daten unvollständig. Passwort via AZA_EMAIL_PASSWORD_0 setzen. " ) )
return
# IMAP Verbindung
mail = imaplib . IMAP4_SSL ( imap_server , imap_port )
mail . login ( email_addr , password )
# Verfügbare Ordner auflisten und besten Match finden
target_folder = None
folder_found = False
# Spezialfall: INBOX muss nicht in der Liste sein, versuche direkt
if self . current_folder == " inbox " :
try :
status_test = mail . select ( " INBOX " , readonly = True )
if status_test [ 0 ] == " OK " :
target_folder = " INBOX "
folder_found = True
print ( f " DEBUG: INBOX direkt gefunden " )
except Exception as e :
print ( f " DEBUG: INBOX existiert nicht: { e } " )
if not folder_found :
try :
status , folders_list = mail . list ( )
if status == " OK " :
# Alle verfügbaren Ordner dekodieren - VERBESSERTE LOGIK
available_folders = [ ]
for folder_line in folders_list :
try :
if isinstance ( folder_line , bytes ) :
folder_str = folder_line . decode ( ' utf-8 ' , errors = ' ignore ' )
else :
folder_str = str ( folder_line )
# IMAP LIST Format: (flags) "delimiter" "folder_name"
# Beispiel: (\HasNoChildren) "." "INBOX"
import re
# Suche nach dem letzten String in Anführungszeichen
matches = re . findall ( r ' " ([^ " ]*) " ' , folder_str )
if matches and len ( matches ) > = 2 :
# Letzter Match ist der Ordnername
folder_name = matches [ - 1 ]
elif matches and len ( matches ) == 1 :
folder_name = matches [ 0 ]
else :
# Fallback: nach Leerzeichen splitten
parts = folder_str . split ( )
folder_name = parts [ - 1 ] if parts else " "
# Nur valide Ordnernamen hinzufügen (nicht "." oder leer)
if folder_name and folder_name != " . " :
available_folders . append ( folder_name )
except Exception as e :
print ( f " DEBUG: Fehler beim Parsen von { folder_line } : { e } " )
continue
# DEBUG: Zeige verfügbare Ordner
print ( f " DEBUG: Verfügbare IMAP-Ordner: { available_folders } " )
print ( f " DEBUG: Suche Ordner für: { self . current_folder } " )
# Versuche verschiedene Ordner-Namen - SEHR WICHTIG: Reihenfolge beachten!
# WICHTIG: Auch migrations-basierte Ordner berücksichtigen!
folder_variants = {
" inbox " : [ " INBOX " ] , # Wird bereits oben direkt versucht
" sent " : [ " INBOX.Sent " , " Sent " , " Sent Items " , " Sent Messages " , " Gesendet " ,
" [Gmail]/Sent Mail " , " [Gmail]/Gesendet " , " INBOX.migriert_von_servertown.Sent Messages " ] ,
" spam " : [ " INBOX.Spam " , " INBOX.Junk " , " Junk " , " Spam " , " Junk E-Mail " ,
" [Gmail]/Spam " , " INBOX.migriert_von_servertown.Spam " , " INBOX.migriert_von_servertown.Junk " ] ,
" drafts " : [ " INBOX.Drafts " , " Drafts " , " Entwürfe " , " [Gmail]/Drafts " ,
" INBOX.migriert_von_servertown.Drafts " ] ,
" trash " : [ " INBOX.Trash " , " Trash " , " Deleted Items " , " Deleted Messages " , " Papierkorb " ,
" [Gmail]/Trash " , " Gelöscht " , " INBOX.migriert_von_servertown.Deleted Messages " ] ,
}
# Richtigen Ordner finden - SEHR flexibles Matching!
if self . current_folder in folder_variants :
for variant in folder_variants [ self . current_folder ] :
# Prüfe ob Ordner existiert (auch Teilstring-Match)
for available in available_folders :
# SEHR flexibles Matching mit mehreren Strategien
variant_clean = variant . lower ( ) . replace ( " " , " " ) . replace ( " _ " , " " ) . replace ( " - " , " " )
available_clean = available . lower ( ) . replace ( " " , " " ) . replace ( " _ " , " " ) . replace ( " - " , " " )
match_found = False
# Strategie 1: Exakte Teilstrings (mit Original-Strings)
if variant . lower ( ) in available . lower ( ) or available . lower ( ) in variant . lower ( ) :
match_found = True
print ( f " DEBUG: Match Strategie 1 (Teilstring): ' { variant } ' <-> ' { available } ' " )
# Strategie 2: Ohne Leerzeichen/Unterstriche
elif variant_clean in available_clean or available_clean in variant_clean :
match_found = True
print ( f " DEBUG: Match Strategie 2 (bereinigt): ' { variant } ' <-> ' { available } ' " )
# Strategie 3: Schlüsselwörter (für "Sent Messages" etc.)
elif self . current_folder == " sent " and ( " sent " in available . lower ( ) or " gesendet " in available . lower ( ) ) :
match_found = True
print ( f " DEBUG: Match Strategie 3 (Schlüsselwort sent): ' { available } ' " )
elif self . current_folder == " spam " and ( " spam " in available . lower ( ) or " junk " in available . lower ( ) ) :
match_found = True
print ( f " DEBUG: Match Strategie 3 (Schlüsselwort spam): ' { available } ' " )
elif self . current_folder == " trash " and ( " deleted " in available . lower ( ) or " trash " in available . lower ( ) or " papierkorb " in available . lower ( ) ) :
match_found = True
print ( f " DEBUG: Match Strategie 3 (Schlüsselwort trash): ' { available } ' " )
if match_found :
try :
# Versuche readonly select zum Testen
# WICHTIG: Ordner mit Leerzeichen/Punkten in Anführungszeichen!
if " " in available or " . " in available :
status_test = mail . select ( f ' " { available } " ' , readonly = True )
else :
status_test = mail . select ( available , readonly = True )
if status_test [ 0 ] == " OK " :
target_folder = available
folder_found = True
# WICHTIG: Speichere Mapping für späteren Zugriff!
self . folder_mapping [ self . current_folder ] = available
print ( f " DEBUG: ✓ Ordner erfolgreich gefunden: { available } für { self . current_folder } " )
break
except Exception as e :
print ( f " DEBUG: Select fehlgeschlagen für { available } : { e } " )
continue
if folder_found :
break
except Exception as e :
print ( f " DEBUG: Fehler beim Ordner-Listing: { e } " )
pass
# FALLBACK für Posteingang: Wenn INBOX nicht existiert, nimm den ersten Ordner
if not folder_found and self . current_folder == " inbox " and ' available_folders ' in locals ( ) and available_folders :
target_folder = available_folders [ 0 ]
folder_found = True
print ( f " DEBUG: FALLBACK - Verwende ersten verfügbaren Ordner als Posteingang: { target_folder } " )
self . root . after ( 0 , lambda tf = target_folder :
self . status_var . set ( f " ⚠ Standard-INBOX nicht gefunden, verwende: { tf } " ) )
# Wenn immer noch nicht gefunden, versuche Ordner zu erstellen (für Spam/Trash)
if not folder_found or target_folder is None :
# Versuche Spam/Trash-Ordner zu erstellen (mit INBOX. Präfix!)
if self . current_folder in [ " spam " , " trash " , " drafts " ] :
folder_names = { " spam " : " INBOX.Spam " , " trash " : " INBOX.Trash " , " drafts " : " INBOX.Drafts " }
new_folder_name = folder_names [ self . current_folder ]
try :
print ( f " DEBUG: Versuche Ordner ' { new_folder_name } ' zu erstellen... " )
mail . create ( new_folder_name )
target_folder = new_folder_name
folder_found = True
self . folder_mapping [ self . current_folder ] = new_folder_name
self . root . after ( 0 , lambda : self . status_var . set ( f " ✓ Ordner ' { new_folder_name } ' wurde erstellt " ) )
print ( f " DEBUG: ✓ Ordner ' { new_folder_name } ' erfolgreich erstellt! " )
except Exception as create_error :
print ( f " DEBUG: Konnte Ordner nicht erstellen: { create_error } " )
# Wenn immer noch nicht gefunden/erstellt, zeige Fehler
if not folder_found or target_folder is None :
error_msg = f " Ordner ' { folder_display } ' nicht gefunden auf Server. Verfügbar: { ' , ' . join ( available_folders [ : 5 ] ) if ' available_folders ' in locals ( ) else ' unbekannt ' } "
self . root . after ( 0 , lambda msg = error_msg : self . status_var . set ( msg ) )
self . root . after ( 0 , lambda msg = error_msg : messagebox . showwarning ( " Ordner nicht gefunden " , msg ) )
mail . logout ( )
return
# Ordner auswählen (jetzt mit Sicherheit der richtige!)
try :
print ( f " DEBUG: Wähle Ordner aus: { target_folder } " )
# WICHTIG: Ordner mit Punkten/Leerzeichen in Anführungszeichen setzen!
if " " in target_folder or " . " in target_folder :
status_select = mail . select ( f ' " { target_folder } " ' )
else :
status_select = mail . select ( target_folder )
if status_select [ 0 ] != " OK " :
error_msg = f " Konnte Ordner ' { target_folder } ' nicht öffnen (Status: { status_select } ) "
self . root . after ( 0 , lambda msg = error_msg : self . status_var . set ( msg ) )
self . root . after ( 0 , lambda msg = error_msg : messagebox . showerror ( " Fehler " , msg ) )
mail . logout ( )
return
else :
# Erfolg - zeige welcher Ordner geladen wird
self . root . after ( 0 , lambda tf = target_folder , fd = folder_display :
self . status_var . set ( f " ✓ Lade { fd } (IMAP: { tf } ) " ) )
except Exception as e :
error_msg = f " Fehler beim Öffnen von ' { target_folder } ' : { str ( e ) } "
self . root . after ( 0 , lambda msg = error_msg : self . status_var . set ( msg ) )
self . root . after ( 0 , lambda msg = error_msg : messagebox . showerror ( " Fehler " , msg ) )
mail . logout ( )
return
# Nur die letzten 20 E-Mails abrufen (für schnelleres Laden)
print ( f " DEBUG: Suche E-Mails... " )
self . root . after ( 0 , lambda : self . status_var . set ( " 🔍 Suche E-Mails... " ) )
status , messages = mail . search ( None , " ALL " )
if status != " OK " :
self . root . after ( 0 , lambda : self . status_var . set ( f " Keine E-Mails in { folder_display } . " ) )
self . emails = [ ]
self . root . after ( 0 , self . _refresh_email_list )
mail . close ( )
mail . logout ( )
return
email_ids = messages [ 0 ] . split ( )
total_count = len ( email_ids )
# WICHTIG: Zeige auch wenn 0 E-Mails gefunden wurden!
if total_count == 0 :
print ( f " DEBUG: Keine E-Mails in Ordner ' { target_folder } ' gefunden! " )
self . root . after ( 0 , lambda fd = folder_display :
self . status_var . set ( f " ✓ { fd } ist leer (keine E-Mails) " ) )
self . emails = [ ]
self . root . after ( 0 , self . _refresh_email_list )
mail . close ( )
mail . logout ( )
return
# WICHTIG: Die neuesten 50 E-Mails (nicht umkehren!)
# E-Mail-IDs sind aufsteigend (1, 2, 3, ...), die höchsten sind die neuesten
email_ids = email_ids [ - 50 : ] # Letzte 50 = neueste 50
print ( f " DEBUG: Gefunden { total_count } E-Mails, lade die letzten { len ( email_ids ) } (IDs: { email_ids [ 0 ] } ... { email_ids [ - 1 ] } )... " )
self . root . after ( 0 , lambda tc = total_count , lc = len ( email_ids ) :
self . status_var . set ( f " 📥 Lade { lc } von { tc } E-Mails... " ) )
fetched_emails = [ ]
print ( f " DEBUG: Beginne E-Mail-Parsing für { len ( email_ids ) } E-Mails... " )
# NICHT reversed! Die Liste ist schon richtig sortiert (älteste zuerst)
# Wir laden von alt nach neu, aber zeigen dann reversed in der Liste
for idx , email_id in enumerate ( email_ids , 1 ) :
try :
print ( f " DEBUG: Lade E-Mail { idx } / { len ( email_ids ) } (ID: { email_id } )... " )
# Live-Status-Update JEDE E-Mail (damit User sieht, dass es lädt)
self . root . after ( 0 , lambda i = idx , total = len ( email_ids ) :
self . status_var . set ( f " 📥 Lade E-Mail { i } / { total } ... " ) )
# Volle E-Mail laden (für korrekte Vorschau)
status , msg_data = mail . fetch ( email_id , " (RFC822) " )
if status != " OK " :
print ( f " DEBUG: FEHLER - Fetch fehlgeschlagen für E-Mail { email_id } : Status= { status } " )
continue
for response_part in msg_data :
if isinstance ( response_part , tuple ) :
msg = email . message_from_bytes ( response_part [ 1 ] )
# Betreff dekodieren
subject = " "
if msg [ " Subject " ] :
decoded = decode_header ( msg [ " Subject " ] )
subject = " "
for part , encoding in decoded :
if isinstance ( part , bytes ) :
subject + = part . decode ( encoding or " utf-8 " , errors = " ignore " )
else :
subject + = str ( part )
# Absender dekodieren
from_addr = msg . get ( " From " , " Unbekannt " )
if from_addr :
decoded = decode_header ( from_addr )
from_addr = " "
for part , encoding in decoded :
if isinstance ( part , bytes ) :
from_addr + = part . decode ( encoding or " utf-8 " , errors = " ignore " )
else :
from_addr + = str ( part )
# Datum
date_str = msg . get ( " Date " , " " )
# Body extrahieren (RICHTIG decoded mit HTML-Unterstützung!)
body = " "
html_body = " "
attachments = [ ] # Liste der Anhänge
if msg . is_multipart ( ) :
for part in msg . walk ( ) :
content_type = part . get_content_type ( )
content_disposition = part . get ( " Content-Disposition " , " " )
# Text-Teil extrahieren (Plain Text)
if content_type == " text/plain " and " attachment " not in content_disposition :
try :
payload = part . get_payload ( decode = True )
if payload :
# Encoding erkennen
charset = part . get_content_charset ( ) or ' utf-8 '
body = payload . decode ( charset , errors = " ignore " )
except :
pass
# HTML-Teil extrahieren
elif content_type == " text/html " and " attachment " not in content_disposition :
try :
payload = part . get_payload ( decode = True )
if payload :
# Encoding erkennen
charset = part . get_content_charset ( ) or ' utf-8 '
html_body = payload . decode ( charset , errors = " ignore " )
except :
pass
# Anhänge sammeln (inkl. inline images)
elif " attachment " in content_disposition or part . get_filename ( ) :
filename = part . get_filename ( )
if filename :
# Dateinamen dekodieren
decoded_filename = decode_header ( filename )
filename_str = " "
for part_text , encoding in decoded_filename :
if isinstance ( part_text , bytes ) :
filename_str + = part_text . decode ( encoding or " utf-8 " , errors = " ignore " )
else :
filename_str + = str ( part_text )
# Größe ermitteln (ungefähr)
payload = part . get_payload ( decode = True )
size = len ( payload ) if payload else 0
size_str = f " { size / 1024 : .1f } KB " if size < 1024 * 1024 else f " { size / ( 1024 * 1024 ) : .1f } MB "
# Prüfe ob es ein Bild ist
is_image = content_type . startswith ( " image/ " )
attachments . append ( {
" filename " : filename_str ,
" size " : size_str ,
" content_type " : content_type ,
" payload " : payload ,
" is_image " : is_image
} )
else :
# Nicht-multipart E-Mail
content_type = msg . get_content_type ( )
try :
payload = msg . get_payload ( decode = True )
if payload :
# Encoding erkennen
charset = msg . get_content_charset ( ) or ' utf-8 '
decoded_text = payload . decode ( charset , errors = " ignore " )
if content_type == " text/html " :
html_body = decoded_text
else :
body = decoded_text
except :
body = msg . get_payload ( )
# HTML zu Text konvertieren (falls kein Plain Text vorhanden)
if not body and html_body :
body = self . _html_to_text ( html_body )
# Vorschau erstellen (erste 150 Zeichen des RICHTIGEN Textes)
preview = body [ : 150 ] . replace ( " \n " , " " ) . replace ( " \r " , " " ) . strip ( )
if len ( body ) > 150 :
preview + = " ... "
if not preview :
preview = " (Keine Vorschau verfügbar) "
fetched_emails . append ( {
" from " : from_addr ,
" subject " : subject or " (Kein Betreff) " ,
" date " : date_str ,
" preview " : preview ,
" body " : body ,
" html_body " : html_body , # HTML-Version speichern
" has_attachments " : len ( attachments ) > 0 ,
" attachments " : attachments ,
" email_id " : email_id . decode ( ) if isinstance ( email_id , bytes ) else str ( email_id )
} )
# E-Mail-Adresse zu Kontakten hinzufügen
if " @ " in from_addr :
# Extrahiere E-Mail aus "Name <email@domain.com>" Format
if " < " in from_addr and " > " in from_addr :
email_only = from_addr . split ( " < " ) [ 1 ] . split ( " > " ) [ 0 ] . strip ( )
else :
email_only = from_addr . strip ( )
self . _add_contact ( email_only )
print ( f " DEBUG: ✓ E-Mail erfolgreich geparst: ID= { email_id } , Datum= ' { date_str [ : 20 ] } ' , Von= ' { from_addr [ : 30 ] } ' , Betreff= ' { subject [ : 30 ] } ' " )
except Exception as e :
print ( f " DEBUG: FEHLER beim Laden der E-Mail { email_id } : { e } " )
import traceback
traceback . print_exc ( )
continue
print ( f " DEBUG: E-Mail-Parsing abgeschlossen. { len ( fetched_emails ) } E-Mails erfolgreich geladen. " )
# Zeige erste und letzte E-Mail VOR der Sortierung
if fetched_emails :
print ( f " DEBUG: VOR Sortierung - Erste E-Mail: Datum= { fetched_emails [ 0 ] [ ' date ' ] [ : 20 ] } , ID= { fetched_emails [ 0 ] [ ' email_id ' ] } " )
print ( f " DEBUG: VOR Sortierung - Letzte E-Mail: Datum= { fetched_emails [ - 1 ] [ ' date ' ] [ : 20 ] } , ID= { fetched_emails [ - 1 ] [ ' email_id ' ] } " )
mail . close ( )
mail . logout ( )
# WICHTIG: E-Mails nach Datum sortieren (neueste zuerst)!
# Verwende email.utils.parsedate_to_datetime für richtige Datums-Sortierung
from email . utils import parsedate_to_datetime
from datetime import datetime , timezone
def get_email_date ( email_dict ) :
""" Konvertiert E-Mail-Datum zu datetime für Sortierung. """
try :
date_str = email_dict . get ( ' date ' , ' ' )
if date_str :
dt = parsedate_to_datetime ( date_str )
# WICHTIG: Konvertiere zu naive datetime (ohne Timezone), um Vergleichsfehler zu vermeiden
if dt and dt . tzinfo is not None :
# Konvertiere zu UTC und entferne Timezone-Info
dt = dt . replace ( tzinfo = None )
return dt if dt else datetime ( 1970 , 1 , 1 )
except Exception as e :
print ( f " DEBUG: Fehler beim Parsen von Datum ' { date_str [ : 30 ] if date_str else ' leer ' } ' : { e } " )
# Fallback: sehr altes Datum
return datetime ( 1970 , 1 , 1 )
# Sortiere nach Datum, neueste zuerst (reverse=True)
try :
fetched_emails . sort ( key = get_email_date , reverse = True )
print ( f " DEBUG: E-Mails nach Datum sortiert - neueste zuerst " )
except Exception as sort_error :
print ( f " DEBUG: FEHLER beim Sortieren: { sort_error } " )
import traceback
traceback . print_exc ( )
# Fallback: Keine Sortierung, verwende wie geladen
pass
# Zeige erste und letzte E-Mail NACH der Sortierung
if fetched_emails :
print ( f " DEBUG: NACH Sortierung - Erste E-Mail: Datum= { fetched_emails [ 0 ] [ ' date ' ] [ : 20 ] } , ID= { fetched_emails [ 0 ] [ ' email_id ' ] } " )
print ( f " DEBUG: NACH Sortierung - Letzte E-Mail: Datum= { fetched_emails [ - 1 ] [ ' date ' ] [ : 20 ] } , ID= { fetched_emails [ - 1 ] [ ' email_id ' ] } " )
self . emails = fetched_emails
print ( f " DEBUG: self.emails gesetzt mit { len ( fetched_emails ) } E-Mails " )
# Cache speichern
import time
cache_key = self . current_folder
self . email_cache [ cache_key ] = fetched_emails
self . cache_timestamp [ cache_key ] = time . time ( )
print ( f " DEBUG: Rufe _refresh_email_list auf... " )
self . root . after ( 0 , self . _refresh_email_list )
count_msg = f " ✓ { len ( fetched_emails ) } E-Mails geladen (von { total_count } im Ordner ' { folder_display } ' ) "
self . root . after ( 0 , lambda : self . status_var . set ( count_msg ) )
print ( f " DEBUG: { count_msg } " )
except Exception as e :
error_msg = str ( e )
self . root . after ( 0 , lambda : self . status_var . set ( f " Fehler beim Laden: { error_msg } " ) )
self . root . after ( 0 , lambda : messagebox . showerror ( " IMAP Fehler " , f " Konnte E-Mails nicht laden: \n { error_msg } " ) )
threading . Thread ( target = worker , daemon = True ) . start ( )
def _manage_accounts ( self ) :
""" Öffnet Konto-Verwaltung. """
acc_win = tk . Toplevel ( self . root )
acc_win . title ( " E-Mail-Konten verwalten " )
acc_win . geometry ( " 600x500 " )
acc_win . configure ( bg = " #FFFFFF " )
acc_win . transient ( self . root )
main_f = tk . Frame ( acc_win , bg = " #FFFFFF " , padx = 20 , pady = 20 )
main_f . pack ( fill = " both " , expand = True )
tk . Label ( main_f , text = " E-Mail-Konten " , bg = " #FFFFFF " , fg = self . fg_dark ,
font = ( " Segoe UI " , 14 , " bold " ) ) . pack ( anchor = " w " , pady = ( 0 , 10 ) )
# Liste der Konten
list_frame = tk . Frame ( main_f , bg = " #FFFFFF " )
list_frame . pack ( fill = " both " , expand = True )
scrollbar = tk . Scrollbar ( list_frame )
scrollbar . pack ( side = " right " , fill = " y " )
acc_listbox = tk . Listbox (
list_frame , font = ( " Segoe UI " , 10 ) ,
relief = " solid " , bd = 1 , yscrollcommand = scrollbar . set
)
acc_listbox . pack ( side = " left " , fill = " both " , expand = True )
scrollbar . config ( command = acc_listbox . yview )
def refresh_acc_list ( ) :
acc_listbox . delete ( 0 , " end " )
for acc in self . accounts :
acc_listbox . insert ( " end " , f " { acc . get ( ' name ' , ' Unbenannt ' ) } ( { acc . get ( ' email ' , ' ' ) } ) " )
refresh_acc_list ( )
# Buttons
btn_f = tk . Frame ( main_f , bg = " #FFFFFF " )
btn_f . pack ( fill = " x " , pady = ( 10 , 0 ) )
def add_account ( ) :
self . _add_account_dialog ( acc_win , refresh_acc_list )
def edit_account ( ) :
sel = acc_listbox . curselection ( )
if not sel :
messagebox . showinfo ( " Hinweis " , " Bitte Konto auswählen. " , parent = acc_win )
return
idx = sel [ 0 ]
if idx < len ( self . accounts ) :
self . _edit_account_dialog ( acc_win , idx , refresh_acc_list )
def remove_account ( ) :
sel = acc_listbox . curselection ( )
if not sel :
messagebox . showinfo ( " Hinweis " , " Bitte Konto auswählen. " , parent = acc_win )
return
idx = sel [ 0 ]
if idx < len ( self . accounts ) :
if messagebox . askyesno ( " Löschen " , f " Konto wirklich löschen? " , parent = acc_win ) :
del self . accounts [ idx ]
if self . current_account == self . accounts [ idx ] if idx < len ( self . accounts ) else None :
self . current_account = self . accounts [ 0 ] if self . accounts else None
refresh_acc_list ( )
self . _save_config ( )
tk . Button ( btn_f , text = " ➕ Neues Konto" , command = add_account ,
bg = self . accent_blue , fg = self . fg_light , relief = " flat " ,
padx = 15 , pady = 8 , cursor = " hand2 " ) . pack ( side = " left " , padx = 5 )
tk . Button ( btn_f , text = " ✏ Bearbeiten " , command = edit_account ,
bg = " #27AE60 " , fg = self . fg_light , relief = " flat " ,
padx = 15 , pady = 8 , cursor = " hand2 " ) . pack ( side = " left " , padx = 5 )
tk . Button ( btn_f , text = " 🗑 Konto löschen " , command = remove_account ,
bg = " #E74C3C " , fg = self . fg_light , relief = " flat " ,
padx = 15 , pady = 8 , cursor = " hand2 " ) . pack ( side = " left " , padx = 5 )
def _add_account_dialog ( self , parent , callback , edit_mode = False , account_data = None , account_idx = None ) :
""" Dialog zum Hinzufügen oder Bearbeiten eines E-Mail-Kontos. """
dialog = tk . Toplevel ( parent )
dialog . title ( " E-Mail-Konto bearbeiten " if edit_mode else " Neues E-Mail-Konto " )
dialog . geometry ( " 550x680 " )
dialog . configure ( bg = " #FFFFFF " )
dialog . transient ( parent )
form = tk . Frame ( dialog , bg = " #FFFFFF " , padx = 20 , pady = 20 )
form . pack ( fill = " both " , expand = True )
title_text = " E-Mail-Konto bearbeiten " if edit_mode else " E-Mail-Konto einrichten "
tk . Label ( form , text = title_text , bg = " #FFFFFF " ,
font = ( " Segoe UI " , 12 , " bold " ) ) . grid ( row = 0 , column = 0 , columnspan = 2 , sticky = " w " , pady = ( 0 , 15 ) )
# Formular-Felder
fields = [
( " Kontoname: " , " name " , " z.B. Praxis Lindengut " ) ,
( " E-Mail-Adresse: " , " email " , " praxis@lindengut.ch " ) ,
( " Passwort: " , " password " , " " ) ,
( " IMAP-Server: " , " imap_server " , " imap.gmail.com " ) ,
( " IMAP-Port: " , " imap_port " , " 993 " ) ,
( " SMTP-Server: " , " smtp_server " , " smtp.gmail.com " ) ,
( " SMTP-Port: " , " smtp_port " , " 587 " ) ,
]
entries = { }
row = 1
for label_text , key , placeholder in fields :
tk . Label ( form , text = label_text , bg = " #FFFFFF " , fg = self . fg_dark ,
font = ( " Segoe UI " , 10 ) ) . grid ( row = row , column = 0 , sticky = " w " , pady = 5 )
entry = tk . Entry ( form , font = ( " Segoe UI " , 10 ) , relief = " solid " , bd = 1 )
if key == " password " :
entry . configure ( show = " * " )
# Bei Bearbeitung: Werte aus bestehendem Konto laden
if edit_mode and account_data :
value = account_data . get ( key , " " )
if value :
entry . insert ( 0 , str ( value ) )
else :
entry . insert ( 0 , placeholder )
entry . grid ( row = row , column = 1 , sticky = " ew " , pady = 5 )
entries [ key ] = entry
row + = 1
form . columnconfigure ( 1 , weight = 1 )
# Info-Text
info_text = (
" Häufige Server: \n \n "
" Gmail: imap.gmail.com (993), smtp.gmail.com (587) \n "
" Outlook: outlook.office365.com (993/587) \n "
" Yahoo: imap.mail.yahoo.com (993/587) \n \n "
" Hinweis: Gmail benötigt ein App-Passwort! "
)
tk . Label ( form , text = info_text , bg = " #F8F9FA " , fg = " #7F8C8D " ,
font = ( " Segoe UI " , 9 ) , justify = " left " , relief = " solid " , bd = 1 ,
padx = 10 , pady = 10 ) . grid ( row = row , column = 0 , columnspan = 2 , sticky = " ew " , pady = ( 10 , 15 ) )
row + = 1
# Test-Button und Status-Label
test_frame = tk . Frame ( form , bg = " #FFFFFF " )
test_frame . grid ( row = row , column = 0 , columnspan = 2 , sticky = " ew " , pady = ( 0 , 15 ) )
test_status_var = tk . StringVar ( value = " " )
test_status_label = tk . Label ( test_frame , textvariable = test_status_var ,
bg = " #FFFFFF " , fg = self . fg_dark ,
font = ( " Segoe UI " , 9 ) , wraplength = 450 , justify = " left " )
test_status_label . pack ( fill = " x " , pady = ( 0 , 10 ) )
def test_connection ( ) :
""" Testet IMAP und SMTP Verbindung. """
test_status_var . set ( " ⏳ Teste Verbindung... " )
dialog . update ( )
test_pw = entries [ " password " ] . get ( ) . strip ( )
account = {
" name " : entries [ " name " ] . get ( ) . strip ( ) ,
" email " : entries [ " email " ] . get ( ) . strip ( ) ,
" imap_server " : entries [ " imap_server " ] . get ( ) . strip ( ) ,
" imap_port " : int ( entries [ " imap_port " ] . get ( ) . strip ( ) or " 993 " ) ,
" smtp_server " : entries [ " smtp_server " ] . get ( ) . strip ( ) ,
" smtp_port " : int ( entries [ " smtp_port " ] . get ( ) . strip ( ) or " 587 " ) ,
}
if not account [ " email " ] or not test_pw :
test_status_var . set ( " Fehler: E-Mail und Passwort sind erforderlich. " )
return
def test_worker ( ) :
results = { " imap " : False , " smtp " : False , " imap_error " : " " , " smtp_error " : " " }
# Test IMAP
try :
mail = imaplib . IMAP4_SSL ( account [ " imap_server " ] , account [ " imap_port " ] )
mail . login ( account [ " email " ] , test_pw )
mail . select ( " INBOX " )
mail . close ( )
mail . logout ( )
results [ " imap " ] = True
except Exception as e :
results [ " imap_error " ] = str ( e )
# Test SMTP
try :
server = smtplib . SMTP ( account [ " smtp_server " ] , account [ " smtp_port " ] )
server . starttls ( )
server . login ( account [ " email " ] , test_pw )
server . quit ( )
results [ " smtp " ] = True
except Exception as e :
results [ " smtp_error " ] = str ( e )
# Ergebnis anzeigen
def show_result ( ) :
if results [ " imap " ] and results [ " smtp " ] :
test_status_var . set ( " ✅ Test erfolgreich! \n ✓ IMAP (Posteingang): Funktioniert \n ✓ SMTP (Postausgang): Funktioniert " )
messagebox . showinfo ( " Test erfolgreich " ,
" Die Verbindung wurde erfolgreich getestet! \n \n "
" ✓ Posteingang (IMAP): OK \n "
" ✓ Postausgang (SMTP): OK \n \n "
" Sie können das Konto jetzt speichern. " ,
parent = dialog )
else :
error_msg = " ❌ Test fehlgeschlagen: \n \n "
if not results [ " imap " ] :
error_msg + = f " ✗ IMAP (Posteingang): { results [ ' imap_error ' ] [ : 100 ] } \n \n "
else :
error_msg + = " ✓ IMAP (Posteingang): OK \n \n "
if not results [ " smtp " ] :
error_msg + = f " ✗ SMTP (Postausgang): { results [ ' smtp_error ' ] [ : 100 ] } \n \n "
else :
error_msg + = " ✓ SMTP (Postausgang): OK \n \n "
test_status_var . set ( error_msg )
# Detaillierte Fehlermeldung
full_error = " Verbindungstest fehlgeschlagen: \n \n "
if not results [ " imap " ] :
full_error + = f " IMAP-Fehler: \n { results [ ' imap_error ' ] } \n \n "
if not results [ " smtp " ] :
full_error + = f " SMTP-Fehler: \n { results [ ' smtp_error ' ] } \n \n "
full_error + = " Bitte überprüfen Sie: \n "
full_error + = " • Server-Adressen und Ports \n "
full_error + = " • E-Mail-Adresse und Passwort \n "
full_error + = " • Bei Gmail: App-Passwort verwenden! "
messagebox . showerror ( " Verbindungstest fehlgeschlagen " , full_error , parent = dialog )
dialog . after ( 0 , show_result )
threading . Thread ( target = test_worker , daemon = True ) . start ( )
tk . Button ( test_frame , text = " 🔍 Verbindung testen " , command = test_connection ,
bg = " #F39C12 " , fg = self . fg_light , relief = " flat " ,
padx = 20 , pady = 8 , cursor = " hand2 " ) . pack ( )
def save_account ( ) :
pw_input = entries [ " password " ] . get ( ) . strip ( )
account = {
" name " : entries [ " name " ] . get ( ) . strip ( ) ,
" email " : entries [ " email " ] . get ( ) . strip ( ) ,
" _password " : pw_input ,
" imap_server " : entries [ " imap_server " ] . get ( ) . strip ( ) ,
" imap_port " : int ( entries [ " imap_port " ] . get ( ) . strip ( ) or " 993 " ) ,
" smtp_server " : entries [ " smtp_server " ] . get ( ) . strip ( ) ,
" smtp_port " : int ( entries [ " smtp_port " ] . get ( ) . strip ( ) or " 587 " ) ,
}
if not account [ " email " ] or not pw_input :
messagebox . showerror ( " Fehler " , " E-Mail und Passwort sind erforderlich. " , parent = dialog )
return
if edit_mode and account_idx is not None :
# Bestehendes Konto aktualisieren
self . accounts [ account_idx ] = account
if self . current_account == account_data :
self . current_account = account
messagebox . showinfo ( " Erfolg " , " Konto wurde aktualisiert! " , parent = parent )
else :
# Neues Konto hinzufügen
self . accounts . append ( account )
if not self . current_account :
self . current_account = account
messagebox . showinfo ( " Erfolg " , " Konto wurde hinzugefügt! " , parent = parent )
self . _save_config ( )
callback ( )
dialog . destroy ( )
if not edit_mode :
self . _fetch_emails ( )
btn_f = tk . Frame ( form , bg = " #FFFFFF " )
btn_f . grid ( row = row + 1 , column = 0 , columnspan = 2 , sticky = " ew " , pady = ( 0 , 0 ) )
save_text = " 💾 Änderungen speichern " if edit_mode else " 💾 Speichern "
tk . Button ( btn_f , text = save_text , command = save_account ,
bg = self . accent_blue , fg = self . fg_light , relief = " flat " ,
padx = 20 , pady = 8 , cursor = " hand2 " ) . pack ( side = " left " , padx = 5 )
tk . Button ( btn_f , text = " Abbrechen " , command = dialog . destroy ,
bg = " #95A5A6 " , fg = self . fg_light , relief = " flat " ,
padx = 20 , pady = 8 , cursor = " hand2 " ) . pack ( side = " left " , padx = 5 )
def _edit_account_dialog ( self , parent , account_idx , callback ) :
""" Dialog zum Bearbeiten eines bestehenden Kontos. """
if account_idx > = len ( self . accounts ) :
return
account_data = self . accounts [ account_idx ]
self . _add_account_dialog ( parent , callback , edit_mode = True ,
account_data = account_data , account_idx = account_idx )
def _save_config ( self ) :
""" Speichert Konfiguration. """
config = load_email_config ( )
config [ " accounts " ] = self . accounts
save_email_config ( config )
def _reply_email ( self ) :
""" Antwortet auf die ausgewählte E-Mail. """
if not self . selected_email :
messagebox . showinfo ( " Hinweis " , " Bitte wählen Sie zuerst eine E-Mail aus. " )
return
# Öffne Compose-Fenster mit vorausgefüllten Daten
compose_win = tk . Toplevel ( self . root )
compose_win . title ( " Antworten " )
compose_win . geometry ( " 700x550 " )
compose_win . configure ( bg = " #FFFFFF " )
# STATUS-ZEILE ZUOBERST (wie im Hauptprogramm)
status_frame = tk . Frame ( compose_win , bg = " #E7F9FD " , height = 40 )
status_frame . pack ( fill = " x " )
status_frame . pack_propagate ( False )
status_var = tk . StringVar ( value = " Bereit zum Diktieren " )
status_label = tk . Label ( status_frame , textvariable = status_var ,
bg = " #E7F9FD " , fg = self . fg_dark ,
font = ( " Segoe UI " , 10 ) )
status_label . pack ( side = " left " , pady = 10 , padx = 20 )
# SCHRIFTGRÖSSEN-SPINBOX (rechts in der Statusleiste)
font_control_frame = tk . Frame ( status_frame , bg = " #E7F9FD " )
font_control_frame . pack ( side = " right " , padx = 20 , pady = 5 )
tk . Label ( font_control_frame , text = " Aa " , bg = " #E7F9FD " , fg = self . fg_dark ,
font = ( " Segoe UI " , 9 ) ) . pack ( side = " left " , padx = ( 0 , 3 ) )
reply_font_size = tk . IntVar ( value = self . font_size_compose )
def on_font_change ( val ) :
""" Ändert die Schriftgröße im Antworten-Fenster. """
try :
size = int ( float ( val ) )
message_text . configure ( font = ( " Segoe UI " , size ) )
except :
pass
font_spinbox = tk . Spinbox ( font_control_frame , from_ = 5 , to = 12 ,
textvariable = reply_font_size ,
width = 3 , bg = " #E7F9FD " , relief = " solid " , bd = 1 ,
command = lambda : on_font_change ( reply_font_size . get ( ) ) ,
font = ( " Segoe UI " , 9 ) )
font_spinbox . pack ( side = " left " )
font_spinbox . bind ( " <KeyRelease> " , lambda e : on_font_change ( reply_font_size . get ( ) ) )
form_frame = tk . Frame ( compose_win , bg = " #FFFFFF " , padx = 20 , pady = 20 )
form_frame . pack ( fill = " both " , expand = True )
# An (vorausgefüllt)
tk . Label ( form_frame , text = " An: " , bg = " #FFFFFF " , fg = self . fg_dark ,
font = ( " Segoe UI " , 10 ) ) . grid ( row = 0 , column = 0 , sticky = " w " , pady = 5 )
# Frame für Entry + Info
to_frame = tk . Frame ( form_frame , bg = " #FFFFFF " )
to_frame . grid ( row = 0 , column = 1 , sticky = " ew " , pady = 5 )
to_entry = tk . Entry ( to_frame , font = ( " Segoe UI " , 10 ) , relief = " solid " , bd = 1 )
to_entry . insert ( 0 , self . selected_email [ ' from ' ] )
to_entry . pack ( fill = " x " )
# Info-Label für mehrere E-Mails
info_label = tk . Label ( to_frame ,
text = " 💡 Mehrere E-Mails mit Komma trennen: max@test.de, anna@test.de " ,
bg = " #FFFFFF " , fg = " #7F8C8D " , font = ( " Segoe UI " , 8 ) , anchor = " w " )
info_label . pack ( fill = " x " , pady = ( 2 , 0 ) )
# E-Mail-Validierung mit blauer Anzeige
def extract_email_address ( text ) :
""" Extrahiert die reine E-Mail-Adresse aus verschiedenen Formaten. """
import re
# Formate: "Name <email@domain.com>", "<email@domain.com>", "email@domain.com"
match = re . search ( r ' <([^>]+)> ' , text )
if match :
return match . group ( 1 ) . strip ( )
return text . strip ( )
def validate_email_field ( event = None ) :
""" Validiert E-Mail-Adressen und zeigt sie blau an, wenn korrekt. """
import re
email_pattern = r ' ^[a-zA-Z0-9._ % +-]+@[a-zA-Z0-9.-]+ \ .[a-zA-Z] { 2,}$ '
current_text = to_entry . get ( ) . strip ( )
# Multiple E-Mails durch Komma/Semikolon getrennt
recipients = [ addr . strip ( ) for addr in re . split ( ' [,;] ' , current_text ) ] if current_text else [ ]
# Extrahiere reine E-Mail-Adressen und validiere
clean_emails = [ ]
all_valid = True
for addr in recipients :
clean_addr = extract_email_address ( addr )
clean_emails . append ( clean_addr )
if not re . match ( email_pattern , clean_addr ) :
all_valid = False
break
if all_valid and recipients :
# BLAU wenn alle E-Mails korrekt
to_entry . configure ( fg = " #2E86C1 " , bg = " #EBF5FB " )
elif recipients :
# ROT wenn mindestens eine ungültig
to_entry . configure ( fg = " #E74C3C " , bg = " #FADBD8 " )
else :
# Normal wenn leer
to_entry . configure ( fg = self . fg_dark , bg = " #FFFFFF " )
# Bei jeder Änderung validieren
to_entry . bind ( " <KeyRelease> " , validate_email_field )
to_entry . bind ( " <FocusOut> " , validate_email_field )
# Initial validieren
validate_email_field ( )
# Betreff (Re: ...)
tk . Label ( form_frame , text = " Betreff: " , bg = " #FFFFFF " , fg = self . fg_dark ,
font = ( " Segoe UI " , 10 ) ) . grid ( row = 1 , column = 0 , sticky = " w " , pady = 5 )
subject_entry = tk . Entry ( form_frame , font = ( " Segoe UI " , 10 ) , relief = " solid " , bd = 1 )
original_subject = self . selected_email [ ' subject ' ]
if not original_subject . startswith ( " Re: " ) :
original_subject = f " Re: { original_subject } "
subject_entry . insert ( 0 , original_subject )
subject_entry . grid ( row = 1 , column = 1 , sticky = " ew " , pady = 5 )
# Nachricht
tk . Label ( form_frame , text = " Nachricht: " , bg = " #FFFFFF " , fg = self . fg_dark ,
font = ( " Segoe UI " , 10 ) ) . grid ( row = 2 , column = 0 , sticky = " nw " , pady = 5 )
text_frame = tk . Frame ( form_frame , bg = " #FFFFFF " )
text_frame . grid ( row = 2 , column = 1 , sticky = " nsew " , pady = 5 )
message_text = scrolledtext . ScrolledText (
text_frame , wrap = " word " , font = ( " Segoe UI " , self . font_size_compose ) ,
relief = " solid " , bd = 1 , height = 15
)
message_text . pack ( fill = " both " , expand = True )
# Unterschrift automatisch einfügen bei Antworten
reply_text = " "
if self . signature_auto_reply and self . signature :
reply_text = " \n \n " + self . signature + " \n \n "
# Zitat der Original-Nachricht
quote = f " --- \n Am { self . selected_email [ ' date ' ] } schrieb { self . selected_email [ ' from ' ] } : \n > " + \
self . selected_email [ ' body ' ] . replace ( " \n " , " \n > " )
message_text . insert ( " 1.0 " , reply_text + quote )
message_text . mark_set ( " insert " , " 1.0 " )
form_frame . columnconfigure ( 1 , weight = 1 )
form_frame . rowconfigure ( 2 , weight = 1 )
self . compose_to_entry = to_entry
self . compose_subject_entry = subject_entry
self . compose_body_text = message_text
# WICHTIG: Widget zur Liste hinzufügen für dynamische Updates
self . compose_text_widgets . append ( message_text )
# Beim Schließen des Fensters aus Liste entfernen
def on_close ( ) :
if message_text in self . compose_text_widgets :
self . compose_text_widgets . remove ( message_text )
compose_win . destroy ( )
# Buttons
btn_frame = tk . Frame ( compose_win , bg = " #FFFFFF " , pady = 10 )
btn_frame . pack ( fill = " x " , padx = 20 )
def insert_signature ( ) :
""" Fügt Unterschrift am Cursor ein. """
if self . signature :
current_pos = message_text . index ( " insert " )
message_text . insert ( current_pos , " \n \n " + self . signature + " \n \n " )
else :
messagebox . showinfo ( " Keine Unterschrift " ,
" Bitte erstellen Sie zuerst eine Unterschrift über den Button ' ✍ Unterschrift ' . " ,
parent = compose_win )
# DIKTAT DIREKT IM FENSTER (wie im Hauptprogramm)
is_recording = [ False ]
timer_sec = [ 0 ]
timer_running = [ False ]
dictation_btn = [ None ]
def update_timer ( ) :
if timer_running [ 0 ] :
timer_sec [ 0 ] + = 1
mins = timer_sec [ 0 ] / / 60
secs = timer_sec [ 0 ] % 60
status_var . set ( f " 🔴 Aufnahme läuft... { mins } : { secs : 02d } " )
compose_win . after ( 1000 , update_timer )
def start_dictation ( ) :
""" Startet Diktat DIREKT im Fenster. """
if is_recording [ 0 ] :
# Stoppen
try :
timer_running [ 0 ] = False
is_recording [ 0 ] = False
status_var . set ( " ⏳ Verarbeite Audio... " )
dictation_btn [ 0 ] . configure ( text = " 🎤 Diktat starten " , bg = " #27AE60 " )
def worker ( ) :
try :
wav_path = self . recorder . stop_and_save_wav ( )
# OpenAI Whisper Transkription
with open ( wav_path , " rb " ) as f :
transcript_obj = self . client . audio . transcriptions . create (
model = " whisper-1 " ,
file = f ,
language = " de "
)
transcribed_text = transcript_obj . text . strip ( )
# Temp-Datei löschen
try :
os . remove ( wav_path )
except :
pass
# Text einfügen
def insert_text ( ) :
if transcribed_text :
current_pos = message_text . index ( " insert " )
message_text . insert ( current_pos , transcribed_text + " " )
status_var . set ( " ✓ Transkription abgeschlossen " )
else :
status_var . set ( " Kein Audio gehört " )
compose_win . after ( 0 , insert_text )
except Exception as e :
def show_error ( ) :
status_var . set ( f " ❌ Fehler: { str ( e ) } " )
compose_win . after ( 0 , show_error )
threading . Thread ( target = worker , daemon = True ) . start ( )
except Exception as e :
status_var . set ( f " ❌ Fehler: { str ( e ) } " )
else :
# Starten
if not self . client :
messagebox . showerror ( " Fehler " , " OpenAI API-Key fehlt! \n \n Bitte .env Datei mit OPENAI_API_KEY erstellen. " , parent = compose_win )
return
try :
self . recorder . start ( )
is_recording [ 0 ] = True
timer_running [ 0 ] = True
timer_sec [ 0 ] = 0
status_var . set ( " 🔴 Aufnahme läuft... 0:00 " )
dictation_btn [ 0 ] . configure ( text = " ⏹ Aufnahme stoppen " , bg = " #E74C3C " )
update_timer ( )
except Exception as e :
status_var . set ( f " ❌ Fehler: { str ( e ) } " )
def format_with_ai ( ) :
""" Formatiert NUR den neuen Text mit KI, behält Zitat unten bei. """
if not self . client :
messagebox . showerror ( " Fehler " , " OpenAI API-Key fehlt! \n \n Bitte .env Datei mit OPENAI_API_KEY erstellen. " , parent = compose_win )
return
current_text = message_text . get ( " 1.0 " , " end-1c " )
if not current_text . strip ( ) :
messagebox . showinfo ( " Kein Text " , " Bitte geben Sie zuerst Text ein. " , parent = compose_win )
return
# Text am Zitat-Trennzeichen splitten
# Trennzeichen: "---" oder "Am ... schrieb"
separator_found = False
new_text_part = current_text
quoted_part = " "
# Suche nach "---" Trennzeichen
if " \n --- \n " in current_text :
parts = current_text . split ( " \n --- \n " , 1 )
new_text_part = parts [ 0 ]
quoted_part = " \n --- \n " + parts [ 1 ]
separator_found = True
# Alternative: Suche nach "Am ... schrieb"
elif " \n Am " in current_text and " schrieb " in current_text :
import re
match = re . search ( r ' \ n(Am .+ schrieb .+: \ n>) ' , current_text )
if match :
split_pos = match . start ( )
new_text_part = current_text [ : split_pos ]
quoted_part = current_text [ split_pos : ]
separator_found = True
if not new_text_part . strip ( ) :
messagebox . showinfo ( " Kein neuer Text " , " Bitte schreiben Sie zuerst Ihre Antwort. " , parent = compose_win )
return
status_var . set ( " ⏳ KI formatiert Ihren Text... " )
def worker ( ) :
try :
prompt = """ Du bist ein Assistent, der diktierten E-Mail-Text professionell formatiert.
WICHTIGE REGELN :
1. Erkenne gesprochene Befehle und setze sie um ( schreibe sie NICHT aus ) :
- " neue Zeile " oder " Neue Zeile " → füge einen Zeilenumbruch ein
- " neuer Absatz " oder " Neuer Absatz " → füge zwei Zeilenumbrüche ein ( Leerzeile )
- " erstens " , " zweitens " , " drittens " → ersetze durch " 1. " , " 2. " , " 3. "
- " Punkt eins " , " Punkt zwei " → ersetze durch " 1. " , " 2. "
2. Formatiere den Text wie einen professionellen E - Mail - Brief :
- Korrekte Absätze
- Sinnvolle Zeilenumbrüche
- Aufzählungen mit Nummern ( 1. , 2. , 3. )
- Korrekte Groß - / Kleinschreibung
- Entferne übermäßige Leerzeichen
3. Ändere NICHT den Inhalt oder die Bedeutung !
4. Behalte den Ton und Stil bei ( formell / informell )
5. Gib NUR den formatierten Text zurück , KEINE Erklärungen !
JETZT FORMATIERE NUR DIESEN TEXT ( NICHT das Zitat darunter ! ) :
"""
response = self . client . chat . completions . create (
model = " gpt-4o " ,
messages = [
{ " role " : " system " , " content " : " Du bist ein Experte für E-Mail-Formatierung. Formatiere NUR den neuen Text, nicht das Zitat. " } ,
{ " role " : " user " , " content " : prompt + new_text_part }
] ,
temperature = 0.3 ,
max_tokens = 1500
)
formatted_text = response . choices [ 0 ] . message . content . strip ( )
# Füge Zitat wieder hinzu (falls vorhanden)
final_text = formatted_text + quoted_part
def update_text ( ) :
message_text . delete ( " 1.0 " , " end " )
message_text . insert ( " 1.0 " , final_text )
status_var . set ( " ✓ Ihr Text wurde formatiert (Zitat erhalten) " )
compose_win . after ( 0 , update_text )
except Exception as e :
def show_error ( ) :
status_var . set ( f " ❌ Fehler: { str ( e ) } " )
messagebox . showerror ( " Fehler " , f " Formatierung fehlgeschlagen: \n { str ( e ) } " , parent = compose_win )
compose_win . after ( 0 , show_error )
threading . Thread ( target = worker , daemon = True ) . start ( )
def insert_image ( ) :
""" Fügt ein Bild als Platzhalter in den Text ein. """
from tkinter import filedialog
# Datei-Dialog für Bildauswahl
file_path = filedialog . askopenfilename (
parent = compose_win ,
title = " Bild auswählen " ,
filetypes = [
( " Bilddateien " , " *.png *.jpg *.jpeg *.gif *.bmp " ) ,
( " Alle Dateien " , " *.* " )
]
)
if file_path :
# Füge Platzhalter für Bild ein (E-Mail wird später mit Attachment gesendet)
import os
filename = os . path . basename ( file_path )
current_pos = message_text . index ( " insert " )
message_text . insert ( current_pos , f " \n [📷 BILD: { filename } ] \n " )
status_var . set ( f " ✓ Bild eingefügt: { filename } " )
def copy_text ( ) :
""" Kopiert markierten Text in die Zwischenablage. """
try :
selected_text = message_text . get ( " sel.first " , " sel.last " )
compose_win . clipboard_clear ( )
compose_win . clipboard_append ( selected_text )
status_var . set ( " ✓ Text kopiert " )
except tk . TclError :
# Nichts markiert
status_var . set ( " Kein Text markiert " )
def paste_text ( ) :
""" Fügt Text aus der Zwischenablage ein. """
try :
clipboard_text = compose_win . clipboard_get ( )
current_pos = message_text . index ( " insert " )
message_text . insert ( current_pos , clipboard_text )
status_var . set ( " ✓ Text eingefügt " )
except tk . TclError :
# Zwischenablage leer
status_var . set ( " Zwischenablage leer " )
# Tastenkürzel für Copy/Paste (falls nicht automatisch vorhanden)
message_text . bind ( " <Control-c> " , lambda e : copy_text ( ) )
message_text . bind ( " <Control-v> " , lambda e : paste_text ( ) )
message_text . bind ( " <Control-x> " , lambda e : ( copy_text ( ) , message_text . delete ( " sel.first " , " sel.last " ) if message_text . tag_ranges ( " sel " ) else None ) )
# Rechtsklick-Kontextmenü
context_menu = tk . Menu ( message_text , tearoff = 0 )
context_menu . add_command ( label = " ✂ Ausschneiden (Strg+X) " , command = lambda : ( copy_text ( ) , message_text . delete ( " sel.first " , " sel.last " ) if message_text . tag_ranges ( " sel " ) else None ) )
context_menu . add_command ( label = " 📋 Kopieren (Strg+C) " , command = copy_text )
context_menu . add_command ( label = " 📄 Einfügen (Strg+V) " , command = paste_text )
context_menu . add_separator ( )
context_menu . add_command ( label = " 🗑 Alles löschen " , command = lambda : message_text . delete ( " 1.0 " , " end " ) )
def show_context_menu ( event ) :
try :
context_menu . tk_popup ( event . x_root , event . y_root )
finally :
context_menu . grab_release ( )
message_text . bind ( " <Button-3> " , show_context_menu ) # Rechtsklick
# Buttons (mit Diktat-Button)
dictation_btn [ 0 ] = tk . Button ( btn_frame , text = " 🎤 Diktat starten " , command = start_dictation ,
bg = " #27AE60 " , fg = " #FFFFFF " , font = ( " Segoe UI " , 10 , " bold " ) ,
relief = " flat " , padx = 15 , pady = 5 , cursor = " hand2 " )
dictation_btn [ 0 ] . pack ( side = " left " , padx = ( 0 , 5 ) )
tk . Button ( btn_frame , text = " ✨ KI formatiert " , command = format_with_ai ,
bg = " #9B59B6 " , fg = " #FFFFFF " ,
font = ( " Segoe UI " , 10 , " bold " ) , relief = " flat " , padx = 15 , pady = 5 ,
cursor = " hand2 " ) . pack ( side = " left " , padx = 5 )
tk . Button ( btn_frame , text = " 📷 Bild " , command = insert_image ,
bg = " #E67E22 " , fg = " #FFFFFF " ,
font = ( " Segoe UI " , 10 ) , relief = " flat " , padx = 15 , pady = 5 ,
cursor = " hand2 " ) . pack ( side = " left " , padx = 5 )
tk . Button ( btn_frame , text = " ✍ Unterschrift einfügen " , command = insert_signature ,
bg = " #3498DB " , fg = " #FFFFFF " ,
font = ( " Segoe UI " , 10 ) , relief = " flat " , padx = 15 , pady = 5 ,
cursor = " hand2 " ) . pack ( side = " left " , padx = 5 )
tk . Button ( btn_frame , text = " 📨 Senden " ,
command = lambda : self . _send_reply ( compose_win , to_entry , subject_entry , message_text ) ,
bg = " #2ECC71 " , fg = " #FFFFFF " ,
font = ( " Segoe UI " , 10 , " bold " ) , relief = " flat " , padx = 20 , pady = 5 ,
cursor = " hand2 " ) . pack ( side = " left " , padx = 5 )
tk . Button ( btn_frame , text = " Abbrechen " , command = on_close ,
bg = " #95A5A6 " , fg = " #FFFFFF " ,
font = ( " Segoe UI " , 10 ) , relief = " flat " , padx = 20 , pady = 5 ,
cursor = " hand2 " ) . pack ( side = " left " , padx = 5 )
# Auch beim X-Button das Widget entfernen
compose_win . protocol ( " WM_DELETE_WINDOW " , on_close )
def _send_reply ( self , window , to_entry_widget , subject_entry_widget , message_text_widget ) :
""" Sendet die Antwort-E-Mail (korrigierte Version mit echten Widgets). """
to = to_entry_widget . get ( ) . strip ( )
subject = subject_entry_widget . get ( ) . strip ( )
body = message_text_widget . get ( " 1.0 " , " end-1c " ) . strip ( )
# Validierung
if not to :
messagebox . showwarning ( " Fehlende Daten " , " Bitte geben Sie mindestens einen Empfänger ein. " , parent = window )
return
if not subject :
messagebox . showwarning ( " Fehlende Daten " , " Bitte geben Sie einen Betreff ein. " , parent = window )
return
if not body :
messagebox . showwarning ( " Fehlende Daten " , " Bitte geben Sie eine Nachricht ein. " , parent = window )
return
# E-Mail-Adresse(n) validieren UND extrahieren
import re
email_pattern = r ' ^[a-zA-Z0-9._ % +-]+@[a-zA-Z0-9.-]+ \ .[a-zA-Z] { 2,}$ '
def extract_email_address ( text ) :
""" Extrahiert die reine E-Mail-Adresse aus verschiedenen Formaten. """
# Formate: "Name <email@domain.com>", "<email@domain.com>", "email@domain.com"
match = re . search ( r ' <([^>]+)> ' , text )
if match :
return match . group ( 1 ) . strip ( )
return text . strip ( )
# Multiple E-Mails durch Komma/Semikolon getrennt
raw_recipients = [ addr . strip ( ) for addr in re . split ( ' [,;] ' , to ) ]
recipients = [ extract_email_address ( addr ) for addr in raw_recipients ]
invalid_emails = [ addr for addr in recipients if not re . match ( email_pattern , addr ) ]
if invalid_emails :
messagebox . showerror ( " Ungültige E-Mail-Adresse(n) " ,
f " Folgende E-Mail-Adressen sind ungültig: \n \n " + " \n " . join ( invalid_emails ) +
f " \n \n Bitte korrigieren Sie diese und versuchen Sie es erneut. " ,
parent = window )
return
# Senden im Thread
def worker ( ) :
try :
if not self . current_account :
self . root . after ( 0 , lambda : messagebox . showerror ( " Fehler " , " Kein E-Mail-Konto konfiguriert! " , parent = window ) )
return
# SMTP-Verbindung
smtp_server = self . current_account . get ( " smtp_server " , " " )
smtp_port = int ( self . current_account . get ( " smtp_port " , 587 ) )
email_user = self . current_account . get ( " email " , " " )
email_password = self . current_account . get ( " _password " , " " )
if not all ( [ smtp_server , smtp_port , email_user , email_password ] ) :
self . root . after ( 0 , lambda : messagebox . showerror ( " Fehler " ,
" E-Mail-Konto unvollständig konfiguriert! \n \n Bitte setzen Sie: \n • AZA_EMAIL_PASSWORD_0 (ENV-Variable) \n • SMTP-Server, SMTP-Port, E-Mail-Adresse " ,
parent = window ) )
return
# E-Mail erstellen
from email . mime . text import MIMEText
from email . mime . multipart import MIMEMultipart
msg = MIMEMultipart ( )
msg [ " From " ] = email_user
msg [ " To " ] = " , " . join ( recipients ) # Mehrere Empfänger
msg [ " Subject " ] = subject
msg . attach ( MIMEText ( body , " plain " , " utf-8 " ) )
# SMTP senden
server = smtplib . SMTP ( smtp_server , smtp_port , timeout = 10 )
server . starttls ( )
server . login ( email_user , email_password )
server . send_message ( msg )
server . quit ( )
# In Sent-Ordner speichern
try :
mail = imaplib . IMAP4_SSL (
self . current_account . get ( " imap_server " , " " ) ,
int ( self . current_account . get ( " imap_port " , 993 ) ) ,
timeout = 10
)
mail . login ( email_user , email_password )
sent_folder = self . _find_or_create_sent_folder ( mail )
# E-Mail als RFC822 speichern
import email
mail . append ( sent_folder , ' ' , imaplib . Time2Internaldate ( time . time ( ) ) , msg . as_bytes ( ) )
mail . logout ( )
except Exception as e :
print ( f " DEBUG: Fehler beim Speichern in Sent: { e } " )
# Erfolg
self . root . after ( 0 , lambda : messagebox . showinfo ( " Erfolg " , " E-Mail wurde erfolgreich gesendet! " , parent = window ) )
self . root . after ( 0 , window . destroy )
# Cache löschen
if " sent " in self . email_cache :
del self . email_cache [ " sent " ]
if " sent " in self . cache_timestamp :
del self . cache_timestamp [ " sent " ]
except smtplib . SMTPAuthenticationError :
self . root . after ( 0 , lambda : messagebox . showerror ( " Authentifizierung fehlgeschlagen " ,
" E-Mail-Adresse oder Passwort falsch! \n \n Tipps: \n • Bei Gmail: App-Passwort verwenden \n • Passwort überprüfen \n • SMTP-Einstellungen prüfen " ,
parent = window ) )
except smtplib . SMTPException as e :
self . root . after ( 0 , lambda : messagebox . showerror ( " SMTP-Fehler " ,
f " E-Mail konnte nicht gesendet werden: \n \n { str ( e ) } \n \n Bitte überprüfen Sie: \n • SMTP-Server \n • SMTP-Port \n • Internetverbindung " ,
parent = window ) )
except Exception as e :
self . root . after ( 0 , lambda : messagebox . showerror ( " Fehler " ,
f " Unerwarteter Fehler: \n \n { str ( e ) } \n \n Bitte kontaktieren Sie den Support. " ,
parent = window ) )
threading . Thread ( target = worker , daemon = True ) . start ( )
def _dictate_reply ( self ) :
""" Diktiert eine Antwort-E-Mail. """
if not self . selected_email :
messagebox . showinfo ( " Hinweis " , " Bitte wählen Sie zuerst eine E-Mail aus. " )
return
messagebox . showinfo ( " Diktieren " ,
" Diktat-Funktion: \n \n "
" Diese Funktion ermöglicht es Ihnen, eine Antwort zu diktieren. \n \n "
" Hinweis: Diese Funktion benötigt Integration mit dem \n "
" Hauptprogramm (basis14.py) für Spracherkennung. \n \n "
" Verwenden Sie vorerst ' ↩ Antworten ' und dann das \n "
" Diktat-Fenster aus dem Hauptprogramm. " )
def _ai_reply_yes ( self ) :
""" Generiert eine kurze bejahende KI-Antwort. """
if not self . selected_email :
messagebox . showinfo ( " Hinweis " , " Bitte wählen Sie zuerst eine E-Mail aus. " )
return
messagebox . showinfo ( " KI-Antwort " ,
" KI Ja-Antwort: \n \n "
" Diese Funktion wird eine kurze, höfliche und \n "
" bejahende Antwort generieren. \n \n "
" Beispiel: ' Vielen Dank für Ihre Nachricht. \n "
" Ja, das passt sehr gut. Ich bestätige den Termin. \n \n "
" Mit freundlichen Grüßen ' " )
def _ai_reply_no ( self ) :
""" Generiert eine kurze verneinende KI-Antwort. """
if not self . selected_email :
messagebox . showinfo ( " Hinweis " , " Bitte wählen Sie zuerst eine E-Mail aus. " )
return
messagebox . showinfo ( " KI-Antwort " ,
" KI Nein-Antwort: \n \n "
" Diese Funktion wird eine kurze, höfliche und \n "
" verneinende Antwort generieren. \n \n "
" Beispiel: ' Vielen Dank für Ihre Anfrage. \n "
" Leider ist dies zu diesem Zeitpunkt nicht möglich. \n \n "
" Mit freundlichen Grüßen ' " )
def _ai_summarize ( self ) :
""" KI-Zusammenfassung der ausgewählten E-Mail. """
if not self . selected_email :
messagebox . showinfo ( " Hinweis " , " Bitte wählen Sie zuerst eine E-Mail aus. " )
return
if not self . client :
messagebox . showerror ( " Fehler " , " OpenAI API-Schlüssel nicht gefunden. \n Bitte .env Datei mit OPENAI_API_KEY erstellen. " )
return
email_body = self . selected_email . get ( ' body ' , ' ' )
if not email_body or len ( email_body . strip ( ) ) < 50 :
messagebox . showinfo ( " Hinweis " , " E-Mail ist zu kurz für eine Zusammenfassung. " )
return
# Dialog für Zusammenfassung
summary_win = tk . Toplevel ( self . root )
summary_win . title ( " 📝 KI Zusammenfassung " )
summary_win . geometry ( " 600x400 " )
summary_win . configure ( bg = " #FFFFFF " )
summary_win . transient ( self . root )
tk . Label ( summary_win , text = " KI Zusammenfassung " , bg = " #FFFFFF " , fg = self . fg_dark ,
font = ( " Segoe UI " , 14 , " bold " ) ) . pack ( pady = ( 20 , 10 ) , padx = 20 , anchor = " w " )
status_label = tk . Label ( summary_win , text = " ⏳ Zusammenfassung wird generiert... " ,
bg = " #FFFFFF " , fg = " #666666 " , font = ( " Segoe UI " , 10 ) )
status_label . pack ( pady = 10 )
text_frame = tk . Frame ( summary_win , bg = " #FFFFFF " , padx = 20 , pady = 10 )
text_frame . pack ( fill = " both " , expand = True )
summary_text = scrolledtext . ScrolledText (
text_frame , wrap = " word " , font = ( " Segoe UI " , 10 ) ,
relief = " solid " , bd = 1 , height = 12 , state = " disabled "
)
summary_text . pack ( fill = " both " , expand = True )
def generate ( ) :
try :
prompt = f """ Fasse diese E-Mail kurz und prägnant zusammen. Extrahiere die wichtigsten Punkte:
E - Mail :
Von : { self . selected_email [ ' from ' ] }
Betreff : { self . selected_email [ ' subject ' ] }
Inhalt :
{ email_body }
Erstelle eine kompakte Zusammenfassung mit :
1. Hauptthema ( 1 Satz )
2. Wichtigste Punkte ( 2 - 3 Stichpunkte )
3. Erforderliche Aktion ( falls vorhanden )
Antwort auf Deutsch und medizinisch - professionell . """
response = self . client . chat . completions . create (
model = " gpt-4o-mini " ,
messages = [ { " role " : " user " , " content " : prompt } ] ,
temperature = 0.3 ,
max_tokens = 300
)
summary = response . choices [ 0 ] . message . content . strip ( )
def update_ui ( ) :
status_label . config ( text = " ✅ Zusammenfassung erstellt " )
summary_text . configure ( state = " normal " )
summary_text . delete ( " 1.0 " , " end " )
summary_text . insert ( " 1.0 " , summary )
summary_text . configure ( state = " disabled " )
summary_win . after ( 0 , update_ui )
except Exception as e :
def show_error ( ) :
status_label . config ( text = " ❌ Fehler bei der Generierung " )
messagebox . showerror ( " Fehler " , f " KI-Zusammenfassung fehlgeschlagen: \n { str ( e ) } " , parent = summary_win )
summary_win . after ( 0 , show_error )
threading . Thread ( target = generate , daemon = True ) . start ( )
tk . Button ( summary_win , text = " Schließen " , command = summary_win . destroy ,
bg = " #95A5A6 " , fg = self . fg_light , relief = " flat " ,
padx = 20 , pady = 8 , cursor = " hand2 " ) . pack ( pady = ( 10 , 20 ) )
def _ai_reply_suggestions ( self ) :
""" Zeigt KI-generierte Antwort-Vorschläge. """
if not self . selected_email :
messagebox . showinfo ( " Hinweis " , " Bitte wählen Sie zuerst eine E-Mail aus. " )
return
if not self . client :
messagebox . showerror ( " Fehler " , " OpenAI API-Schlüssel nicht gefunden. \n Bitte .env Datei mit OPENAI_API_KEY erstellen. " )
return
# Dialog für Antwort-Vorschläge
suggestions_win = tk . Toplevel ( self . root )
suggestions_win . title ( " 💡 KI Antwort-Vorschläge " )
suggestions_win . geometry ( " 800x900 " )
suggestions_win . configure ( bg = " #FFFFFF " )
suggestions_win . transient ( self . root )
tk . Label ( suggestions_win , text = " KI Antwort-Vorschläge " , bg = " #FFFFFF " , fg = self . fg_dark ,
font = ( " Segoe UI " , 14 , " bold " ) ) . pack ( pady = ( 20 , 10 ) , padx = 20 , anchor = " w " )
tk . Label ( suggestions_win , text = " Wählen Sie einen Vorschlag oder bearbeiten Sie ihn: " ,
bg = " #FFFFFF " , fg = " #666666 " , font = ( " Segoe UI " , 10 ) ) . pack ( pady = ( 0 , 10 ) , padx = 20 , anchor = " w " )
status_label = tk . Label ( suggestions_win , text = " ⏳ KI generiert Antwort-Vorschläge... " ,
bg = " #FFFFFF " , fg = " #666666 " , font = ( " Segoe UI " , 10 ) )
status_label . pack ( pady = 10 )
# Scrollbarer Container für Vorschläge
canvas = tk . Canvas ( suggestions_win , bg = " #FFFFFF " , highlightthickness = 0 )
scrollbar = tk . Scrollbar ( suggestions_win , orient = " vertical " , command = canvas . yview )
suggestions_frame = tk . Frame ( canvas , bg = " #FFFFFF " )
canvas . configure ( yscrollcommand = scrollbar . set )
scrollbar . pack ( side = " right " , fill = " y " )
canvas . pack ( side = " left " , fill = " both " , expand = True , padx = ( 20 , 0 ) , pady = 10 )
canvas_frame = canvas . create_window ( ( 0 , 0 ) , window = suggestions_frame , anchor = " nw " )
def on_frame_configure ( event ) :
canvas . configure ( scrollregion = canvas . bbox ( " all " ) )
def on_canvas_configure ( event ) :
canvas . itemconfig ( canvas_frame , width = event . width )
suggestions_frame . bind ( " <Configure> " , on_frame_configure )
canvas . bind ( " <Configure> " , on_canvas_configure )
def generate ( ) :
try :
email_body = self . selected_email . get ( ' body ' , ' ' )
prompt = f """ Du bist Arzt in einer Praxis. Generiere 6 verschiedene Antwort-Vorschläge auf diese E-Mail:
Von : { self . selected_email [ ' from ' ] }
Betreff : { self . selected_email [ ' subject ' ] }
Inhalt :
{ email_body }
Erstelle 6 Antworten :
1. KURZ & POSITIV : Sehr kurze , freundliche Zusage ( 1 - 2 Sätze )
2. AUSFÜHRLICH & POSITIV : Detaillierte , positive Antwort mit allen Details ( 4 - 6 Sätze )
3. NEUTRAL : Sachliche , neutrale Antwort ( 3 - 4 Sätze )
4. RÜCKFRAGE : Höfliche Nachfrage zu Details ( 2 - 3 Sätze )
5. HÖFLICHE ABSAGE : Freundliche , aber bestimmte Ablehnung mit Grund ( 3 - 4 Sätze )
6. KURZ & ABLEHNEND : Sehr kurze , höfliche Absage ( 1 - 2 Sätze )
Format für jede Antwort :
[ OPTION X ]
< Antworttext >
Alle auf Deutsch , höflich und professionell . Keine Grußformel am Ende . """
response = self . client . chat . completions . create (
model = " gpt-4o-mini " ,
messages = [ { " role " : " user " , " content " : prompt } ] ,
temperature = 0.7 ,
max_tokens = 1200
)
suggestions_text = response . choices [ 0 ] . message . content . strip ( )
# Parse die Antworten
suggestions = [ ]
parts = suggestions_text . split ( " [OPTION " )
for part in parts [ 1 : ] : # Überspringe ersten leeren Teil
lines = part . strip ( ) . split ( " \n " , 1 )
if len ( lines ) == 2 :
suggestions . append ( lines [ 1 ] . strip ( ) )
# Kategorien für Farben (Pastellfarben mit Opazität)
suggestion_types = [
{ " label " : " ✅ Kurz & Positiv " , " bg " : " #C8E6C9 " , " text_bg " : " #E8F5E9 " } , # Helles Pastellgrün
{ " label " : " ✅ Ausführlich & Positiv " , " bg " : " #B2DFDB " , " text_bg " : " #E0F2F1 " } , # Helles Pastelltürkis
{ " label " : " ⚪ Neutral " , " bg " : " #E0E0E0 " , " text_bg " : " #F5F5F5 " } , # Hellgrau
{ " label " : " ❓ Rückfrage " , " bg " : " #BBDEFB " , " text_bg " : " #E3F2FD " } , # Helles Pastellblau
{ " label " : " ❌ Höfliche Absage " , " bg " : " #FFCCBC " , " text_bg " : " #FBE9E7 " } , # Helles Pastellorange
{ " label " : " ❌ Kurz & Ablehnend " , " bg " : " #FFCDD2 " , " text_bg " : " #FFEBEE " } # Helles Pastellrot
]
def update_ui ( ) :
status_label . config ( text = " ✅ Vorschläge generiert - Klicken Sie auf einen, um ihn zu verwenden: " )
for i , suggestion in enumerate ( suggestions ) :
if i > = len ( suggestion_types ) :
break
suggestion_type = suggestion_types [ i ]
# Frame für jeden Vorschlag mit farbigem Hintergrund
frame = tk . Frame ( suggestions_frame , bg = suggestion_type [ ' bg ' ] , relief = " solid " , bd = 1 )
frame . pack ( fill = " x " , pady = 5 )
# Header mit Kategorie
header_frame = tk . Frame ( frame , bg = suggestion_type [ ' bg ' ] )
header_frame . pack ( fill = " x " )
tk . Label ( header_frame , text = suggestion_type [ ' label ' ] , bg = suggestion_type [ ' bg ' ] ,
fg = " #424242 " , font = ( " Segoe UI " , 10 , " bold " ) ,
anchor = " w " , padx = 10 , pady = 8 ) . pack ( fill = " x " )
# Text-Widget mit hellerem Hintergrund
text_widget = tk . Text ( frame , wrap = " word " , font = ( " Segoe UI " , 10 ) ,
relief = " flat " , bd = 0 , bg = suggestion_type [ ' text_bg ' ] , height = 4 )
text_widget . pack ( fill = " both " , padx = 10 , pady = 5 )
text_widget . insert ( " 1.0 " , suggestion )
# Button zum Verwenden
def use_suggestion ( s = suggestion ) :
self . _use_reply_suggestion ( s )
suggestions_win . destroy ( )
btn_frame = tk . Frame ( frame , bg = suggestion_type [ ' bg ' ] )
btn_frame . pack ( fill = " x " , pady = 8 , padx = 10 )
tk . Button ( btn_frame , text = " 📧 Verwenden " , command = use_suggestion ,
bg = self . accent_blue , fg = self . fg_light , relief = " flat " ,
font = ( " Segoe UI " , 9 ) , padx = 15 , pady = 6 , cursor = " hand2 " ) . pack ( side = " right " )
suggestions_win . after ( 0 , update_ui )
except Exception as e :
def show_error ( ) :
status_label . config ( text = " ❌ Fehler bei der Generierung " )
messagebox . showerror ( " Fehler " , f " KI-Vorschläge fehlgeschlagen: \n { str ( e ) } " , parent = suggestions_win )
suggestions_win . after ( 0 , show_error )
threading . Thread ( target = generate , daemon = True ) . start ( )
def _use_reply_suggestion ( self , suggestion_text ) :
""" Verwendet einen KI-Vorschlag für die Antwort. """
if not self . selected_email :
return
# Öffne Compose-Fenster mit KI-Vorschlag
compose_win = tk . Toplevel ( self . root )
compose_win . title ( " Antworten mit KI-Vorschlag " )
compose_win . geometry ( " 700x500 " )
compose_win . configure ( bg = " #FFFFFF " )
form_frame = tk . Frame ( compose_win , bg = " #FFFFFF " , padx = 20 , pady = 20 )
form_frame . pack ( fill = " both " , expand = True )
# An
tk . Label ( form_frame , text = " An: " , bg = " #FFFFFF " , fg = self . fg_dark ,
font = ( " Segoe UI " , 10 ) ) . grid ( row = 0 , column = 0 , sticky = " w " , pady = 5 )
to_entry = tk . Entry ( form_frame , font = ( " Segoe UI " , 10 ) , relief = " solid " , bd = 1 )
to_entry . insert ( 0 , self . selected_email [ ' from ' ] )
to_entry . grid ( row = 0 , column = 1 , sticky = " ew " , pady = 5 )
# Betreff
tk . Label ( form_frame , text = " Betreff: " , bg = " #FFFFFF " , fg = self . fg_dark ,
font = ( " Segoe UI " , 10 ) ) . grid ( row = 1 , column = 0 , sticky = " w " , pady = 5 )
subject_entry = tk . Entry ( form_frame , font = ( " Segoe UI " , 10 ) , relief = " solid " , bd = 1 )
original_subject = self . selected_email [ ' subject ' ]
if not original_subject . startswith ( " Re: " ) :
original_subject = f " Re: { original_subject } "
subject_entry . insert ( 0 , original_subject )
subject_entry . grid ( row = 1 , column = 1 , sticky = " ew " , pady = 5 )
# Nachricht mit KI-Vorschlag
tk . Label ( form_frame , text = " Nachricht: " , bg = " #FFFFFF " , fg = self . fg_dark ,
font = ( " Segoe UI " , 10 ) ) . grid ( row = 2 , column = 0 , sticky = " nw " , pady = 5 )
text_frame = tk . Frame ( form_frame , bg = " #FFFFFF " )
text_frame . grid ( row = 2 , column = 1 , sticky = " nsew " , pady = 5 )
message_text = scrolledtext . ScrolledText (
text_frame , wrap = " word " , font = ( " Segoe UI " , self . font_size_compose ) ,
relief = " solid " , bd = 1 , height = 15
)
message_text . pack ( fill = " both " , expand = True )
# KI-Vorschlag + automatische Unterschrift bei Antworten
full_text = suggestion_text
if self . signature_auto_reply and self . signature :
full_text + = " \n \n " + self . signature
message_text . insert ( " 1.0 " , full_text )
form_frame . columnconfigure ( 1 , weight = 1 )
form_frame . rowconfigure ( 2 , weight = 1 )
self . compose_to_entry = to_entry
self . compose_subject_entry = subject_entry
self . compose_body_text = message_text
# WICHTIG: Widget zur Liste hinzufügen für dynamische Updates
self . compose_text_widgets . append ( message_text )
# Beim Schließen des Fensters aus Liste entfernen
def on_close ( ) :
if message_text in self . compose_text_widgets :
self . compose_text_widgets . remove ( message_text )
compose_win . destroy ( )
# Buttons
btn_frame = tk . Frame ( compose_win , bg = " #FFFFFF " , pady = 10 )
btn_frame . pack ( fill = " x " , padx = 20 )
def insert_signature ( ) :
""" Fügt Unterschrift am Ende ein. """
if self . signature :
current_text = message_text . get ( " 1.0 " , " end-1c " )
if not current_text . endswith ( self . signature ) :
message_text . insert ( " end " , " \n \n " + self . signature )
else :
messagebox . showinfo ( " Keine Unterschrift " ,
" Bitte erstellen Sie zuerst eine Unterschrift über den Button ' ✍ Unterschrift ' . " ,
parent = compose_win )
tk . Button ( btn_frame , text = " 📨 Senden " , command = lambda : self . _send_email ( compose_win ) ,
bg = self . accent_blue , fg = self . fg_light ,
font = ( " Segoe UI " , 10 ) , relief = " flat " , padx = 20 , pady = 8 ,
cursor = " hand2 " ) . pack ( side = " left " , padx = 5 )
tk . Button ( btn_frame , text = " ✍ Unterschrift einfügen " , command = insert_signature ,
bg = " #27AE60 " , fg = self . fg_light ,
font = ( " Segoe UI " , 10 ) , relief = " flat " , padx = 20 , pady = 8 ,
cursor = " hand2 " ) . pack ( side = " left " , padx = 5 )
tk . Button ( btn_frame , text = " Abbrechen " , command = on_close ,
bg = " #95A5A6 " , fg = self . fg_light ,
font = ( " Segoe UI " , 10 ) , relief = " flat " , padx = 20 , pady = 8 ,
cursor = " hand2 " ) . pack ( side = " left " , padx = 5 )
# Auch beim X-Button das Widget entfernen
compose_win . protocol ( " WM_DELETE_WINDOW " , on_close )
def _email_templates ( self ) :
""" Zeigt E-Mail-Vorlagen mit KI-Anpassung. """
templates_win = tk . Toplevel ( self . root )
templates_win . title ( " 📋 E-Mail-Vorlagen " )
templates_win . geometry ( " 700x600 " )
templates_win . configure ( bg = " #FFFFFF " )
templates_win . transient ( self . root )
tk . Label ( templates_win , text = " E-Mail-Vorlagen " , bg = " #FFFFFF " , fg = self . fg_dark ,
font = ( " Segoe UI " , 14 , " bold " ) ) . pack ( pady = ( 20 , 10 ) , padx = 20 , anchor = " w " )
tk . Label ( templates_win , text = " Wählen Sie eine Vorlage - KI passt sie automatisch an: " ,
bg = " #FFFFFF " , fg = " #666666 " , font = ( " Segoe UI " , 10 ) ) . pack ( pady = ( 0 , 20 ) , padx = 20 , anchor = " w " )
# Vorlagen-Container
templates_frame = tk . Frame ( templates_win , bg = " #FFFFFF " )
templates_frame . pack ( fill = " both " , expand = True , padx = 20 , pady = 10 )
templates = [
{
" name " : " ✅ Terminbestätigung " ,
" type " : " termin_bestaetigung " ,
" description " : " Bestätigt einen Termin mit Datum, Uhrzeit und Vorbereitung "
} ,
{
" name " : " 📋 Befund mitteilen " ,
" type " : " befund " ,
" description " : " Teilt Befundergebnisse höflich mit und bietet Besprechung an "
} ,
{
" name " : " 💊 Rezept-Information " ,
" type " : " rezept " ,
" description " : " Informiert über ausgestelltes Rezept und Abholung "
} ,
{
" name " : " ❌ Terminabsage " ,
" type " : " absage " ,
" description " : " Sagt Termin höflich ab und bietet Alternativen "
} ,
{
" name " : " 📞 Rückruf-Bitte " ,
" type " : " rueckruf " ,
" description " : " Bittet um Rückruf zu bestimmten Zeiten "
} ,
{
" name " : " ℹ Allgemeine Info" ,
" type " : " info " ,
" description " : " Allgemeine Information über Praxisabläufe "
}
]
for template in templates :
frame = tk . Frame ( templates_frame , bg = " #F8F9FA " , relief = " solid " , bd = 1 )
frame . pack ( fill = " x " , pady = 5 )
tk . Label ( frame , text = template [ ' name ' ] , bg = " #F8F9FA " , fg = self . fg_dark ,
font = ( " Segoe UI " , 11 , " bold " ) , anchor = " w " , padx = 15 , pady = 8 ) . pack ( fill = " x " )
tk . Label ( frame , text = template [ ' description ' ] , bg = " #F8F9FA " , fg = " #666666 " ,
font = ( " Segoe UI " , 9 ) , anchor = " w " , padx = 15 , wraplength = 600 ) . pack ( fill = " x " , pady = ( 0 , 8 ) )
def use_template ( t = template ) :
templates_win . destroy ( )
self . _create_email_from_template ( t )
tk . Button ( frame , text = " 📧 Verwenden " , command = use_template ,
bg = self . accent_blue , fg = self . fg_light , relief = " flat " ,
padx = 15 , pady = 6 , cursor = " hand2 " ) . pack ( anchor = " e " , padx = 15 , pady = ( 0 , 10 ) )
tk . Button ( templates_win , text = " Abbrechen " , command = templates_win . destroy ,
bg = " #95A5A6 " , fg = self . fg_light , relief = " flat " ,
padx = 20 , pady = 8 , cursor = " hand2 " ) . pack ( pady = ( 10 , 20 ) )
def _create_email_from_template ( self , template ) :
""" Erstellt eine E-Mail aus Vorlage mit KI-Anpassung. """
if not self . client :
messagebox . showerror ( " Fehler " , " OpenAI API-Schlüssel nicht gefunden. " )
return
# Dialog mit Parametern
params_win = tk . Toplevel ( self . root )
params_win . title ( f " 📋 { template [ ' name ' ] } " )
params_win . geometry ( " 500x400 " )
params_win . configure ( bg = " #FFFFFF " )
params_win . transient ( self . root )
tk . Label ( params_win , text = template [ ' name ' ] , bg = " #FFFFFF " , fg = self . fg_dark ,
font = ( " Segoe UI " , 12 , " bold " ) ) . pack ( pady = ( 20 , 10 ) , padx = 20 , anchor = " w " )
# Empfänger
form_frame = tk . Frame ( params_win , bg = " #FFFFFF " , padx = 20 , pady = 10 )
form_frame . pack ( fill = " both " , expand = True )
tk . Label ( form_frame , text = " Empfänger: " , bg = " #FFFFFF " , fg = self . fg_dark ,
font = ( " Segoe UI " , 10 ) ) . grid ( row = 0 , column = 0 , sticky = " w " , pady = 5 )
recipient_entry = tk . Entry ( form_frame , font = ( " Segoe UI " , 10 ) , relief = " solid " , bd = 1 )
if self . selected_email :
recipient_entry . insert ( 0 , self . selected_email [ ' from ' ] )
recipient_entry . grid ( row = 0 , column = 1 , sticky = " ew " , pady = 5 )
# Details (z.B. Datum, Uhrzeit, etc.)
tk . Label ( form_frame , text = " Details/Kontext: " , bg = " #FFFFFF " , fg = self . fg_dark ,
font = ( " Segoe UI " , 10 ) ) . grid ( row = 1 , column = 0 , sticky = " nw " , pady = 5 )
details_text = scrolledtext . ScrolledText (
form_frame , wrap = " word " , font = ( " Segoe UI " , 10 ) ,
relief = " solid " , bd = 1 , height = 8
)
details_text . grid ( row = 1 , column = 1 , sticky = " nsew " , pady = 5 )
# Platzhalter-Text je nach Vorlagentyp
placeholder = {
" termin_bestaetigung " : " z.B.: Montag, 17.02.2026, 14:00 Uhr \n Bitte nüchtern erscheinen " ,
" befund " : " z.B.: Blutwerte sind gut, Cholesterin leicht erhöht " ,
" rezept " : " z.B.: Antibiotikum, 7 Tage Einnahme " ,
" absage " : " z.B.: Notfall in der Praxis, Alternative: nächste Woche " ,
" rueckruf " : " z.B.: Befundbesprechung, vormittags erreichbar " ,
" info " : " z.B.: Praxis geschlossen am 20.02, Vertretung: Dr. Müller "
} . get ( template [ ' type ' ] , " Zusätzliche Informationen... " )
details_text . insert ( " 1.0 " , placeholder )
form_frame . columnconfigure ( 1 , weight = 1 )
form_frame . rowconfigure ( 1 , weight = 1 )
status_label = tk . Label ( params_win , text = " " , bg = " #FFFFFF " , fg = " #666666 " ,
font = ( " Segoe UI " , 9 ) )
status_label . pack ( pady = 5 )
def generate_from_template ( ) :
recipient = recipient_entry . get ( ) . strip ( )
details = details_text . get ( " 1.0 " , " end-1c " ) . strip ( )
if not recipient :
messagebox . showerror ( " Fehler " , " Bitte Empfänger angeben. " , parent = params_win )
return
status_label . config ( text = " ⏳ KI erstellt E-Mail... " )
params_win . update ( )
def worker ( ) :
try :
context = " "
if self . selected_email :
context = f " \n \n Kontext (vorherige E-Mail): \n { self . selected_email . get ( ' body ' , ' ' ) [ : 500 ] } "
template_prompts = {
" termin_bestaetigung " : f " Erstelle eine professionelle Terminbestätigungs-E-Mail mit: { details } { context } " ,
" befund " : f " Erstelle eine einfühlsame E-Mail, die Befundergebnisse mitteilt: { details } { context } " ,
" rezept " : f " Erstelle eine E-Mail über ein ausgestelltes Rezept: { details } { context } " ,
" absage " : f " Erstelle eine höfliche Terminabsage mit Erklärung und Alternative: { details } { context } " ,
" rueckruf " : f " Erstelle eine höfliche Rückruf-Bitte: { details } { context } " ,
" info " : f " Erstelle eine informative E-Mail: { details } { context } "
}
prompt = f """ Du bist Arzt in der Praxis Lindengut AG. { template_prompts . get ( template [ ' type ' ] , ' ' ) }
Erstelle eine professionelle , höfliche E - Mail auf Deutsch .
- Direkte Anrede ( Sehr geehrte / r . . . )
- Klare , verständliche Sprache
- Freundlicher Ton
- Schließe mit : " Mit freundlichen Grüßen \n Praxis Lindengut AG "
Nur den E - Mail - Text , keine zusätzlichen Erklärungen . """
response = self . client . chat . completions . create (
model = " gpt-4o " ,
messages = [ { " role " : " user " , " content " : prompt } ] ,
temperature = 0.7 ,
max_tokens = 500
)
email_content = response . choices [ 0 ] . message . content . strip ( )
def open_compose ( ) :
params_win . destroy ( )
# Compose-Fenster öffnen
compose_win = tk . Toplevel ( self . root )
compose_win . title ( " Neue E-Mail aus Vorlage " )
compose_win . geometry ( " 700x500 " )
compose_win . configure ( bg = " #FFFFFF " )
form = tk . Frame ( compose_win , bg = " #FFFFFF " , padx = 20 , pady = 20 )
form . pack ( fill = " both " , expand = True )
tk . Label ( form , text = " An: " , bg = " #FFFFFF " , fg = self . fg_dark ,
font = ( " Segoe UI " , 10 ) ) . grid ( row = 0 , column = 0 , sticky = " w " , pady = 5 )
to_e = tk . Entry ( form , font = ( " Segoe UI " , 10 ) , relief = " solid " , bd = 1 )
to_e . insert ( 0 , recipient )
to_e . grid ( row = 0 , column = 1 , sticky = " ew " , pady = 5 )
tk . Label ( form , text = " Betreff: " , bg = " #FFFFFF " , fg = self . fg_dark ,
font = ( " Segoe UI " , 10 ) ) . grid ( row = 1 , column = 0 , sticky = " w " , pady = 5 )
subj_e = tk . Entry ( form , font = ( " Segoe UI " , 10 ) , relief = " solid " , bd = 1 )
subj_e . insert ( 0 , template [ ' name ' ] . replace ( " ✅ " , " " ) . replace ( " 📋 " , " " ) . replace ( " 💊 " , " " ) . replace ( " ❌ " , " " ) . replace ( " 📞 " , " " ) . replace ( " ℹ " , " " ) . strip ( ) )
subj_e . grid ( row = 1 , column = 1 , sticky = " ew " , pady = 5 )
tk . Label ( form , text = " Nachricht: " , bg = " #FFFFFF " , fg = self . fg_dark ,
font = ( " Segoe UI " , 10 ) ) . grid ( row = 2 , column = 0 , sticky = " nw " , pady = 5 )
text_f = tk . Frame ( form , bg = " #FFFFFF " )
text_f . grid ( row = 2 , column = 1 , sticky = " nsew " , pady = 5 )
msg_text = scrolledtext . ScrolledText (
text_f , wrap = " word " , font = ( " Segoe UI " , 10 ) ,
relief = " solid " , bd = 1 , height = 15
)
msg_text . pack ( fill = " both " , expand = True )
msg_text . insert ( " 1.0 " , email_content )
form . columnconfigure ( 1 , weight = 1 )
form . rowconfigure ( 2 , weight = 1 )
self . compose_to_entry = to_e
self . compose_subject_entry = subj_e
self . compose_body_text = msg_text
btn_f = tk . Frame ( compose_win , bg = " #FFFFFF " , pady = 10 )
btn_f . pack ( fill = " x " , padx = 20 )
tk . Button ( btn_f , text = " 📨 Senden " , command = lambda : self . _send_email ( compose_win ) ,
bg = self . accent_blue , fg = self . fg_light ,
font = ( " Segoe UI " , 10 ) , relief = " flat " , padx = 20 , pady = 8 ,
cursor = " hand2 " ) . pack ( side = " left " , padx = 5 )
tk . Button ( btn_f , text = " Abbrechen " , command = compose_win . destroy ,
bg = " #95A5A6 " , fg = self . fg_light ,
font = ( " Segoe UI " , 10 ) , relief = " flat " , padx = 20 , pady = 8 ,
cursor = " hand2 " ) . pack ( side = " left " , padx = 5 )
params_win . after ( 0 , open_compose )
except Exception as e :
def show_error ( ) :
status_label . config ( text = " ❌ Fehler " )
messagebox . showerror ( " Fehler " , f " KI-Vorlage fehlgeschlagen: \n { str ( e ) } " , parent = params_win )
params_win . after ( 0 , show_error )
threading . Thread ( target = worker , daemon = True ) . start ( )
btn_frame = tk . Frame ( params_win , bg = " #FFFFFF " , pady = 10 )
btn_frame . pack ( fill = " x " , padx = 20 )
tk . Button ( btn_frame , text = " 🤖 KI-E-Mail generieren " , command = generate_from_template ,
bg = self . accent_blue , fg = self . fg_light , relief = " flat " ,
padx = 20 , pady = 10 , cursor = " hand2 " , font = ( " Segoe UI " , 10 , " bold " ) ) . pack ( pady = 10 )
tk . Button ( btn_frame , text = " Abbrechen " , command = params_win . destroy ,
bg = " #95A5A6 " , fg = self . fg_light , relief = " flat " ,
padx = 20 , pady = 8 , cursor = " hand2 " ) . pack ( )
def _delete_selected ( self ) :
""" Verschiebt die ausgewählte E-Mail in den Papierkorb (Trash). """
if not self . selected_email :
messagebox . showinfo ( " Hinweis " , " Bitte wählen Sie zuerst eine E-Mail aus. " )
return
if not self . current_account :
messagebox . showerror ( " Fehler " , " Kein Konto konfiguriert. " )
return
email_id = self . selected_email . get ( " email_id " )
if not email_id :
messagebox . showerror ( " Fehler " , " E-Mail-ID nicht gefunden. " )
return
if not messagebox . askyesno ( " Löschen " ,
f " Diese E-Mail in den Papierkorb verschieben? \n \n Von: { self . selected_email [ ' from ' ] } \n Betreff: { self . selected_email [ ' subject ' ] } " ) :
return
self . status_var . set ( " ⏳ Verschiebe in Papierkorb... " )
def worker ( ) :
try :
acc = self . current_account
imap_server = acc . get ( " imap_server " , " " )
imap_port = acc . get ( " imap_port " , 993 )
email_addr = acc . get ( " email " , " " )
password = acc . get ( " _password " , " " )
# IMAP Verbindung
mail = imaplib . IMAP4_SSL ( imap_server , imap_port , timeout = 10 )
mail . login ( email_addr , password )
# Aktuellen Ordner auswählen
current_imap = self . folder_mapping . get ( self . current_folder , " INBOX " )
mail . select ( f ' " { current_imap } " ' if " " in current_imap else current_imap )
# Papierkorb-Ordner finden
trash_folder = self . _find_trash_folder ( mail )
if trash_folder :
# E-Mail in Papierkorb kopieren
mail . copy ( email_id , f ' " { trash_folder } " ' if " " in trash_folder else trash_folder )
# Original E-Mail löschen
mail . store ( email_id , ' +FLAGS ' , ' \\ Deleted ' )
mail . expunge ( )
mail . close ( )
mail . logout ( )
# Cache löschen und neu laden
cache_key = self . current_folder
if cache_key in self . email_cache :
del self . email_cache [ cache_key ]
if cache_key in self . cache_timestamp :
del self . cache_timestamp [ cache_key ]
self . root . after ( 0 , lambda : self . status_var . set ( " ✓ E-Mail gelöscht " ) )
self . root . after ( 0 , self . _fetch_emails )
except Exception as e :
self . root . after ( 0 , lambda : messagebox . showerror ( " Fehler " , f " Konnte E-Mail nicht löschen: \n { str ( e ) } " ) )
self . root . after ( 0 , lambda : self . status_var . set ( " ❌ Fehler beim Löschen " ) )
threading . Thread ( target = worker , daemon = True ) . start ( )
def _find_trash_folder ( self , mail ) :
""" Findet den Papierkorb-Ordner auf dem Server. """
trash_variants = [
" INBOX.Trash " ,
" INBOX.Deleted " ,
" [Gmail]/Trash " ,
" Trash " ,
" Deleted " ,
" Papierkorb " ,
" INBOX.migriert_von_servertown.Deleted Messages "
]
for variant in trash_variants :
try :
if " . " in variant or " " in variant :
status = mail . select ( f ' " { variant } " ' , readonly = True )
else :
status = mail . select ( variant , readonly = True )
if status [ 0 ] == " OK " :
return variant
except :
continue
return None
def _remove_email_from_list ( self , idx ) :
""" Entfernt E-Mail aus der lokalen Liste. """
if idx < len ( self . emails ) :
del self . emails [ idx ]
self . _refresh_email_list ( )
def _html_to_text ( self , html ) :
""" Konvertiert HTML zu Plain Text. """
if not html :
return " "
import re
# Entferne Script und Style Tags
text = re . sub ( r ' <script[^>]*>.*?</script> ' , ' ' , html , flags = re . DOTALL | re . IGNORECASE )
text = re . sub ( r ' <style[^>]*>.*?</style> ' , ' ' , text , flags = re . DOTALL | re . IGNORECASE )
# Ersetze <br>, <p>, <div> mit Zeilenumbrüchen
text = re . sub ( r ' <br \ s*/?>|<p>|<div> ' , ' \n ' , text , flags = re . IGNORECASE )
text = re . sub ( r ' </p>|</div> ' , ' \n ' , text , flags = re . IGNORECASE )
# Ersetze Listen
text = re . sub ( r ' <li> ' , ' \n • ' , text , flags = re . IGNORECASE )
# Entferne alle HTML-Tags
text = re . sub ( r ' <[^>]+> ' , ' ' , text )
# HTML-Entities dekodieren
import html
text = html . unescape ( text )
# Mehrfache Leerzeilen entfernen
text = re . sub ( r ' \ n { 3,} ' , ' \n \n ' , text )
# Leerzeichen normalisieren
text = re . sub ( r ' [ \ t]+ ' , ' ' , text )
return text . strip ( )
def _format_email_body ( self , body ) :
""" Formatiert E-Mail-Text für bessere Lesbarkeit (z.B. Visitenkarten). """
if not body :
return body
import re
# 1. URLs erkennen und mit Zeilenumbruch trennen
# Pattern für URLs
url_pattern = r ' (https?://[^ \ s]+) '
# 2. E-Mail-Adressen erkennen
email_pattern = r ' ([a-zA-Z0-9._ % +-]+@[a-zA-Z0-9.-]+ \ .[a-zA-Z] { 2,}) '
# 3. Telefonnummern erkennen (z.B. 052 566 11 01 oder +41 52 566 11 01)
phone_pattern = r ' (Tel \ .? \ s*:? \ s*[ \ d \ s \ + \ - \ ( \ )]+) '
formatted = body
# Ersetze mehrere Spaces durch einen
formatted = re . sub ( r ' { 2,} ' , ' ' , formatted )
# Füge Zeilenumbrüche vor wichtigen Keywords
keywords = [
r ' (Tel \ .) ' ,
r ' (Fax \ .) ' ,
r ' (www \ .) ' ,
r ' (http) ' ,
r ' (Visitenkarte:) ' ,
r ' ( \ d {4} \ s+[A-ZÄÖÜ]) ' , # PLZ + Ort (z.B. "8400 Winterthur")
]
for keyword in keywords :
formatted = re . sub ( keyword , r ' \ n \ 1 ' , formatted )
# Füge Zeilenumbruch nach E-Mail-Adressen
formatted = re . sub ( email_pattern , r ' \ 1 \ n ' , formatted )
# Entferne doppelte Zeilenumbrüche (max 2)
formatted = re . sub ( r ' \ n { 3,} ' , ' \n \n ' , formatted )
# Entferne Leerzeichen am Zeilenanfang
lines = formatted . split ( ' \n ' )
lines = [ line . strip ( ) for line in lines ]
formatted = ' \n ' . join ( lines )
return formatted
def _download_attachment ( self , attachment ) :
""" Lädt einen E-Mail-Anhang direkt in den Download-Ordner herunter. """
# Windows Download-Ordner ermitteln
import os
downloads_folder = os . path . join ( os . path . expanduser ( " ~ " ) , " Downloads " )
# Sicherstellen, dass der Ordner existiert
if not os . path . exists ( downloads_folder ) :
os . makedirs ( downloads_folder )
filename = attachment [ ' filename ' ]
save_path = os . path . join ( downloads_folder , filename )
# Falls Datei bereits existiert, einen eindeutigen Namen generieren
if os . path . exists ( save_path ) :
base , ext = os . path . splitext ( filename )
counter = 1
while os . path . exists ( save_path ) :
save_path = os . path . join ( downloads_folder , f " { base } ( { counter } ) { ext } " )
counter + = 1
try :
# Payload in Datei schreiben
with open ( save_path , " wb " ) as f :
f . write ( attachment [ ' payload ' ] )
self . status_var . set ( f " ✓ Anhang gespeichert: { os . path . basename ( save_path ) } " )
messagebox . showinfo ( " Erfolg " ,
f " Anhang wurde im Download-Ordner gespeichert: \n \n { os . path . basename ( save_path ) } \n \n ( { downloads_folder } ) " )
except Exception as e :
messagebox . showerror ( " Fehler " , f " Anhang konnte nicht gespeichert werden: \n \n { str ( e ) } " )
def _mark_as_spam ( self ) :
""" Markiert die ausgewählte E-Mail als Spam. """
if not self . selected_email :
messagebox . showinfo ( " Hinweis " , " Bitte wählen Sie zuerst eine E-Mail aus. " )
return
if not self . current_account :
messagebox . showerror ( " Fehler " , " Kein Konto konfiguriert. " )
return
email_id = self . selected_email . get ( " email_id " )
if not email_id :
messagebox . showerror ( " Fehler " , " E-Mail-ID nicht gefunden. " )
return
if not messagebox . askyesno ( " Spam " ,
f " Diese E-Mail als Spam markieren? \n \n Von: { self . selected_email [ ' from ' ] } \n Betreff: { self . selected_email [ ' subject ' ] } " ) :
return
self . status_var . set ( " ⏳ Verschiebe zu Spam... " )
def worker ( ) :
try :
acc = self . current_account
imap_server = acc . get ( " imap_server " , " " )
imap_port = acc . get ( " imap_port " , 993 )
email_addr = acc . get ( " email " , " " )
password = acc . get ( " _password " , " " )
# IMAP Verbindung
mail = imaplib . IMAP4_SSL ( imap_server , imap_port , timeout = 10 )
mail . login ( email_addr , password )
# Aktuellen Ordner auswählen
current_imap = self . folder_mapping . get ( self . current_folder , " INBOX " )
mail . select ( f ' " { current_imap } " ' if " " in current_imap else current_imap )
# Spam-Ordner finden
spam_folder = self . _find_spam_folder ( mail )
if not spam_folder :
self . root . after ( 0 , lambda : messagebox . showerror ( " Fehler " , " Spam-Ordner nicht gefunden auf dem Server. " ) )
mail . close ( )
mail . logout ( )
return
# Zurück zum ursprünglichen Ordner
mail . select ( f ' " { current_imap } " ' if " " in current_imap else current_imap )
# E-Mail kopieren zum Spam-Ordner
mail . copy ( email_id , f ' " { spam_folder } " ' if " " in spam_folder else spam_folder )
# Original E-Mail löschen
mail . store ( email_id , ' +FLAGS ' , ' \\ Deleted ' )
mail . expunge ( )
mail . close ( )
mail . logout ( )
print ( f " DEBUG: E-Mail erfolgreich zu Spam verschoben: { spam_folder } " )
# Cache löschen - WICHTIG: Beide Ordner (aktuell + spam)!
cache_key = self . current_folder
if cache_key in self . email_cache :
del self . email_cache [ cache_key ]
print ( f " DEBUG: Cache gelöscht für: { cache_key } " )
if cache_key in self . cache_timestamp :
del self . cache_timestamp [ cache_key ]
# AUCH Spam-Cache löschen, damit beim Öffnen neu geladen wird!
if " spam " in self . email_cache :
del self . email_cache [ " spam " ]
print ( f " DEBUG: Spam-Cache gelöscht " )
if " spam " in self . cache_timestamp :
del self . cache_timestamp [ " spam " ]
self . root . after ( 0 , lambda : self . status_var . set ( " ✓ E-Mail als Spam markiert und verschoben " ) )
self . root . after ( 0 , self . _fetch_emails )
except Exception as e :
self . root . after ( 0 , lambda : messagebox . showerror ( " Fehler " , f " Konnte E-Mail nicht als Spam markieren: \n { str ( e ) } " ) )
self . root . after ( 0 , lambda : self . status_var . set ( " ❌ Fehler beim Spam-Markieren " ) )
threading . Thread ( target = worker , daemon = True ) . start ( )
def _mark_as_not_spam ( self ) :
""" Markiert eine Spam-E-Mail als kein Spam (verschiebt zu Posteingang). """
if not self . selected_email :
messagebox . showinfo ( " Hinweis " , " Bitte wählen Sie zuerst eine E-Mail aus. " )
return
if not self . current_account :
messagebox . showerror ( " Fehler " , " Kein Konto konfiguriert. " )
return
# Nur sinnvoll im Spam-Ordner
if self . current_folder != " spam " :
messagebox . showinfo ( " Hinweis " , " Diese Funktion ist nur im Spam-Ordner verfügbar. " )
return
email_id = self . selected_email . get ( " email_id " )
if not email_id :
messagebox . showerror ( " Fehler " , " E-Mail-ID nicht gefunden. " )
return
if not messagebox . askyesno ( " Kein Spam " ,
f " Diese E-Mail ist kein Spam? \n \n (Wird zum Posteingang verschoben) \n \n Von: { self . selected_email [ ' from ' ] } \n Betreff: { self . selected_email [ ' subject ' ] } " ) :
return
self . status_var . set ( " ⏳ Verschiebe zum Posteingang... " )
def worker ( ) :
try :
acc = self . current_account
imap_server = acc . get ( " imap_server " , " " )
imap_port = acc . get ( " imap_port " , 993 )
email_addr = acc . get ( " email " , " " )
password = acc . get ( " _password " , " " )
# IMAP Verbindung
mail = imaplib . IMAP4_SSL ( imap_server , imap_port , timeout = 10 )
mail . login ( email_addr , password )
# Spam-Ordner auswählen
spam_imap = self . folder_mapping . get ( " spam " , " Junk " )
mail . select ( f ' " { spam_imap } " ' if " " in spam_imap else spam_imap )
# E-Mail zu INBOX kopieren
mail . copy ( email_id , " INBOX " )
# Original aus Spam löschen
mail . store ( email_id , ' +FLAGS ' , ' \\ Deleted ' )
mail . expunge ( )
mail . close ( )
mail . logout ( )
# Cache löschen und neu laden
if " spam " in self . email_cache :
del self . email_cache [ " spam " ]
if " spam " in self . cache_timestamp :
del self . cache_timestamp [ " spam " ]
self . root . after ( 0 , lambda : self . status_var . set ( " ✓ E-Mail zum Posteingang verschoben " ) )
self . root . after ( 0 , self . _fetch_emails )
except Exception as e :
self . root . after ( 0 , lambda : messagebox . showerror ( " Fehler " , f " Konnte E-Mail nicht verschieben: \n { str ( e ) } " ) )
self . root . after ( 0 , lambda : self . status_var . set ( " ❌ Fehler beim Verschieben " ) )
threading . Thread ( target = worker , daemon = True ) . start ( )
def _find_spam_folder ( self , mail ) :
""" Findet den Spam-Ordner auf dem Server. """
print ( " DEBUG: Suche Spam-Ordner... " )
# Priorisiere INBOX.Spam und INBOX.Junk (wie vom Server verlangt)
spam_variants = [
" INBOX.Spam " ,
" INBOX.Junk " ,
" [Gmail]/Spam " ,
" Junk " ,
" Spam " ,
" Junk E-Mail " ,
" INBOX.migriert_von_servertown.Spam " ,
" INBOX.migriert_von_servertown.Junk "
]
# Liste alle verfügbaren Ordner (für besseres Debugging)
try :
status , folder_list = mail . list ( )
if status == " OK " :
available = [ f . decode ( ) if isinstance ( f , bytes ) else f for f in folder_list ]
print ( f " DEBUG: Verfügbare Ordner auf Server: \n { chr ( 10 ) . join ( available [ : 10 ] ) } " ) # Erste 10 Ordner
except Exception as e :
print ( f " DEBUG: Konnte Ordnerliste nicht abrufen: { e } " )
for variant in spam_variants :
try :
# Ordner mit Punkt in Anführungszeichen
if " . " in variant or " " in variant :
status = mail . select ( f ' " { variant } " ' , readonly = True )
else :
status = mail . select ( variant , readonly = True )
if status [ 0 ] == " OK " :
print ( f " DEBUG: ✓ Spam-Ordner gefunden: { variant } " )
return variant
except Exception as e :
print ( f " DEBUG: ✗ Spam-Ordner ' { variant } ' nicht verfügbar: { e } " )
continue
# Falls nicht gefunden, versuche zu erstellen
print ( " DEBUG: Kein Spam-Ordner gefunden, versuche zu erstellen... " )
try :
print ( " DEBUG: Erstelle INBOX.Spam Ordner... " )
mail . create ( " INBOX.Spam " )
# Prüfe ob erfolgreich
status = mail . select ( " INBOX.Spam " , readonly = True )
if status [ 0 ] == " OK " :
print ( " DEBUG: ✓ INBOX.Spam erfolgreich erstellt! " )
return " INBOX.Spam "
except Exception as e :
print ( f " DEBUG: ✗ Konnte Spam-Ordner nicht erstellen: { e } " )
print ( " DEBUG: FEHLER - Kein Spam-Ordner verfügbar! " )
return None
def _select_folder ( self , folder_id ) :
""" Wechselt den Ordner und lädt E-Mails. """
folder_names = {
" inbox " : " Posteingang " ,
" sent " : " Gesendet " ,
" spam " : " Spam " ,
" drafts " : " Entwürfe " ,
" trash " : " Papierkorb "
}
self . current_folder = folder_id
# Header aktualisieren
self . email_list_header . configure ( text = folder_names . get ( folder_id , " Ordner " ) )
# Aktiven Button hervorheben
for btn , btn_folder in self . folder_buttons :
if btn_folder == folder_id :
btn . configure ( bg = self . bg_medium )
else :
btn . configure ( bg = self . bg_dark )
self . status_var . set ( f " { folder_names . get ( folder_id , ' Ordner ' ) } wird geladen... " )
self . _fetch_emails ( )
def _manage_signature ( self ) :
""" Öffnet Unterschriften-Verwaltung. """
sig_win = tk . Toplevel ( self . root )
sig_win . title ( " ✍ E-Mail-Unterschrift verwalten " )
sig_win . geometry ( " 900x750 " )
sig_win . configure ( bg = " #FFFFFF " )
sig_win . transient ( self . root )
# Header
header_frame = tk . Frame ( sig_win , bg = " #3498DB " , height = 60 )
header_frame . pack ( fill = " x " )
header_frame . pack_propagate ( False )
tk . Label ( header_frame , text = " ✍ E-Mail-Unterschrift " , bg = " #3498DB " , fg = " #FFFFFF " ,
font = ( " Segoe UI " , 16 , " bold " ) ) . pack ( pady = 15 , padx = 20 , anchor = " w " )
# Haupt-Container
main_frame = tk . Frame ( sig_win , bg = " #FFFFFF " , padx = 30 , pady = 20 )
main_frame . pack ( fill = " both " , expand = True )
# Anleitung
info_frame = tk . Frame ( main_frame , bg = " #E8F4F8 " , relief = " solid " , bd = 1 )
info_frame . pack ( fill = " x " , pady = ( 0 , 20 ) )
tk . Label ( info_frame , text = " 💡 Tipp: Erstellen Sie eine professionelle E-Mail-Signatur " ,
bg = " #E8F4F8 " , fg = " #2C3E50 " , font = ( " Segoe UI " , 11 , " bold " ) ,
anchor = " w " ) . pack ( fill = " x " , padx = 15 , pady = ( 10 , 5 ) )
tips_text = """ Sie können folgende Elemente einfügen:
• Name , Titel und Position
• Praxisname und Adresse
• Telefon , E - Mail und Website
• Öffnungszeiten oder wichtige Hinweise
• Bild oder Logo ( über Button " 📷 Bild hinzufügen " )
Formatierung : Verwenden Sie Leerzeilen für Abstände , | für Trennzeichen . """
tk . Label ( info_frame , text = tips_text , bg = " #E8F4F8 " , fg = " #555555 " ,
font = ( " Segoe UI " , 9 ) , justify = " left " , anchor = " w " ) . pack ( fill = " x " , padx = 15 , pady = ( 0 , 10 ) )
# Unterschriften-Editor
tk . Label ( main_frame , text = " Unterschrift bearbeiten: " , bg = " #FFFFFF " , fg = self . fg_dark ,
font = ( " Segoe UI " , 11 , " bold " ) , anchor = " w " ) . pack ( fill = " x " , pady = ( 0 , 5 ) )
editor_frame = tk . Frame ( main_frame , bg = " #FFFFFF " )
editor_frame . pack ( fill = " both " , expand = True , pady = ( 0 , 15 ) )
# Text-Editor mit Scrollbar
text_scroll = tk . Scrollbar ( editor_frame )
text_scroll . pack ( side = " right " , fill = " y " )
sig_text = tk . Text ( editor_frame , wrap = " word " , font = ( " Segoe UI " , 10 ) ,
relief = " solid " , bd = 1 , yscrollcommand = text_scroll . set , height = 12 )
sig_text . pack ( side = " left " , fill = " both " , expand = True )
text_scroll . config ( command = sig_text . yview )
# Aktuelle Unterschrift laden
sig_text . insert ( " 1.0 " , self . signature )
# Button-Leiste für Editor
btn_editor_frame = tk . Frame ( main_frame , bg = " #FFFFFF " )
btn_editor_frame . pack ( fill = " x " , pady = ( 0 , 15 ) )
def add_template ( ) :
""" Fügt eine Beispiel-Vorlage ein. """
template = f """ ──────────────────────────────
Mit freundlichen Grüßen
Dr . med . [ Ihr Name ]
[ Facharzt für . . . ]
Praxis [ Praxisname ]
[ Straße und Hausnummer ]
[ PLZ Stadt ]
Tel : [ Telefonnummer ]
E - Mail : [ E - Mail ]
Web : [ Website ]
Sprechzeiten : Mo - Fr 8 : 00 - 18 : 00 Uhr
─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ """
sig_text . delete ( " 1.0 " , " end " )
sig_text . insert ( " 1.0 " , template )
def add_image ( ) :
""" Fügt ein Bild/Logo zur Unterschrift hinzu. """
file_path = filedialog . askopenfilename (
title = " Bild/Logo auswählen " ,
filetypes = [
( " Bild-Dateien " , " *.png *.jpg *.jpeg *.gif " ) ,
( " Alle Dateien " , " *.* " )
]
)
if file_path :
# Speichere Bildpfad in Unterschrift
current_text = sig_text . get ( " 1.0 " , " end-1c " )
sig_text . insert ( " end " , f " \n \n [BILD: { file_path } ] " )
messagebox . showinfo ( " Bild hinzugefügt " ,
f " Bild wurde zur Unterschrift hinzugefügt: \n { os . path . basename ( file_path ) } " ,
parent = sig_win )
tk . Button ( btn_editor_frame , text = " 📋 Vorlage einfügen " , command = add_template ,
bg = " #95A5A6 " , fg = self . fg_light , relief = " flat " ,
font = ( " Segoe UI " , 9 ) , padx = 12 , pady = 6 , cursor = " hand2 " ) . pack ( side = " left " , padx = 5 )
tk . Button ( btn_editor_frame , text = " 📷 Bild hinzufügen " , command = add_image ,
bg = " #95A5A6 " , fg = self . fg_light , relief = " flat " ,
font = ( " Segoe UI " , 9 ) , padx = 12 , pady = 6 , cursor = " hand2 " ) . pack ( side = " left " , padx = 5 )
# Automatische Einfüge-Optionen
tk . Label ( main_frame , text = " Automatisches Einfügen: " , bg = " #FFFFFF " , fg = self . fg_dark ,
font = ( " Segoe UI " , 11 , " bold " ) , anchor = " w " ) . pack ( fill = " x " , pady = ( 5 , 5 ) )
auto_frame = tk . Frame ( main_frame , bg = " #F8F9FA " , relief = " solid " , bd = 1 )
auto_frame . pack ( fill = " x " , pady = ( 0 , 15 ) )
var_auto_new = tk . BooleanVar ( value = self . signature_auto_new )
var_auto_reply = tk . BooleanVar ( value = self . signature_auto_reply )
tk . Checkbutton ( auto_frame , text = " ✅ Unterschrift automatisch bei neuen E-Mails einfügen " ,
variable = var_auto_new , bg = " #F8F9FA " , fg = self . fg_dark ,
font = ( " Segoe UI " , 10 ) , anchor = " w " , selectcolor = " #FFFFFF " ) . pack ( fill = " x " , padx = 15 , pady = 8 )
tk . Checkbutton ( auto_frame , text = " ✅ Unterschrift automatisch bei Antworten einfügen " ,
variable = var_auto_reply , bg = " #F8F9FA " , fg = self . fg_dark ,
font = ( " Segoe UI " , 10 ) , anchor = " w " , selectcolor = " #FFFFFF " ) . pack ( fill = " x " , padx = 15 , pady = ( 0 , 8 ) )
# Vorschau
tk . Label ( main_frame , text = " Vorschau: " , bg = " #FFFFFF " , fg = self . fg_dark ,
font = ( " Segoe UI " , 11 , " bold " ) , anchor = " w " ) . pack ( fill = " x " , pady = ( 5 , 5 ) )
preview_frame = tk . Frame ( main_frame , bg = " #F0F0F0 " , relief = " solid " , bd = 1 )
preview_frame . pack ( fill = " both " , expand = True )
preview_text = tk . Text ( preview_frame , wrap = " word " , font = ( " Segoe UI " , 9 ) ,
relief = " flat " , bd = 0 , bg = " #F0F0F0 " , fg = " #333333 " ,
state = " disabled " , height = 6 )
preview_text . pack ( fill = " both " , expand = True , padx = 10 , pady = 10 )
def update_preview ( ) :
""" Aktualisiert die Vorschau. """
preview_text . config ( state = " normal " )
preview_text . delete ( " 1.0 " , " end " )
current_sig = sig_text . get ( " 1.0 " , " end-1c " )
preview_text . insert ( " 1.0 " , current_sig )
preview_text . config ( state = " disabled " )
sig_text . bind ( " <KeyRelease> " , lambda e : update_preview ( ) )
update_preview ( )
# Speichern/Abbrechen
btn_frame = tk . Frame ( main_frame , bg = " #FFFFFF " )
btn_frame . pack ( fill = " x " , pady = ( 15 , 0 ) )
def save_signature ( ) :
""" Speichert die Unterschrift. """
self . signature = sig_text . get ( " 1.0 " , " end-1c " )
self . signature_auto_new = var_auto_new . get ( )
self . signature_auto_reply = var_auto_reply . get ( )
# Sofort speichern
config = load_email_config ( )
config [ " signature " ] = self . signature
config [ " signature_auto_new " ] = self . signature_auto_new
config [ " signature_auto_reply " ] = self . signature_auto_reply
save_email_config ( config )
messagebox . showinfo ( " Gespeichert " , " Unterschrift wurde erfolgreich gespeichert! " , parent = sig_win )
sig_win . destroy ( )
tk . Button ( btn_frame , text = " 💾 Speichern " , command = save_signature ,
bg = self . accent_blue , fg = self . fg_light , relief = " flat " ,
font = ( " Segoe UI " , 10 , " bold " ) , padx = 25 , pady = 10 , cursor = " hand2 " ) . pack ( side = " left " , padx = 5 )
tk . Button ( btn_frame , text = " Abbrechen " , command = sig_win . destroy ,
bg = " #95A5A6 " , fg = self . fg_light , relief = " flat " ,
font = ( " Segoe UI " , 10 ) , padx = 25 , pady = 10 , cursor = " hand2 " ) . pack ( side = " left " , padx = 5 )
def main ( ) :
root = tk . Tk ( )
app = EmailApp ( root )
root . mainloop ( )
if __name__ == " __main__ " :
main ( )