123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265 |
- # -*- coding: utf-8 -*-
- # Copyright 2019 New Vector Ltd
- #
- # 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.mime.multipart
- import email.utils
- import logging
- from email.mime.multipart import MIMEMultipart
- from email.mime.text import MIMEText
- from typing import List
- from synapse.api.errors import StoreError
- from synapse.logging.context import make_deferred_yieldable
- from synapse.metrics.background_process_metrics import run_as_background_process
- from synapse.types import UserID
- from synapse.util import stringutils
- try:
- from synapse.push.mailer import load_jinja2_templates
- except ImportError:
- load_jinja2_templates = None
- logger = logging.getLogger(__name__)
- class AccountValidityHandler(object):
- def __init__(self, hs):
- self.hs = hs
- self.config = hs.config
- self.store = self.hs.get_datastore()
- self.sendmail = self.hs.get_sendmail()
- self.clock = self.hs.get_clock()
- self._account_validity = self.hs.config.account_validity
- if self._account_validity.renew_by_email_enabled and load_jinja2_templates:
- # Don't do email-specific configuration if renewal by email is disabled.
- try:
- app_name = self.hs.config.email_app_name
- self._subject = self._account_validity.renew_email_subject % {
- "app": app_name
- }
- self._from_string = self.hs.config.email_notif_from % {"app": app_name}
- except Exception:
- # If substitution failed, fall back to the bare strings.
- self._subject = self._account_validity.renew_email_subject
- self._from_string = self.hs.config.email_notif_from
- self._raw_from = email.utils.parseaddr(self._from_string)[1]
- self._template_html, self._template_text = load_jinja2_templates(
- self.config.email_template_dir,
- [
- self.config.email_expiry_template_html,
- self.config.email_expiry_template_text,
- ],
- apply_format_ts_filter=True,
- apply_mxc_to_http_filter=True,
- public_baseurl=self.config.public_baseurl,
- )
- # Check the renewal emails to send and send them every 30min.
- def send_emails():
- # run as a background process to make sure that the database transactions
- # have a logcontext to report to
- return run_as_background_process(
- "send_renewals", self._send_renewal_emails
- )
- self.clock.looping_call(send_emails, 30 * 60 * 1000)
- async def _send_renewal_emails(self):
- """Gets the list of users whose account is expiring in the amount of time
- configured in the ``renew_at`` parameter from the ``account_validity``
- configuration, and sends renewal emails to all of these users as long as they
- have an email 3PID attached to their account.
- """
- expiring_users = await self.store.get_users_expiring_soon()
- if expiring_users:
- for user in expiring_users:
- await self._send_renewal_email(
- user_id=user["user_id"], expiration_ts=user["expiration_ts_ms"]
- )
- async def send_renewal_email_to_user(self, user_id: str):
- expiration_ts = await self.store.get_expiration_ts_for_user(user_id)
- await self._send_renewal_email(user_id, expiration_ts)
- async def _send_renewal_email(self, user_id: str, expiration_ts: int):
- """Sends out a renewal email to every email address attached to the given user
- with a unique link allowing them to renew their account.
- Args:
- user_id: ID of the user to send email(s) to.
- expiration_ts: Timestamp in milliseconds for the expiration date of
- this user's account (used in the email templates).
- """
- addresses = await self._get_email_addresses_for_user(user_id)
- # Stop right here if the user doesn't have at least one email address.
- # In this case, they will have to ask their server admin to renew their
- # account manually.
- # We don't need to do a specific check to make sure the account isn't
- # deactivated, as a deactivated account isn't supposed to have any
- # email address attached to it.
- if not addresses:
- return
- try:
- user_display_name = await self.store.get_profile_displayname(
- UserID.from_string(user_id).localpart
- )
- if user_display_name is None:
- user_display_name = user_id
- except StoreError:
- user_display_name = user_id
- renewal_token = await self._get_renewal_token(user_id)
- url = "%s_matrix/client/unstable/account_validity/renew?token=%s" % (
- self.hs.config.public_baseurl,
- renewal_token,
- )
- template_vars = {
- "display_name": user_display_name,
- "expiration_ts": expiration_ts,
- "url": url,
- }
- 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.store.set_renewal_mail_status(user_id=user_id, email_sent=True)
- async def _get_email_addresses_for_user(self, user_id: str) -> List[str]:
- """Retrieve the list of email addresses attached to a user's account.
- Args:
- user_id: ID of the user to lookup email addresses for.
- Returns:
- Email addresses for this account.
- """
- threepids = await self.store.user_get_threepids(user_id)
- addresses = []
- for threepid in threepids:
- if threepid["medium"] == "email":
- addresses.append(threepid["address"])
- return addresses
- async def _get_renewal_token(self, user_id: str) -> str:
- """Generates a 32-byte long random string that will be inserted into the
- user's renewal email's unique link, then saves it into the database.
- Args:
- user_id: ID of the user to generate a string for.
- Returns:
- The generated string.
- Raises:
- StoreError(500): Couldn't generate a unique string after 5 attempts.
- """
- attempts = 0
- while attempts < 5:
- try:
- renewal_token = stringutils.random_string(32)
- await self.store.set_renewal_token_for_user(user_id, renewal_token)
- return renewal_token
- except StoreError:
- attempts += 1
- raise StoreError(500, "Couldn't generate a unique string as refresh string.")
- async def renew_account(self, renewal_token: str) -> bool:
- """Renews the account attached to a given renewal token by pushing back the
- expiration date by the current validity period in the server's configuration.
- Args:
- renewal_token: Token sent with the renewal request.
- Returns:
- Whether the provided token is valid.
- """
- try:
- user_id = await self.store.get_user_from_renewal_token(renewal_token)
- except StoreError:
- return False
- logger.debug("Renewing an account for user %s", user_id)
- await self.renew_account_for_user(user_id)
- return True
- async def renew_account_for_user(
- self, user_id: str, expiration_ts: int = None, email_sent: bool = False
- ) -> int:
- """Renews the account attached to a given user by pushing back the
- expiration date by the current validity period in the server's
- configuration.
- Args:
- renewal_token: Token sent with the renewal request.
- expiration_ts: New expiration date. Defaults to now + validity period.
- email_sen: Whether an email has been sent for this validity period.
- Defaults to False.
- Returns:
- New expiration date for this account, as a timestamp in
- milliseconds since epoch.
- """
- if expiration_ts is None:
- expiration_ts = self.clock.time_msec() + self._account_validity.period
- await self.store.set_account_validity_for_user(
- user_id=user_id, expiration_ts=expiration_ts, email_sent=email_sent
- )
- return expiration_ts
|