Skip to content
This repository has been archived by the owner on Apr 26, 2024. It is now read-only.

Split multiplart email sending into a dedicated handler #9977

Merged
merged 6 commits into from
May 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/9977.misc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Split multipart email sending into a dedicated handler.
55 changes: 10 additions & 45 deletions synapse/handlers/account_validity.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,9 @@
import email.mime.multipart
import email.utils
import logging
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from typing import TYPE_CHECKING, List, Optional, Tuple

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.types import UserID
from synapse.util import stringutils
Expand All @@ -36,9 +33,11 @@ def __init__(self, hs: "HomeServer"):
self.hs = hs
self.config = hs.config
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._app_name = self.hs.config.email_app_name

self._account_validity_enabled = (
hs.config.account_validity.account_validity_enabled
)
Expand All @@ -63,23 +62,10 @@ def __init__(self, hs: "HomeServer"):
self._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
)

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.
if hs.config.run_background_tasks:
self.clock.looping_call(self._send_renewal_emails, 30 * 60 * 1000)
Expand Down Expand Up @@ -159,38 +145,17 @@ async def _send_renewal_email(self, user_id: str, expiration_ts: int) -> None:
}

html_text = self._template_html.render(**template_vars)
html_part = MIMEText(html_text, "html", "utf8")

plain_text = self._template_text.render(**template_vars)
text_part = MIMEText(plain_text, "plain", "utf8")

for address in addresses:
raw_to = email.utils.parseaddr(address)[1]

multipart_msg = MIMEMultipart("alternative")
multipart_msg["Subject"] = self._subject
multipart_msg["From"] = self._from_string
multipart_msg["To"] = 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 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.send_email_handler.send_email(
email_address=raw_to,
subject=self._renew_email_subject,
app_name=self._app_name,
html=html_text,
text=plain_text,
)

await self.store.set_renewal_mail_status(user_id=user_id, email_sent=True)
Expand Down
98 changes: 98 additions & 0 deletions synapse/handlers/send_email.py
Original file line number Diff line number Diff line change
@@ -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:
babolivier marked this conversation as resolved.
Show resolved Hide resolved
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,
)
)
53 changes: 8 additions & 45 deletions synapse/push/mailer.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import email.mime.multipart
import email.utils
import logging
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

import bleach
Expand All @@ -27,7 +23,6 @@
from synapse.api.errors import StoreError
from synapse.config.emailconfig import EmailSubjectConfig
from synapse.events import EventBase
from synapse.logging.context import make_deferred_yieldable
from synapse.push.presentable_names import (
calculate_room_name,
descriptor_from_member_events,
Expand Down Expand Up @@ -108,7 +103,7 @@ def __init__(
self.template_html = template_html
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.state_store = self.hs.get_storage().state
self.macaroon_gen = self.hs.get_macaroon_generator()
Expand Down Expand Up @@ -310,17 +305,6 @@ async def send_email(
self, email_address: str, subject: str, extra_template_vars: Dict[str, Any]
) -> None:
"""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 = {
"app_name": self.app_name,
"server_name": self.hs.config.server.server_name,
Expand All @@ -329,35 +313,14 @@ async def send_email(
template_vars.update(extra_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)
text_part = MIMEText(plain_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.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,
)

await self.send_email_handler.send_email(
email_address=email_address,
subject=subject,
app_name=self.app_name,
html=html_text,
text=plain_text,
)

async def _get_room_vars(
Expand Down
5 changes: 5 additions & 0 deletions synapse/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@
from synapse.handlers.room_member import RoomMemberHandler, RoomMemberMasterHandler
from synapse.handlers.room_member_worker import RoomMemberWorkerHandler
from synapse.handlers.search import SearchHandler
from synapse.handlers.send_email import SendEmailHandler
from synapse.handlers.set_password import SetPasswordHandler
from synapse.handlers.space_summary import SpaceSummaryHandler
from synapse.handlers.sso import SsoHandler
Expand Down Expand Up @@ -549,6 +550,10 @@ def get_deactivate_account_handler(self) -> DeactivateAccountHandler:
def get_search_handler(self) -> SearchHandler:
return SearchHandler(self)

@cache_in_self
def get_send_email_handler(self) -> SendEmailHandler:
return SendEmailHandler(self)

@cache_in_self
def get_set_password_handler(self) -> SetPasswordHandler:
return SetPasswordHandler(self)
Expand Down