Add email.tlsname config option (#17849)

The existing `email.smtp_host` config option is used for two distinct
purposes: it is resolved into the IP address to connect to, and used to
(request via SNI and) validate the server's certificate if TLS is
enabled. This new option allows specifying a different name for the
second purpose.

This is especially helpful, if `email.smtp_host` isn't a global FQDN,
but something that resolves only locally (e.g. "localhost" to connect
through the loopback interface, or some other internally routed name),
that one cannot get a valid certificate for.
Alternatives would of course be to specify a global FQDN as
`email.smtp_host`, or to disable TLS entirely, both of which might be
undesirable, depending on the SMTP server configuration.
This commit is contained in:
cynhr 2024-12-18 01:05:38 +01:00 committed by GitHub
parent 57bf44941e
commit f1ecf46647
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 68 additions and 37 deletions

View file

@ -0,0 +1 @@
Added the `email.tlsname` config option. This allows specifying the domain name used to validate the SMTP server's TLS certificate separately from the `email.smtp_host` to connect to.

View file

@ -673,8 +673,9 @@ This setting has the following sub-options:
TLS via STARTTLS *if the SMTP server supports it*. If this option is set, TLS via STARTTLS *if the SMTP server supports it*. If this option is set,
Synapse will refuse to connect unless the server supports STARTTLS. Synapse will refuse to connect unless the server supports STARTTLS.
* `enable_tls`: By default, if the server supports TLS, it will be used, and the server * `enable_tls`: By default, if the server supports TLS, it will be used, and the server
must present a certificate that is valid for 'smtp_host'. If this option must present a certificate that is valid for `tlsname`. If this option
is set to false, TLS will not be used. is set to false, TLS will not be used.
* `tlsname`: The domain name the SMTP server's TLS certificate must be valid for, defaulting to `smtp_host`.
* `notif_from`: defines the "From" address to use when sending emails. * `notif_from`: defines the "From" address to use when sending emails.
It must be set if email sending is enabled. The placeholder '%(app)s' will be replaced by the application name, It must be set if email sending is enabled. The placeholder '%(app)s' will be replaced by the application name,
which is normally set in `app_name`, but may be overridden by the which is normally set in `app_name`, but may be overridden by the
@ -741,6 +742,7 @@ email:
force_tls: true force_tls: true
require_transport_security: true require_transport_security: true
enable_tls: false enable_tls: false
tlsname: mail.server.example.com
notif_from: "Your Friendly %(app)s homeserver <noreply@example.com>" notif_from: "Your Friendly %(app)s homeserver <noreply@example.com>"
app_name: my_branded_matrix_server app_name: my_branded_matrix_server
enable_notifs: true enable_notifs: true

View file

@ -110,6 +110,7 @@ class EmailConfig(Config):
raise ConfigError( raise ConfigError(
"email.require_transport_security requires email.enable_tls to be true" "email.require_transport_security requires email.enable_tls to be true"
) )
self.email_tlsname = email_config.get("tlsname", None)
if "app_name" in email_config: if "app_name" in email_config:
self.email_app_name = email_config["app_name"] self.email_app_name = email_config["app_name"]

View file

@ -47,15 +47,45 @@ logger = logging.getLogger(__name__)
_is_old_twisted = parse_version(twisted.__version__) < parse_version("21") _is_old_twisted = parse_version(twisted.__version__) < parse_version("21")
class _NoTLSESMTPSender(ESMTPSender): class _BackportESMTPSender(ESMTPSender):
"""Extend ESMTPSender to disable TLS """Extend old versions of ESMTPSender to configure TLS.
Unfortunately, before Twisted 21.2, ESMTPSender doesn't give an easy way to disable Unfortunately, before Twisted 21.2, ESMTPSender doesn't give an easy way to
TLS, so we override its internal method which it uses to generate a context factory. disable TLS, or to configure the hostname used for TLS certificate validation.
This backports the `hostname` parameter for that functionality.
""" """
__hostname: Optional[str]
def __init__(self, *args: Any, **kwargs: Any) -> None:
""""""
self.__hostname = kwargs.pop("hostname", None)
super().__init__(*args, **kwargs)
def _getContextFactory(self) -> Optional[IOpenSSLContextFactory]: def _getContextFactory(self) -> Optional[IOpenSSLContextFactory]:
return None if self.context is not None:
return self.context
elif self.__hostname is None:
return None # disable TLS if hostname is None
return optionsForClientTLS(self.__hostname)
class _BackportESMTPSenderFactory(ESMTPSenderFactory):
"""An ESMTPSenderFactory for _BackportESMTPSender.
This backports the `hostname` parameter, to disable or configure TLS.
"""
__hostname: Optional[str]
def __init__(self, *args: Any, **kwargs: Any) -> None:
self.__hostname = kwargs.pop("hostname", None)
super().__init__(*args, **kwargs)
def protocol(self, *args: Any, **kwargs: Any) -> ESMTPSender: # type: ignore
# this overrides ESMTPSenderFactory's `protocol` attribute, with a Callable
# instantiating our _BackportESMTPSender, providing the hostname parameter
return _BackportESMTPSender(*args, **kwargs, hostname=self.__hostname)
async def _sendmail( async def _sendmail(
@ -71,6 +101,7 @@ async def _sendmail(
require_tls: bool = False, require_tls: bool = False,
enable_tls: bool = True, enable_tls: bool = True,
force_tls: bool = False, force_tls: bool = False,
tlsname: Optional[str] = None,
) -> None: ) -> None:
"""A simple wrapper around ESMTPSenderFactory, to allow substitution in tests """A simple wrapper around ESMTPSenderFactory, to allow substitution in tests
@ -88,39 +119,33 @@ async def _sendmail(
enable_tls: True to enable STARTTLS. If this is False and require_tls is True, enable_tls: True to enable STARTTLS. If this is False and require_tls is True,
the request will fail. the request will fail.
force_tls: True to enable Implicit TLS. force_tls: True to enable Implicit TLS.
tlsname: the domain name expected as the TLS certificate's commonname,
defaults to smtphost.
""" """
msg = BytesIO(msg_bytes) msg = BytesIO(msg_bytes)
d: "Deferred[object]" = Deferred() d: "Deferred[object]" = Deferred()
if not enable_tls:
tlsname = None
elif tlsname is None:
tlsname = smtphost
def build_sender_factory(**kwargs: Any) -> ESMTPSenderFactory: factory: IProtocolFactory = (
return ESMTPSenderFactory( _BackportESMTPSenderFactory if _is_old_twisted else ESMTPSenderFactory
username, )(
password, username,
from_addr, password,
to_addr, from_addr,
msg, to_addr,
d, msg,
heloFallback=True, d,
requireAuthentication=require_auth, heloFallback=True,
requireTransportSecurity=require_tls, requireAuthentication=require_auth,
**kwargs, requireTransportSecurity=require_tls,
) hostname=tlsname,
)
factory: IProtocolFactory
if _is_old_twisted:
# before twisted 21.2, we have to override the ESMTPSender protocol to disable
# TLS
factory = build_sender_factory()
if not enable_tls:
factory.protocol = _NoTLSESMTPSender
else:
# for twisted 21.2 and later, there is a 'hostname' parameter which we should
# set to enable TLS.
factory = build_sender_factory(hostname=smtphost if enable_tls else None)
if force_tls: if force_tls:
factory = TLSMemoryBIOFactory(optionsForClientTLS(smtphost), True, factory) factory = TLSMemoryBIOFactory(optionsForClientTLS(tlsname), True, factory)
endpoint = HostnameEndpoint( endpoint = HostnameEndpoint(
reactor, smtphost, smtpport, timeout=30, bindAddress=None reactor, smtphost, smtpport, timeout=30, bindAddress=None
@ -148,6 +173,7 @@ class SendEmailHandler:
self._require_transport_security = hs.config.email.require_transport_security self._require_transport_security = hs.config.email.require_transport_security
self._enable_tls = hs.config.email.enable_smtp_tls self._enable_tls = hs.config.email.enable_smtp_tls
self._force_tls = hs.config.email.force_tls self._force_tls = hs.config.email.force_tls
self._tlsname = hs.config.email.email_tlsname
self._sendmail = _sendmail self._sendmail = _sendmail
@ -227,4 +253,5 @@ class SendEmailHandler:
require_tls=self._require_transport_security, require_tls=self._require_transport_security,
enable_tls=self._enable_tls, enable_tls=self._enable_tls,
force_tls=self._force_tls, force_tls=self._force_tls,
tlsname=self._tlsname,
) )

View file

@ -163,6 +163,7 @@ class SendEmailHandlerTestCaseIPv4(HomeserverTestCase):
"email": { "email": {
"notif_from": "noreply@test", "notif_from": "noreply@test",
"force_tls": True, "force_tls": True,
"tlsname": "example.org",
}, },
} }
) )
@ -186,10 +187,9 @@ class SendEmailHandlerTestCaseIPv4(HomeserverTestCase):
self.assertEqual(host, self.reactor.lookups["localhost"]) self.assertEqual(host, self.reactor.lookups["localhost"])
self.assertEqual(port, 465) self.assertEqual(port, 465)
# We need to make sure that TLS is happenning # We need to make sure that TLS is happenning
self.assertIsInstance( context_factory = client_factory._wrappedFactory._testingContextFactory
client_factory._wrappedFactory._testingContextFactory, self.assertIsInstance(context_factory, ClientTLSOptions)
ClientTLSOptions, self.assertEqual(context_factory._hostname, "example.org") # tlsname
)
# And since we use endpoints, they go through reactor.connectTCP # And since we use endpoints, they go through reactor.connectTCP
# which works differently to connectSSL on the testing reactor # which works differently to connectSSL on the testing reactor