import time from flask import url_for from email import message_from_string from email.policy import default as email_policy from changedetectionio.diff import HTML_REMOVED_STYLE, HTML_ADDED_STYLE, HTML_CHANGED_STYLE, REMOVED_PLACEMARKER_OPEN, \ CHANGED_PLACEMARKER_OPEN, ADDED_PLACEMARKER_OPEN from changedetectionio.notification_service import NotificationContextData from changedetectionio.tests.util import set_original_response, set_modified_response, set_more_modified_response, live_server_setup, \ wait_for_all_checks, \ set_longer_modified_response, delete_all_watches import logging # NOTE - RELIES ON mailserver as hostname running, see github build recipes smtp_test_server = 'mailserver' ALL_MARKUP_TOKENS = ''.join(f"TOKEN: '{t}'\n{{{{{t}}}}}\n" for t in NotificationContextData().keys()) from changedetectionio.notification import ( default_notification_body, default_notification_format, default_notification_title, valid_notification_formats, ) def get_last_message_from_smtp_server(): import requests time.sleep(1) # wait for any smtp connects to die off port = 11080 # HTTP server port number # Make HTTP GET request to Flask server response = requests.get(f'http://{smtp_test_server}:{port}/') data = response.text logging.info("get_last_message_from_smtp_server..") logging.info(data) return data # Requires running the test SMTP server def test_check_notification_email_formats_default_HTML(client, live_server, measure_memory_usage, datastore_path): ## live_server_setup(live_server) # Setup on conftest per function set_original_response(datastore_path=datastore_path) notification_url = f'mailto://changedetection@{smtp_test_server}:11025/?to=fff@home.com' ##################### # Set this up for when we remove the notification from the watch, it should fallback with these details res = client.post( url_for("settings.settings_page"), data={"application-notification_urls": notification_url, "application-notification_title": "fallback-title " + default_notification_title, "application-notification_body": "some text\nfallback-body
" + default_notification_body, "application-notification_format": 'html', "requests-time_between_check-minutes": 180, 'application-fetch_backend': "html_requests"}, follow_redirects=True ) assert b"Settings updated." in res.data # Add a watch and trigger a HTTP POST test_url = url_for('test_endpoint', _external=True) res = client.post( url_for("ui.ui_views.form_quick_watch_add"), data={"url": test_url, "tags": 'nice one'}, follow_redirects=True ) assert b"Watch added" in res.data wait_for_all_checks(client) set_longer_modified_response(datastore_path=datastore_path) time.sleep(2) client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) wait_for_all_checks(client) time.sleep(3) msg_raw = get_last_message_from_smtp_server() assert len(msg_raw) >= 1 # Parse the email properly using Python's email library msg = message_from_string(msg_raw, policy=email_policy) # The email should have two bodies (multipart/alternative with text/plain and text/html) assert msg.is_multipart() assert msg.get_content_type() == 'multipart/alternative' # Get the parts parts = list(msg.iter_parts()) assert len(parts) == 2 # First part should be text/plain (the auto-generated plaintext version) text_part = parts[0] assert text_part.get_content_type() == 'text/plain' text_content = text_part.get_content() assert '(added) So let\'s see what happens.\r\n' in text_content # The plaintext part assert 'fallback-body\r\n' in text_content # The plaintext part # Second part should be text/html html_part = parts[1] assert html_part.get_content_type() == 'text/html' html_content = html_part.get_content() assert 'some text
' in html_content # We converted \n from the notification body assert 'fallback-body
' in html_content # kept the original
assert '(added) So let\'s see what happens.
' in html_content # the html part delete_all_watches(client) def test_check_notification_plaintext_format(client, live_server, measure_memory_usage, datastore_path): set_original_response(datastore_path=datastore_path) notification_url = f'mailto://changedetection@{smtp_test_server}:11025/?to=fff@home.com' ##################### # Set this up for when we remove the notification from the watch, it should fallback with these details res = client.post( url_for("settings.settings_page"), data={"application-notification_urls": notification_url, "application-notification_title": "fallback-title {{watch_title}} {{ diff_added.splitlines()[0] if diff_added else 'diff added didnt split' }} " + default_notification_title, "application-notification_body": f"some text\n" + default_notification_body + f"\nMore output test\n{ALL_MARKUP_TOKENS}", "application-notification_format": 'text', "requests-time_between_check-minutes": 180, 'application-fetch_backend': "html_requests"}, follow_redirects=True ) assert b"Settings updated." in res.data # Add a watch and trigger a HTTP POST test_url = url_for('test_endpoint', _external=True) uuid = client.application.config.get('DATASTORE').add_watch(url=test_url) client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) time.sleep(2) set_longer_modified_response(datastore_path=datastore_path) client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) wait_for_all_checks(client) time.sleep(3) msg_raw = get_last_message_from_smtp_server() assert len(msg_raw) >= 1 #time.sleep(60) # Parse the email properly using Python's email library msg = message_from_string(msg_raw, policy=email_policy) # Subject/title got marked up subject = msg['subject'] # Subject should always be plaintext and never marked up to anything else assert REMOVED_PLACEMARKER_OPEN not in subject assert CHANGED_PLACEMARKER_OPEN not in subject assert ADDED_PLACEMARKER_OPEN not in subject assert 'diff added didnt split' not in subject assert '(changed) Which is across' in subject assert 'PLACEMARKER' not in subject # The email should be plain text only (not multipart) assert not msg.is_multipart() assert msg.get_content_type() == 'text/plain' # Get the plain text content text_content = msg.get_content() assert '(added) So let\'s see what happens.\r\n' in text_content # The plaintext part # Should NOT contain HTML assert '
' not in text_content # We should not have HTML in plain text delete_all_watches(client) def test_check_notification_html_color_format(client, live_server, measure_memory_usage, datastore_path): set_original_response(datastore_path=datastore_path) notification_url = f'mailto://changedetection@{smtp_test_server}:11025/?to=fff@home.com' ##################### # Set this up for when we remove the notification from the watch, it should fallback with these details res = client.post( url_for("settings.settings_page"), data={"application-notification_urls": notification_url, "application-notification_title": "fallback-title {{watch_title}} - diff_added_lines_test : '{{ diff_added.splitlines()[0] if diff_added else 'diff added didnt split' }}' " + default_notification_title, "application-notification_body": f"some text\n{default_notification_body}\nMore output test\n{ALL_MARKUP_TOKENS}", "application-notification_format": 'htmlcolor', "requests-time_between_check-minutes": 180, 'application-fetch_backend': "html_requests"}, follow_redirects=True ) assert b"Settings updated." in res.data # Add a watch and trigger a HTTP POST test_url = url_for('test_endpoint', _external=True) res = client.post( url_for("ui.ui_views.form_quick_watch_add"), data={"url": test_url, "tags": 'nice one'}, follow_redirects=True ) assert b"Watch added" in res.data wait_for_all_checks(client) set_longer_modified_response(datastore_path=datastore_path) time.sleep(2) client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) wait_for_all_checks(client) time.sleep(3) msg_raw = get_last_message_from_smtp_server() assert len(msg_raw) >= 1 # Parse the email properly using Python's email library msg = message_from_string(msg_raw, policy=email_policy) # Subject/title got marked up subject = msg['subject'] # Subject should always be plaintext and never marked up to anything else assert REMOVED_PLACEMARKER_OPEN not in subject assert CHANGED_PLACEMARKER_OPEN not in subject assert ADDED_PLACEMARKER_OPEN not in subject assert 'diff added didnt split' not in subject assert '(changed) Which is across' in subject assert 'PLACEMARKER' not in subject assert 'head title' in subject assert "span" not in subject assert 'background-color' not in subject # The email should have two bodies (multipart/alternative with text/plain and text/html) assert msg.is_multipart() assert msg.get_content_type() == 'multipart/alternative' # Get the parts parts = list(msg.iter_parts()) assert len(parts) == 2 # First part should be text/plain (the auto-generated plaintext version) text_part = parts[0] assert text_part.get_content_type() == 'text/plain' text_content = text_part.get_content() assert 'So let\'s see what happens.\r\n' in text_content # The plaintext part assert '(added)' not in text_content # Because apprise only dumb converts the html to text # Second part should be text/html with color styling html_part = parts[1] assert html_part.get_content_type() == 'text/html' html_content = html_part.get_content() assert HTML_CHANGED_STYLE or HTML_REMOVED_STYLE in html_content assert HTML_ADDED_STYLE in html_content assert '<' not in html_content assert 'some text
' in html_content delete_all_watches(client) def test_check_notification_markdown_format(client, live_server, measure_memory_usage, datastore_path): set_original_response(datastore_path=datastore_path) notification_url = f'mailto://changedetection@{smtp_test_server}:11025/?to=fff@home.com' ##################### # Set this up for when we remove the notification from the watch, it should fallback with these details res = client.post( url_for("settings.settings_page"), data={"application-notification_urls": notification_url, "application-notification_title": "fallback-title diff_added_lines_test : '{{ diff_added.splitlines()[0] if diff_added else 'diff added didnt split' }}' " + default_notification_title, "application-notification_body": "*header*\n\nsome text\n" + default_notification_body, "application-notification_format": 'markdown', "requests-time_between_check-minutes": 180, 'application-fetch_backend': "html_requests"}, follow_redirects=True ) assert b"Settings updated." in res.data # Add a watch and trigger a HTTP POST test_url = url_for('test_endpoint', _external=True) res = client.post( url_for("ui.ui_views.form_quick_watch_add"), data={"url": test_url, "tags": 'nice one'}, follow_redirects=True ) assert b"Watch added" in res.data wait_for_all_checks(client) set_longer_modified_response(datastore_path=datastore_path) time.sleep(2) client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) wait_for_all_checks(client) time.sleep(3) msg_raw = get_last_message_from_smtp_server() assert len(msg_raw) >= 1 # Parse the email properly using Python's email library msg = message_from_string(msg_raw, policy=email_policy) # The email should have two bodies (multipart/alternative with text/plain and text/html) assert msg.is_multipart() assert msg.get_content_type() == 'multipart/alternative' subject = msg['subject'] # Subject should always be plaintext and never marked up to anything else assert REMOVED_PLACEMARKER_OPEN not in subject assert CHANGED_PLACEMARKER_OPEN not in subject assert ADDED_PLACEMARKER_OPEN not in subject assert 'diff added didnt split' not in subject assert '(changed) Which is across' in subject # Get the parts parts = list(msg.iter_parts()) assert len(parts) == 2 # First part should be text/plain (the auto-generated plaintext version) text_part = parts[0] assert text_part.get_content_type() == 'text/plain' text_content = text_part.get_content() # We wont see anything in the "FALLBACK" text but that's OK (no added/strikethrough etc) assert 'So let\'s see what happens.\r\n' in text_content # The plaintext part # Second part should be text/html and roughly converted from markdown to HTML html_part = parts[1] assert html_part.get_content_type() == 'text/html' html_content = html_part.get_content() assert '

header

' in html_content assert 'So let\'s see what happens.
' in html_content # Additions are in markdown # the '
' will come from apprises conversion, not from our code, we would rather use '
' correctly # the '
' is actually a nice way to know if apprise done the conversion. delete_all_watches(client) # Custom notification body with HTML, that is either sent as HTML or rendered to plaintext and sent def test_check_notification_email_formats_default_Text_override_HTML(client, live_server, measure_memory_usage, datastore_path): # HTML problems? see this # https://github.com/caronc/apprise/issues/633 set_original_response(datastore_path=datastore_path) notification_url = f'mailto://changedetection@{smtp_test_server}:11025/?to=fff@home.com' notification_body = f""" My Webpage

Test

{default_notification_body} """ ##################### # Set this up for when we remove the notification from the watch, it should fallback with these details res = client.post( url_for("settings.settings_page"), data={"application-notification_urls": notification_url, "application-notification_title": "fallback-title " + default_notification_title, "application-notification_body": notification_body, "application-notification_format": 'text', "requests-time_between_check-minutes": 180, 'application-fetch_backend': "html_requests"}, follow_redirects=True ) assert b"Settings updated." in res.data # Add a watch and trigger a HTTP POST test_url = url_for('test_endpoint',content_type="text/html", _external=True) res = client.post( url_for("ui.ui_views.form_quick_watch_add"), data={"url": test_url, "tags": 'nice one'}, follow_redirects=True ) assert b"Watch added" in res.data #################################### FIRST SITUATION, PLAIN TEXT NOTIFICATION IS WANTED BUT WE HAVE HTML IN OUR TEMPLATE AND CONTENT ########## wait_for_all_checks(client) set_longer_modified_response(datastore_path=datastore_path) time.sleep(2) client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) wait_for_all_checks(client) time.sleep(3) msg_raw = get_last_message_from_smtp_server() assert len(msg_raw) >= 1 # with open('/tmp/m.txt', 'w') as f: # f.write(msg_raw) # Parse the email properly using Python's email library msg = message_from_string(msg_raw, policy=email_policy) # The email should not have two bodies, should be TEXT only assert not msg.is_multipart() assert msg.get_content_type() == 'text/plain' # Get the plain text content text_content = msg.get_content() assert '(added) So let\'s see what happens.\r\n' in text_content # The plaintext part assert '' in text_content # even tho they added html, they selected plaintext so it should have not got converted #################################### SECOND SITUATION, HTML IS CORRECTLY PASSED THROUGH TO THE EMAIL #################### set_original_response(datastore_path=datastore_path) # Now override as HTML format res = client.post( url_for("ui.ui_edit.edit_page", uuid="first"), data={ "url": test_url, "notification_format": 'html', 'fetch_backend': "html_requests", "time_between_check_use_default": "y"}, follow_redirects=True ) assert b"Updated watch." in res.data wait_for_all_checks(client) time.sleep(3) msg_raw = get_last_message_from_smtp_server() assert len(msg_raw) >= 1 # Parse the email properly using Python's email library msg = message_from_string(msg_raw, policy=email_policy) # The email should have two bodies (multipart/alternative) assert msg.is_multipart() assert msg.get_content_type() == 'multipart/alternative' # Get the parts parts = list(msg.iter_parts()) assert len(parts) == 2 # First part should be text/plain text_part = parts[0] assert text_part.get_content_type() == 'text/plain' text_content = text_part.get_content() assert '(removed) So let\'s see what happens.\r\n' in text_content # The plaintext part # Second part should be text/html html_part = parts[1] assert html_part.get_content_type() == 'text/html' html_content = html_part.get_content() assert '(removed) So let\'s see what happens.' in html_content # the html part assert '<!DOCTYPE html' not in html_content assert 'Test' in html_content assert '<' not in html_content delete_all_watches(client) def test_check_plaintext_document_plaintext_notification_smtp(client, live_server, measure_memory_usage, datastore_path): """When following a plaintext document, notification in Plain Text format is sent correctly""" import os with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: f.write("Some nice plain text\nwhich we add some extra data\nover here\n") notification_url = f'mailto://changedetection@{smtp_test_server}:11025/?to=fff@home.com' notification_body = f"""{default_notification_body}""" ##################### # Set this up for when we remove the notification from the watch, it should fallback with these details res = client.post( url_for("settings.settings_page"), data={"application-notification_urls": notification_url, "application-notification_title": "fallback-title " + default_notification_title, "application-notification_body": f"{notification_body}\nMore output test\n{ALL_MARKUP_TOKENS}", "application-notification_format": 'text', "requests-time_between_check-minutes": 180, 'application-fetch_backend': "html_requests"}, follow_redirects=True ) assert b"Settings updated." in res.data # Add our URL to the import page test_url = url_for('test_endpoint', content_type="text/plain", _external=True) uuid = client.application.config.get('DATASTORE').add_watch(url=test_url) client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) wait_for_all_checks(client) # Change the content with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: f.write("Some nice plain text\nwhich we add some extra data\nAnd let's talk about tags\nover here\n") time.sleep(1) client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) wait_for_all_checks(client) # Parse the email properly using Python's email library msg = message_from_string(get_last_message_from_smtp_server(), policy=email_policy) assert not msg.is_multipart() assert msg.get_content_type() == 'text/plain' body = msg.get_content() # nothing is escaped, raw html stuff in text/plain assert 'talk about <title> tags' in body assert '(added)' in body assert '<br' not in body assert '<' not in body assert '<pre' not in body delete_all_watches(client) def test_check_plaintext_document_html_notifications(client, live_server, measure_memory_usage, datastore_path): """When following a plaintext document, notification in Plain Text format is sent correctly""" import os with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: f.write(" Some nice plain text\nwhich we add some extra data\nover here\n") notification_url = f'mailto://changedetection@{smtp_test_server}:11025/?to=fff@home.com' notification_body = f"""{default_notification_body}""" ##################### # Set this up for when we remove the notification from the watch, it should fallback with these details res = client.post( url_for("settings.settings_page"), data={"application-notification_urls": notification_url, "application-notification_title": "fallback-title " + default_notification_title, "application-notification_body": f"{notification_body}\nMore output test\n{ALL_MARKUP_TOKENS}", "application-notification_format": 'html', "requests-time_between_check-minutes": 180, 'application-fetch_backend': "html_requests"}, follow_redirects=True ) assert b"Settings updated." in res.data # Add our URL to the import page test_url = url_for('test_endpoint', content_type="text/plain", _external=True) uuid = client.application.config.get('DATASTORE').add_watch(url=test_url) client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) wait_for_all_checks(client) # Change the content with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: f.write(" Some nice plain text\nwhich we add some extra data\nAnd let's talk about <title> tags\nover here\n") time.sleep(2) client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) wait_for_all_checks(client) # Parse the email properly using Python's email library msg = message_from_string(get_last_message_from_smtp_server(), policy=email_policy) # The email should have two bodies (multipart/alternative) assert msg.is_multipart() assert msg.get_content_type() == 'multipart/alternative' # Get the parts parts = list(msg.iter_parts()) assert len(parts) == 2 # First part should be text/plain text_part = parts[0] assert text_part.get_content_type() == 'text/plain' text_content = text_part.get_content() html_part = parts[1] assert html_part.get_content_type() == 'text/html' html_content = html_part.get_content() assert 'And let\'s talk about <title> tags\r\n' in text_content assert '<br' not in text_content assert '<span' not in text_content assert 'talk about <title>' not in html_content # the html part, should have got marked up to < etc assert 'talk about <title>' in html_content # Should be the HTML, but not HTML Color assert 'background-color' not in html_content assert '<br>(added) And let's talk about <title> tags<br>' in html_content assert 'PLACEMARKER' not in html_content assert '<br' not in html_content assert '<pre role="article"' in html_content # Should have got wrapped nicely in email_helpers.py # And now for the whitespace retention assert '    Some nice plain text' in html_content assert '(added) And let' in html_content # just to show a single whitespace didnt get touched delete_all_watches(client) def test_check_plaintext_document_html_color_notifications(client, live_server, measure_memory_usage, datastore_path): """When following a plaintext document, notification in Plain Text format is sent correctly""" import os with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: f.write("Some nice plain text\nwhich we add some extra data\nover here\n") notification_url = f'mailto://changedetection@{smtp_test_server}:11025/?to=fff@home.com' notification_body = f"""{default_notification_body}""" ##################### # Set this up for when we remove the notification from the watch, it should fallback with these details res = client.post( url_for("settings.settings_page"), data={"application-notification_urls": notification_url, "application-notification_title": "fallback-title " + default_notification_title, "application-notification_body": f"{notification_body}\nMore output test\n{ALL_MARKUP_TOKENS}", "application-notification_format": 'htmlcolor', "requests-time_between_check-minutes": 180, 'application-fetch_backend': "html_requests"}, follow_redirects=True ) assert b"Settings updated." in res.data # Add our URL to the import page test_url = url_for('test_endpoint', content_type="text/plain", _external=True) uuid = client.application.config.get('DATASTORE').add_watch(url=test_url) client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) wait_for_all_checks(client) # Change the content with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: f.write("Some nice plain text\nwhich we add some extra data\nAnd let's talk about <title> tags\nover here\n") time.sleep(1) client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) wait_for_all_checks(client) # Parse the email properly using Python's email library msg = message_from_string(get_last_message_from_smtp_server(), policy=email_policy) # The email should have two bodies (multipart/alternative) assert msg.is_multipart() assert msg.get_content_type() == 'multipart/alternative' # Get the parts parts = list(msg.iter_parts()) assert len(parts) == 2 # First part should be text/plain text_part = parts[0] assert text_part.get_content_type() == 'text/plain' text_content = text_part.get_content() html_part = parts[1] assert html_part.get_content_type() == 'text/html' html_content = html_part.get_content() assert 'And let\'s talk about <title> tags\r\n' in text_content assert '<br' not in text_content assert '<span' not in text_content assert 'talk about <title>' not in html_content # the html part, should have got marked up to < etc assert 'talk about <title>' in html_content # Should be the HTML, but not HTML Color assert 'background-color' in html_content assert '(added) And let' not in html_content assert '<br' not in html_content assert '<br>' in html_content assert '<pre role="article"' in html_content # Should have got wrapped nicely in email_helpers.py delete_all_watches(client) def test_check_html_document_plaintext_notification(client, live_server, measure_memory_usage, datastore_path): """When following a HTML document, notification in Plain Text format is sent correctly""" import os with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: f.write("<html><body>some stuff<br>and more stuff<br>and even more stuff<br></body></html>") notification_url = f'mailto://changedetection@{smtp_test_server}:11025/?to=fff@home.com' notification_body = f"""{default_notification_body}""" ##################### # Set this up for when we remove the notification from the watch, it should fallback with these details res = client.post( url_for("settings.settings_page"), data={"application-notification_urls": notification_url, "application-notification_title": "fallback-title " + default_notification_title, "application-notification_body": f"{notification_body}\nMore output test\n{ALL_MARKUP_TOKENS}", "application-notification_format": 'text', "requests-time_between_check-minutes": 180, 'application-fetch_backend': "html_requests"}, follow_redirects=True ) assert b"Settings updated." in res.data # Add our URL to the import page test_url = url_for('test_endpoint', content_type="text/html", _external=True) uuid = client.application.config.get('DATASTORE').add_watch(url=test_url) client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) wait_for_all_checks(client) with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: f.write("<html><body>sxome stuff<br>and more stuff<br>lets slip this in<br>and this in<br>and even more stuff<br><tag></body></html>") time.sleep(0.1) client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) wait_for_all_checks(client) # Parse the email properly using Python's email library msg = message_from_string(get_last_message_from_smtp_server(), policy=email_policy) assert not msg.is_multipart() assert msg.get_content_type() == 'text/plain' body = msg.get_content() assert '<tag>' in body # Should have got converted from original HTML to plaintext assert '(changed) some stuff\r\n' in body assert 'PLACEMARKER' not in body assert '(into) sxome stuff\r\n' in body assert '(added) lets slip this in\r\n' in body assert '(added) and this in\r\n' in body assert ' ' not in body delete_all_watches(client) def test_check_html_notification_with_apprise_format_is_html(client, live_server, measure_memory_usage, datastore_path): ## live_server_setup(live_server) # Setup on conftest per function set_original_response(datastore_path=datastore_path) notification_url = f'mailto://changedetection@{smtp_test_server}:11025/?to=fff@home.com&format=html' ##################### # Set this up for when we remove the notification from the watch, it should fallback with these details res = client.post( url_for("settings.settings_page"), data={"application-notification_urls": notification_url, "application-notification_title": "fallback-title " + default_notification_title, "application-notification_body": "some text\nfallback-body<br> " + default_notification_body, "application-notification_format": 'html', "requests-time_between_check-minutes": 180, 'application-fetch_backend': "html_requests"}, follow_redirects=True ) assert b"Settings updated." in res.data # Add a watch and trigger a HTTP POST test_url = url_for('test_endpoint', _external=True) res = client.post( url_for("ui.ui_views.form_quick_watch_add"), data={"url": test_url, "tags": 'nice one'}, follow_redirects=True ) assert b"Watch added" in res.data wait_for_all_checks(client) set_longer_modified_response(datastore_path=datastore_path) time.sleep(2) client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) wait_for_all_checks(client) time.sleep(3) msg_raw = get_last_message_from_smtp_server() assert len(msg_raw) >= 1 # Parse the email properly using Python's email library msg = message_from_string(msg_raw, policy=email_policy) # The email should have two bodies (multipart/alternative with text/plain and text/html) assert msg.is_multipart() assert msg.get_content_type() == 'multipart/alternative' # Get the parts parts = list(msg.iter_parts()) assert len(parts) == 2 # First part should be text/plain (the auto-generated plaintext version) text_part = parts[0] assert text_part.get_content_type() == 'text/plain' text_content = text_part.get_content() assert '(added) So let\'s see what happens.\r\n' in text_content # The plaintext part assert 'fallback-body\r\n' in text_content # The plaintext part # Second part should be text/html html_part = parts[1] assert html_part.get_content_type() == 'text/html' html_content = html_part.get_content() assert 'some text<br>' in html_content # We converted \n from the notification body assert 'fallback-body<br>' in html_content # kept the original <br> assert '(added) So let\'s see what happens.<br>' in html_content # the html part delete_all_watches(client)