send_email.py 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  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, Optional
  20. from pkg_resources import parse_version
  21. import twisted
  22. from twisted.internet.defer import Deferred
  23. from twisted.internet.interfaces import IOpenSSLContextFactory, IReactorTCP
  24. from twisted.mail.smtp import ESMTPSender, ESMTPSenderFactory
  25. from synapse.logging.context import make_deferred_yieldable
  26. if TYPE_CHECKING:
  27. from synapse.server import HomeServer
  28. logger = logging.getLogger(__name__)
  29. _is_old_twisted = parse_version(twisted.__version__) < parse_version("21")
  30. class _NoTLSESMTPSender(ESMTPSender):
  31. """Extend ESMTPSender to disable TLS
  32. Unfortunately, before Twisted 21.2, ESMTPSender doesn't give an easy way to disable
  33. TLS, so we override its internal method which it uses to generate a context factory.
  34. """
  35. def _getContextFactory(self) -> Optional[IOpenSSLContextFactory]:
  36. return None
  37. async def _sendmail(
  38. reactor: IReactorTCP,
  39. smtphost: str,
  40. smtpport: int,
  41. from_addr: str,
  42. to_addr: str,
  43. msg_bytes: bytes,
  44. username: Optional[bytes] = None,
  45. password: Optional[bytes] = None,
  46. require_auth: bool = False,
  47. require_tls: bool = False,
  48. enable_tls: bool = True,
  49. ) -> None:
  50. """A simple wrapper around ESMTPSenderFactory, to allow substitution in tests
  51. Params:
  52. reactor: reactor to use to make the outbound connection
  53. smtphost: hostname to connect to
  54. smtpport: port to connect to
  55. from_addr: "From" address for email
  56. to_addr: "To" address for email
  57. msg_bytes: Message content
  58. username: username to authenticate with, if auth is enabled
  59. password: password to give when authenticating
  60. require_auth: if auth is not offered, fail the request
  61. require_tls: if TLS is not offered, fail the reqest
  62. enable_tls: True to enable TLS. If this is False and require_tls is True,
  63. the request will fail.
  64. """
  65. msg = BytesIO(msg_bytes)
  66. d: "Deferred[object]" = Deferred()
  67. def build_sender_factory(**kwargs: Any) -> ESMTPSenderFactory:
  68. return ESMTPSenderFactory(
  69. username,
  70. password,
  71. from_addr,
  72. to_addr,
  73. msg,
  74. d,
  75. heloFallback=True,
  76. requireAuthentication=require_auth,
  77. requireTransportSecurity=require_tls,
  78. **kwargs,
  79. )
  80. if _is_old_twisted:
  81. # before twisted 21.2, we have to override the ESMTPSender protocol to disable
  82. # TLS
  83. factory = build_sender_factory()
  84. if not enable_tls:
  85. factory.protocol = _NoTLSESMTPSender
  86. else:
  87. # for twisted 21.2 and later, there is a 'hostname' parameter which we should
  88. # set to enable TLS.
  89. factory = build_sender_factory(hostname=smtphost if enable_tls else None)
  90. reactor.connectTCP(
  91. smtphost,
  92. smtpport,
  93. factory,
  94. timeout=30,
  95. bindAddress=None,
  96. )
  97. await make_deferred_yieldable(d)
  98. class SendEmailHandler:
  99. def __init__(self, hs: "HomeServer"):
  100. self.hs = hs
  101. self._reactor = hs.get_reactor()
  102. self._from = hs.config.email.email_notif_from
  103. self._smtp_host = hs.config.email.email_smtp_host
  104. self._smtp_port = hs.config.email.email_smtp_port
  105. user = hs.config.email.email_smtp_user
  106. self._smtp_user = user.encode("utf-8") if user is not None else None
  107. passwd = hs.config.email.email_smtp_pass
  108. self._smtp_pass = passwd.encode("utf-8") if passwd is not None else None
  109. self._require_transport_security = hs.config.email.require_transport_security
  110. self._enable_tls = hs.config.email.enable_smtp_tls
  111. self._sendmail = _sendmail
  112. async def send_email(
  113. self,
  114. email_address: str,
  115. subject: str,
  116. app_name: str,
  117. html: str,
  118. text: str,
  119. ) -> None:
  120. """Send a multipart email with the given information.
  121. Args:
  122. email_address: The address to send the email to.
  123. subject: The email's subject.
  124. app_name: The app name to include in the From header.
  125. html: The HTML content to include in the email.
  126. text: The plain text content to include in the email.
  127. """
  128. try:
  129. from_string = self._from % {"app": app_name}
  130. except (KeyError, TypeError):
  131. from_string = self._from
  132. raw_from = email.utils.parseaddr(from_string)[1]
  133. raw_to = email.utils.parseaddr(email_address)[1]
  134. if raw_to == "":
  135. raise RuntimeError("Invalid 'to' address")
  136. html_part = MIMEText(html, "html", "utf8")
  137. text_part = MIMEText(text, "plain", "utf8")
  138. multipart_msg = MIMEMultipart("alternative")
  139. multipart_msg["Subject"] = subject
  140. multipart_msg["From"] = from_string
  141. multipart_msg["To"] = email_address
  142. multipart_msg["Date"] = email.utils.formatdate()
  143. multipart_msg["Message-ID"] = email.utils.make_msgid()
  144. multipart_msg.attach(text_part)
  145. multipart_msg.attach(html_part)
  146. logger.info("Sending email to %s" % email_address)
  147. await self._sendmail(
  148. self._reactor,
  149. self._smtp_host,
  150. self._smtp_port,
  151. raw_from,
  152. raw_to,
  153. multipart_msg.as_string().encode("utf8"),
  154. username=self._smtp_user,
  155. password=self._smtp_pass,
  156. require_auth=self._smtp_user is not None,
  157. require_tls=self._require_transport_security,
  158. enable_tls=self._enable_tls,
  159. )