Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Try to reorder parts #3

Open
wants to merge 2 commits into
base: cid_content_change_and_data_embeddings
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions mail_embed_image/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ Mail Embed Image
This module finds images attached to outgoing emails and replaces their urls
with cids. This will avoid rendering issues with some email clients.

It also provides 2 options to embed internal URL images in a mail body:
- CIDs: add fileparts as CIDs
- Data URLs: add images as data URLs

This option is configurable in an company settings variables.

**Table of contents**

.. contents::
Expand Down Expand Up @@ -60,6 +66,7 @@ Contributors
* George Daramouskas <[email protected]>
* Giovanni Francesco Capalbo <[email protected]>
* Italo LOPES <[email protected]>
* Stéphane Mangin <[email protected]>

Maintainers
~~~~~~~~~~~
Expand Down
5 changes: 5 additions & 0 deletions mail_embed_image/__manifest__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Copyright 2019 Therp BV <https://therp.nl>
# Copyright 2024 Camptocamp SA
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
{
"name": "Mail Embed Image",
Expand All @@ -9,8 +10,12 @@
"summary": "Replace img.src's which start with http with inline cids",
"website": "https://github.com/OCA/social",
"depends": [
"mail",
"web",
],
"data": [
"views/res_config_settings_views.xml",
],
"installable": True,
"application": False,
}
3 changes: 3 additions & 0 deletions mail_embed_image/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# Copyright 2019 Therp BV <https://therp.nl>
# Copyright 2024 Camptocamp SA
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from . import ir_mail_server
from . import company
from . import res_config_settings
17 changes: 17 additions & 0 deletions mail_embed_image/models/company.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Copyright 2024 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import fields, models


class ResCompany(models.Model):
_inherit = "res.company"

image_embedding_method = fields.Selection(
selection=[
("none", "No attachment"),
("cid", "CIDs attachment"),
("data", "Data SRC"),
],
default="cid", # previous module version only supported CID
required=True,
)
100 changes: 86 additions & 14 deletions mail_embed_image/models/ir_mail_server.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
# Copyright 2019 Therp BV <https://therp.nl>
# Copyright 2024 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

import logging
import uuid
from base64 import b64encode
from email.mime.image import MIMEImage

from email.mime.multipart import MIMEMultipart
import requests
from lxml.html import fromstring, tostring

Expand Down Expand Up @@ -32,9 +36,12 @@ def build_email(
body_alternative=None,
subtype_alternative="plain",
):
image_embedding_method = self.env.company.image_embedding_method
fileparts = None
if subtype == "html":
if subtype == "html" and image_embedding_method != "none":
body, fileparts = self._build_email_replace_img_src(body)

# TODO check if we can add attachments here.
result = super(IrMailServer, self).build_email(
email_from=email_from,
email_to=email_to,
Expand All @@ -53,32 +60,91 @@ def build_email(
subtype_alternative=subtype_alternative,
)
if fileparts:
# Multipart method MUST be multipart/related for CIDs embedding
# Gmail and Office won't process the images otherwise
if image_embedding_method == "cid":
result.set_type("multipart/related")

for fpart in fileparts:
result.attach(fpart)

# after all part where added, we need to reorganize the parts
# before reorganisation, the parts are in this shape:
# - boundary 1
# - text/plain
# - text/html
# - image/png
# After, the parts are in this shape:
# - boundary 1
# - multipart/alternative
# - boundary 2
# - text/plain
# - text/html
# - image/png

# maybe I could user: result.make_alternative() / result.make_related()

all_parts = []

# if an attachment is present, the parts are already in the right order
# in this case, we don't need to reorganize the parts
# but if we find later text/plain or text/html parts, we will need to append them to the first multipart/alternative.
#
# It possible to have multiple parts of type multipart/alternative, but it's not a common case.

# TODO try to used: _add_multipart() method from email/message.py
for part in result.iter_parts():
if part.get_content_type() == 'multipart/alternative':
all_parts.append(part)

if not all_parts:
all_parts = [MIMEMultipart("alternative")]

for part in result.iter_parts():
print(part.get_content_type())
if part.get_content_type() in ["text/html", "text/plain"]:
all_parts[0].attach(part)
elif part.get_content_type() == "multipart/alternative":
pass
else:
all_parts.append(part)
result.set_payload(all_parts)

return result

def _build_email_replace_img_src(self, html_body):
"""Replace img src with base64 encoded image."""
if not html_body:
return html_body

base_url = self.env["ir.config_parameter"].get_param("web.base.url")
image_embedding_method = self.env.company.image_embedding_method
root = fromstring(html_body)
images = root.xpath("//img")
fileparts = []
for img in images:
src = img.get("src")
if src and not src.startswith("data:") and not src.startswith("base64:"):
try:
response = requests.get(src, timeout=10)
_logger.debug("Fetching image from %s", src)
if response.status_code == 200:
# Limit results to only internal resources to avoid malicious external
# image injections
for img in root.xpath(
".//img[starts-with(@src, '%s')]"
"| .//img[starts-with(@src, '/web/image')]" % (base_url)
):
image_path = img.get("src")
try:
response = requests.get(image_path, timeout=10)
_logger.debug("Fetching image from %s", image_path)
if response.status_code == 200:
image_content = response.content
filepart = MIMEImage(image_content)
if image_embedding_method == "data":
raw_content = filepart.get_payload(decode=True)
base_64_content = b64encode(raw_content).decode("utf-8")
mimetype = filepart.get_content_type()
img.set("src", f"data:{mimetype};base64,{base_64_content}")
elif image_embedding_method == "cid":
cid = uuid.uuid4().hex
# convert cid to rfc2047 encoding
filename_encoded = "=?utf-8?b?%s?=" % b64encode(
cid.encode("utf-8")
).decode("utf-8")
image_content = response.content
filepart = MIMEImage(image_content)
filepart.add_header("Content-ID", f"<{cid}>")
filepart.add_header(
"Content-Disposition",
Expand All @@ -87,6 +153,12 @@ def _build_email_replace_img_src(self, html_body):
)
img.set("src", f"cid:{cid}")
fileparts.append(filepart)
except Exception as e:
_logger.warning("Could not get %s: %s", img.get("src"), str(e))
else:
_logger.warning(
"Could not get %s: HTTP status code %s",
img.get("src"),
response.status_code,
)
except Exception as e:
_logger.warning("Could not get %s: %s", img.get("src"), str(e))
return tostring(root, encoding="unicode"), fileparts
12 changes: 12 additions & 0 deletions mail_embed_image/models/res_config_settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Copyright 2024 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import fields, models


class ResConfigSettings(models.TransientModel):
_inherit = "res.config.settings"

image_embedding_method = fields.Selection(
related="company_id.image_embedding_method",
readonly=False,
)
1 change: 1 addition & 0 deletions mail_embed_image/readme/CONTRIBUTORS.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
* George Daramouskas <[email protected]>
* Giovanni Francesco Capalbo <[email protected]>
* Italo LOPES <[email protected]>
* Stéphane Mangin <[email protected]>
6 changes: 6 additions & 0 deletions mail_embed_image/readme/DESCRIPTION.rst
Original file line number Diff line number Diff line change
@@ -1,2 +1,8 @@
This module finds images attached to outgoing emails and replaces their urls
with cids. This will avoid rendering issues with some email clients.

It also provides 2 options to embed internal URL images in a mail body:
- CIDs: add fileparts as CIDs
- Data URLs: add images as data URLs

This option is configurable in an company settings variables.
10 changes: 10 additions & 0 deletions mail_embed_image/static/description/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,15 @@ <h1 class="title">Mail Embed Image</h1>
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/social/tree/16.0/mail_embed_image"><img alt="OCA/social" src="https://img.shields.io/badge/github-OCA%2Fsocial-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/social-16-0/social-16-0-mail_embed_image"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/social&amp;target_branch=16.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
<p>This module finds images attached to outgoing emails and replaces their urls
with cids. This will avoid rendering issues with some email clients.</p>
<dl class="docutils">
<dt>It also provides 2 options to embed internal URL images in a mail body:</dt>
<dd><ul class="first last simple">
<li>CIDs: add fileparts as CIDs</li>
<li>Data URLs: add images as data URLs</li>
</ul>
</dd>
</dl>
<p>This option is configurable in an company settings variables.</p>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
Expand Down Expand Up @@ -405,6 +414,7 @@ <h2><a class="toc-backref" href="#toc-entry-4">Contributors</a></h2>
<li>George Daramouskas &lt;<a class="reference external" href="mailto:gdaramouskas&#64;therp.nl">gdaramouskas&#64;therp.nl</a>&gt;</li>
<li>Giovanni Francesco Capalbo &lt;<a class="reference external" href="mailto:giovanni&#64;therp.nl">giovanni&#64;therp.nl</a>&gt;</li>
<li>Italo LOPES &lt;<a class="reference external" href="mailto:italo.lopes&#64;camptocamp.com">italo.lopes&#64;camptocamp.com</a>&gt;</li>
<li>Stéphane Mangin &lt;<a class="reference external" href="mailto:stephane.mangin&#64;camptocamp.com">stephane.mangin&#64;camptocamp.com</a>&gt;</li>
</ul>
</div>
<div class="section" id="maintainers">
Expand Down
Loading