angle-uparrow-clockwisearrow-counterclockwisearrow-down-uparrow-leftatcalendarcard-listchatcheckenvelopefolderhouseinfo-circlepencilpeoplepersonperson-fillperson-plusphoneplusquestion-circlesearchtagtrashx

электронной почте HTML с помощью BeautifulSoup

В документации к BeautifulSoup говорится, что он экономит программистам часы или дни работы. Это преуменьшение.

30 августа 2021 Обновленный 30 августа 2021
post main image
https://unsplash.com/@francesphotos

Я создал программу чтения электронной почты IMAP , используя IMAPClient и Flask. Устройство для чтения электронной почты IMAP декодирует электронное письмо в действительный HTML. Затем ему нужно отобразить этот HTML через браузер. Пока все работает нормально.

В этом посте я описываю, как я реализовал опцию в моем IMAP E-Mail Reader для блокировки небезопасных ресурсов в HTML. Для этого я использую BeautifulSoup и операции регулярных выражений Python.

Зачем блокировать небезопасные ресурсы

Внешние ресурсы в HTML обычно представляют собой файлы, которые включены в веб-страницу. Примерами являются images, stylesheets, библиотеки JavaScript. Проблема в том, что они подключают вас к удаленным системам. Если вы заботитесь о конфиденциальности, вы хотите избежать этого.

В своем браузере Firefox я использую uBlockOrigin, это не просто блокировщик рекламы. С сайта:

'uBlock Origin - это бесплатное и кроссплатформенное расширение браузера с открытым исходным кодом для фильтрации контента, в первую очередь направленное на нейтрализацию вторжения в частную жизнь эффективным, userдружественным методом'.

Большинство электронных писем HTML , которые мы получаем, содержат ссылки на внешние ресурсы, часто images. Подключившись к такому изображению, вы можете быть отслежены. Многие программы электронной почты пытаются блокировать эти внешние ресурсы по умолчанию и предлагают опцию их разрешения. В результате электронная почта может выглядеть странно, а конфиденциальность имеет свою цену.

Также могут быть письма HTML с намеренно добавленным кодом, Javascript, чтобы взломать ваш компьютер. Мы должны удалить этот код.

Почему BeautifulSoup

Вот некоторые способы, которые мы можем использовать для фильтрации электронной почты HTML :

  • re: Python операции с регулярными выражениями
  • BeautifulSoup: библиотека для соскабливания информации с веб-страниц и модификации содержимого
  • Scrapy: веб-скрейпинг framework

Python's re очень низкого уровня. Я часто использую его, но здесь он не кажется лучшим выбором. Scrapy - это framework и, вероятно, перебор. Остается BeautifulSoup.

Производительность не так важна для моего IMAP E-Mail Reader. Мне не нужно фильтровать тысячи страниц. Фильтрация нужна только при показе письма. В высокопроизводительной среде мы можем хранить отфильтрованные письма.

Парсер BeautifulSoup

Вначале у меня возникли проблемы с парсером (по умолчанию) 'html.parser'. Он работал, но в нескольких тегах изображений url изображения не заменялся. Конечно, это была моя ошибка, TLDR. BeautifulSoup рекомендует использовать либо парсер lxml, либо парсер html5lib.

Поскольку я хотел чистое решение Python , я выбрал html5parser, который обрабатывает HTML так, как это делает веб-браузер. Это очень важно. Без BeautifulSoup пришлось бы потратить целую вечность, чтобы написать код, способный справиться с (намеренно) плохим HTML.

Вывод BeautifulSoup

BeautifulSoup - это библиотека для извлечения данных из HTML. Это хорошо, но в нашем случае мы сначала удаляем элементы и изменяем элементы, а затем показываем результат в браузере.

BeautifulSoup имеет несколько вариантов вывода, но он всегда изменяет по крайней мере несколько элементов. Иногда это может быть хорошо, например, добавление недостающих тегов, но в других случаях это может быть не то, что мы хотим. Что-то вроде: давайте подождем и посмотрим.

Как блокировать небезопасные ресурсы

Самые важные вещи, которые мы должны сделать:

  • Полностью удалить небезопасные ресурсы
  • Заменить небезопасные ресурсы
  • Исправить небезопасные ресурсы

Полностью удалить небезопасные ресурсы

Мы всегда должны удалять все Javascript из письма HTML . Мы также хотим удалить другие элементы, например, ссылки на stylesheets.

Замена небезопасных ресурсов

Если мы удалим images , то письмо может испортиться. Чтобы предотвратить это, мы заменяем images в письме локальным изображением, прозрачным пикселем.

Исправление плохих ресурсов

Некоторые ссылки могут не содержать атрибута:

target="_blank"

Мы также хотим добавить этот атрибут:

rel="noopener noreferrer"

Это предотвращает передачу информации о реферере на целевой сайт.

Но подождите, есть еще CSS

В письме HTML могут быть CSS-стили, ссылающиеся на внешние images, шрифты. Сначала я хотел использовать пакет CSSUtils, но он не очень прощающий. Например:

background-image: url ('my_url')

генерирует исключение, потому что между 'url' и '(' есть пробел. Я также не смог найти другой подходящий пакет, поэтому решил использовать операции регулярных выражений Python.

Я хочу заменить CSS-код, который содержит 'url()' в части значения. На странице HTML мы можем иметь:

  • Встроенный CSS
  • CSS-элементы

Чтобы сократить код, для встроенного CSS мы полностью удаляем свойство, а для CSS-элементов заменяем часть значения на 'url()'.

HTMLMailFixer Class

Для фильтрации писем HTML я создал класс HTMLMailFixer, код которого прост для понимания.

# html_mail_fixer.py

from bs4 import BeautifulSoup
import re

class HTMLMailFixer:
    
    def __init__(
        self,
        parser='html5lib',
    ):
        self.__parser = parser
        self.forbidden = [
            'script', 'object', 'iframe',
        ]
        self.__allow_remote_resources = None
        self.__block_img_url = None
        self.__soup = None

    def __remove_forbidden_elems(self):
        for elem in self.__soup():
            if elem.name in self.forbidden:
                # remove
                elem.extract()

    def __fix_a_elems(self):
        for a_elem in self.__soup.find_all('a'):
            # add/replace
            a_elem['target'] = '_blank'
            a_elem['rel'] = 'noopener noreferrer'

    def __remove_link_elems(self):
        for link_elem in self.__soup.find_all('link'):
            link_href = link_elem.get('href')
            if link_href is None:
                continue
            # remove
            link_elem.extract()

    def __fix_img_elems(self):
        for img_elem in self.__soup.find_all('img'):
            img_src = img_elem.get('src')
            if img_src is None:
                continue
            # replace
            img_elem['src'] = self.__block_img_url

    def __fix_style_elems(self):
        """
        objective: remove any property with a value starting with 'url('
        actual: replace value 'url(url)' by 'url()'
        """
        match_url_start_pattern = re.compile(r':\s*url\s*\(', re.I)
        for style_elem in self.__soup.find_all('style'):
            # 'contents': [
            #    '\n.section-block {\n    padding: 1em;\n    background-image: url(https://whatever_image_1);\n}\n#logo {\n    margin-top: 10em;\n    background-image: url(https://whatever_image_1);\n}\n'
            # ]
            new_contents = []
            for content in style_elem.contents:
                chunks = re.split(match_url_start_pattern, content) 
                for i, chunk in enumerate(chunks):
                    if i == 0:
                        continue
                    chunks[i] = re.sub(r'.*?\)', ')', chunk)
                # reconstruct
                new_content = ': url(\'\''.join(chunks)
                new_contents.append(new_content)

            # replace
            style_elem.string.replace_with('\n'.join(new_contents))

    def __fix_inline_style(self):
        """
        search elems with style attribute.
        remove any property name:value having a value starting with 'url('
        """
        match_url_start_pattern = re.compile(r'^url\s*\(', re.I)
        for elem in self.__soup.find_all(attrs={'style': True}):
            style_attr = elem['style']
            new_props = []
            props = style_attr
            if ';' in style_attr:
                props = style_attr.split(';')
            for i, prop in enumerate(props):
                if ':' not in prop:
                    # malformed, skip
                    continue
                name, value = prop.split(':', 1)
                if isinstance(value, list):
                    # malformed, skip
                    continue
                value = value.strip()
                if re.match(match_url_start_pattern, value):
                    # found value starting with 'url(' so skip
                    continue
                new_props.append(name + ': ' + value)

            # replace
            elem['style'] = '; '.join(new_props)

    def fix_all(
        self,
        html=None,
        allow_remote_resources=False,
        block_img_url=None,
    ):
        self.__allow_remote_resources = allow_remote_resources
        self.__block_img_url = block_img_url

        # start soup
        self.__soup = BeautifulSoup(html, self.__parser)

        # remove, modify html elements
        self.__remove_forbidden_elems()
        self.__fix_a_elems()
        if not self.__allow_remote_resources:
            self.__remove_link_elems()
            if self.__block_img_url is not None:
                self.__fix_img_elems()
            self.__fix_inline_style()
            self.__fix_style_elems()
        # output
        output = str(self.__soup)
        return re.sub(r'\n\n+', '\n\n', output)

Использование:

from .htm_mail_fixer import HTMLMailFixer

html = 'YOUR_HTML'
allow_remote_resources = False
block_img_url = 'YOUR_PIXEL_URL'

html_mail_fixer = HTMLMailFixer()

html_fixed = html_mail_fixer.fix_all(
    html=html,
    allow_remote_resources=allow_remote_resources,
    block_img_url=block_img_url,
)

Пример

Это простой пример с некоторыми уродливыми HTML и CSS:

html = """<!DOCTYPE html PUBLIC "- / /w3c / /dtd html 4.01 transitional / /en">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/css/bootstrap.min.css" rel="stylesheet"><style>
.section-block{background-image: url ( ' https://whatever_image_1') !important;
font-size: 1.1em}
#logo {
   margin-top: 10em;background-image: url ( https://whatever_image_1 )}
.wino { color: #ff0000 }
</style>
</head><body style="font: inherit; font-size: 100%; margin:0; padding:0; background-image: url( ' https://whatever_image_2' )">

<a href="https://whatever_a_href_1"><img src="https://whatever_img_src_1" width="60"></a>
<p>Amount: &euro; 1,50</p>
<a href=https://whatever_a_href_2 target=_top><img src=https://whatever_img_src_2 width="60"></a>
<script> a = 'b' </script>
"""

После передачи его в HTMLMailFixer результат таков:

html_fixed = <!DOCTYPE html PUBLIC "- / /w3c / /dtd html 4.01 transitional / /en">
<html><head><style>
.section-block{background-image: url('') !important;
font-size: 1.1em}
#logo {
   margin-top: 10em;background-image: url('')}
.wino { color: #ff0000 }
</style>
</head><body style="font: inherit;  font-size: 100%;  margin: 0;  padding: 0">

<a href="https://whatever_a_href_1" rel="noopener noreferrer" target="_blank"><img src="YOUR_PIXEL_URL" width="60"/></a>
<p>Amount: € 1,50</p>
<a href="https://whatever_a_href_2" rel="noopener noreferrer" target="_blank"><img src="YOUR_PIXEL_URL" width="60"/></a>

</body></html>

Обратите внимание, что BeautifulSoup сделал некоторые изменения, такие как добавление недостающих тегов и преобразование в UTF-8.

Откуда мы знаем, что все замены были сделаны?

Что касается элементов HTML , мы не знаем. Если BeautifulSoup потерпит неудачу, мы потерпим неудачу. К счастью, парсер html5lib парсит как браузер. Что касается свойств CSS, я сделал быструю и грязную замену. Я должен более детально изучить, как CSS может включать внешние ресурсы.

Резюме

Использовать BeautifulSoup было несложно, он очень мощный и сэкономил мне много времени. Было бы здорово, если бы было что-то подобное для разбора и модификации (плохого) CCS. В любом случае, конечный результат - письма HTML фильтруются и отображаются в моем браузере с удаленными и заблокированными небезопасными ресурсами. С помощью кнопки я могу разрешить внешние ресурсы.

Блокирование внешних ресурсов должно быть более гибким. Например, мы всегда хотим блокировать Facebook, Google, но разрешить другие ресурсы.

Я не претендую на то, что представленное здесь решение идеально, это только начало.

Ссылки / кредиты

Beautiful Soup Documentation
https://www.crummy.com/software/BeautifulSoup/bs4/doc/

HTML <link> Tag
https://www.w3schools.com/Tags/tag_link.asp

Подробнее

BeautifulSoup

Оставить комментарий

Комментируйте анонимно или войдите в систему, чтобы прокомментировать.

Комментарии

Оставьте ответ

Ответьте анонимно или войдите в систему, чтобы ответить.