#13 Add mail_checker.py

This commit is contained in:
2026-02-09 15:45:46 +01:00
parent eec2beab4c
commit 8804d21c32
+234
View File
@@ -0,0 +1,234 @@
#!/usr/bin/env python3
from email.mime.multipart import MIMEMultipart
import imaplib
import email
from email.utils import parseaddr
from email.header import decode_header
from unicodedata import name
import psycopg2
from psycopg2.extras import RealDictCursor
import requests
import json
import time
import logging
from typing import Optional
from sqlalchemy import extract
from models.database import engine, SessionLocal
import models.models as models
import re
# Konfiguration (anpassen!)
CONFIG = {
'imap_server': 'imap.gmx.net', # z.B. imap.gmx.net, outlook.office365.com
'imap_port': 993,
'username': 'sv.stthomas@gmx.at',
'password': '[Jy010]Q&7te2qR&2x0d', # App-Passwort!
'db_url': 'postgresql://user:pass@localhost:5432/deine_db', # psycopg2.connect()
'ollama_url': 'http://localhost:11434/api/generate', # Optional
'ollama_model': 'mistral', # Oder llama3
'categories': ['invoice', 'inquiry', 'spam', 'other'], # Für KI-Prompt
'poll_interval': 300, # Sekunden
'smtp_server': 'mail.gmx.net', # smtp.gmx.com oder mail.gmx.net
'smtp_port': 587, # TLS, oder 465 SSL
'smtp_username': 'zisco@gmx.at',
'smtp_password': '[Jy010]Q&7te2qR&2x0d', # App-Passwort!
'from_email': 'sv.stthomas@gmx.at', # Absender-Name
}
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
def connect_imap() -> imaplib.IMAP4_SSL:
"""IMAP verbinden, readonly für SEEN-Flag-Schutz."""
mail = imaplib.IMAP4_SSL(CONFIG['imap_server'], CONFIG['imap_port'])
mail.login(CONFIG['username'], CONFIG['password'])
mail.select('INBOX', readonly=True) # Kein SEEN setzen![web:14][web:17]
return mail
def parse_email(msg_data: bytes) -> dict:
"""Mail parsen: Sender, Subject, Body."""
msg = email.message_from_bytes(msg_data)
sender = decode_header(msg['From'])[0][0]
if isinstance(sender, bytes):
sender = sender.decode()
subject = decode_header(msg['Subject'])[0][0]
if isinstance(subject, bytes):
subject = subject.decode()
body = ""
if msg.is_multipart():
for part in msg.walk():
if part.get_content_type() == 'text/plain':
body = part.get_payload(decode=True).decode(errors='ignore')
break
else:
body = msg.get_payload(decode=True).decode(errors='ignore')
return {
'message_id': msg['Message-ID'],
'sender': sender,
'subject': subject,
'body_text': body[:5000] # Limit für LLM
}
def classify_with_ollama(body: str) -> str:
"""Optionale lokale KI-Klassifikation."""
try:
prompt = f"""Klassifiziere diese Mail in eine Kategorie: {', '.join(CONFIG['categories'])}.
Nur die Kategorie als JSON-Antwort: {{"category": "invoice"}}"""
payload = {
'model': CONFIG['ollama_model'],
'prompt': prompt + "\n\nMail: " + body,
'stream': False
}
resp = requests.post(CONFIG['ollama_url'], json=payload, timeout=30)
result = resp.json()['response']
category = json.loads(result)['category']
return category if category in CONFIG['categories'] else 'other'
except Exception as e:
logging.error(f"Ollama Fehler: {e}")
return 'unknown'
def clean_sender_name(name):
if not name:
return "Unknown", "Unknown"
# 1. Handle CamelCase (e.g., 'HubertHaider' -> 'Hubert Haider')
# This looks for a lowercase letter followed by an uppercase letter
name = re.sub(r'([a-z])([A-Z])', r'\1 \2', name)
# 2. Split by the first space found
parts = name.split(maxsplit=1)
first_name = parts[0]
last_name = parts[1] if len(parts) > 1 else ""
return first_name, last_name
def save_to_postgres(first_name: str, sur_name: str, address: str):
try:
# find by name
db = SessionLocal()
member = db.query(models.Members).filter(
models.Members.member_email == None,
models.Members.member_firstname == first_name,
models.Members.member_surname == sur_name,
).first()
if member:
member.member_email = address
db.commit()
logging.info(f"Updated Member ID {member.id} with email {address}")
else:
logging.warning(f"No member with name {first_name} {sur_name} found or mail {address} already stored!")
except Exception as e:
logging.error(f"DB Fehler: {e}")
finally:
if 'db' in locals():
db.close()
import smtplib
from email.mime.text import MIMEText
from email.message import EmailMessage
def send_confirmation(sender_email: str, sender_firstname: str, message_id: str, subject: str):
msg = MIMEMultipart()
msg['Subject'] = f'Re: {subject}' # Re: für Threading
msg['From'] = CONFIG['from_email']
msg['To'] = sender_email
msg['In-Reply-To'] = message_id # Threading![web:45]
msg['References'] = message_id
body = f"""
Hallo {sender_firstname},
vielen Dank für Deine Mail! Sie wurde erfolgreich empfangen und verarbeitet.
Du erhältst zukünftig Deine Zahlscheine per Mail.
Mit freundlichen Grüßen,
Dein lokaler Sportverein KI-Assistent
"""
msg.attach(MIMEText(body, 'plain'))
try:
with smtplib.SMTP(CONFIG['smtp_server'], CONFIG['smtp_port']) as smtp:
smtp.starttls() # TLS
smtp.login(CONFIG['smtp_username'], CONFIG['smtp_password'])
#smtp.send_message(msg)
recipients = [sender_email, CONFIG['from_email']] # Add from_email as BCC
smtp.sendmail(CONFIG['from_email'], recipients, msg.as_string())
logging.info(f"Bestätigung gesendet an {sender_email}")
#TODO: In DB markieren als bestätigt
#TODO: Re: Mail als gelesen markieren
return True
except Exception as e:
logging.error(f"SMTP Fehler: {e}")
return False
def main():
while True:
try:
mail = connect_imap()
status, messages = mail.search(None, 'UNSEEN SUBJECT "Zahlschein"') # Nur neue mit Subject![web:11][web:20]
mail_ids = messages[0].split()
for mail_id in mail_ids: # [-5:] letzte 5 neue
status, data = mail.fetch(mail_id, '(RFC822)')
raw_email = data[0][1]
msg = email.message_from_bytes(raw_email)
from_header = msg.get("From")
# 1. Extract Address safely
address = parseaddr(from_header)[1]
# 2. Extract and Decode Name safely
decoded_parts = email.header.decode_header(from_header)
name_parts = []
for content, charset in decoded_parts:
if isinstance(content, bytes):
name_parts.append(content.decode(charset or 'utf-8', errors='replace'))
else:
name_parts.append(content)
# Clean the name (remove the <email> part if parseaddr left it in the name)
full_name = "".join(name_parts)
clean_name = parseaddr(full_name)[0] or "Subscriber"
print(f"Processing: {clean_name} <{address}>")
# clean sender name
first_name, sur_name = clean_sender_name(clean_name)
save_to_postgres(first_name,
sur_name,
address)
mail.select('INBOX', readonly=False)
mail.store(mail_id, '+FLAGS', '\\Seen')
mail.select('INBOX', readonly=True)
# Send confirmation reply
msg_id = msg['Message-ID']
subject = msg['Subject'] or ''
if send_confirmation(address, first_name, msg_id, subject):
# Optional: In DB markieren als bestätigt
pass
mail.logout()
except Exception as e:
logging.error(f"Fehler: {e}")
time.sleep(CONFIG['poll_interval'])
if __name__ == '__main__':
main()