mirror of
https://mau.dev/maunium/synapse.git
synced 2024-10-01 01:36:05 -04:00
23740eaa3d
During the migration the automated script to update the copyright headers accidentally got rid of some of the existing copyright lines. Reinstate them.
231 lines
8.1 KiB
Python
231 lines
8.1 KiB
Python
#
|
|
# 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,
|
|
)
|