Skip to content

Commit

Permalink
Added support for data uris, test SVG, split process tests
Browse files Browse the repository at this point in the history
  • Loading branch information
Bart van der Schoor committed Mar 1, 2024
1 parent 13b7d63 commit f4e7453
Show file tree
Hide file tree
Showing 4 changed files with 192 additions and 93 deletions.
33 changes: 29 additions & 4 deletions mail_editor/process.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from mimetypes import guess_type
from typing import NamedTuple, Optional
from urllib.parse import urlparse
from urllib.request import urlopen

from django.conf import settings
from django.contrib.staticfiles import finders
Expand Down Expand Up @@ -89,7 +90,9 @@ def process_html(
continue
cid = cid_for_bytes(data.content)
url_cid_cache[url] = cid
image_attachments[cid] = CIDAttachment(cid, data.content, data.content_type)
image_attachments[cid] = CIDAttachment(
cid, data.content, data.content_type
)

elem.set("src", f"cid:{cid}")

Expand All @@ -98,7 +101,7 @@ def process_html(
url = elem.get("href")
if not url:
continue
# resolving back to paths is needed so css-inliner's can use a file:// base_url
# resolving back to paths is needed so css-inliner can use a file:// base_url
partial_file_path = _find_static_path_for_inliner(url, static_url)
if partial_file_path:
elem.set("href", partial_file_path)
Expand All @@ -113,6 +116,8 @@ def process_html(
if inline_css:
result = _html_inline_css(result)

# TODO support inlining CSS referenced images?

return ProcessedHTML(result, list(image_attachments.values()))


Expand All @@ -130,7 +135,10 @@ def load_image(

url = make_url_absolute(url, base_url)

if url.startswith(static_url):
if url.startswith("data:"):
data = read_data_uri(url)

elif url.startswith(static_url):
file_name = url[len(static_url) :]
file_path = os.path.join(settings.STATIC_ROOT, file_name)

Expand Down Expand Up @@ -163,6 +171,24 @@ def load_image(
return None


def read_data_uri(uri: str) -> Optional[FileData]:
assert uri.startswith("data:")

try:
with urlopen(uri) as response:
content = response.read()
content_type = response.headers.get("Content-Type")
if content and content_type:
return FileData(content, content_type)
except Exception:
# TODO stricter exception types
# we never want errors to block important mail
# maybe we should log though
pass

return None


def read_image_file(path: str) -> Optional[FileData]:
try:
with open(path, "rb") as f:
Expand All @@ -181,7 +207,6 @@ def make_url_absolute(url: str, base_url: str = "") -> str:
"""
base_url: https://domain
"""
# TODO surely there is a standard and proper way to do this?
# TODO we're using the path part as file path so we should handle sneaky attempts to use relative ".."
try:
parse = urlparse(url)
Expand Down
7 changes: 7 additions & 0 deletions testapp/static/image.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
147 changes: 147 additions & 0 deletions tests/test_process_helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import base64
import os

from django.conf import settings
from django.test import TestCase

from mail_editor.process import (
cid_for_bytes,
load_image,
make_url_absolute,
read_data_uri,
read_image_file,
)


class ProcessHelpersTestCase(TestCase):
def test_make_url_absolute(self):
tests = [
("http://example.com", "/foo", "http://example.com/foo"),
("http://example.com", "foo", "http://example.com/foo"),
("http://example.com", "", "http://example.com"),
("http://example.com", "http://example.com/foo", "http://example.com/foo"),
("http://example.com", "", "http://example.com"),
("", "http://example.com/foo", "http://example.com/foo"),
("", "foo", "/foo"),
("", "", "/"),
# extras
("http://example.com", "tel:123456789", "tel:123456789"),
("http://example.com", "mailto:foo@example.com", "mailto:foo@example.com"),
(
"http://example.com",
"data:image/png;base64,xyz",
"data:image/png;base64,xyz",
),
]
for i, (base, url, expected) in enumerate(tests):
with self.subTest((i, base, url)):
self.assertEqual(make_url_absolute(url, base), expected)

def test_cid_for_bytes(self):
self.assertEqual(cid_for_bytes(b"abc"), cid_for_bytes(b"abc"))
self.assertNotEqual(cid_for_bytes(b"12356"), cid_for_bytes(b"abc"))

def test_read_image_file(self):
with self.subTest("exists"):
path = os.path.join(settings.STATIC_ROOT, "logo.png")
with open(path, "rb") as f:
expected = f.read()

data = read_image_file(path)
self.assertEqual(data.content, expected)
self.assertEqual(data.content_type, "image/png")

with self.subTest("not exists"):
path = os.path.join(settings.STATIC_ROOT, "not_exists.png")
data = read_image_file(path)
self.assertIsNone(data)

def test_read_data_uri(self):
path = os.path.join(settings.STATIC_ROOT, "logo.png")
with open(path, "rb") as f:
png_data = f.read()
png_b64 = base64.b64encode(png_data).decode("utf8")

with self.subTest("valid"):
datauri = f"data:image/png;base64,{png_b64}"
data = read_data_uri(datauri)
self.assertEqual(data.content, png_data)
self.assertEqual(data.content_type, "image/png")

with self.subTest("bad content"):
datauri = "data:image/png;base64,xxxxxxxxxxx"
data = read_data_uri(datauri)
self.assertIsNone(data)

def test_load_image(self):
# TODO properly test both collected STATIC_ROOT and the development staticfiles.finders fallback

static_url = make_url_absolute(settings.STATIC_URL, "http://testserver")
media_url = make_url_absolute(settings.MEDIA_URL, "http://testserver")

with self.subTest("static URL & PNG"):
with open(os.path.join(settings.STATIC_ROOT, "logo.png"), "rb") as f:
expected = f.read()

data = load_image(
"/static/logo.png", "http://testserver", static_url, media_url
)
self.assertEqual(data.content, expected)
self.assertEqual(data.content_type, "image/png")

with self.subTest("static URL & SVG"):
data = load_image(
"/static/image.svg", "http://testserver", static_url, media_url
)
self.assertIsNone(data)

with self.subTest("media URL & JPG"):
with open(os.path.join(settings.MEDIA_ROOT, "logo.jpg"), "rb") as f:
expected = f.read()

data = load_image(
"/media/logo.jpg", "http://testserver", static_url, media_url
)
self.assertEqual(data.content, expected)
self.assertEqual(data.content_type, "image/jpeg")

with self.subTest("datauri & PNG"):
path = os.path.join(settings.STATIC_ROOT, "logo.png")
with open(path, "rb") as f:
png_data = f.read()
png_b64 = base64.b64encode(png_data).decode("utf8")

data = load_image(
f"data:image/png;base64,{png_b64}",
"http://testserver",
static_url,
media_url,
)
self.assertEqual(data.content, png_data)
self.assertEqual(data.content_type, "image/png")

with self.subTest("datauri & SVG"):
path = os.path.join(settings.STATIC_ROOT, "image.svg")
with open(path, "rb") as f:
svg_data = f.read()
svg_b64 = base64.b64encode(svg_data).decode("utf8")

data = load_image(
f"data:image/svg+xml;base64,{svg_b64}",
"http://testserver",
static_url,
media_url,
)
self.assertIsNone(data)

with self.subTest("static not exists"):
data = load_image(
"/static/not_exists.png", "http://testserver", static_url, media_url
)
self.assertIsNone(data)

with self.subTest("media not exists"):
data = load_image(
"/media/not_exists.png", "http://testserver", static_url, media_url
)
self.assertIsNone(data)
98 changes: 9 additions & 89 deletions tests/test_process_html.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,15 @@
import os
from unittest.mock import patch

from django.conf import settings
from django.test import TestCase

from mail_editor.process import (
FileData, cid_for_bytes,
load_image,
make_url_absolute,
process_html,
read_image_file,
)
from mail_editor.process import FileData, process_html


class ProcessTestCase(TestCase):
"""
note the patched helpers are individually and more exhaustively tested elsewhere
"""

@patch("mail_editor.process.cid_for_bytes", return_value="MY_CID")
@patch("mail_editor.process.load_image", return_value=FileData(b"abc", "image/jpg"))
def test_extract_images(self, m1, m2):
Expand Down Expand Up @@ -50,14 +46,14 @@ def test_extract_images__keeps_absolute_url_when_not_loadable(self, m):
def test_fix_anchor_urls(self):
html = """
<html><body>
<a href="/foo">bar</a>
<a href="https://external.com/foo">bar</a>
<a href="/foo">foo</a>
<a href="https://external.com/bar">bar</a>
</body></html>
"""
expected_html = """
<html><head></head><body>
<a href="https://example.com/foo">bar</a>
<a href="https://external.com/foo">bar</a>
<a href="https://example.com/foo">foo</a>
<a href="https://external.com/bar">bar</a>
</body></html>
"""
result = process_html(html, "https://example.com")
Expand Down Expand Up @@ -121,79 +117,3 @@ def test_inline_css_from_link__removes_external_link(self):
result = process_html(html, "https://example.com")
self.assertHTMLEqual(result.html, expected_html)
self.assertEqual(result.cid_attachments, [])

def test_svg(self):
# TODO test what happens
pass


class ProcessHelpersTestCase(TestCase):
def test_make_url_absolute(self):
tests = [
("http://example.com", "/foo", "http://example.com/foo"),
("http://example.com", "foo", "http://example.com/foo"),
("http://example.com", "", "http://example.com"),
("http://example.com", "http://example.com/foo", "http://example.com/foo"),
("http://example.com", "", "http://example.com"),
("", "http://example.com/foo", "http://example.com/foo"),
("", "foo", "/foo"),
("", "", "/"),
# extras
("http://example.com", "tel:123456789", "tel:123456789"),
("http://example.com", "mailto:foo@example.com", "mailto:foo@example.com"),
]
for i, (base, url, expected) in enumerate(tests):
with self.subTest((i, base, url)):
self.assertEqual(make_url_absolute(url, base), expected)

def test_cid_for_bytes(self):
self.assertEqual(cid_for_bytes(b"abc"), cid_for_bytes(b"abc"))
self.assertNotEqual(cid_for_bytes(b"123"), cid_for_bytes(b"abc"))

def test_read_image_file(self):
with self.subTest("exists"):
path = os.path.join(settings.STATIC_ROOT, "logo.png")
with open(path, "rb") as f:
expected = f.read()

data = read_image_file(path)
self.assertEqual(data.content, expected)
self.assertEqual(data.content_type, "image/png")

with self.subTest("not exists"):
path = os.path.join(settings.STATIC_ROOT, "not_exists.png")

data = read_image_file(path)
self.assertIsNone(data)

def test_load_image(self):
# TODO properly test both collected STATIC_ROOT and the development staticfiles.finders fallback

static_url = make_url_absolute(settings.STATIC_URL, "http://testserver")
media_url = make_url_absolute(settings.MEDIA_URL, "http://testserver")

with self.subTest("static & png"):
with open(os.path.join(settings.STATIC_ROOT, "logo.png"), "rb") as f:
expected = f.read()

data = load_image(
"/static/logo.png", "http://testserver", static_url, media_url
)
self.assertEqual(data.content, expected)
self.assertEqual(data.content_type, "image/png")

with self.subTest("media & jpg"):
with open(os.path.join(settings.MEDIA_ROOT, "logo.jpg"), "rb") as f:
expected = f.read()

data = load_image(
"/media/logo.jpg", "http://testserver", static_url, media_url
)
self.assertEqual(data.content, expected)
self.assertEqual(data.content_type, "image/jpeg")

with self.subTest("not exists"):
data = load_image(
"/static/not_exists.png", "http://testserver", static_url, media_url
)
self.assertIsNone(data)

0 comments on commit f4e7453

Please sign in to comment.