intermittent commit
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import logging
|
||||
import smtplib
|
||||
import ssl
|
||||
from dataclasses import dataclass
|
||||
@@ -15,6 +16,8 @@ from govoplan_mail.backend.dev.mock_mailbox import (
|
||||
record_smtp_delivery,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SmtpConfigurationError(ValueError):
|
||||
"""Raised when SMTP settings are incomplete or inconsistent."""
|
||||
@@ -56,6 +59,10 @@ class SmtpSendResult:
|
||||
return len(self.envelope_recipients) - len(self.refused_recipients)
|
||||
|
||||
|
||||
def _log_smtp_cleanup_failure(action: str, exc: BaseException) -> None:
|
||||
logger.debug("SMTP cleanup failed while %s: %s", action, exc, exc_info=True)
|
||||
|
||||
|
||||
def _require_smtp_config(config: SmtpConfig) -> tuple[str, int]:
|
||||
if not config.host:
|
||||
raise SmtpConfigurationError("SMTP host is required")
|
||||
@@ -89,8 +96,8 @@ def _open_smtp(config: SmtpConfig) -> smtplib.SMTP:
|
||||
# on GC, but explicit cleanup is safer when the variable exists.
|
||||
try:
|
||||
smtp.quit() # type: ignore[possibly-undefined]
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as cleanup_exc:
|
||||
_log_smtp_cleanup_failure("opening connection", cleanup_exc)
|
||||
raise
|
||||
|
||||
|
||||
@@ -134,11 +141,12 @@ def test_smtp_login(*, smtp_config: SmtpConfig) -> SmtpLoginTestResult:
|
||||
finally:
|
||||
try:
|
||||
smtp.quit()
|
||||
except Exception:
|
||||
except Exception as quit_exc:
|
||||
_log_smtp_cleanup_failure("testing login quit", quit_exc)
|
||||
try:
|
||||
smtp.close()
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as close_exc:
|
||||
_log_smtp_cleanup_failure("testing login close", close_exc)
|
||||
|
||||
|
||||
def prepare_test_message(
|
||||
@@ -161,12 +169,12 @@ def prepare_test_message(
|
||||
del test_message[header]
|
||||
|
||||
# Replace potential previous marker headers if the user test-sends an EML twice.
|
||||
for header in ["X-MultiMailer-Test-Send"]:
|
||||
for header in ["X-GovOPlaN-Test-Send"]:
|
||||
if header in test_message:
|
||||
del test_message[header]
|
||||
|
||||
test_message["To"] = formataddr((test_recipient_name or test_recipient, test_recipient))
|
||||
test_message["X-MultiMailer-Test-Send"] = "true"
|
||||
test_message["X-GovOPlaN-Test-Send"] = "true"
|
||||
return test_message
|
||||
|
||||
|
||||
@@ -177,43 +185,97 @@ def _send_smtp_payload(
|
||||
envelope_from: str,
|
||||
envelope_recipients: list[str],
|
||||
) -> SmtpSendResult:
|
||||
host, port, recipients = _prepare_smtp_send(
|
||||
smtp_config=smtp_config,
|
||||
envelope_from=envelope_from,
|
||||
envelope_recipients=envelope_recipients,
|
||||
)
|
||||
|
||||
if is_mock_smtp_host(smtp_config.host):
|
||||
_accepted, refused = _send_mock_smtp_payload(
|
||||
message,
|
||||
smtp_config=smtp_config,
|
||||
envelope_from=envelope_from,
|
||||
envelope_recipients=recipients,
|
||||
)
|
||||
return _smtp_send_result(
|
||||
smtp_config=smtp_config,
|
||||
host=host,
|
||||
port=port,
|
||||
envelope_from=envelope_from,
|
||||
envelope_recipients=recipients,
|
||||
refused=refused,
|
||||
)
|
||||
|
||||
refused = _send_network_smtp_payload(
|
||||
message,
|
||||
smtp_config=smtp_config,
|
||||
envelope_from=envelope_from,
|
||||
envelope_recipients=recipients,
|
||||
)
|
||||
return _smtp_send_result(
|
||||
smtp_config=smtp_config,
|
||||
host=host,
|
||||
port=port,
|
||||
envelope_from=envelope_from,
|
||||
envelope_recipients=recipients,
|
||||
refused=refused,
|
||||
)
|
||||
|
||||
|
||||
def _prepare_smtp_send(
|
||||
*,
|
||||
smtp_config: SmtpConfig,
|
||||
envelope_from: str,
|
||||
envelope_recipients: list[str],
|
||||
) -> tuple[str, int, list[str]]:
|
||||
host, port = _require_smtp_config(smtp_config)
|
||||
if not envelope_from:
|
||||
raise SmtpConfigurationError("SMTP envelope sender is required")
|
||||
if not envelope_recipients:
|
||||
recipients = [recipient for recipient in envelope_recipients if recipient]
|
||||
if not recipients:
|
||||
raise SmtpConfigurationError("at least one SMTP envelope recipient is required")
|
||||
return host, port, recipients
|
||||
|
||||
if is_mock_smtp_host(smtp_config.host):
|
||||
if consume_fail_next_smtp():
|
||||
raise SmtpSendError("Mock SMTP configured to fail the next send")
|
||||
failures = get_failures()
|
||||
reject_text = str(failures.get("smtp_reject_recipients_containing") or "").strip().lower()
|
||||
refused: dict[str, tuple[int, bytes]] = {}
|
||||
accepted = list(envelope_recipients)
|
||||
if reject_text:
|
||||
refused = {
|
||||
recipient: (550, b"mock recipient rejected")
|
||||
for recipient in envelope_recipients
|
||||
if reject_text in recipient.lower()
|
||||
}
|
||||
accepted = [recipient for recipient in envelope_recipients if recipient not in refused]
|
||||
if not accepted:
|
||||
raise SmtpSendError(f"all mock SMTP recipients were refused: {_decode_refused(refused)}")
|
||||
record_smtp_delivery(
|
||||
message,
|
||||
envelope_from=envelope_from,
|
||||
envelope_recipients=accepted,
|
||||
smtp_host=smtp_config.host,
|
||||
)
|
||||
return SmtpSendResult(
|
||||
host=host,
|
||||
port=port,
|
||||
security=smtp_config.security.value,
|
||||
envelope_from=envelope_from,
|
||||
envelope_recipients=list(envelope_recipients),
|
||||
refused_recipients=_decode_refused(refused),
|
||||
)
|
||||
|
||||
def _send_mock_smtp_payload(
|
||||
message: EmailMessage | bytes,
|
||||
*,
|
||||
smtp_config: SmtpConfig,
|
||||
envelope_from: str,
|
||||
envelope_recipients: list[str],
|
||||
) -> tuple[list[str], dict[str, tuple[int, bytes]]]:
|
||||
if consume_fail_next_smtp():
|
||||
raise SmtpSendError("Mock SMTP configured to fail the next send")
|
||||
failures = get_failures()
|
||||
reject_text = str(failures.get("smtp_reject_recipients_containing") or "").strip().lower()
|
||||
refused: dict[str, tuple[int, bytes]] = {}
|
||||
accepted = list(envelope_recipients)
|
||||
if reject_text:
|
||||
refused = {
|
||||
recipient: (550, b"mock recipient rejected")
|
||||
for recipient in envelope_recipients
|
||||
if reject_text in recipient.lower()
|
||||
}
|
||||
accepted = [recipient for recipient in envelope_recipients if recipient not in refused]
|
||||
if not accepted:
|
||||
raise SmtpSendError(f"all mock SMTP recipients were refused: {_decode_refused(refused)}")
|
||||
record_smtp_delivery(
|
||||
message,
|
||||
envelope_from=envelope_from,
|
||||
envelope_recipients=accepted,
|
||||
smtp_host=smtp_config.host,
|
||||
)
|
||||
return accepted, refused
|
||||
|
||||
|
||||
def _send_network_smtp_payload(
|
||||
message: EmailMessage | bytes,
|
||||
*,
|
||||
smtp_config: SmtpConfig,
|
||||
envelope_from: str,
|
||||
envelope_recipients: list[str],
|
||||
) -> dict[str, tuple[int, bytes]]:
|
||||
try:
|
||||
smtp = _open_smtp(smtp_config)
|
||||
except smtplib.SMTPAuthenticationError as exc:
|
||||
@@ -266,12 +328,24 @@ def _send_smtp_payload(
|
||||
finally:
|
||||
try:
|
||||
smtp.quit()
|
||||
except Exception:
|
||||
except Exception as quit_exc:
|
||||
_log_smtp_cleanup_failure("sending message quit", quit_exc)
|
||||
try:
|
||||
smtp.close()
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as close_exc:
|
||||
_log_smtp_cleanup_failure("sending message close", close_exc)
|
||||
return refused
|
||||
|
||||
|
||||
def _smtp_send_result(
|
||||
*,
|
||||
smtp_config: SmtpConfig,
|
||||
host: str,
|
||||
port: int,
|
||||
envelope_from: str,
|
||||
envelope_recipients: list[str],
|
||||
refused: dict[str, tuple[int, bytes]],
|
||||
) -> SmtpSendResult:
|
||||
return SmtpSendResult(
|
||||
host=host,
|
||||
port=port,
|
||||
|
||||
Reference in New Issue
Block a user