Skip to content

Commit f1ecf46

Browse files
authored
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.
1 parent 57bf449 commit f1ecf46

File tree

5 files changed

+69
-38
lines changed

5 files changed

+69
-38
lines changed

changelog.d/17849.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
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.

docs/usage/configuration/config_documentation.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -673,8 +673,9 @@ This setting has the following sub-options:
673673
TLS via STARTTLS *if the SMTP server supports it*. If this option is set,
674674
Synapse will refuse to connect unless the server supports STARTTLS.
675675
* `enable_tls`: By default, if the server supports TLS, it will be used, and the server
676-
must present a certificate that is valid for 'smtp_host'. If this option
676+
must present a certificate that is valid for `tlsname`. If this option
677677
is set to false, TLS will not be used.
678+
* `tlsname`: The domain name the SMTP server's TLS certificate must be valid for, defaulting to `smtp_host`.
678679
* `notif_from`: defines the "From" address to use when sending emails.
679680
It must be set if email sending is enabled. The placeholder '%(app)s' will be replaced by the application name,
680681
which is normally set in `app_name`, but may be overridden by the
@@ -741,6 +742,7 @@ email:
741742
force_tls: true
742743
require_transport_security: true
743744
enable_tls: false
745+
tlsname: mail.server.example.com
744746
notif_from: "Your Friendly %(app)s homeserver <[email protected]>"
745747
app_name: my_branded_matrix_server
746748
enable_notifs: true

synapse/config/emailconfig.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ def read_config(self, config: JsonDict, **kwargs: Any) -> None:
110110
raise ConfigError(
111111
"email.require_transport_security requires email.enable_tls to be true"
112112
)
113+
self.email_tlsname = email_config.get("tlsname", None)
113114

114115
if "app_name" in email_config:
115116
self.email_app_name = email_config["app_name"]

synapse/handlers/send_email.py

Lines changed: 60 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -47,15 +47,45 @@
4747
_is_old_twisted = parse_version(twisted.__version__) < parse_version("21")
4848

4949

50-
class _NoTLSESMTPSender(ESMTPSender):
51-
"""Extend ESMTPSender to disable TLS
50+
class _BackportESMTPSender(ESMTPSender):
51+
"""Extend old versions of ESMTPSender to configure TLS.
5252
53-
Unfortunately, before Twisted 21.2, ESMTPSender doesn't give an easy way to disable
54-
TLS, so we override its internal method which it uses to generate a context factory.
53+
Unfortunately, before Twisted 21.2, ESMTPSender doesn't give an easy way to
54+
disable TLS, or to configure the hostname used for TLS certificate validation.
55+
This backports the `hostname` parameter for that functionality.
5556
"""
5657

58+
__hostname: Optional[str]
59+
60+
def __init__(self, *args: Any, **kwargs: Any) -> None:
61+
""""""
62+
self.__hostname = kwargs.pop("hostname", None)
63+
super().__init__(*args, **kwargs)
64+
5765
def _getContextFactory(self) -> Optional[IOpenSSLContextFactory]:
58-
return None
66+
if self.context is not None:
67+
return self.context
68+
elif self.__hostname is None:
69+
return None # disable TLS if hostname is None
70+
return optionsForClientTLS(self.__hostname)
71+
72+
73+
class _BackportESMTPSenderFactory(ESMTPSenderFactory):
74+
"""An ESMTPSenderFactory for _BackportESMTPSender.
75+
76+
This backports the `hostname` parameter, to disable or configure TLS.
77+
"""
78+
79+
__hostname: Optional[str]
80+
81+
def __init__(self, *args: Any, **kwargs: Any) -> None:
82+
self.__hostname = kwargs.pop("hostname", None)
83+
super().__init__(*args, **kwargs)
84+
85+
def protocol(self, *args: Any, **kwargs: Any) -> ESMTPSender: # type: ignore
86+
# this overrides ESMTPSenderFactory's `protocol` attribute, with a Callable
87+
# instantiating our _BackportESMTPSender, providing the hostname parameter
88+
return _BackportESMTPSender(*args, **kwargs, hostname=self.__hostname)
5989

6090

6191
async def _sendmail(
@@ -71,6 +101,7 @@ async def _sendmail(
71101
require_tls: bool = False,
72102
enable_tls: bool = True,
73103
force_tls: bool = False,
104+
tlsname: Optional[str] = None,
74105
) -> None:
75106
"""A simple wrapper around ESMTPSenderFactory, to allow substitution in tests
76107
@@ -88,39 +119,33 @@ async def _sendmail(
88119
enable_tls: True to enable STARTTLS. If this is False and require_tls is True,
89120
the request will fail.
90121
force_tls: True to enable Implicit TLS.
122+
tlsname: the domain name expected as the TLS certificate's commonname,
123+
defaults to smtphost.
91124
"""
92125
msg = BytesIO(msg_bytes)
93126
d: "Deferred[object]" = Deferred()
94-
95-
def build_sender_factory(**kwargs: Any) -> ESMTPSenderFactory:
96-
return ESMTPSenderFactory(
97-
username,
98-
password,
99-
from_addr,
100-
to_addr,
101-
msg,
102-
d,
103-
heloFallback=True,
104-
requireAuthentication=require_auth,
105-
requireTransportSecurity=require_tls,
106-
**kwargs,
107-
)
108-
109-
factory: IProtocolFactory
110-
if _is_old_twisted:
111-
# before twisted 21.2, we have to override the ESMTPSender protocol to disable
112-
# TLS
113-
factory = build_sender_factory()
114-
115-
if not enable_tls:
116-
factory.protocol = _NoTLSESMTPSender
117-
else:
118-
# for twisted 21.2 and later, there is a 'hostname' parameter which we should
119-
# set to enable TLS.
120-
factory = build_sender_factory(hostname=smtphost if enable_tls else None)
127+
if not enable_tls:
128+
tlsname = None
129+
elif tlsname is None:
130+
tlsname = smtphost
131+
132+
factory: IProtocolFactory = (
133+
_BackportESMTPSenderFactory if _is_old_twisted else ESMTPSenderFactory
134+
)(
135+
username,
136+
password,
137+
from_addr,
138+
to_addr,
139+
msg,
140+
d,
141+
heloFallback=True,
142+
requireAuthentication=require_auth,
143+
requireTransportSecurity=require_tls,
144+
hostname=tlsname,
145+
)
121146

122147
if force_tls:
123-
factory = TLSMemoryBIOFactory(optionsForClientTLS(smtphost), True, factory)
148+
factory = TLSMemoryBIOFactory(optionsForClientTLS(tlsname), True, factory)
124149

125150
endpoint = HostnameEndpoint(
126151
reactor, smtphost, smtpport, timeout=30, bindAddress=None
@@ -148,6 +173,7 @@ def __init__(self, hs: "HomeServer"):
148173
self._require_transport_security = hs.config.email.require_transport_security
149174
self._enable_tls = hs.config.email.enable_smtp_tls
150175
self._force_tls = hs.config.email.force_tls
176+
self._tlsname = hs.config.email.email_tlsname
151177

152178
self._sendmail = _sendmail
153179

@@ -227,4 +253,5 @@ async def send_email(
227253
require_tls=self._require_transport_security,
228254
enable_tls=self._enable_tls,
229255
force_tls=self._force_tls,
256+
tlsname=self._tlsname,
230257
)

tests/handlers/test_send_email.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ def test_send_email(self) -> None:
163163
"email": {
164164
"notif_from": "noreply@test",
165165
"force_tls": True,
166+
"tlsname": "example.org",
166167
},
167168
}
168169
)
@@ -186,10 +187,9 @@ def test_send_email_force_tls(self) -> None:
186187
self.assertEqual(host, self.reactor.lookups["localhost"])
187188
self.assertEqual(port, 465)
188189
# We need to make sure that TLS is happenning
189-
self.assertIsInstance(
190-
client_factory._wrappedFactory._testingContextFactory,
191-
ClientTLSOptions,
192-
)
190+
context_factory = client_factory._wrappedFactory._testingContextFactory
191+
self.assertIsInstance(context_factory, ClientTLSOptions)
192+
self.assertEqual(context_factory._hostname, "example.org") # tlsname
193193
# And since we use endpoints, they go through reactor.connectTCP
194194
# which works differently to connectSSL on the testing reactor
195195

0 commit comments

Comments
 (0)