Files
breakpilot-lehrer/backend-lehrer/email_service.py
Benjamin Boenisch 5a31f52310 Initial commit: breakpilot-lehrer - Lehrer KI Platform
Services: Admin-Lehrer, Backend-Lehrer, Studio v2, Website,
Klausur-Service, School-Service, Voice-Service, Geo-Service,
BreakPilot Drive, Agent-Core

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 23:47:26 +01:00

396 lines
13 KiB
Python

"""
BreakPilot Email Service
Ermoeglicht den Versand von Emails via SMTP.
Verwendet Mailpit im Entwicklungsmodus.
"""
import os
import smtplib
import logging
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.utils import formataddr
from typing import Optional, List
from dataclasses import dataclass
from datetime import datetime
logger = logging.getLogger(__name__)
# SMTP Konfiguration aus Umgebungsvariablen
SMTP_HOST = os.getenv("SMTP_HOST", "localhost")
SMTP_PORT = int(os.getenv("SMTP_PORT", "1025"))
SMTP_USERNAME = os.getenv("SMTP_USERNAME", "")
SMTP_PASSWORD = os.getenv("SMTP_PASSWORD", "")
SMTP_FROM_NAME = os.getenv("SMTP_FROM_NAME", "BreakPilot")
SMTP_FROM_ADDR = os.getenv("SMTP_FROM_ADDR", "noreply@breakpilot.app")
SMTP_USE_TLS = os.getenv("SMTP_USE_TLS", "false").lower() == "true"
@dataclass
class EmailResult:
"""Ergebnis eines Email-Versands."""
success: bool
message_id: Optional[str] = None
error: Optional[str] = None
recipient: Optional[str] = None
sent_at: Optional[str] = None
class EmailService:
"""Service fuer den Email-Versand."""
def __init__(
self,
host: str = SMTP_HOST,
port: int = SMTP_PORT,
username: str = SMTP_USERNAME,
password: str = SMTP_PASSWORD,
from_name: str = SMTP_FROM_NAME,
from_addr: str = SMTP_FROM_ADDR,
use_tls: bool = SMTP_USE_TLS
):
self.host = host
self.port = port
self.username = username
self.password = password
self.from_name = from_name
self.from_addr = from_addr
self.use_tls = use_tls
def _get_connection(self):
"""Erstellt eine SMTP-Verbindung."""
if self.use_tls:
smtp = smtplib.SMTP_SSL(self.host, self.port)
else:
smtp = smtplib.SMTP(self.host, self.port)
if self.username and self.password:
smtp.login(self.username, self.password)
return smtp
def send_email(
self,
to_email: str,
subject: str,
body_text: str,
body_html: Optional[str] = None,
reply_to: Optional[str] = None,
cc: Optional[List[str]] = None,
bcc: Optional[List[str]] = None
) -> EmailResult:
"""
Sendet eine Email.
Args:
to_email: Empfaenger-Email
subject: Betreff
body_text: Plaintext-Inhalt
body_html: Optional HTML-Inhalt
reply_to: Optional Reply-To Adresse
cc: Optional CC-Empfaenger
bcc: Optional BCC-Empfaenger
Returns:
EmailResult mit Erfolg/Fehler
"""
try:
# Message erstellen
if body_html:
msg = MIMEMultipart("alternative")
msg.attach(MIMEText(body_text, "plain", "utf-8"))
msg.attach(MIMEText(body_html, "html", "utf-8"))
else:
msg = MIMEText(body_text, "plain", "utf-8")
msg["Subject"] = subject
msg["From"] = formataddr((self.from_name, self.from_addr))
msg["To"] = to_email
if reply_to:
msg["Reply-To"] = reply_to
if cc:
msg["Cc"] = ", ".join(cc)
# Alle Empfaenger sammeln
recipients = [to_email]
if cc:
recipients.extend(cc)
if bcc:
recipients.extend(bcc)
# Senden
with self._get_connection() as smtp:
smtp.sendmail(self.from_addr, recipients, msg.as_string())
logger.info(f"Email sent to {to_email}: {subject}")
return EmailResult(
success=True,
recipient=to_email,
sent_at=datetime.utcnow().isoformat()
)
except smtplib.SMTPException as e:
logger.error(f"SMTP error sending to {to_email}: {e}")
return EmailResult(
success=False,
error=f"SMTP Fehler: {str(e)}",
recipient=to_email
)
except Exception as e:
logger.error(f"Error sending email to {to_email}: {e}")
return EmailResult(
success=False,
error=str(e),
recipient=to_email
)
def send_messenger_notification(
self,
to_email: str,
to_name: str,
sender_name: str,
message_content: str,
reply_link: Optional[str] = None
) -> EmailResult:
"""
Sendet eine Messenger-Benachrichtigung per Email.
Args:
to_email: Empfaenger-Email
to_name: Name des Empfaengers
sender_name: Name des Absenders
message_content: Nachrichteninhalt
reply_link: Optional Link zum Antworten
Returns:
EmailResult
"""
subject = f"Neue Nachricht von {sender_name} - BreakPilot"
# Plaintext Version
body_text = f"""Hallo {to_name},
Sie haben eine neue Nachricht von {sender_name} erhalten:
---
{message_content}
---
"""
if reply_link:
body_text += f"Um zu antworten, klicken Sie hier: {reply_link}\n\n"
body_text += """Mit freundlichen Gruessen
Ihr BreakPilot Team
---
Diese E-Mail wurde automatisch versendet.
Bitte antworten Sie nicht direkt auf diese E-Mail.
"""
# HTML Version
body_html = f"""<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }}
.container {{ max-width: 600px; margin: 0 auto; padding: 20px; }}
.header {{ background: #1a2b4e; color: white; padding: 20px; border-radius: 8px 8px 0 0; }}
.content {{ background: #f8f9fa; padding: 20px; border: 1px solid #e0e0e0; }}
.message-box {{ background: white; padding: 15px; border-radius: 8px; border-left: 4px solid #ffb800; margin: 15px 0; }}
.button {{ display: inline-block; background: #ffb800; color: #1a2b4e; padding: 12px 24px; text-decoration: none; border-radius: 6px; font-weight: bold; }}
.footer {{ padding: 15px; font-size: 12px; color: #666; text-align: center; }}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h2 style="margin: 0;">Neue Nachricht</h2>
</div>
<div class="content">
<p>Hallo <strong>{to_name}</strong>,</p>
<p>Sie haben eine neue Nachricht von <strong>{sender_name}</strong> erhalten:</p>
<div class="message-box">
{message_content.replace(chr(10), '<br>')}
</div>
"""
if reply_link:
body_html += f'<p><a href="{reply_link}" class="button">Nachricht beantworten</a></p>'
body_html += """
</div>
<div class="footer">
<p>Mit freundlichen Gruessen<br>Ihr BreakPilot Team</p>
<p style="font-size: 11px; color: #999;">
Diese E-Mail wurde automatisch versendet.<br>
Bitte antworten Sie nicht direkt auf diese E-Mail.
</p>
</div>
</div>
</body>
</html>
"""
return self.send_email(
to_email=to_email,
subject=subject,
body_text=body_text,
body_html=body_html
)
def send_jitsi_invitation(
self,
to_email: str,
to_name: str,
organizer_name: str,
meeting_title: str,
meeting_date: str,
meeting_time: str,
jitsi_url: str,
additional_info: Optional[str] = None
) -> EmailResult:
"""
Sendet eine Jitsi-Meeting-Einladung per Email.
Args:
to_email: Empfaenger-Email
to_name: Name des Empfaengers
organizer_name: Name des Organisators
meeting_title: Titel des Meetings
meeting_date: Datum des Meetings (z.B. "20. Dezember 2024")
meeting_time: Uhrzeit des Meetings (z.B. "14:00 Uhr")
jitsi_url: Der Jitsi-Meeting-Link
additional_info: Optional zusaetzliche Informationen
Returns:
EmailResult
"""
subject = f"Einladung: {meeting_title} - {meeting_date}"
# Plaintext Version
body_text = f"""Hallo {to_name},
{organizer_name} laedt Sie zu einem Videogespraech ein.
TERMIN: {meeting_title}
DATUM: {meeting_date}
UHRZEIT: {meeting_time}
Treten Sie dem Meeting bei:
{jitsi_url}
"""
if additional_info:
body_text += f"HINWEISE:\n{additional_info}\n\n"
body_text += """TECHNISCHE VORAUSSETZUNGEN:
- Aktueller Webbrowser (Chrome, Firefox, Safari oder Edge)
- Keine Installation erforderlich
- Optional: Kopfhoerer fuer bessere Audioqualitaet
Bei technischen Problemen wenden Sie sich bitte an den Organisator.
Mit freundlichen Gruessen
Ihr BreakPilot Team
"""
# HTML Version
body_html = f"""<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }}
.container {{ max-width: 600px; margin: 0 auto; padding: 20px; }}
.header {{ background: linear-gradient(135deg, #8b5cf6, #6366f1); color: white; padding: 30px; border-radius: 12px 12px 0 0; text-align: center; }}
.header h2 {{ margin: 0 0 10px 0; font-size: 24px; }}
.content {{ background: #f8f9fa; padding: 25px; border: 1px solid #e0e0e0; }}
.meeting-info {{ background: white; padding: 20px; border-radius: 12px; margin: 20px 0; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }}
.info-row {{ display: flex; padding: 10px 0; border-bottom: 1px solid #eee; }}
.info-row:last-child {{ border-bottom: none; }}
.info-label {{ font-weight: 600; color: #666; width: 100px; }}
.info-value {{ color: #333; }}
.join-button {{ display: block; background: linear-gradient(135deg, #8b5cf6, #6366f1); color: white; padding: 16px 32px; text-decoration: none; border-radius: 8px; font-weight: bold; font-size: 18px; text-align: center; margin: 25px 0; }}
.join-button:hover {{ opacity: 0.9; }}
.requirements {{ background: #e8f5e9; padding: 15px; border-radius: 8px; margin: 20px 0; }}
.requirements h4 {{ margin: 0 0 10px 0; color: #2e7d32; }}
.requirements ul {{ margin: 0; padding-left: 20px; }}
.footer {{ padding: 20px; font-size: 12px; color: #666; text-align: center; }}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h2>Einladung zum Videogespraech</h2>
<p style="margin: 0; opacity: 0.9;">{meeting_title}</p>
</div>
<div class="content">
<p>Hallo <strong>{to_name}</strong>,</p>
<p><strong>{organizer_name}</strong> laedt Sie zu einem Videogespraech ein.</p>
<div class="meeting-info">
<div class="info-row">
<span class="info-label">Termin:</span>
<span class="info-value">{meeting_title}</span>
</div>
<div class="info-row">
<span class="info-label">Datum:</span>
<span class="info-value">{meeting_date}</span>
</div>
<div class="info-row">
<span class="info-label">Uhrzeit:</span>
<span class="info-value">{meeting_time}</span>
</div>
</div>
<a href="{jitsi_url}" class="join-button">Meeting beitreten</a>
"""
if additional_info:
body_html += f"""
<div style="background: #fff3e0; padding: 15px; border-radius: 8px; margin: 20px 0;">
<h4 style="margin: 0 0 10px 0; color: #e65100;">Hinweise:</h4>
<p style="margin: 0;">{additional_info}</p>
</div>
"""
body_html += """
<div class="requirements">
<h4>Technische Voraussetzungen:</h4>
<ul>
<li>Aktueller Webbrowser (Chrome, Firefox, Safari oder Edge)</li>
<li>Keine Installation erforderlich</li>
<li>Optional: Kopfhoerer fuer bessere Audioqualitaet</li>
</ul>
</div>
<p style="font-size: 14px; color: #666;">
Bei technischen Problemen wenden Sie sich bitte an den Organisator.
</p>
</div>
<div class="footer">
<p>Mit freundlichen Gruessen<br>Ihr BreakPilot Team</p>
</div>
</div>
</body>
</html>
"""
return self.send_email(
to_email=to_email,
subject=subject,
body_text=body_text,
body_html=body_html
)
# Globale Instanz
email_service = EmailService()