You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

224 rivejä
8.0 KiB

  1. # Copyright 2021 The Matrix.org C.I.C. Foundation
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. import email.utils
  15. import logging
  16. from email.mime.multipart import MIMEMultipart
  17. from email.mime.text import MIMEText
  18. from io import BytesIO
  19. from typing import TYPE_CHECKING, Any, Dict, Optional
  20. from pkg_resources import parse_version
  21. import twisted
  22. from twisted.internet.defer import Deferred
  23. from twisted.internet.endpoints import HostnameEndpoint
  24. from twisted.internet.interfaces import IOpenSSLContextFactory, IProtocolFactory
  25. from twisted.internet.ssl import optionsForClientTLS
  26. from twisted.mail.smtp import ESMTPSender, ESMTPSenderFactory
  27. from twisted.protocols.tls import TLSMemoryBIOFactory
  28. from synapse.logging.context import make_deferred_yieldable
  29. from synapse.types import ISynapseReactor
  30. if TYPE_CHECKING:
  31. from synapse.server import HomeServer
  32. logger = logging.getLogger(__name__)
  33. _is_old_twisted = parse_version(twisted.__version__) < parse_version("21")
  34. class _NoTLSESMTPSender(ESMTPSender):
  35. """Extend ESMTPSender to disable TLS
  36. Unfortunately, before Twisted 21.2, ESMTPSender doesn't give an easy way to disable
  37. TLS, so we override its internal method which it uses to generate a context factory.
  38. """
  39. def _getContextFactory(self) -> Optional[IOpenSSLContextFactory]:
  40. return None
  41. async def _sendmail(
  42. reactor: ISynapseReactor,
  43. smtphost: str,
  44. smtpport: int,
  45. from_addr: str,
  46. to_addr: str,
  47. msg_bytes: bytes,
  48. username: Optional[bytes] = None,
  49. password: Optional[bytes] = None,
  50. require_auth: bool = False,
  51. require_tls: bool = False,
  52. enable_tls: bool = True,
  53. force_tls: bool = False,
  54. ) -> None:
  55. """A simple wrapper around ESMTPSenderFactory, to allow substitution in tests
  56. Params:
  57. reactor: reactor to use to make the outbound connection
  58. smtphost: hostname to connect to
  59. smtpport: port to connect to
  60. from_addr: "From" address for email
  61. to_addr: "To" address for email
  62. msg_bytes: Message content
  63. username: username to authenticate with, if auth is enabled
  64. password: password to give when authenticating
  65. require_auth: if auth is not offered, fail the request
  66. require_tls: if TLS is not offered, fail the reqest
  67. enable_tls: True to enable STARTTLS. If this is False and require_tls is True,
  68. the request will fail.
  69. force_tls: True to enable Implicit TLS.
  70. """
  71. msg = BytesIO(msg_bytes)
  72. d: "Deferred[object]" = Deferred()
  73. def build_sender_factory(**kwargs: Any) -> ESMTPSenderFactory:
  74. return ESMTPSenderFactory(
  75. username,
  76. password,
  77. from_addr,
  78. to_addr,
  79. msg,
  80. d,
  81. heloFallback=True,
  82. requireAuthentication=require_auth,
  83. requireTransportSecurity=require_tls,
  84. **kwargs,
  85. )
  86. factory: IProtocolFactory
  87. if _is_old_twisted:
  88. # before twisted 21.2, we have to override the ESMTPSender protocol to disable
  89. # TLS
  90. factory = build_sender_factory()
  91. if not enable_tls:
  92. factory.protocol = _NoTLSESMTPSender
  93. else:
  94. # for twisted 21.2 and later, there is a 'hostname' parameter which we should
  95. # set to enable TLS.
  96. factory = build_sender_factory(hostname=smtphost if enable_tls else None)
  97. if force_tls:
  98. factory = TLSMemoryBIOFactory(optionsForClientTLS(smtphost), True, factory)
  99. endpoint = HostnameEndpoint(
  100. reactor, smtphost, smtpport, timeout=30, bindAddress=None
  101. )
  102. await make_deferred_yieldable(endpoint.connect(factory))
  103. await make_deferred_yieldable(d)
  104. class SendEmailHandler:
  105. def __init__(self, hs: "HomeServer"):
  106. self.hs = hs
  107. self._reactor = hs.get_reactor()
  108. self._from = hs.config.email.email_notif_from
  109. self._smtp_host = hs.config.email.email_smtp_host
  110. self._smtp_port = hs.config.email.email_smtp_port
  111. user = hs.config.email.email_smtp_user
  112. self._smtp_user = user.encode("utf-8") if user is not None else None
  113. passwd = hs.config.email.email_smtp_pass
  114. self._smtp_pass = passwd.encode("utf-8") if passwd is not None else None
  115. self._require_transport_security = hs.config.email.require_transport_security
  116. self._enable_tls = hs.config.email.enable_smtp_tls
  117. self._force_tls = hs.config.email.force_tls
  118. self._sendmail = _sendmail
  119. async def send_email(
  120. self,
  121. email_address: str,
  122. subject: str,
  123. app_name: str,
  124. html: str,
  125. text: str,
  126. additional_headers: Optional[Dict[str, str]] = None,
  127. ) -> None:
  128. """Send a multipart email with the given information.
  129. Args:
  130. email_address: The address to send the email to.
  131. subject: The email's subject.
  132. app_name: The app name to include in the From header.
  133. html: The HTML content to include in the email.
  134. text: The plain text content to include in the email.
  135. additional_headers: A map of additional headers to include.
  136. """
  137. try:
  138. from_string = self._from % {"app": app_name}
  139. except (KeyError, TypeError):
  140. from_string = self._from
  141. raw_from = email.utils.parseaddr(from_string)[1]
  142. raw_to = email.utils.parseaddr(email_address)[1]
  143. if raw_to == "":
  144. raise RuntimeError("Invalid 'to' address")
  145. html_part = MIMEText(html, "html", "utf-8")
  146. text_part = MIMEText(text, "plain", "utf-8")
  147. multipart_msg = MIMEMultipart("alternative")
  148. multipart_msg["Subject"] = subject
  149. multipart_msg["From"] = from_string
  150. multipart_msg["To"] = email_address
  151. multipart_msg["Date"] = email.utils.formatdate()
  152. multipart_msg["Message-ID"] = email.utils.make_msgid()
  153. # Discourage automatic responses to Synapse's emails.
  154. # Per RFC 3834, automatic responses should not be sent if the "Auto-Submitted"
  155. # header is present with any value other than "no". See
  156. # https://www.rfc-editor.org/rfc/rfc3834.html#section-5.1
  157. multipart_msg["Auto-Submitted"] = "auto-generated"
  158. # Also include a Microsoft-Exchange specific header:
  159. # https://learn.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxcmail/ced68690-498a-4567-9d14-5c01f974d8b1
  160. # which suggests it can take the value "All" to "suppress all auto-replies",
  161. # or a comma separated list of auto-reply classes to suppress.
  162. # The following stack overflow question has a little more context:
  163. # https://stackoverflow.com/a/25324691/5252017
  164. # https://stackoverflow.com/a/61646381/5252017
  165. multipart_msg["X-Auto-Response-Suppress"] = "All"
  166. if additional_headers:
  167. for header, value in additional_headers.items():
  168. multipart_msg[header] = value
  169. multipart_msg.attach(text_part)
  170. multipart_msg.attach(html_part)
  171. logger.info("Sending email to %s" % email_address)
  172. await self._sendmail(
  173. self._reactor,
  174. self._smtp_host,
  175. self._smtp_port,
  176. raw_from,
  177. raw_to,
  178. multipart_msg.as_string().encode("utf8"),
  179. username=self._smtp_user,
  180. password=self._smtp_pass,
  181. require_auth=self._smtp_user is not None,
  182. require_tls=self._require_transport_security,
  183. enable_tls=self._enable_tls,
  184. force_tls=self._force_tls,
  185. )