Skip to content
This repository was archived by the owner on Apr 26, 2024. It is now read-only.

Commit e8519e0

Browse files
authored
Support Implicit TLS for sending emails (#13317)
Previously, TLS could only be used with STARTTLS. Add a new option `force_tls`, where TLS is used from the start. Implicit TLS is recommended over STARTLS, see https://datatracker.ietf.org/doc/html/rfc8314 Fixes #8046. Signed-off-by: Jan Schär <[email protected]>
1 parent 908aeac commit e8519e0

File tree

5 files changed

+99
-13
lines changed

5 files changed

+99
-13
lines changed

changelog.d/13317.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Support Implicit TLS for sending emails, enabled by the new option `force_tls`. Contributed by Jan Schär.

docs/usage/configuration/config_documentation.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3187,9 +3187,17 @@ Server admins can configure custom templates for email content. See
31873187

31883188
This setting has the following sub-options:
31893189
* `smtp_host`: The hostname of the outgoing SMTP server to use. Defaults to 'localhost'.
3190-
* `smtp_port`: The port on the mail server for outgoing SMTP. Defaults to 25.
3190+
* `smtp_port`: The port on the mail server for outgoing SMTP. Defaults to 465 if `force_tls` is true, else 25.
3191+
3192+
_Changed in Synapse 1.64.0:_ the default port is now aware of `force_tls`.
31913193
* `smtp_user` and `smtp_pass`: Username/password for authentication to the SMTP server. By default, no
31923194
authentication is attempted.
3195+
* `force_tls`: By default, Synapse connects over plain text and then optionally upgrades
3196+
to TLS via STARTTLS. If this option is set to true, TLS is used from the start (Implicit TLS),
3197+
and the option `require_transport_security` is ignored.
3198+
It is recommended to enable this if supported by your mail server.
3199+
3200+
_New in Synapse 1.64.0._
31933201
* `require_transport_security`: Set to true to require TLS transport security for SMTP.
31943202
By default, Synapse will connect over plain text, and will then switch to
31953203
TLS via STARTTLS *if the SMTP server supports it*. If this option is set,
@@ -3254,6 +3262,7 @@ email:
32543262
smtp_port: 587
32553263
smtp_user: "exampleusername"
32563264
smtp_pass: "examplepassword"
3265+
force_tls: true
32573266
require_transport_security: true
32583267
enable_tls: false
32593268
notif_from: "Your Friendly %(app)s homeserver <[email protected]>"

synapse/config/emailconfig.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,14 +85,19 @@ def read_config(self, config: JsonDict, **kwargs: Any) -> None:
8585
if email_config is None:
8686
email_config = {}
8787

88+
self.force_tls = email_config.get("force_tls", False)
8889
self.email_smtp_host = email_config.get("smtp_host", "localhost")
89-
self.email_smtp_port = email_config.get("smtp_port", 25)
90+
self.email_smtp_port = email_config.get(
91+
"smtp_port", 465 if self.force_tls else 25
92+
)
9093
self.email_smtp_user = email_config.get("smtp_user", None)
9194
self.email_smtp_pass = email_config.get("smtp_pass", None)
9295
self.require_transport_security = email_config.get(
9396
"require_transport_security", False
9497
)
9598
self.enable_smtp_tls = email_config.get("enable_tls", True)
99+
if self.force_tls and not self.enable_smtp_tls:
100+
raise ConfigError("email.force_tls requires email.enable_tls to be true")
96101
if self.require_transport_security and not self.enable_smtp_tls:
97102
raise ConfigError(
98103
"email.require_transport_security requires email.enable_tls to be true"

synapse/handlers/send_email.py

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,12 @@
2323

2424
import twisted
2525
from twisted.internet.defer import Deferred
26-
from twisted.internet.interfaces import IOpenSSLContextFactory, IReactorTCP
26+
from twisted.internet.interfaces import IOpenSSLContextFactory
27+
from twisted.internet.ssl import optionsForClientTLS
2728
from twisted.mail.smtp import ESMTPSender, ESMTPSenderFactory
2829

2930
from synapse.logging.context import make_deferred_yieldable
31+
from synapse.types import ISynapseReactor
3032

3133
if TYPE_CHECKING:
3234
from synapse.server import HomeServer
@@ -48,7 +50,7 @@ def _getContextFactory(self) -> Optional[IOpenSSLContextFactory]:
4850

4951

5052
async def _sendmail(
51-
reactor: IReactorTCP,
53+
reactor: ISynapseReactor,
5254
smtphost: str,
5355
smtpport: int,
5456
from_addr: str,
@@ -59,6 +61,7 @@ async def _sendmail(
5961
require_auth: bool = False,
6062
require_tls: bool = False,
6163
enable_tls: bool = True,
64+
force_tls: bool = False,
6265
) -> None:
6366
"""A simple wrapper around ESMTPSenderFactory, to allow substitution in tests
6467
@@ -73,8 +76,9 @@ async def _sendmail(
7376
password: password to give when authenticating
7477
require_auth: if auth is not offered, fail the request
7578
require_tls: if TLS is not offered, fail the reqest
76-
enable_tls: True to enable TLS. If this is False and require_tls is True,
79+
enable_tls: True to enable STARTTLS. If this is False and require_tls is True,
7780
the request will fail.
81+
force_tls: True to enable Implicit TLS.
7882
"""
7983
msg = BytesIO(msg_bytes)
8084
d: "Deferred[object]" = Deferred()
@@ -105,13 +109,23 @@ def build_sender_factory(**kwargs: Any) -> ESMTPSenderFactory:
105109
# set to enable TLS.
106110
factory = build_sender_factory(hostname=smtphost if enable_tls else None)
107111

108-
reactor.connectTCP(
109-
smtphost,
110-
smtpport,
111-
factory,
112-
timeout=30,
113-
bindAddress=None,
114-
)
112+
if force_tls:
113+
reactor.connectSSL(
114+
smtphost,
115+
smtpport,
116+
factory,
117+
optionsForClientTLS(smtphost),
118+
timeout=30,
119+
bindAddress=None,
120+
)
121+
else:
122+
reactor.connectTCP(
123+
smtphost,
124+
smtpport,
125+
factory,
126+
timeout=30,
127+
bindAddress=None,
128+
)
115129

116130
await make_deferred_yieldable(d)
117131

@@ -132,6 +146,7 @@ def __init__(self, hs: "HomeServer"):
132146
self._smtp_pass = passwd.encode("utf-8") if passwd is not None else None
133147
self._require_transport_security = hs.config.email.require_transport_security
134148
self._enable_tls = hs.config.email.enable_smtp_tls
149+
self._force_tls = hs.config.email.force_tls
135150

136151
self._sendmail = _sendmail
137152

@@ -189,4 +204,5 @@ async def send_email(
189204
require_auth=self._smtp_user is not None,
190205
require_tls=self._require_transport_security,
191206
enable_tls=self._enable_tls,
207+
force_tls=self._force_tls,
192208
)

tests/handlers/test_send_email.py

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
from twisted.mail import interfaces, smtp
2424

2525
from tests.server import FakeTransport
26-
from tests.unittest import HomeserverTestCase
26+
from tests.unittest import HomeserverTestCase, override_config
2727

2828

2929
@implementer(interfaces.IMessageDelivery)
@@ -110,3 +110,58 @@ def test_send_email(self):
110110
user, msg = message_delivery.messages.pop()
111111
self.assertEqual(str(user), "[email protected]")
112112
self.assertIn(b"Subject: test subject", msg)
113+
114+
@override_config(
115+
{
116+
"email": {
117+
"notif_from": "noreply@test",
118+
"force_tls": True,
119+
},
120+
}
121+
)
122+
def test_send_email_force_tls(self):
123+
"""Happy-path test that we can send email to an Implicit TLS server."""
124+
h = self.hs.get_send_email_handler()
125+
d = ensureDeferred(
126+
h.send_email(
127+
"[email protected]", "test subject", "Tests", "HTML content", "Text content"
128+
)
129+
)
130+
# there should be an attempt to connect to localhost:465
131+
self.assertEqual(len(self.reactor.sslClients), 1)
132+
(
133+
host,
134+
port,
135+
client_factory,
136+
contextFactory,
137+
_timeout,
138+
_bindAddress,
139+
) = self.reactor.sslClients[0]
140+
self.assertEqual(host, "localhost")
141+
self.assertEqual(port, 465)
142+
143+
# wire it up to an SMTP server
144+
message_delivery = _DummyMessageDelivery()
145+
server_protocol = smtp.ESMTP()
146+
server_protocol.delivery = message_delivery
147+
# make sure that the server uses the test reactor to set timeouts
148+
server_protocol.callLater = self.reactor.callLater # type: ignore[assignment]
149+
150+
client_protocol = client_factory.buildProtocol(None)
151+
client_protocol.makeConnection(FakeTransport(server_protocol, self.reactor))
152+
server_protocol.makeConnection(
153+
FakeTransport(
154+
client_protocol,
155+
self.reactor,
156+
peer_address=IPv4Address("TCP", "127.0.0.1", 1234),
157+
)
158+
)
159+
160+
# the message should now get delivered
161+
self.get_success(d, by=0.1)
162+
163+
# check it arrived
164+
self.assertEqual(len(message_delivery.messages), 1)
165+
user, msg = message_delivery.messages.pop()
166+
self.assertEqual(str(user), "[email protected]")
167+
self.assertIn(b"Subject: test subject", msg)

0 commit comments

Comments
 (0)