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

Commit

Permalink
Support Implicit TLS for sending emails (#13317)
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
jscissr committed Jul 25, 2022
1 parent 908aeac commit e8519e0
Show file tree
Hide file tree
Showing 5 changed files with 99 additions and 13 deletions.
1 change: 1 addition & 0 deletions changelog.d/13317.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Support Implicit TLS for sending emails, enabled by the new option `force_tls`. Contributed by Jan Schär.
11 changes: 10 additions & 1 deletion docs/usage/configuration/config_documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -3187,9 +3187,17 @@ Server admins can configure custom templates for email content. See

This setting has the following sub-options:
* `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
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.
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,
Expand Down Expand Up @@ -3254,6 +3262,7 @@ email:
smtp_port: 587
smtp_user: "exampleusername"
smtp_pass: "examplepassword"
force_tls: true
require_transport_security: true
enable_tls: false
notif_from: "Your Friendly %(app)s homeserver <noreply@example.com>"
Expand Down
7 changes: 6 additions & 1 deletion synapse/config/emailconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,14 +85,19 @@ def read_config(self, config: JsonDict, **kwargs: Any) -> None:
if email_config is None:
email_config = {}

self.force_tls = email_config.get("force_tls", False)
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_pass = email_config.get("smtp_pass", None)
self.require_transport_security = email_config.get(
"require_transport_security", False
)
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:
raise ConfigError(
"email.require_transport_security requires email.enable_tls to be true"
Expand Down
36 changes: 26 additions & 10 deletions synapse/handlers/send_email.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,12 @@

import twisted
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 synapse.logging.context import make_deferred_yieldable
from synapse.types import ISynapseReactor

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


async def _sendmail(
reactor: IReactorTCP,
reactor: ISynapseReactor,
smtphost: str,
smtpport: int,
from_addr: str,
Expand All @@ -59,6 +61,7 @@ async def _sendmail(
require_auth: bool = False,
require_tls: bool = False,
enable_tls: bool = True,
force_tls: bool = False,
) -> None:
"""A simple wrapper around ESMTPSenderFactory, to allow substitution in tests
Expand All @@ -73,8 +76,9 @@ async def _sendmail(
password: password to give when authenticating
require_auth: if auth is not offered, fail the request
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.
force_tls: True to enable Implicit TLS.
"""
msg = BytesIO(msg_bytes)
d: "Deferred[object]" = Deferred()
Expand Down Expand Up @@ -105,13 +109,23 @@ def build_sender_factory(**kwargs: Any) -> ESMTPSenderFactory:
# set to enable TLS.
factory = build_sender_factory(hostname=smtphost if enable_tls else None)

reactor.connectTCP(
smtphost,
smtpport,
factory,
timeout=30,
bindAddress=None,
)
if force_tls:
reactor.connectSSL(
smtphost,
smtpport,
factory,
optionsForClientTLS(smtphost),
timeout=30,
bindAddress=None,
)
else:
reactor.connectTCP(
smtphost,
smtpport,
factory,
timeout=30,
bindAddress=None,
)

await make_deferred_yieldable(d)

Expand All @@ -132,6 +146,7 @@ def __init__(self, hs: "HomeServer"):
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._enable_tls = hs.config.email.enable_smtp_tls
self._force_tls = hs.config.email.force_tls

self._sendmail = _sendmail

Expand Down Expand Up @@ -189,4 +204,5 @@ async def send_email(
require_auth=self._smtp_user is not None,
require_tls=self._require_transport_security,
enable_tls=self._enable_tls,
force_tls=self._force_tls,
)
57 changes: 56 additions & 1 deletion tests/handlers/test_send_email.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from twisted.mail import interfaces, smtp

from tests.server import FakeTransport
from tests.unittest import HomeserverTestCase
from tests.unittest import HomeserverTestCase, override_config


@implementer(interfaces.IMessageDelivery)
Expand Down Expand Up @@ -110,3 +110,58 @@ def test_send_email(self):
user, msg = message_delivery.messages.pop()
self.assertEqual(str(user), "foo@bar.com")
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)

0 comments on commit e8519e0

Please sign in to comment.