""" E-Mail-Versand via SMTP (smtplib + asyncio.to_thread). Konfiguration pro Firma in smtp_configs. Kein externer Mail-Dienst nötig. """ import asyncio import smtplib import ssl from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from typing import TYPE_CHECKING from uuid import UUID from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.core.config import settings from app.models.smtp_config import SmtpConfig if TYPE_CHECKING: from app.models.user import User def _html_wrapper(title: str, body: str) -> str: return f"""
Dein Firmen-Account wurde erfolgreich erstellt. Du kannst dich ab sofort anmelden.
Zum Login """ await self._send(user.email, f"Willkommen bei {settings.app_name}", _html_wrapper("Willkommen", body), cfg) async def send_invite(self, user: "User", invited_by: "User", raw_token: str, db: AsyncSession) -> None: cfg = await self._load_smtp(user.company_id, db) invite_url = f"{settings.frontend_url}/invite/accept#{raw_token}" body = f"""{invited_by.full_name} hat dich zu {settings.app_name} eingeladen.
Klicke auf den Button, um dein Konto zu aktivieren und ein Passwort festzulegen.
Der Link ist 7 Tage gültig.
Oder kopiere diesen Link: {invite_url}
""" await self._send( user.email, f"{invited_by.full_name} hat dich zu {settings.app_name} eingeladen", _html_wrapper("Einladung", body), cfg, ) async def send_password_reset(self, user: "User", raw_token: str, db: AsyncSession) -> None: cfg = await self._load_smtp(user.company_id, db) reset_url = f"{settings.frontend_url}/auth/reset-password#{raw_token}" body = f"""Hallo {user.first_name},
du hast eine Anfrage zum Zurücksetzen deines Passworts gestellt.
Klicke auf den Button – der Link ist 1 Stunde gültig.
Falls du diese Anfrage nicht gestellt hast, kannst du diese E-Mail ignorieren.
""" await self._send(user.email, "Passwort zurücksetzen", _html_wrapper("Passwort zurücksetzen", body), cfg) async def send_substitute_notification( self, substitute: "User", requester: "User", absence, db: AsyncSession ) -> None: """Informiert die eingetragene Vertretung über eine genehmigte Abwesenheit.""" cfg = await self._load_smtp(substitute.company_id, db) start = absence.start_date.strftime("%d.%m.%Y") end = absence.end_date.strftime("%d.%m.%Y") zeitraum = start if start == end else f"{start} – {end}" body = f"""Hallo {substitute.first_name},
{requester.full_name} ist im Zeitraum {zeitraum} abwesend und hat dich als Vertretung benannt.
Abwesenheiten ansehen """ await self._send( substitute.email, f"Vertretung für {requester.full_name} ({zeitraum})", _html_wrapper("Vertretung", body), cfg, ) async def send_pending_approvals_digest( self, approver: "User", items: list[dict], db: AsyncSession ) -> None: """Tägliche Zusammenfassung offener Anträge an eine genehmigende Person.""" cfg = await self._load_smtp(approver.company_id, db) rows = "".join( f"Hallo {approver.first_name}, es warten {len(items)} Anträge auf deine Genehmigung:
Hallo {user.first_name}, du hast noch {remaining} Urlaubstage, die am {datum} verfallen.
Plane deinen Urlaub rechtzeitig ein, damit nichts verloren geht.
Urlaub planen """ await self._send( user.email, f"Dein Resturlaub verfällt am {datum}", _html_wrapper("Resturlaub", body), cfg, ) async def send_certificate_overdue_digest( self, hr_user: "User", items: list[dict], db: AsyncSession ) -> None: cfg = await self._load_smtp(hr_user.company_id, db) rows = "".join( f"Hallo {hr_user.first_name}, für {len(items)} Krankmeldung(en) fehlt die Arbeitsunfähigkeitsbescheinigung:
Deine SMTP-Konfiguration für {settings.app_name} funktioniert korrekt.
Server: {cfg.host}:{cfg.port}
""" await self._send(to, f"{settings.app_name} – SMTP-Test", _html_wrapper("SMTP-Test", body), cfg) email_service = EmailService()