From e81121097787d94464089d7bd0f91d6e8f247dda Mon Sep 17 00:00:00 2001 From: Stefan Hacker Date: Sat, 11 Apr 2026 18:31:19 +0200 Subject: [PATCH] feat: System-Email-Benachrichtigungen bei Freigaben und Benutzer-Erstellung Automatische E-Mail-Benachrichtigungen ueber die konfigurierte System-Email bei folgenden Ereignissen: - Datei/Ordner mit Benutzer geteilt -> Empfaenger wird benachrichtigt - Share-Link heruntergeladen -> Ersteller wird benachrichtigt (mit IP) - Kalender mit Benutzer geteilt -> Empfaenger wird benachrichtigt - Adressbuch mit Benutzer geteilt -> Empfaenger wird benachrichtigt - Passwort-Eintrag/-Ordner geteilt -> Empfaenger wird benachrichtigt - Admin erstellt neuen Benutzer -> Neuer Benutzer wird benachrichtigt Alle Benachrichtigungen sind fail-safe (try/except), damit die eigentliche Aktion nie durch Email-Fehler blockiert wird. Emails werden nur gesendet wenn System-SMTP konfiguriert ist UND der Empfaenger eine Email-Adresse hat. Neuer Service: app/services/system_mail.py mit zentralem Helper Co-Authored-By: Claude Opus 4.6 (1M context) --- backend/app/api/calendar.py | 9 ++ backend/app/api/contacts.py | 9 ++ backend/app/api/files.py | 17 ++++ backend/app/api/passwords.py | 9 ++ backend/app/api/users.py | 8 ++ backend/app/services/system_mail.py | 152 ++++++++++++++++++++++++++++ 6 files changed, 204 insertions(+) create mode 100644 backend/app/services/system_mail.py diff --git a/backend/app/api/calendar.py b/backend/app/api/calendar.py index 93300ac..419be8c 100644 --- a/backend/app/api/calendar.py +++ b/backend/app/api/calendar.py @@ -269,6 +269,7 @@ def share_calendar(cal_id): existing = CalendarShare.query.filter_by( calendar_id=cal_id, shared_with_id=target.id ).first() + is_new = not existing if existing: existing.permission = permission else: @@ -278,6 +279,14 @@ def share_calendar(cal_id): db.session.add(share) db.session.commit() + + if is_new: + try: + from app.services.system_mail import notify_calendar_shared + notify_calendar_shared(cal.name, user.username, target, permission) + except Exception: + pass + return jsonify({'message': f'Kalender mit {username} geteilt'}), 200 diff --git a/backend/app/api/contacts.py b/backend/app/api/contacts.py index 1e0498c..22313da 100644 --- a/backend/app/api/contacts.py +++ b/backend/app/api/contacts.py @@ -246,6 +246,7 @@ def share_addressbook(book_id): existing = AddressBookShare.query.filter_by( address_book_id=book_id, shared_with_id=target.id ).first() + is_new = not existing if existing: existing.permission = permission else: @@ -255,6 +256,14 @@ def share_addressbook(book_id): db.session.add(share) db.session.commit() + + if is_new: + try: + from app.services.system_mail import notify_contacts_shared + notify_contacts_shared(book.name, user.username, target, permission) + except Exception: + pass + return jsonify({'message': f'Adressbuch mit {username} geteilt'}), 200 diff --git a/backend/app/api/files.py b/backend/app/api/files.py index bde1daa..50e5cbd 100644 --- a/backend/app/api/files.py +++ b/backend/app/api/files.py @@ -368,6 +368,7 @@ def set_permission(file_id): existing = FilePermission.query.filter_by( file_id=file_id, user_id=target_user_id ).first() + is_new = not existing if existing: existing.permission = permission else: @@ -375,6 +376,15 @@ def set_permission(file_id): db.session.add(perm) db.session.commit() + + # Notify user via email + if is_new: + try: + from app.services.system_mail import notify_file_shared_with_user + notify_file_shared_with_user(f.name, user.username, target) + except Exception: + pass + return jsonify({'message': 'Berechtigung gesetzt'}), 200 @@ -536,6 +546,13 @@ def share_download(token): link.download_count += 1 db.session.commit() + # Notify creator about download + try: + from app.services.system_mail import notify_share_link_accessed + notify_share_link_accessed(link, f.name, request.remote_addr) + except Exception: + pass + return send_file(str(filepath), mimetype=f.mime_type, as_attachment=True, download_name=f.name) diff --git a/backend/app/api/passwords.py b/backend/app/api/passwords.py index 6530974..d1c2594 100644 --- a/backend/app/api/passwords.py +++ b/backend/app/api/passwords.py @@ -240,6 +240,7 @@ def share_password(): shareable_type=shareable_type, shareable_id=shareable_id, shared_with_id=target.id ).first() + is_new = not existing if existing: existing.permission = permission else: @@ -254,6 +255,14 @@ def share_password(): db.session.add(share) db.session.commit() + + if is_new: + try: + from app.services.system_mail import notify_password_shared + notify_password_shared(shareable_type, user.username, target, permission) + except Exception: + pass + return jsonify({'message': f'Mit {username} geteilt'}), 200 diff --git a/backend/app/api/users.py b/backend/app/api/users.py index 1cb4c12..79dff23 100644 --- a/backend/app/api/users.py +++ b/backend/app/api/users.py @@ -58,6 +58,14 @@ def create_user(): db.session.add(user) db.session.commit() + + # Notify new user via email + try: + from app.services.system_mail import notify_user_created + notify_user_created(user, request.current_user.username) + except Exception: + pass + return jsonify(user.to_dict(include_email=True)), 201 diff --git a/backend/app/services/system_mail.py b/backend/app/services/system_mail.py new file mode 100644 index 0000000..dfbe1a7 --- /dev/null +++ b/backend/app/services/system_mail.py @@ -0,0 +1,152 @@ +"""System email sending helper. Uses the admin-configured SMTP settings.""" +import smtplib +from email.mime.text import MIMEText +from email.mime.multipart import MIMEMultipart + + +def send_system_email(to, subject, body_text, body_html=None): + """Send an email via the system SMTP configuration. + + Returns (success: bool, error: str|None) + """ + from app.models.settings import AppSettings + + host = AppSettings.get('system_smtp_host', '') + port = int(AppSettings.get('system_smtp_port', '587')) + ssl = AppSettings.get_bool('system_smtp_ssl', default=True) + username = AppSettings.get('system_smtp_username', '') + password = AppSettings.get('system_smtp_password', '') + from_addr = AppSettings.get('system_email_from', '') or username + + if not host or not username or not password: + return False, 'System-Email nicht konfiguriert' + + try: + 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['From'] = from_addr + msg['To'] = to + msg['Subject'] = subject + + if ssl and port == 465: + server = smtplib.SMTP_SSL(host, port, timeout=10) + else: + server = smtplib.SMTP(host, port, timeout=10) + server.starttls() + + server.login(username, password) + server.sendmail(from_addr, [to], msg.as_string()) + server.quit() + return True, None + + except Exception as e: + return False, str(e) + + +def notify_share_link_accessed(share_link, file_name, accessor_ip): + """Notify the share link creator that someone accessed their shared file.""" + from app.models.user import User + + creator = User.query.get(share_link.created_by) + if not creator or not creator.email: + return + + subject = f'Mini-Cloud: Zugriff auf geteilte Datei "{file_name}"' + body = ( + f'Hallo {creator.username},\n\n' + f'Jemand hat auf deine geteilte Datei zugegriffen:\n\n' + f' Datei: {file_name}\n' + f' IP-Adresse: {accessor_ip}\n' + f' Downloads bisher: {share_link.download_count}\n\n' + f'Falls du diesen Zugriff nicht erwartet hast, kannst du den ' + f'Freigabe-Link in deiner Mini-Cloud loeschen.\n\n' + f'Deine Mini-Cloud' + ) + send_system_email(creator.email, subject, body) + + +def notify_file_shared_with_user(file_name, owner_username, target_user): + """Notify a user that a file/folder was shared with them.""" + if not target_user.email: + return + + subject = f'Mini-Cloud: "{file_name}" wurde mit dir geteilt' + body = ( + f'Hallo {target_user.username},\n\n' + f'{owner_username} hat "{file_name}" mit dir geteilt.\n\n' + f'Melde dich in deiner Mini-Cloud an, um die Datei zu sehen.\n\n' + f'Deine Mini-Cloud' + ) + send_system_email(target_user.email, subject, body) + + +def notify_calendar_shared(calendar_name, owner_username, target_user, permission): + """Notify a user that a calendar was shared with them.""" + if not target_user.email: + return + + perm_text = 'Lesen' if permission == 'read' else 'Lesen und Schreiben' + subject = f'Mini-Cloud: Kalender "{calendar_name}" wurde mit dir geteilt' + body = ( + f'Hallo {target_user.username},\n\n' + f'{owner_username} hat den Kalender "{calendar_name}" mit dir geteilt.\n' + f'Berechtigung: {perm_text}\n\n' + f'Melde dich in deiner Mini-Cloud an, um den Kalender zu sehen.\n\n' + f'Deine Mini-Cloud' + ) + send_system_email(target_user.email, subject, body) + + +def notify_contacts_shared(addressbook_name, owner_username, target_user, permission): + """Notify a user that an address book was shared with them.""" + if not target_user.email: + return + + perm_text = 'Lesen' if permission == 'read' else 'Lesen und Schreiben' + subject = f'Mini-Cloud: Adressbuch "{addressbook_name}" wurde mit dir geteilt' + body = ( + f'Hallo {target_user.username},\n\n' + f'{owner_username} hat das Adressbuch "{addressbook_name}" mit dir geteilt.\n' + f'Berechtigung: {perm_text}\n\n' + f'Melde dich in deiner Mini-Cloud an.\n\n' + f'Deine Mini-Cloud' + ) + send_system_email(target_user.email, subject, body) + + +def notify_password_shared(shareable_type, owner_username, target_user, permission): + """Notify a user that passwords were shared with them.""" + if not target_user.email: + return + + type_text = 'Ein Passwort-Ordner' if shareable_type == 'folder' else 'Ein Passwort-Eintrag' + subject = f'Mini-Cloud: {type_text} wurde mit dir geteilt' + body = ( + f'Hallo {target_user.username},\n\n' + f'{owner_username} hat {type_text.lower()} mit dir geteilt.\n' + f'Berechtigung: {permission}\n\n' + f'Melde dich in deiner Mini-Cloud an, um die Passwoerter zu sehen.\n\n' + f'Deine Mini-Cloud' + ) + send_system_email(target_user.email, subject, body) + + +def notify_user_created(user, created_by_username): + """Notify a newly created user about their account.""" + if not user.email: + return + + subject = 'Mini-Cloud: Dein Konto wurde erstellt' + body = ( + f'Hallo {user.username},\n\n' + f'{created_by_username} hat ein Konto fuer dich in der Mini-Cloud erstellt.\n\n' + f'Benutzername: {user.username}\n\n' + f'Bitte melde dich an und aendere dein Passwort.\n\n' + f'Deine Mini-Cloud' + ) + send_system_email(user.email, subject, body)