|
|
|
|
@@ -1,4 +1,6 @@
|
|
|
|
|
import os
|
|
|
|
|
import re
|
|
|
|
|
import smtplib
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
import loguru
|
|
|
|
|
@@ -7,7 +9,7 @@ from PySide6 import QtWidgets
|
|
|
|
|
from src import LOG_DIR, Icon
|
|
|
|
|
from src import settings as config
|
|
|
|
|
|
|
|
|
|
from .dialog_sources.Ui_mail_preview import Ui_eMailPreview as MailPreviewDialog
|
|
|
|
|
from .dialog_sources.mail_preview_ui import Ui_eMailPreview as MailPreviewDialog
|
|
|
|
|
from .mailTemplate import MailTemplateDialog
|
|
|
|
|
|
|
|
|
|
log = loguru.logger
|
|
|
|
|
@@ -15,37 +17,61 @@ log.remove()
|
|
|
|
|
log.add(sys.stdout, level="INFO")
|
|
|
|
|
log.add(f"{LOG_DIR}/application.log", rotation="1 MB", retention="10 days")
|
|
|
|
|
|
|
|
|
|
CSS_RESET = "<style>html,body{margin:0;padding:0}p{margin:0}</style>"
|
|
|
|
|
|
|
|
|
|
empty_signature = """<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
|
|
|
|
|
empty_signature = """"""
|
|
|
|
|
|
|
|
|
|
<html><head><meta name="qrichtext" content="1" /><meta charset="utf-8" /><style
|
|
|
|
|
type="text/css">
|
|
|
|
|
|
|
|
|
|
p, li { white-space: pre-wrap; }
|
|
|
|
|
def _escape_braces_in_style(html: str) -> str:
|
|
|
|
|
"""
|
|
|
|
|
Double curly braces ONLY inside <style>...</style> blocks so that
|
|
|
|
|
str.format(...) won't treat CSS as placeholders. The doubled braces
|
|
|
|
|
will automatically render back to single braces after formatting.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
hr { height: 1px; border-width: 0; }
|
|
|
|
|
def repl(m):
|
|
|
|
|
start, css, end = m.group(1), m.group(2), m.group(3)
|
|
|
|
|
css_escaped = css.replace("{", "{{").replace("}", "}}")
|
|
|
|
|
return f"{start}{css_escaped}{end}"
|
|
|
|
|
|
|
|
|
|
li.unchecked::marker { content: "\2610"; }
|
|
|
|
|
return re.sub(
|
|
|
|
|
r"(<style[^>]*>)(.*?)(</style>)",
|
|
|
|
|
repl,
|
|
|
|
|
html,
|
|
|
|
|
flags=re.IGNORECASE | re.DOTALL,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
li.checked::marker { content: "\2612"; }
|
|
|
|
|
|
|
|
|
|
</style></head><body style=" font-family:''Segoe UI''; font-size:9pt; font-weight:400;
|
|
|
|
|
font-style:normal;">
|
|
|
|
|
def _split_eml_headers_body(eml_text: str) -> tuple[str, str]:
|
|
|
|
|
"""
|
|
|
|
|
Return (headers, body_html). Robustly split on first blank line.
|
|
|
|
|
Accepts lines that contain only spaces/tabs as the separator.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px;
|
|
|
|
|
margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p></body></html>
|
|
|
|
|
"""
|
|
|
|
|
parts = re.split(r"\r?\n[ \t]*\r?\n", eml_text, maxsplit=1)
|
|
|
|
|
if len(parts) == 2:
|
|
|
|
|
return parts[0], parts[1]
|
|
|
|
|
# Fallback: try to split right after the Content-Transfer-Encoding line
|
|
|
|
|
m = re.search(
|
|
|
|
|
r"(?:^|\r?\n)Content-Transfer-Encoding:.*?(?:\r?\n)",
|
|
|
|
|
eml_text,
|
|
|
|
|
flags=re.I | re.S,
|
|
|
|
|
)
|
|
|
|
|
if m:
|
|
|
|
|
return eml_text[: m.end()], eml_text[m.end() :]
|
|
|
|
|
return "", eml_text # last resort: treat entire content as body
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Mail_Dialog(QtWidgets.QDialog, MailPreviewDialog):
|
|
|
|
|
def __init__(
|
|
|
|
|
self,
|
|
|
|
|
app_id,
|
|
|
|
|
app_name,
|
|
|
|
|
app_subject,
|
|
|
|
|
prof_name,
|
|
|
|
|
prof_mail,
|
|
|
|
|
app_id=None,
|
|
|
|
|
app_name=None,
|
|
|
|
|
app_subject=None,
|
|
|
|
|
prof_name=None,
|
|
|
|
|
prof_mail=None,
|
|
|
|
|
accepted_books=None,
|
|
|
|
|
ordered_books=None,
|
|
|
|
|
parent=None,
|
|
|
|
|
default_mail="Information zum Semesterapparat",
|
|
|
|
|
):
|
|
|
|
|
@@ -58,6 +84,7 @@ class Mail_Dialog(QtWidgets.QDialog, MailPreviewDialog):
|
|
|
|
|
self.subject = app_subject
|
|
|
|
|
self.profname = prof_name
|
|
|
|
|
self.books = accepted_books if accepted_books is not None else []
|
|
|
|
|
self.ordered_books = ordered_books if ordered_books is not None else []
|
|
|
|
|
self.mail_data = ""
|
|
|
|
|
self.signature = self.determine_signature()
|
|
|
|
|
self.prof_mail = prof_mail
|
|
|
|
|
@@ -65,52 +92,29 @@ class Mail_Dialog(QtWidgets.QDialog, MailPreviewDialog):
|
|
|
|
|
self.prof_name.setText(prof_name)
|
|
|
|
|
self.mail_name.setText(self.prof_mail)
|
|
|
|
|
self.load_mail_templates()
|
|
|
|
|
# if none of the radio buttons is checked, disable the accept button of the dialog
|
|
|
|
|
self.setWindowIcon(Icon("mail").icon)
|
|
|
|
|
self.btn_okay.setEnabled(False)
|
|
|
|
|
Icon("edit_note", self.newTemplate)
|
|
|
|
|
self.newTemplate.clicked.connect(self.open_new_template)
|
|
|
|
|
|
|
|
|
|
if default_mail is not None:
|
|
|
|
|
# get the nearest match to the default mail
|
|
|
|
|
for i in range(self.comboBox.count()):
|
|
|
|
|
if default_mail in self.comboBox.itemText(i):
|
|
|
|
|
default_mail = self.comboBox.itemText(i)
|
|
|
|
|
break
|
|
|
|
|
self.comboBox.setCurrentText(default_mail)
|
|
|
|
|
self.comboBox.currentIndexChanged.connect(self.set_mail)
|
|
|
|
|
|
|
|
|
|
# re-render when user changes greeting via radio buttons
|
|
|
|
|
self.gender_female.clicked.connect(self.set_mail)
|
|
|
|
|
self.gender_male.clicked.connect(self.set_mail)
|
|
|
|
|
self.gender_non.clicked.connect(self.set_mail)
|
|
|
|
|
|
|
|
|
|
# reflect initial state (OK disabled until a greeting is chosen)
|
|
|
|
|
self._update_ok_button()
|
|
|
|
|
self.btn_okay.clicked.connect(self.createAndSendMail)
|
|
|
|
|
|
|
|
|
|
def open_new_template(self):
|
|
|
|
|
log.info("Opening new template dialog")
|
|
|
|
|
# TODO: implement new mail template dialog
|
|
|
|
|
dialog = MailTemplateDialog()
|
|
|
|
|
dialog.updateSignal.connect(self.load_mail_templates)
|
|
|
|
|
dialog.exec()
|
|
|
|
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
def determine_signature(self):
|
|
|
|
|
if config.mail.signature is empty_signature or config.mail.signature == "":
|
|
|
|
|
return """Mit freundlichen Grüßen
|
|
|
|
|
Ihr Semesterapparatsteam
|
|
|
|
|
Mail: semesterapparate@ph-freiburg.de
|
|
|
|
|
Tel.: 0761/682-778 | 07617682-545"""
|
|
|
|
|
else:
|
|
|
|
|
return config.mail.signature
|
|
|
|
|
|
|
|
|
|
def load_mail_templates(self):
|
|
|
|
|
# print("loading mail templates")
|
|
|
|
|
log.info("Loading mail templates")
|
|
|
|
|
mail_templates = os.listdir("mail_vorlagen")
|
|
|
|
|
log.info(f"Mail templates: {mail_templates}")
|
|
|
|
|
self.comboBox.clear()
|
|
|
|
|
for template in mail_templates:
|
|
|
|
|
self.comboBox.addItem(template)
|
|
|
|
|
|
|
|
|
|
# add these helpers inside Mail_Dialog
|
|
|
|
|
def get_greeting(self):
|
|
|
|
|
prof = self.profname.split(" ")[0]
|
|
|
|
|
if self.gender_male.isChecked():
|
|
|
|
|
@@ -124,45 +128,104 @@ Tel.: 0761/682-778 | 07617682-545"""
|
|
|
|
|
name = f"{self.profname.split(' ')[1]} {self.profname.split(' ')[0]}"
|
|
|
|
|
return f"Guten Tag {name},"
|
|
|
|
|
|
|
|
|
|
def _update_ok_button(self):
|
|
|
|
|
checked = (
|
|
|
|
|
self.gender_male.isChecked()
|
|
|
|
|
or self.gender_female.isChecked()
|
|
|
|
|
or self.gender_non.isChecked()
|
|
|
|
|
)
|
|
|
|
|
self.btn_okay.setEnabled(checked)
|
|
|
|
|
|
|
|
|
|
def _on_gender_toggled(self, checked: bool):
|
|
|
|
|
# Only refresh when a button becomes checked
|
|
|
|
|
if checked:
|
|
|
|
|
self.set_mail()
|
|
|
|
|
|
|
|
|
|
def open_new_template(self):
|
|
|
|
|
log.info("Opening new template dialog")
|
|
|
|
|
dialog = MailTemplateDialog()
|
|
|
|
|
dialog.updateSignal.connect(self.load_mail_templates)
|
|
|
|
|
dialog.exec()
|
|
|
|
|
|
|
|
|
|
def determine_signature(self):
|
|
|
|
|
# use equality, not identity
|
|
|
|
|
if (
|
|
|
|
|
config.mail.signature == empty_signature
|
|
|
|
|
or config.mail.signature.strip() == ""
|
|
|
|
|
):
|
|
|
|
|
return """Mit freundlichen Grüßen
|
|
|
|
|
Ihr Semesterapparatsteam
|
|
|
|
|
Mail: semesterapparate@ph-freiburg.de
|
|
|
|
|
Tel.: 0761/682-778 | 0761/682-545"""
|
|
|
|
|
else:
|
|
|
|
|
return config.mail.signature
|
|
|
|
|
|
|
|
|
|
def load_mail_templates(self):
|
|
|
|
|
log.info("Loading mail templates")
|
|
|
|
|
mail_templates = [
|
|
|
|
|
f for f in os.listdir("mail_vorlagen") if f.lower().endswith(".eml")
|
|
|
|
|
]
|
|
|
|
|
log.info(f"Mail templates: {mail_templates}")
|
|
|
|
|
self.comboBox.clear()
|
|
|
|
|
for template in mail_templates:
|
|
|
|
|
self.comboBox.addItem(template)
|
|
|
|
|
|
|
|
|
|
def set_mail(self):
|
|
|
|
|
log.info("Setting mail")
|
|
|
|
|
self._update_ok_button() # keep OK enabled state in sync
|
|
|
|
|
|
|
|
|
|
email_template = self.comboBox.currentText()
|
|
|
|
|
if email_template == "":
|
|
|
|
|
if not email_template:
|
|
|
|
|
log.debug("No mail template selected")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
with open(f"mail_vorlagen/{email_template}", "r", encoding="utf-8") as f:
|
|
|
|
|
mail_template = f.read()
|
|
|
|
|
eml_text = f.read()
|
|
|
|
|
|
|
|
|
|
# header label for UI (unchanged)
|
|
|
|
|
email_header = email_template.split(".eml")[0]
|
|
|
|
|
if "{AppNr}" in email_template:
|
|
|
|
|
email_header = email_template.split(".eml")[0]
|
|
|
|
|
email_header = email_header.format(AppNr=self.appid, AppName=self.appname)
|
|
|
|
|
email_header = email_header.format(AppNr=self.appid, AppName=self.appname)
|
|
|
|
|
self.mail_header.setText(email_header)
|
|
|
|
|
self.mail_data = mail_template.split("<html>")[0]
|
|
|
|
|
mail_html = mail_template.split("<html>")[1]
|
|
|
|
|
mail_html = "<html>" + mail_html
|
|
|
|
|
Appname = self.appname
|
|
|
|
|
mail_html = mail_html.format(
|
|
|
|
|
Profname=self.profname.split(" ")[0],
|
|
|
|
|
Appname=Appname,
|
|
|
|
|
AppNr=self.appid,
|
|
|
|
|
AppSubject=self.subject,
|
|
|
|
|
greeting=self.get_greeting(),
|
|
|
|
|
signature=self.signature,
|
|
|
|
|
newEditions="<br>".join(
|
|
|
|
|
[
|
|
|
|
|
f"{book.title} von {book.author} (ISBN: {book.isbn}, Auflage: {book.edition}, In Bibliothek: {'ja' if getattr(book, 'library_location', 1) == 1 else 'nein'})"
|
|
|
|
|
for book in self.books
|
|
|
|
|
]
|
|
|
|
|
)
|
|
|
|
|
if self.books
|
|
|
|
|
else "keine neuen Auflagen gefunden",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
self.mail_body.setHtml(mail_html)
|
|
|
|
|
headers, body_html = _split_eml_headers_body(eml_text)
|
|
|
|
|
body_html = _escape_braces_in_style(body_html)
|
|
|
|
|
|
|
|
|
|
# compute greeting from the current toggle selection
|
|
|
|
|
greeting = self.get_greeting()
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
body_html = body_html.format(
|
|
|
|
|
Profname=self.profname.split(" ")[
|
|
|
|
|
0
|
|
|
|
|
], # last name if your template uses {Profname}
|
|
|
|
|
Appname=self.appname,
|
|
|
|
|
AppNr=self.appid,
|
|
|
|
|
AppSubject=self.subject,
|
|
|
|
|
greeting=greeting,
|
|
|
|
|
signature=self.signature,
|
|
|
|
|
newEditions="\n".join(
|
|
|
|
|
[
|
|
|
|
|
f"- {book.title} (ISBN: {','.join(book.isbn)}, Auflage: {book.edition if book.edition else 'nicht bekannt'}, In Bibliothek: {'ja' if getattr(book, 'signature', None) is not None and 'Handbibliothek' not in str(book.library_location) else 'nein'}, Typ: {book.get_book_type()}) Aktuelle Auflage: {book.old_book.edition if book.old_book and book.old_book.edition else 'nicht bekannt'}"
|
|
|
|
|
for book in (self.books or [])
|
|
|
|
|
]
|
|
|
|
|
)
|
|
|
|
|
if self.books
|
|
|
|
|
else "keine neuen Auflagen gefunden",
|
|
|
|
|
newEditionsOrdered="\n".join(
|
|
|
|
|
[
|
|
|
|
|
f" - {book.title}, ISBN: {','.join(book.isbn)}, Bibliotheksstandort : {book.library_location if book.library_location else 'N/A'}, Link: {book.link}"
|
|
|
|
|
for book in (self.ordered_books or [])
|
|
|
|
|
]
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
log.error(f"Template formatting failed: {e}")
|
|
|
|
|
|
|
|
|
|
self.mail_body.setPlainText(body_html)
|
|
|
|
|
|
|
|
|
|
def createAndSendMail(self):
|
|
|
|
|
log.info("Sending mail")
|
|
|
|
|
import smtplib
|
|
|
|
|
from email.mime.multipart import MIMEMultipart
|
|
|
|
|
from email.mime.text import MIMEText
|
|
|
|
|
|
|
|
|
|
@@ -176,31 +239,29 @@ Tel.: 0761/682-778 | 07617682-545"""
|
|
|
|
|
message["From"] = sender_email
|
|
|
|
|
message["To"] = self.prof_mail
|
|
|
|
|
message["Subject"] = self.mail_header.text()
|
|
|
|
|
# include a Fcc to the senders sent folder
|
|
|
|
|
message["cc"] = "semesterapparate@ph-freiburg.de"
|
|
|
|
|
message["Cc"] = "semesterapparate@ph-freiburg.de"
|
|
|
|
|
|
|
|
|
|
mail_body = self.mail_body.toPlainText()
|
|
|
|
|
# strange_string = """p, li { white-space: pre-wrap; }
|
|
|
|
|
# hr { height: 1px; border-width: 0; }
|
|
|
|
|
# li.unchecked::marker { content: "\2610"; }
|
|
|
|
|
# li.checked::marker { content: "\2612"; }
|
|
|
|
|
# """
|
|
|
|
|
# mail_body.replace(strange_string, "")
|
|
|
|
|
message.attach(MIMEText(mail_body, "Plain", "utf-8"))
|
|
|
|
|
|
|
|
|
|
mail_body = self.mail_body.toHtml()
|
|
|
|
|
message.attach(MIMEText(mail_body, "html"))
|
|
|
|
|
mail = message.as_string()
|
|
|
|
|
|
|
|
|
|
with smtplib.SMTP_SSL(smtp_server, port) as server:
|
|
|
|
|
server.connect(smtp_server, port)
|
|
|
|
|
# server.connect(smtp_server, port)
|
|
|
|
|
# server.auth(mechanism="PLAIN")
|
|
|
|
|
server.connect(smtp_server, port) # not needed for SMTP_SSL
|
|
|
|
|
if config.mail.use_user_name is True:
|
|
|
|
|
# print(config["mail"]["user_name"])
|
|
|
|
|
|
|
|
|
|
server.login(config.mail.user_name, password)
|
|
|
|
|
else:
|
|
|
|
|
server.login(sender_email, password)
|
|
|
|
|
server.sendmail(sender_email, tolist, mail)
|
|
|
|
|
|
|
|
|
|
# print("Mail sent")
|
|
|
|
|
# end active process
|
|
|
|
|
server.quit()
|
|
|
|
|
pass
|
|
|
|
|
log.info("Mail sent, closing connection to server and dialog")
|
|
|
|
|
# close the dialog
|
|
|
|
|
|
|
|
|
|
self.accept()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -225,8 +286,6 @@ def launch_gui(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
app = QtWidgets.QApplication(sys.argv)
|
|
|
|
|
Dialog = QtWidgets.QDialog()
|
|
|
|
|
ui = Mail_Dialog()
|
|
|
|
|
|