[FIX] mass_mailing: ensure base64 images in mso comments get converted

Base64 images get converted to attachments. However, if they are in a
mso comment, they were not converted.

X-original-commit: 2fa0694227a7beb6a000c2e8219fdd8c073cf348
Part-of: odoo/odoo#124465
This commit is contained in:
Antoine Guenet
2023-06-09 17:01:39 +02:00
parent 897c47a5ac
commit 3bfccb19b1
2 changed files with 98 additions and 42 deletions
+43 -38
View File
@@ -1310,47 +1310,52 @@ class MassMailing(models.Model):
"""
root = lxml.html.fromstring(body_html)
did_modify_body = False
nodes_to_convert = []
for node in root.iter('img'):
match = image_re.match(node.attrib.get('src', ''))
if match:
mime = match.group(1) # unused
image = match.group(2).encode() # base64 image as bytes
nodes_to_convert.append(node)
urls = self._create_attachments_from_inline_images(nodes_to_convert)
for (node, url) in zip(nodes_to_convert, urls):
did_modify_body = True
node.attrib['src'] = url
comments_to_convert = []
conversion_info = [] # list of tuples (image: base64 image, node: lxml node, old_url: string or None))
with requests.Session() as session:
for comment in [c for c in root.iter(lxml.etree.Comment) if mso_re.match(c.text)]:
for match in re.findall(r'<v:image[^>]*>', comment.text):
url = re.search(r'src=\s*\"([^\"]+)\"', match)[1]
# Make sure we have an absolute URL by adding a scheme and host if needed.
absolute_url = url if '//' in url else f"{self.get_base_url()}{url if url.startswith('/') else f'/{url}'}"
target_width_match = re.search(r'width:\s*([0-9\.]+)\s*px', match)
target_height_match = re.search(r'height:\s*([0-9\.]+)\s*px', match)
if target_width_match and target_height_match:
target_width = float(target_width_match[1])
target_height = float(target_height_match[1])
try:
image = self._get_image_by_url(absolute_url, session)
except (ImportValidationError, UnidentifiedImageError):
# Url invalid or doesn't resolve to a valid image.
# Note: We choose to ignore errors so as not to
# break the entire process just for one image's
# responsive cropping behavior).
pass
else:
image_processor = tools.ImageProcess(image)
image = image_processor.crop_resize(target_width, target_height, 0, 0)
comments_to_convert.append((comment, url, base64.b64encode(image.source)))
images_in_comments = [image for (_, _, image) in comments_to_convert]
urls = self._create_attachments_from_inline_images(images_in_comments)
for ((comment, old_url, image), url) in zip(comments_to_convert, urls):
for node in [n for n in root.iter('img', lxml.etree.Comment) if n.tag == 'img' or mso_re.match(n.text)]:
if node.tag == 'img':
# Convert base64 images in img tags to attachments.
match = image_re.match(node.attrib.get('src', ''))
if match:
image = match.group(2).encode() # base64 image as bytes
conversion_info.append((image, node, None))
else:
# Convert base64 images in mso comments to attachments.
for match in re.findall(r'<img[^>]*src="(data:image/[A-Za-z]+;base64,[^"]*)"', node.text):
image = re.sub(r'data:image/[A-Za-z]+;base64,', '', match).encode() # base64 image as bytes
conversion_info.append((image, node, match))
# Crop VML images.
for match in re.findall(r'<v:image[^>]*>', node.text):
url = re.search(r'src=\s*\"([^\"]+)\"', match)[1]
# Make sure we have an absolute URL by adding a scheme and host if needed.
absolute_url = url if '//' in url else f"{self.get_base_url()}{url if url.startswith('/') else f'/{url}'}"
target_width_match = re.search(r'width:\s*([0-9\.]+)\s*px', match)
target_height_match = re.search(r'height:\s*([0-9\.]+)\s*px', match)
if target_width_match and target_height_match:
target_width = float(target_width_match[1])
target_height = float(target_height_match[1])
try:
image = self._get_image_by_url(absolute_url, session)
except (ImportValidationError, UnidentifiedImageError):
# Url invalid or doesn't resolve to a valid image.
# Note: We choose to ignore errors so as not to
# break the entire process just for one image's
# responsive cropping behavior).
pass
else:
image_processor = tools.ImageProcess(image)
image = image_processor.crop_resize(target_width, target_height, 0, 0)
conversion_info.append((base64.b64encode(image.source), node, url))
# Apply the changes.
urls = self._create_attachments_from_inline_images([image for (image, _, _) in conversion_info])
for ((image, node, old_url), new_url) in zip(conversion_info, urls):
did_modify_body = True
comment.text = comment.text.replace(old_url, url)
if node.tag == 'img':
node.attrib['src'] = new_url
else:
node.text = node.text.replace(old_url, new_url)
if did_modify_body:
return lxml.html.tostring(root, encoding='unicode')
@@ -28,10 +28,13 @@ class TestMassMailValues(MassMailCommon):
@users('user_marketing')
def test_mailing_body_cropped_vml_image(self):
# Outlook needs background images to be converted to VML but there is no
# way to emulate `background-size: cover` that works for Windows Mail as
# well. We therefore need to crop the image in the VML version to mimick
# the style of other email clients.
""" Testing mail mailing responsive bg-image cropping for Outlook.
Outlook needs background images to be converted to VML but there is no
way to emulate `background-size: cover` that works for Windows Mail as
well. We therefore need to crop the image in the VML version to mimick
the style of other email clients.
"""
attachment = {}
def patched_get_image(self, url, session):
return base64.b64decode(BASE_64_STRING)
@@ -70,6 +73,54 @@ class TestMassMailValues(MassMailCommon):
</html>
""".strip())
@users('user_marketing')
def test_mailing_body_inline_image(self):
""" Testing mail mailing base64 image conversion to attachment.
This test ensures that the base64 images are correctly converted to
attachments, even when they appear in MSO conditional comments.
"""
attachments = []
original_images_to_urls = self.env['mailing.mailing']._create_attachments_from_inline_images
def patched_images_to_urls(self, b64images):
urls = original_images_to_urls(b64images)
for url in urls:
(attachment_id, attachment_token) = re.search(r'/web/image/(?P<id>[0-9]+)\?access_token=(?P<token>.*)', url).groups()
attachments.append({
'id': attachment_id,
'token': attachment_token,
})
return urls
with patch("odoo.addons.mass_mailing.models.mailing.MassMailing._create_attachments_from_inline_images",
new=patched_images_to_urls):
mailing = self.env['mailing.mailing'].create({
'name': 'Test',
'subject': 'Test',
'state': 'draft',
'mailing_model_id': self.env['ir.model']._get('res.partner').id,
'body_html': f"""
<html><body>
<img src="data:image/png;base64,{BASE_64_STRING}">
<img src="data:image/jpg;base64,{BASE_64_STRING}">
<!--[if mso]>
<img src="data:image/png;base64,{BASE_64_STRING}">
<img src="data:image/jpg;base64,{BASE_64_STRING}">
<![endif]-->
</body></html>
""",
})
self.assertEqual(len(attachments), 4)
self.assertEqual(str(mailing.body_html), f"""
<html><body>
<img src="/web/image/{attachments[0]['id']}?access_token={attachments[0]['token']}">
<img src="/web/image/{attachments[1]['id']}?access_token={attachments[1]['token']}">
<!--[if mso]>
<img src="/web/image/{attachments[2]['id']}?access_token={attachments[2]['token']}">
<img src="/web/image/{attachments[3]['id']}?access_token={attachments[3]['token']}">
<![endif]-->
</body></html>
""".strip())
@users('user_marketing')
def test_mailing_body_responsive(self):
""" Testing mail mailing responsive mail body