Harden mail connector network boundaries
This commit is contained in:
@@ -8,6 +8,12 @@ from dataclasses import dataclass
|
||||
from email.message import EmailMessage
|
||||
from email.utils import formataddr
|
||||
|
||||
from govoplan_core.security.outbound_http import (
|
||||
OutboundHttpError,
|
||||
create_outbound_connection,
|
||||
validate_outbound_host,
|
||||
)
|
||||
|
||||
from govoplan_mail.backend.config import SmtpConfig, TransportSecurity
|
||||
from govoplan_mail.backend.dev.mock_mailbox import (
|
||||
consume_fail_next_smtp,
|
||||
@@ -19,6 +25,33 @@ from govoplan_mail.backend.dev.mock_mailbox import (
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class _OutboundPolicySMTP(smtplib.SMTP):
|
||||
def _get_socket(self, host: str, port: int, timeout: float | None): # type: ignore[no-untyped-def]
|
||||
return create_outbound_connection(
|
||||
host,
|
||||
port,
|
||||
timeout=timeout,
|
||||
source_address=self.source_address,
|
||||
label="SMTP connector",
|
||||
)
|
||||
|
||||
|
||||
class _OutboundPolicySMTPSSL(smtplib.SMTP_SSL):
|
||||
def _get_socket(self, host: str, port: int, timeout: float | None): # type: ignore[no-untyped-def]
|
||||
sock = create_outbound_connection(
|
||||
host,
|
||||
port,
|
||||
timeout=timeout,
|
||||
source_address=self.source_address,
|
||||
label="SMTP connector",
|
||||
)
|
||||
try:
|
||||
return self.context.wrap_socket(sock, server_hostname=self._host)
|
||||
except Exception:
|
||||
sock.close()
|
||||
raise
|
||||
|
||||
|
||||
class SmtpConfigurationError(ValueError):
|
||||
"""Raised when SMTP settings are incomplete or inconsistent."""
|
||||
|
||||
@@ -75,14 +108,23 @@ def _require_smtp_config(config: SmtpConfig) -> tuple[str, int]:
|
||||
|
||||
def _open_smtp(config: SmtpConfig) -> smtplib.SMTP:
|
||||
host, port = _require_smtp_config(config)
|
||||
try:
|
||||
validate_outbound_host(host, port=port, label="SMTP connector")
|
||||
except OutboundHttpError as exc:
|
||||
raise SmtpConfigurationError(str(exc)) from exc
|
||||
context = ssl.create_default_context()
|
||||
|
||||
try:
|
||||
if config.security == TransportSecurity.TLS:
|
||||
smtp: smtplib.SMTP = smtplib.SMTP_SSL(host=host, port=port, timeout=config.timeout_seconds, context=context)
|
||||
smtp: smtplib.SMTP = _OutboundPolicySMTPSSL(
|
||||
host=host,
|
||||
port=port,
|
||||
timeout=config.timeout_seconds,
|
||||
context=context,
|
||||
)
|
||||
smtp.ehlo()
|
||||
else:
|
||||
smtp = smtplib.SMTP(host=host, port=port, timeout=config.timeout_seconds)
|
||||
smtp = _OutboundPolicySMTP(host=host, port=port, timeout=config.timeout_seconds)
|
||||
smtp.ehlo()
|
||||
if config.security == TransportSecurity.STARTTLS:
|
||||
smtp.starttls(context=context)
|
||||
|
||||
Reference in New Issue
Block a user