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>pull/13383/head
parent
908aeac44a
commit
e8519e0ed2
|
@ -0,0 +1 @@
|
||||||
|
Support Implicit TLS for sending emails, enabled by the new option `force_tls`. Contributed by Jan Schär.
|
|
@ -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>"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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,13 +109,23 @@ 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)
|
||||||
|
|
||||||
reactor.connectTCP(
|
if force_tls:
|
||||||
smtphost,
|
reactor.connectSSL(
|
||||||
smtpport,
|
smtphost,
|
||||||
factory,
|
smtpport,
|
||||||
timeout=30,
|
factory,
|
||||||
bindAddress=None,
|
optionsForClientTLS(smtphost),
|
||||||
)
|
timeout=30,
|
||||||
|
bindAddress=None,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
reactor.connectTCP(
|
||||||
|
smtphost,
|
||||||
|
smtpport,
|
||||||
|
factory,
|
||||||
|
timeout=30,
|
||||||
|
bindAddress=None,
|
||||||
|
)
|
||||||
|
|
||||||
await make_deferred_yieldable(d)
|
await make_deferred_yieldable(d)
|
||||||
|
|
||||||
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue