changes to the email processing

This commit is contained in:
2026-09-17 20:13:26 +02:00
parent ee1683abe1
commit 4943aff944
3 changed files with 60 additions and 69 deletions
+4 -4
View File
@@ -48,7 +48,7 @@ import Web.modules.log.audit_log as al
import push_notifications as pn import push_notifications as pn
import Web.modules.inventarsystem.pdf_export as pdf_export import Web.modules.inventarsystem.pdf_export as pdf_export
import Web.modules.inventarsystem.excel_export as excel_export import Web.modules.inventarsystem.excel_export as excel_export
from Web.modules.emailservice.email import send from Web.modules.emailservice.email import send, send_pdf
import datetime import datetime
from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.schedulers.background import BackgroundScheduler
from bson.objectid import ObjectId, InvalidId from bson.objectid import ObjectId, InvalidId
@@ -5543,7 +5543,7 @@ def student_card_single_barcode_download(card_id):
if not cfg.MODULES.is_enabled('mail'): if not cfg.MODULES.is_enabled('mail'):
flash('Das E-Mail-Add-on ist deaktiviert. Der Ausweis kann weiterhin als PDF heruntergeladen werden.', 'warning') flash('Das E-Mail-Add-on ist deaktiviert. Der Ausweis kann weiterhin als PDF heruntergeladen werden.', 'warning')
return redirect(url_for('student_cards_admin')) return redirect(url_for('student_cards_admin'))
sent = send( sent = send_pdf(
recipient_email, recipient_email,
f'Bibliotheksausweis {card["AusweisId"]}', f'Bibliotheksausweis {card["AusweisId"]}',
f'Anbei erhalten Sie den Bibliotheksausweis für {card.get("SchülerName", "")} als PDF.', f'Anbei erhalten Sie den Bibliotheksausweis für {card.get("SchülerName", "")} als PDF.',
@@ -9153,7 +9153,7 @@ def admin_audit_export_pdf_official():
flash('Das E-Mail-Add-on ist deaktiviert. Der Auditbericht kann weiterhin als PDF heruntergeladen werden.', 'warning') flash('Das E-Mail-Add-on ist deaktiviert. Der Auditbericht kann weiterhin als PDF heruntergeladen werden.', 'warning')
return redirect(url_for('admin_audit_dashboard')) return redirect(url_for('admin_audit_dashboard'))
filename = f'audit-official-report-{datetime.datetime.now(ZoneInfo("Europe/Berlin")).strftime("%Y%m%d-%H%M%S")}.pdf' filename = f'audit-official-report-{datetime.datetime.now(ZoneInfo("Europe/Berlin")).strftime("%Y%m%d-%H%M%S")}.pdf'
sent = send( sent = send_pdf(
recipient_email, recipient_email,
'Amtlicher Auditbericht', 'Amtlicher Auditbericht',
'Anbei erhalten Sie den aktuellen amtlichen Auditbericht des Inventarsystems als PDF.', 'Anbei erhalten Sie den aktuellen amtlichen Auditbericht des Inventarsystems als PDF.',
@@ -9549,7 +9549,7 @@ def admin_create_invoice(borrow_id):
if not cfg.MODULES.is_enabled('mail'): if not cfg.MODULES.is_enabled('mail'):
flash('Das E-Mail-Add-on ist deaktiviert. Die PDF-Rechnung kann weiterhin heruntergeladen werden.', 'warning') flash('Das E-Mail-Add-on ist deaktiviert. Die PDF-Rechnung kann weiterhin heruntergeladen werden.', 'warning')
return redirect(url_for('admin_borrowings')) return redirect(url_for('admin_borrowings'))
sent = send( sent = send_pdf(
recipient_email, recipient_email,
f'Rechnung {invoice_number} - {item_name}', f'Rechnung {invoice_number} - {item_name}',
f'Anbei erhalten Sie die Rechnung {invoice_number} zum Element {item_name}.', f'Anbei erhalten Sie die Rechnung {invoice_number} zum Element {item_name}.',
+54 -63
View File
@@ -3,7 +3,7 @@ from email.mime.text import MIMEText
from email.mime.application import MIMEApplication from email.mime.application import MIMEApplication
import smtplib import smtplib
import time import time
import os
import Web.modules.database.settings as cfg import Web.modules.database.settings as cfg
@@ -28,89 +28,70 @@ def _normalize_recipients(email: list | str) -> list[str]:
return [str(recipient).strip() for recipient in (email or []) if str(recipient).strip()] return [str(recipient).strip() for recipient in (email or []) if str(recipient).strip()]
def send( def _send_message(email: list | str, subject: str, note: str, sender: str, attachment=None) -> bool:
email: list | str, """Send a plain/HTML message, optionally with one PDF attachment."""
subject: str, if not cfg.MODULES.is_enabled("mail"):
text_body: str, print("Debug: Module not enabled")
html_body: str = None, return False
attachments: list = None
) -> bool:
"""Sends the email with Plain Text, HTML, and optional File Attachments support."""
if isinstance(email, str):
email = [email]
if attachments is None: recipients = _normalize_recipients(email)
attachments = [] if not recipients:
return False
TEXT_SIGNATURE = ( body_message = note
"\n\n--\n"
"Mit freundlichen Grüßen\n"
"Ihr Invario Team\n\n"
"Invario UG\n"
"Am Sportplatz 10\n"
"83052 Bruckmühl"
)
HTML_SIGNATURE = """ HTML_SIGNATURE = f"""
<br><br> <table cellpadding="0" cellspacing="0" border="0" style="font-family: Arial, Helvetica, sans-serif; font-size: 13px; color: #333333; line-height: 1.5;">
<table cellpadding="0" cellspacing="0" border="0" style="font-family: Arial, Helvetica, sans-serif; font-size: 13px; color: #555555; line-height: 1.5; border-top: 1px solid #eaebed; padding-top: 15px; width: 100%;">
<tr> <tr>
<td> <td>
<p style="margin:0 0 5px 0;">Mit freundlichen Grüßen,</p> <p style="margin:0 0 12px 0;">Mit freundlichen Grüßen</p>
<p style="margin:0;"><strong style="font-size:15px; color: #333333;">Ihr Invario Team</strong></p><br> <p style="margin:0;"><strong style="font-size:16px;">Automatisierter Email Verteiler für die Schule: {cfg.get_school_info().get("name")}</strong><br></p><br>
<p style="margin:0 0 0 0; font-size: 12px; color: #888888;"> <p style="margin:12px 0 0 0;"><strong>Invario UG</strong><br>Am Sportplatz 10<br>83052 Bruckmühl</p>
<strong>Invario UG</strong><br>
Am Sportplatz 10<br>
83052 Bruckmühl
</p>
</td> </td>
</tr> </tr>
</table> </table>
""" """
text_content = f"{text_body}{TEXT_SIGNATURE}" text_content = f"{body_message}\n\nMit freundlichen Grüßen\n{sender}\n"
if html_body: html_content = f"""
html_content = f"<html><body style='background-color: #f9f9f9; padding: 20px;'><div style='background-color: #ffffff; padding: 30px; border-radius: 8px; max-width: 600px; margin: 0 auto; box-shadow: 0px 2px 5px rgba(0,0,0,0.05);'>{html_body}{HTML_SIGNATURE}</div></body></html>" <html>
else: <body>
# Fallback if only text is provided <p>{body_message}</p>
html_safe_text = text_body.replace('\n', '<br>') <br>
html_content = f"<html><body><p style='font-family: Arial, sans-serif; color: #333333;'>{html_safe_text}</p>{HTML_SIGNATURE}</body></html>" {HTML_SIGNATURE}
</body>
</html>
"""
mails_per_second = 10 mails_per_second = 10
interval = 1.0 / mails_per_second interval = 1.0 / mails_per_second
smtp = None
smtp = None
try: try:
smtp = _build_smtp_client() smtp = _build_smtp_client()
for i, recipient in enumerate(email): for i, recipient in enumerate(recipients):
start_time = time.time() start_time = time.time()
# Root message structure for email with attachments msg = MIMEMultipart("mixed" if attachment else "alternative")
msg = MIMEMultipart("mixed") msg["Subject"] = str(subject)
msg["Subject"] = subject from_address = "no-reply@invario-software.de"
msg["From"] = "Invario Team <no-reply@invario-software.de>" msg["From"] = f"{sender} <{from_address}>"
msg["To"] = str(recipient) msg["To"] = str(recipient)
# Sub-container for Plain Text and HTML bodies body = MIMEMultipart("alternative") if attachment else msg
msg_body = MIMEMultipart("alternative") body.attach(MIMEText(text_content, "plain"))
msg_body.attach(MIMEText(text_content, "plain")) body.attach(MIMEText(html_content, "html"))
msg_body.attach(MIMEText(html_content, "html")) if attachment:
msg.attach(msg_body) msg.attach(body)
attachment_payload, filename = attachment
# Attach files if provided pdf_part = MIMEApplication(attachment_payload, _subtype="pdf")
for file_path in attachments: pdf_part.add_header("Content-Disposition", "attachment", filename=filename)
if file_path and os.path.isfile(file_path): msg.attach(pdf_part)
filename = os.path.basename(file_path)
with open(file_path, "rb") as file:
part = MIMEApplication(file.read(), Name=filename)
part['Content-Disposition'] = f'attachment; filename="{filename}"'
msg.attach(part)
smtp.sendmail( smtp.sendmail(
from_addr="no-reply@invario-software.de", from_addr=from_address,
to_addrs=[recipient], to_addrs=[recipient],
msg=msg.as_string() msg=msg.as_string()
) )
@@ -118,7 +99,7 @@ def send(
elapsed_time = time.time() - start_time elapsed_time = time.time() - start_time
sleep_time = interval - elapsed_time sleep_time = interval - elapsed_time
if sleep_time > 0 and i < len(email) - 1: if sleep_time > 0 and i < len(recipients) - 1:
time.sleep(sleep_time) time.sleep(sleep_time)
return True return True
@@ -130,4 +111,14 @@ def send(
try: try:
smtp.quit() smtp.quit()
except Exception: except Exception:
pass pass
def send(email: list | str, subject: str, note: str, sender: str) -> bool:
"""Send an HTML/plain email without an attachment."""
return _send_message(email, subject, note, sender)
def send_pdf(email: list | str, subject: str, note: str, sender: str, pdf_bytes: bytes, filename: str) -> bool:
"""Send an email with a PDF attachment."""
return _send_message(email, subject, note, sender, attachment=(pdf_bytes, filename))
+2 -2
View File
@@ -3,7 +3,7 @@ import Web.modules.terminplaner.backend_server as appointment_service
import Web.modules.database.settings as cfg import Web.modules.database.settings as cfg
import Web.modules.database.termine as termin import Web.modules.database.termine as termin
import Web.modules.database.user as us import Web.modules.database.user as us
from Web.modules.emailservice.email import send from Web.modules.emailservice.email import send, send_pdf
from Web.modules.terminplaner.backend_server import _resolve_public_base_url from Web.modules.terminplaner.backend_server import _resolve_public_base_url
import csv import csv
import io import io
@@ -542,7 +542,7 @@ def export_pdf_brief(plan_id):
if not cfg.MODULES.is_enabled('mail'): if not cfg.MODULES.is_enabled('mail'):
flash('Das E-Mail-Add-on ist deaktiviert. Der Einladungsbrief kann weiterhin als PDF heruntergeladen werden.', 'warning') flash('Das E-Mail-Add-on ist deaktiviert. Der Einladungsbrief kann weiterhin als PDF heruntergeladen werden.', 'warning')
return redirect(url_for('terminplaner.main', tenant=tenant_id or None)) return redirect(url_for('terminplaner.main', tenant=tenant_id or None))
sent = send( sent = send_pdf(
recipient_email, recipient_email,
f'Einladung zur Terminbuchung: {plan_daten["titel"]}', f'Einladung zur Terminbuchung: {plan_daten["titel"]}',
f'Anbei erhalten Sie den Einladungsbrief für {plan_daten["titel"]} als PDF.', f'Anbei erhalten Sie den Einladungsbrief für {plan_daten["titel"]} als PDF.',