diff --git a/misc/requirements.txt b/misc/requirements.txt index 92b8545..b177a05 100644 --- a/misc/requirements.txt +++ b/misc/requirements.txt @@ -35,3 +35,4 @@ typing_extensions==4.12.2 tzdata==2024.1 webargs==8.6.0 Werkzeug==3.0.4 +ntlm-auth==1.5.0 \ No newline at end of file diff --git a/src/server/BreCal/services/schedule_routines.py b/src/server/BreCal/services/schedule_routines.py index c452d45..99118c7 100644 --- a/src/server/BreCal/services/schedule_routines.py +++ b/src/server/BreCal/services/schedule_routines.py @@ -3,6 +3,7 @@ import pydapper import smtplib import json import os +import base64 from email.message import EmailMessage from BreCal.schemas import model, defs @@ -110,8 +111,33 @@ def SendEmails(email_dict): pooledConnection = getPoolConnection() commands = pydapper.using(pooledConnection) + def _parse_bool(value) -> bool: + if isinstance(value, bool): + return value + if value is None: + return False + return str(value).strip().lower() in ("1", "true", "yes", "y", "on") + + def _smtp_auth_ntlm(connection, username: str, password: str, domain: str | None = None, workstation: str | None = None): + try: + from ntlm_auth.ntlm import NtlmContext + except Exception as exc: + raise RuntimeError("NTLM auth requested but ntlm-auth is not installed") from exc + + context = NtlmContext(username, password, domain=domain, workstation=workstation) + negotiate = context.step() + code, challenge = connection.docmd("AUTH", "NTLM " + base64.b64encode(negotiate).decode("ascii")) + if code != 334: + raise smtplib.SMTPException(f"NTLM negotiate failed: {code} {challenge}") + challenge_bytes = base64.b64decode(challenge.strip()) + authenticate = context.step(challenge_bytes) + code, msg = connection.docmd(base64.b64encode(authenticate).decode("ascii")) + if code != 235: + raise smtplib.SMTPAuthenticationError(code, msg) + encryption = defs.email_credentials.get("encryption") or defs.email_credentials.get("encryption_method") or "STARTTLS" encryption_norm = str(encryption).strip().upper().replace(" ", "").replace("_", "").replace("-", "") + use_ntlm_auth = _parse_bool(defs.email_credentials.get("USE_NTLM_AUTH")) if encryption_norm in ("SSLTLS", "SSL", "TLS"): conn = smtplib.SMTP_SSL(defs.email_credentials["server"], defs.email_credentials["port"]) else: @@ -127,7 +153,13 @@ def SendEmails(email_dict): logging.warning("Unknown email encryption '%s'; defaulting to STARTTLS.", encryption) conn.starttls() conn.ehlo() - conn.login(defs.email_credentials["sender"], defs.email_credentials["password_send"]) + if use_ntlm_auth: + ntlm_user = defs.email_credentials.get("ntlm_user") or defs.email_credentials["sender"] + ntlm_domain = defs.email_credentials.get("ntlm_domain") + ntlm_workstation = defs.email_credentials.get("ntlm_workstation") + _smtp_auth_ntlm(conn, ntlm_user, defs.email_credentials["password_send"], domain=ntlm_domain, workstation=ntlm_workstation) + else: + conn.login(defs.email_credentials["sender"], defs.email_credentials["password_send"]) current_path = os.path.dirname(os.path.abspath(__file__))