# 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 json import logging import textwrap from dataclasses import dataclass from email.utils import make_msgid, parseaddr from typing import Any, Optional import nh3 from flask_babel import gettext as __ from superset import app 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.core import HeaderDataType, send_email_smtp from superset.utils.decorators import statsd_gauge 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 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 @staticmethod def _get_smtp_domain() -> str: return parseaddr(app.config["SMTP_MAIL_FROM"])[1].split("@")[1] @staticmethod def _error_template(text: str) -> str: return __( """ Error: %(text)s """, text=text, ) 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 csv_data = None 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, ) # 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, ) else: html_table = "" call_to_action = __(app.config["EMAIL_REPORTS_CTA"]) img_tags = [] for msgid in images.keys(): img_tags.append( f"""