Split multiplart email sending into a dedicated handler (#9977)

Co-authored-by: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com>
pull/10013/head
Brendan Abolivier 2021-05-17 12:33:38 +02:00 committed by GitHub
parent 6660912226
commit 41ac128fd3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 121 additions and 89 deletions

1
changelog.d/9977.misc Normal file
View File

@ -0,0 +1 @@
Split multipart email sending into a dedicated handler.

View File

@ -15,12 +15,9 @@
import email.mime.multipart import email.mime.multipart
import email.utils import email.utils
import logging import logging
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from typing import TYPE_CHECKING, List, Optional, Tuple from typing import TYPE_CHECKING, List, Optional, Tuple
from synapse.api.errors import StoreError, SynapseError from synapse.api.errors import StoreError, SynapseError
from synapse.logging.context import make_deferred_yieldable
from synapse.metrics.background_process_metrics import wrap_as_background_process from synapse.metrics.background_process_metrics import wrap_as_background_process
from synapse.types import UserID from synapse.types import UserID
from synapse.util import stringutils from synapse.util import stringutils
@ -36,9 +33,11 @@ class AccountValidityHandler:
self.hs = hs self.hs = hs
self.config = hs.config self.config = hs.config
self.store = self.hs.get_datastore() self.store = self.hs.get_datastore()
self.sendmail = self.hs.get_sendmail() self.send_email_handler = self.hs.get_send_email_handler()
self.clock = self.hs.get_clock() self.clock = self.hs.get_clock()
self._app_name = self.hs.config.email_app_name
self._account_validity_enabled = ( self._account_validity_enabled = (
hs.config.account_validity.account_validity_enabled hs.config.account_validity.account_validity_enabled
) )
@ -63,23 +62,10 @@ class AccountValidityHandler:
self._template_text = ( self._template_text = (
hs.config.account_validity.account_validity_template_text hs.config.account_validity.account_validity_template_text
) )
account_validity_renew_email_subject = ( self._renew_email_subject = (
hs.config.account_validity.account_validity_renew_email_subject hs.config.account_validity.account_validity_renew_email_subject
) )
try:
app_name = hs.config.email_app_name
self._subject = account_validity_renew_email_subject % {"app": app_name}
self._from_string = hs.config.email_notif_from % {"app": app_name}
except Exception:
# If substitution failed, fall back to the bare strings.
self._subject = account_validity_renew_email_subject
self._from_string = hs.config.email_notif_from
self._raw_from = email.utils.parseaddr(self._from_string)[1]
# Check the renewal emails to send and send them every 30min. # Check the renewal emails to send and send them every 30min.
if hs.config.run_background_tasks: if hs.config.run_background_tasks:
self.clock.looping_call(self._send_renewal_emails, 30 * 60 * 1000) self.clock.looping_call(self._send_renewal_emails, 30 * 60 * 1000)
@ -159,38 +145,17 @@ class AccountValidityHandler:
} }
html_text = self._template_html.render(**template_vars) html_text = self._template_html.render(**template_vars)
html_part = MIMEText(html_text, "html", "utf8")
plain_text = self._template_text.render(**template_vars) plain_text = self._template_text.render(**template_vars)
text_part = MIMEText(plain_text, "plain", "utf8")
for address in addresses: for address in addresses:
raw_to = email.utils.parseaddr(address)[1] raw_to = email.utils.parseaddr(address)[1]
multipart_msg = MIMEMultipart("alternative") await self.send_email_handler.send_email(
multipart_msg["Subject"] = self._subject email_address=raw_to,
multipart_msg["From"] = self._from_string subject=self._renew_email_subject,
multipart_msg["To"] = address app_name=self._app_name,
multipart_msg["Date"] = email.utils.formatdate() html=html_text,
multipart_msg["Message-ID"] = email.utils.make_msgid() text=plain_text,
multipart_msg.attach(text_part)
multipart_msg.attach(html_part)
logger.info("Sending renewal email to %s", address)
await make_deferred_yieldable(
self.sendmail(
self.hs.config.email_smtp_host,
self._raw_from,
raw_to,
multipart_msg.as_string().encode("utf8"),
reactor=self.hs.get_reactor(),
port=self.hs.config.email_smtp_port,
requireAuthentication=self.hs.config.email_smtp_user is not None,
username=self.hs.config.email_smtp_user,
password=self.hs.config.email_smtp_pass,
requireTransportSecurity=self.hs.config.require_transport_security,
)
) )
await self.store.set_renewal_mail_status(user_id=user_id, email_sent=True) await self.store.set_renewal_mail_status(user_id=user_id, email_sent=True)

View File

@ -0,0 +1,98 @@
# Copyright 2021 The Matrix.org C.I.C. Foundation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import email.utils
import logging
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from typing import TYPE_CHECKING
from synapse.logging.context import make_deferred_yieldable
if TYPE_CHECKING:
from synapse.server import HomeServer
logger = logging.getLogger(__name__)
class SendEmailHandler:
def __init__(self, hs: "HomeServer"):
self.hs = hs
self._sendmail = hs.get_sendmail()
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
self._smtp_user = hs.config.email.email_smtp_user
self._smtp_pass = hs.config.email.email_smtp_pass
self._require_transport_security = hs.config.email.require_transport_security
async def send_email(
self,
email_address: str,
subject: str,
app_name: str,
html: str,
text: str,
) -> 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.
"""
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", "utf8")
text_part = MIMEText(text, "plain", "utf8")
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()
multipart_msg.attach(text_part)
multipart_msg.attach(html_part)
logger.info("Sending email to %s" % email_address)
await make_deferred_yieldable(
self._sendmail(
self._smtp_host,
raw_from,
raw_to,
multipart_msg.as_string().encode("utf8"),
reactor=self._reactor,
port=self._smtp_port,
requireAuthentication=self._smtp_user is not None,
username=self._smtp_user,
password=self._smtp_pass,
requireTransportSecurity=self._require_transport_security,
)
)

View File

@ -12,12 +12,8 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import email.mime.multipart
import email.utils
import logging import logging
import urllib.parse import urllib.parse
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Optional, TypeVar from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Optional, TypeVar
import bleach import bleach
@ -27,7 +23,6 @@ from synapse.api.constants import EventTypes, Membership
from synapse.api.errors import StoreError from synapse.api.errors import StoreError
from synapse.config.emailconfig import EmailSubjectConfig from synapse.config.emailconfig import EmailSubjectConfig
from synapse.events import EventBase from synapse.events import EventBase
from synapse.logging.context import make_deferred_yieldable
from synapse.push.presentable_names import ( from synapse.push.presentable_names import (
calculate_room_name, calculate_room_name,
descriptor_from_member_events, descriptor_from_member_events,
@ -108,7 +103,7 @@ class Mailer:
self.template_html = template_html self.template_html = template_html
self.template_text = template_text self.template_text = template_text
self.sendmail = self.hs.get_sendmail() self.send_email_handler = hs.get_send_email_handler()
self.store = self.hs.get_datastore() self.store = self.hs.get_datastore()
self.state_store = self.hs.get_storage().state self.state_store = self.hs.get_storage().state
self.macaroon_gen = self.hs.get_macaroon_generator() self.macaroon_gen = self.hs.get_macaroon_generator()
@ -310,17 +305,6 @@ class Mailer:
self, email_address: str, subject: str, extra_template_vars: Dict[str, Any] self, email_address: str, subject: str, extra_template_vars: Dict[str, Any]
) -> None: ) -> None:
"""Send an email with the given information and template text""" """Send an email with the given information and template text"""
try:
from_string = self.hs.config.email_notif_from % {"app": self.app_name}
except TypeError:
from_string = self.hs.config.email_notif_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")
template_vars = { template_vars = {
"app_name": self.app_name, "app_name": self.app_name,
"server_name": self.hs.config.server.server_name, "server_name": self.hs.config.server.server_name,
@ -329,35 +313,14 @@ class Mailer:
template_vars.update(extra_template_vars) template_vars.update(extra_template_vars)
html_text = self.template_html.render(**template_vars) html_text = self.template_html.render(**template_vars)
html_part = MIMEText(html_text, "html", "utf8")
plain_text = self.template_text.render(**template_vars) plain_text = self.template_text.render(**template_vars)
text_part = MIMEText(plain_text, "plain", "utf8")
multipart_msg = MIMEMultipart("alternative") await self.send_email_handler.send_email(
multipart_msg["Subject"] = subject email_address=email_address,
multipart_msg["From"] = from_string subject=subject,
multipart_msg["To"] = email_address app_name=self.app_name,
multipart_msg["Date"] = email.utils.formatdate() html=html_text,
multipart_msg["Message-ID"] = email.utils.make_msgid() text=plain_text,
multipart_msg.attach(text_part)
multipart_msg.attach(html_part)
logger.info("Sending email to %s" % email_address)
await make_deferred_yieldable(
self.sendmail(
self.hs.config.email_smtp_host,
raw_from,
raw_to,
multipart_msg.as_string().encode("utf8"),
reactor=self.hs.get_reactor(),
port=self.hs.config.email_smtp_port,
requireAuthentication=self.hs.config.email_smtp_user is not None,
username=self.hs.config.email_smtp_user,
password=self.hs.config.email_smtp_pass,
requireTransportSecurity=self.hs.config.require_transport_security,
)
) )
async def _get_room_vars( async def _get_room_vars(

View File

@ -104,6 +104,7 @@ from synapse.handlers.room_list import RoomListHandler
from synapse.handlers.room_member import RoomMemberHandler, RoomMemberMasterHandler from synapse.handlers.room_member import RoomMemberHandler, RoomMemberMasterHandler
from synapse.handlers.room_member_worker import RoomMemberWorkerHandler from synapse.handlers.room_member_worker import RoomMemberWorkerHandler
from synapse.handlers.search import SearchHandler from synapse.handlers.search import SearchHandler
from synapse.handlers.send_email import SendEmailHandler
from synapse.handlers.set_password import SetPasswordHandler from synapse.handlers.set_password import SetPasswordHandler
from synapse.handlers.space_summary import SpaceSummaryHandler from synapse.handlers.space_summary import SpaceSummaryHandler
from synapse.handlers.sso import SsoHandler from synapse.handlers.sso import SsoHandler
@ -549,6 +550,10 @@ class HomeServer(metaclass=abc.ABCMeta):
def get_search_handler(self) -> SearchHandler: def get_search_handler(self) -> SearchHandler:
return SearchHandler(self) return SearchHandler(self)
@cache_in_self
def get_send_email_handler(self) -> SendEmailHandler:
return SendEmailHandler(self)
@cache_in_self @cache_in_self
def get_set_password_handler(self) -> SetPasswordHandler: def get_set_password_handler(self) -> SetPasswordHandler:
return SetPasswordHandler(self) return SetPasswordHandler(self)