# Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you 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 logging import textwrap from dataclasses import dataclass from datetime import datetime from email.utils import make_msgid, parseaddr from typing import Any, Optional import nh3 from flask import current_app from flask_babel import gettext as __ from pytz import timezone from superset import is_feature_enabled from superset.exceptions import SupersetErrorsException from superset.reports.models import ReportRecipientType from superset.reports.notifications.base import BaseNotification from superset.reports.notifications.exceptions import NotificationError from superset.utils import json from superset.utils.core import HeaderDataType, send_email_smtp from superset.utils.decorators import statsd_gauge from superset.utils.link_redirect import process_html_links logger = logging.getLogger(__name__) TABLE_TAGS = {"table", "th", "tr", "td", "thead", "tbody", "tfoot"} TABLE_ATTRIBUTES = {"colspan", "rowspan", "halign", "border", "class"} ALLOWED_TAGS = { "a", "abbr", "acronym", "b", "blockquote", "br", "code", "div", "em", "i", "li", "ol", "p", "strong", "ul", }.union(TABLE_TAGS) ALLOWED_TABLE_ATTRIBUTES = {tag: TABLE_ATTRIBUTES for tag in TABLE_TAGS} ALLOWED_ATTRIBUTES = { "a": {"href", "title"}, "abbr": {"title"}, "acronym": {"title"}, **ALLOWED_TABLE_ATTRIBUTES, } @dataclass class EmailContent: body: str header_data: Optional[HeaderDataType] = None data: Optional[dict[str, Any]] = None pdf: Optional[dict[str, bytes]] = None images: Optional[dict[str, bytes]] = None class EmailNotification(BaseNotification): # pylint: disable=too-few-public-methods """ Sends an email notification for a report recipient """ type = ReportRecipientType.EMAIL now = datetime.now(timezone("UTC")) @property def _name(self) -> str: """Include date format in the name if feature flag is enabled""" return ( self._parse_name(self._content.name) if is_feature_enabled("DATE_FORMAT_IN_EMAIL_SUBJECT") else self._content.name ) @staticmethod def _get_smtp_domain() -> str: return parseaddr(current_app.config["SMTP_MAIL_FROM"])[1].split("@")[1] def _error_template(self, text: str) -> str: call_to_action = self._get_call_to_action() return __( """
Your report/alert was unable to be generated because of the following error: %(text)s
Please check your dashboard/chart for errors.
""", # noqa: E501 text=text, url=self._content.url, call_to_action=call_to_action, ) def _get_content(self) -> EmailContent: if self._content.text: return EmailContent(body=self._error_template(self._content.text)) # Get the domain from the 'From' address .. # and make a message id without the < > in the end domain = self._get_smtp_domain() images = {} if self._content.screenshots: images = { make_msgid(domain)[1:-1]: screenshot for screenshot in self._content.screenshots } # Strip any malicious HTML from the description # pylint: disable=no-member description = nh3.clean( self._content.description or "", tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRIBUTES, ) # Rewrite external links to go through the redirect warning page description = process_html_links(description) # Strip malicious HTML from embedded data, allowing only table elements if self._content.embedded_data is not None: df = self._content.embedded_data # pylint: disable=no-member html_table = nh3.clean( df.to_html(na_rep="", index=True, escape=True), # pandas will escape the HTML in cells already, so passing # more allowed tags here will not work tags=TABLE_TAGS, attributes=ALLOWED_TABLE_ATTRIBUTES, ) html_table = process_html_links(html_table) else: html_table = "" img_tags = [] for msgid in images.keys(): img_tags.append( f"""