summary refs log tree commit diff
path: root/synapse/handlers/send_email.py
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--synapse/handlers/send_email.py230
1 files changed, 0 insertions, 230 deletions
diff --git a/synapse/handlers/send_email.py b/synapse/handlers/send_email.py
deleted file mode 100644

index 70cdb0721c..0000000000 --- a/synapse/handlers/send_email.py +++ /dev/null
@@ -1,230 +0,0 @@ -# -# This file is licensed under the Affero General Public License (AGPL) version 3. -# -# Copyright 2021 The Matrix.org C.I.C. Foundation -# Copyright (C) 2023 New Vector, Ltd -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# See the GNU Affero General Public License for more details: -# <https://www.gnu.org/licenses/agpl-3.0.html>. -# -# Originally licensed under the Apache License, Version 2.0: -# <http://www.apache.org/licenses/LICENSE-2.0>. -# -# [This file includes modifications made by New Vector Limited] -# -# - -import email.utils -import logging -from email.mime.multipart import MIMEMultipart -from email.mime.text import MIMEText -from io import BytesIO -from typing import TYPE_CHECKING, Any, Dict, Optional - -from pkg_resources import parse_version - -import twisted -from twisted.internet.defer import Deferred -from twisted.internet.endpoints import HostnameEndpoint -from twisted.internet.interfaces import IOpenSSLContextFactory, IProtocolFactory -from twisted.internet.ssl import optionsForClientTLS -from twisted.mail.smtp import ESMTPSender, ESMTPSenderFactory -from twisted.protocols.tls import TLSMemoryBIOFactory - -from synapse.logging.context import make_deferred_yieldable -from synapse.types import ISynapseReactor - -if TYPE_CHECKING: - from synapse.server import HomeServer - -logger = logging.getLogger(__name__) - -_is_old_twisted = parse_version(twisted.__version__) < parse_version("21") - - -class _NoTLSESMTPSender(ESMTPSender): - """Extend ESMTPSender to disable TLS - - Unfortunately, before Twisted 21.2, ESMTPSender doesn't give an easy way to disable - TLS, so we override its internal method which it uses to generate a context factory. - """ - - def _getContextFactory(self) -> Optional[IOpenSSLContextFactory]: - return None - - -async def _sendmail( - reactor: ISynapseReactor, - smtphost: str, - smtpport: int, - from_addr: str, - to_addr: str, - msg_bytes: bytes, - username: Optional[bytes] = None, - password: Optional[bytes] = None, - 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 - - Params: - reactor: reactor to use to make the outbound connection - smtphost: hostname to connect to - smtpport: port to connect to - from_addr: "From" address for email - to_addr: "To" address for email - msg_bytes: Message content - username: username to authenticate with, if auth is enabled - 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 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() - - def build_sender_factory(**kwargs: Any) -> ESMTPSenderFactory: - return ESMTPSenderFactory( - username, - password, - from_addr, - to_addr, - msg, - d, - heloFallback=True, - requireAuthentication=require_auth, - requireTransportSecurity=require_tls, - **kwargs, - ) - - factory: IProtocolFactory - if _is_old_twisted: - # before twisted 21.2, we have to override the ESMTPSender protocol to disable - # TLS - factory = build_sender_factory() - - if not enable_tls: - factory.protocol = _NoTLSESMTPSender - else: - # for twisted 21.2 and later, there is a 'hostname' parameter which we should - # set to enable TLS. - factory = build_sender_factory(hostname=smtphost if enable_tls else None) - - if force_tls: - factory = TLSMemoryBIOFactory(optionsForClientTLS(smtphost), True, factory) - - endpoint = HostnameEndpoint( - reactor, smtphost, smtpport, timeout=30, bindAddress=None - ) - - await make_deferred_yieldable(endpoint.connect(factory)) - - await make_deferred_yieldable(d) - - -class SendEmailHandler: - def __init__(self, hs: "HomeServer"): - self.hs = hs - - self._reactor = hs.get_reactor() - - self._from = hs.config.email.email_notif_from - self._smtp_host = hs.config.email.email_smtp_host - self._smtp_port = hs.config.email.email_smtp_port - - user = hs.config.email.email_smtp_user - self._smtp_user = user.encode("utf-8") if user is not None else None - passwd = hs.config.email.email_smtp_pass - 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 - - async def send_email( - self, - email_address: str, - subject: str, - app_name: str, - html: str, - text: str, - additional_headers: Optional[Dict[str, str]] = None, - ) -> None: - """Send a multipart email with the given information. - - Args: - email_address: The address to send the email to. - subject: The email's subject. - app_name: The app name to include in the From header. - html: The HTML content to include in the email. - text: The plain text content to include in the email. - additional_headers: A map of additional headers to include. - """ - try: - from_string = self._from % {"app": app_name} - except (KeyError, TypeError): - from_string = self._from - - raw_from = email.utils.parseaddr(from_string)[1] - raw_to = email.utils.parseaddr(email_address)[1] - - if raw_to == "": - raise RuntimeError("Invalid 'to' address") - - html_part = MIMEText(html, "html", "utf-8") - text_part = MIMEText(text, "plain", "utf-8") - - multipart_msg = MIMEMultipart("alternative") - multipart_msg["Subject"] = subject - multipart_msg["From"] = from_string - multipart_msg["To"] = email_address - multipart_msg["Date"] = email.utils.formatdate() - multipart_msg["Message-ID"] = email.utils.make_msgid() - - # Discourage automatic responses to Synapse's emails. - # Per RFC 3834, automatic responses should not be sent if the "Auto-Submitted" - # header is present with any value other than "no". See - # https://www.rfc-editor.org/rfc/rfc3834.html#section-5.1 - multipart_msg["Auto-Submitted"] = "auto-generated" - # Also include a Microsoft-Exchange specific header: - # https://learn.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxcmail/ced68690-498a-4567-9d14-5c01f974d8b1 - # which suggests it can take the value "All" to "suppress all auto-replies", - # or a comma separated list of auto-reply classes to suppress. - # The following stack overflow question has a little more context: - # https://stackoverflow.com/a/25324691/5252017 - # https://stackoverflow.com/a/61646381/5252017 - multipart_msg["X-Auto-Response-Suppress"] = "All" - - if additional_headers: - for header, value in additional_headers.items(): - multipart_msg[header] = value - - multipart_msg.attach(text_part) - multipart_msg.attach(html_part) - - logger.info("Sending email to %s" % email_address) - - await self._sendmail( - self._reactor, - self._smtp_host, - self._smtp_port, - raw_from, - raw_to, - multipart_msg.as_string().encode("utf8"), - username=self._smtp_user, - password=self._smtp_pass, - require_auth=self._smtp_user is not None, - require_tls=self._require_transport_security, - enable_tls=self._enable_tls, - force_tls=self._force_tls, - )