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 <jan@jschaer.ch>
This commit is contained in:
Jan Schär 2022-07-25 17:27:19 +02:00 committed by GitHub
parent 908aeac44a
commit e8519e0ed2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 99 additions and 13 deletions

View File

@ -0,0 +1 @@
Support Implicit TLS for sending emails, enabled by the new option `force_tls`. Contributed by Jan Schär.

View File

@ -3187,9 +3187,17 @@ Server admins can configure custom templates for email content. See
This setting has the following sub-options: This setting has the following sub-options:
* `smtp_host`: The hostname of the outgoing SMTP server to use. Defaults to 'localhost'. * `smtp_host`: The hostname of the outgoing SMTP server to use. Defaults to 'localhost'.
* `smtp_port`: The port on the mail server for outgoing SMTP. Defaults to 25. * `smtp_port`: The port on the mail server for outgoing SMTP. Defaults to 465 if `force_tls` is true, else 25.
_Changed in Synapse 1.64.0:_ the default port is now aware of `force_tls`.
* `smtp_user` and `smtp_pass`: Username/password for authentication to the SMTP server. By default, no * `smtp_user` and `smtp_pass`: Username/password for authentication to the SMTP server. By default, no
authentication is attempted. authentication is attempted.
* `force_tls`: By default, Synapse connects over plain text and then optionally upgrades
to TLS via STARTTLS. If this option is set to true, TLS is used from the start (Implicit TLS),
and the option `require_transport_security` is ignored.
It is recommended to enable this if supported by your mail server.
_New in Synapse 1.64.0._
* `require_transport_security`: Set to true to require TLS transport security for SMTP. * `require_transport_security`: Set to true to require TLS transport security for SMTP.
By default, Synapse will connect over plain text, and will then switch to By default, Synapse will connect over plain text, and will then switch to
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,
@ -3254,6 +3262,7 @@ email:
smtp_port: 587 smtp_port: 587
smtp_user: "exampleusername" smtp_user: "exampleusername"
smtp_pass: "examplepassword" smtp_pass: "examplepassword"
force_tls: true
require_transport_security: true require_transport_security: true
enable_tls: false enable_tls: false
notif_from: "Your Friendly %(app)s homeserver <noreply@example.com>" notif_from: "Your Friendly %(app)s homeserver <noreply@example.com>"

View File

@ -85,14 +85,19 @@ class EmailConfig(Config):
if email_config is None: if email_config is None:
email_config = {} email_config = {}
self.force_tls = email_config.get("force_tls", False)
self.email_smtp_host = email_config.get("smtp_host", "localhost") self.email_smtp_host = email_config.get("smtp_host", "localhost")
self.email_smtp_port = email_config.get("smtp_port", 25) self.email_smtp_port = email_config.get(
"smtp_port", 465 if self.force_tls else 25
)
self.email_smtp_user = email_config.get("smtp_user", None) self.email_smtp_user = email_config.get("smtp_user", None)
self.email_smtp_pass = email_config.get("smtp_pass", None) self.email_smtp_pass = email_config.get("smtp_pass", None)
self.require_transport_security = email_config.get( self.require_transport_security = email_config.get(
"require_transport_security", False "require_transport_security", False
) )
self.enable_smtp_tls = email_config.get("enable_tls", True) self.enable_smtp_tls = email_config.get("enable_tls", True)
if self.force_tls and not self.enable_smtp_tls:
raise ConfigError("email.force_tls requires email.enable_tls to be true")
if self.require_transport_security and not self.enable_smtp_tls: if self.require_transport_security and not self.enable_smtp_tls:
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"

View File

@ -23,10 +23,12 @@ from pkg_resources import parse_version
import twisted import twisted
from twisted.internet.defer import Deferred from twisted.internet.defer import Deferred
from twisted.internet.interfaces import IOpenSSLContextFactory, IReactorTCP from twisted.internet.interfaces import IOpenSSLContextFactory
from twisted.internet.ssl import optionsForClientTLS
from twisted.mail.smtp import ESMTPSender, ESMTPSenderFactory from twisted.mail.smtp import ESMTPSender, ESMTPSenderFactory
from synapse.logging.context import make_deferred_yieldable from synapse.logging.context import make_deferred_yieldable
from synapse.types import ISynapseReactor
if TYPE_CHECKING: if TYPE_CHECKING:
from synapse.server import HomeServer from synapse.server import HomeServer
@ -48,7 +50,7 @@ class _NoTLSESMTPSender(ESMTPSender):
async def _sendmail( async def _sendmail(
reactor: IReactorTCP, reactor: ISynapseReactor,
smtphost: str, smtphost: str,
smtpport: int, smtpport: int,
from_addr: str, from_addr: str,
@ -59,6 +61,7 @@ async def _sendmail(
require_auth: bool = False, require_auth: bool = False,
require_tls: bool = False, require_tls: bool = False,
enable_tls: bool = True, enable_tls: bool = True,
force_tls: bool = False,
) -> None: ) -> None:
"""A simple wrapper around ESMTPSenderFactory, to allow substitution in tests """A simple wrapper around ESMTPSenderFactory, to allow substitution in tests
@ -73,8 +76,9 @@ async def _sendmail(
password: password to give when authenticating password: password to give when authenticating
require_auth: if auth is not offered, fail the request require_auth: if auth is not offered, fail the request
require_tls: if TLS is not offered, fail the reqest require_tls: if TLS is not offered, fail the reqest
enable_tls: True to enable TLS. 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.
""" """
msg = BytesIO(msg_bytes) msg = BytesIO(msg_bytes)
d: "Deferred[object]" = Deferred() d: "Deferred[object]" = Deferred()
@ -105,6 +109,16 @@ async def _sendmail(
# set to enable TLS. # set to enable TLS.
factory = build_sender_factory(hostname=smtphost if enable_tls else None) factory = build_sender_factory(hostname=smtphost if enable_tls else None)
if force_tls:
reactor.connectSSL(
smtphost,
smtpport,
factory,
optionsForClientTLS(smtphost),
timeout=30,
bindAddress=None,
)
else:
reactor.connectTCP( reactor.connectTCP(
smtphost, smtphost,
smtpport, smtpport,
@ -132,6 +146,7 @@ class SendEmailHandler:
self._smtp_pass = passwd.encode("utf-8") if passwd is not None else None self._smtp_pass = passwd.encode("utf-8") if passwd is not None else None
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._sendmail = _sendmail self._sendmail = _sendmail
@ -189,4 +204,5 @@ class SendEmailHandler:
require_auth=self._smtp_user is not None, require_auth=self._smtp_user is not None,
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,
) )

View File

@ -23,7 +23,7 @@ from twisted.internet.defer import ensureDeferred
from twisted.mail import interfaces, smtp from twisted.mail import interfaces, smtp
from tests.server import FakeTransport from tests.server import FakeTransport
from tests.unittest import HomeserverTestCase from tests.unittest import HomeserverTestCase, override_config
@implementer(interfaces.IMessageDelivery) @implementer(interfaces.IMessageDelivery)
@ -110,3 +110,58 @@ class SendEmailHandlerTestCase(HomeserverTestCase):
user, msg = message_delivery.messages.pop() user, msg = message_delivery.messages.pop()
self.assertEqual(str(user), "foo@bar.com") self.assertEqual(str(user), "foo@bar.com")
self.assertIn(b"Subject: test subject", msg) self.assertIn(b"Subject: test subject", msg)
@override_config(
{
"email": {
"notif_from": "noreply@test",
"force_tls": True,
},
}
)
def test_send_email_force_tls(self):
"""Happy-path test that we can send email to an Implicit TLS server."""
h = self.hs.get_send_email_handler()
d = ensureDeferred(
h.send_email(
"foo@bar.com", "test subject", "Tests", "HTML content", "Text content"
)
)
# there should be an attempt to connect to localhost:465
self.assertEqual(len(self.reactor.sslClients), 1)
(
host,
port,
client_factory,
contextFactory,
_timeout,
_bindAddress,
) = self.reactor.sslClients[0]
self.assertEqual(host, "localhost")
self.assertEqual(port, 465)
# wire it up to an SMTP server
message_delivery = _DummyMessageDelivery()
server_protocol = smtp.ESMTP()
server_protocol.delivery = message_delivery
# make sure that the server uses the test reactor to set timeouts
server_protocol.callLater = self.reactor.callLater # type: ignore[assignment]
client_protocol = client_factory.buildProtocol(None)
client_protocol.makeConnection(FakeTransport(server_protocol, self.reactor))
server_protocol.makeConnection(
FakeTransport(
client_protocol,
self.reactor,
peer_address=IPv4Address("TCP", "127.0.0.1", 1234),
)
)
# the message should now get delivered
self.get_success(d, by=0.1)
# check it arrived
self.assertEqual(len(message_delivery.messages), 1)
user, msg = message_delivery.messages.pop()
self.assertEqual(str(user), "foo@bar.com")
self.assertIn(b"Subject: test subject", msg)