fix: Restore all files lost during destructive rebase
A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.
This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).
Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
395
backend/email_service.py
Normal file
395
backend/email_service.py
Normal file
@@ -0,0 +1,395 @@
|
||||
"""
|
||||
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()
|
||||
Reference in New Issue
Block a user