Skip to content

Commit

Permalink
fixup! ✨(emails) use mjml to generate html and text emails
Browse files Browse the repository at this point in the history
  • Loading branch information
wilbrdt committed Aug 19, 2024
1 parent 16b6b87 commit b49a7ad
Show file tree
Hide file tree
Showing 17 changed files with 266 additions and 101 deletions.
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ jobs:
# ---- Email jobs ----
build-mails:
docker:
- image: cimg/node:18.18
- image: cimg/node:20.16.0
auth:
username: $DOCKER_HUB_USER
password: $DOCKER_HUB_PASSWORD
Expand Down
3 changes: 2 additions & 1 deletion .env.dist
Original file line number Diff line number Diff line change
Expand Up @@ -58,5 +58,6 @@ EMAIL_USE_TLS=False
[email protected]
EMAIL_RATE_LIMIT=100/m
EMAIL_MAX_RETRIES=3
EMAIL_SITE_NAME=FUN
EMAIL_SITE_NAME="France Université Numérique"
EMAIL_SITE_BASE_URL=https://fun-mooc.fr
EMAIL_SITE_LOGIN_URL=https://lms.fun-mooc.fr/login
7 changes: 4 additions & 3 deletions src/app/mork/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,12 @@ class Settings(BaseSettings):
EMAIL_HOST_PASSWORD: str = ""
EMAIL_PORT: int = 1025
EMAIL_USE_TLS: bool = False
EMAIL_FROM: str = "[email protected]"
EMAIL_FROM: str = ""
EMAIL_RATE_LIMIT: str = "100/m"
EMAIL_MAX_RETRIES: int = 3
EMAIL_SITE_NAME: str = "FUN"
EMAIL_SITE_BASE_URL: str = "https://fun-mooc.fr"
EMAIL_SITE_NAME: str = ""
EMAIL_SITE_BASE_URL: str = ""
EMAIL_SITE_LOGIN_URL: str = ""

# Celery
broker_url: str = Field("redis://redis:6379/0", alias="MORK_CELERY_BROKER_URL")
Expand Down
7 changes: 4 additions & 3 deletions src/app/mork/mail.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

from mork.conf import settings
from mork.exceptions import EmailSendError
from mork.templatetags.extra_tags import Base64StaticTag
from mork.templatetags.extra_tags import SVGStaticTag

logger = getLogger(__name__)

Expand All @@ -25,7 +25,7 @@ def render_template(template: str, context) -> str:
]
),
autoescape=True,
extensions=[Base64StaticTag],
extensions=[SVGStaticTag],
)
template = template_env.get_template(template)
return template.render(**context)
Expand All @@ -40,6 +40,7 @@ def send_email(email_address: str, username: str):
"site": {
"name": settings.EMAIL_SITE_NAME,
"url": settings.EMAIL_SITE_BASE_URL,
"login_url": settings.EMAIL_SITE_LOGIN_URL,
},
}
html = render_template(
Expand All @@ -56,7 +57,7 @@ def send_email(email_address: str, username: str):
message = MIMEMultipart("alternative")
message["From"] = settings.EMAIL_FROM
message["To"] = email_address
message["Subject"] = "Your account will be closed soon"
message["Subject"] = "Votre compte va bientôt être supprimé"

# Attach the HTML parts. According to RFC 2046, the last part of a multipart
# message, in this case the HTML message, is best and preferred
Expand Down
188 changes: 188 additions & 0 deletions src/app/mork/static/images/logo-fr.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed src/app/mork/static/images/logo_fun.png
Binary file not shown.
2 changes: 1 addition & 1 deletion src/app/mork/templatetags/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
# noqa: D104
"""Template tags module."""
12 changes: 6 additions & 6 deletions src/app/mork/templatetags/extra_tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,17 @@
from jinja2_simple_tags import StandaloneTag

from mork.conf import settings
from mork.utils import image_to_base64
from mork.utils import svg_to_datauri


class Base64StaticTag(StandaloneTag):
"""Extension Jinja tag for converting files to base64."""
class SVGStaticTag(StandaloneTag):
"""Extension Jinja tag for converting SVG files to data URI."""

tags = {"base64_static"}
tags = {"svg_static"}

def render(self, path: str):
"""Return a static file into base64 format."""
"""Return a SVG static file into data URI format."""
full_path = settings.STATIC_PATH / path
if full_path.exists():
return image_to_base64(full_path)
return svg_to_datauri(full_path)
return ""
3 changes: 3 additions & 0 deletions src/app/mork/tests/static/images/red-square.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed src/app/mork/tests/static/images/red-square.webp
Binary file not shown.
23 changes: 10 additions & 13 deletions src/app/mork/tests/templatetags/test_extra_tags.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,28 @@
"""Tests for Mork extra Jinja tags."""

from mork.templatetags.extra_tags import Base64StaticTag
from mork.templatetags.extra_tags import SVGStaticTag
from mork.tests.conftest import TEST_STATIC_PATH


def test_base64tag_render(monkeypatch):
"""Test the Base64StaticTag `render` method returns file encoded in base64."""
static_filepath = TEST_STATIC_PATH / "images/red-square.webp"
def test_svgstatictag_render(monkeypatch):
"""Test the SVGStaticTag `render` method returns file encoded in base64."""
static_filepath = TEST_STATIC_PATH / "images/red-square.svg"

red_square_base64 = (
"data:image/webp;base64, UklGRjAAAABXRUJQVlA4ICQAAABwAQCdASoBAAEACTD+J7ACdA"
"FAAAD+0MkjDRr8XewjvuzAAAA="
)
red_square_base64 = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxIDEiPgo8cGF0aCBkPSJNMCwwaDF2MUgwIiBmaWxsPSIjZjAwIi8+Cjwvc3ZnPg==" # noqa: E501

monkeypatch.setattr(
"mork.templatetags.extra_tags.Base64StaticTag.__init__", lambda x: None
"mork.templatetags.extra_tags.SVGStaticTag.__init__", lambda x: None
)

assert Base64StaticTag().render(static_filepath) == red_square_base64
assert SVGStaticTag().render(static_filepath) == red_square_base64


def test_base64tag_render_unknown_file(monkeypatch):
"""Test that the Base64StaticTag `render` method should return an empty string."""
"""Test that the SVGStaticTag `render` method should return an empty string."""
static_filepath = "unknown-static-file.txt"

monkeypatch.setattr(
"mork.templatetags.extra_tags.Base64StaticTag.__init__", lambda x: None
"mork.templatetags.extra_tags.SVGStaticTag.__init__", lambda x: None
)

assert Base64StaticTag().render(static_filepath) == ""
assert SVGStaticTag().render(static_filepath) == ""
3 changes: 3 additions & 0 deletions src/app/mork/tests/test_mail.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ def test_render_template():
"site": {
"name": "Example site",
"url": "http://base_url.com",
"login_url": "http://url.com/login",
},
}
render_html = render_template("warning_email.html", template_vars)
Expand All @@ -26,6 +27,7 @@ def test_render_template():
assert template_vars["fullname"] in render_html
assert template_vars["site"]["name"] in render_html
assert template_vars["site"]["url"] in render_html
assert template_vars["site"]["login_url"] in render_html
assert 'src="data:' in render_html

render_text = render_template("warning_email.txt", template_vars)
Expand All @@ -34,6 +36,7 @@ def test_render_template():
assert template_vars["fullname"] in render_text
assert template_vars["site"]["name"] in render_text
assert template_vars["site"]["url"] in render_text
assert template_vars["site"]["login_url"] in render_text
assert "data:" in render_text


Expand Down
17 changes: 6 additions & 11 deletions src/app/mork/tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,14 @@
"""Tests for utilitary functions."""
"""Tests for utility functions."""

from mork.tests.conftest import TEST_STATIC_PATH
from mork.utils import image_to_base64
from mork.utils import svg_to_datauri


def test_utils_image_to_base64_path():
def test_utils_svg_to_datauri_path():
"""Image to base64 from path."""

red_square_base64 = (
"data:image/webp;base64, UklGRjAAAABXRUJQVlA4ICQAAABwAQCdASoBAAEACTD+J7ACdA"
"FAAAD+0MkjDRr8XewjvuzAAAA="
)
red_square_base64 = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxIDEiPgo8cGF0aCBkPSJNMCwwaDF2MUgwIiBmaWxsPSIjZjAwIi8+Cjwvc3ZnPg==" # noqa: E501

assert (
image_to_base64(TEST_STATIC_PATH / "images/red-square.webp")
== red_square_base64
svg_to_datauri(TEST_STATIC_PATH / "images/red-square.svg") == red_square_base64
)

assert image_to_base64("not_found.png") == ""
31 changes: 5 additions & 26 deletions src/app/mork/utils.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,10 @@
"""Utilitary functions."""
"""Utility functions."""

import base64
from pathlib import Path

from PIL import ImageFile as PillowImageFile
from datauri import DataURI


def image_to_base64(path: Path | str):
"""Return the src string of the base64 encoding of an image.
Strongly inspired by Joanie's `image_to_base64`.
"""
pil_parser = PillowImageFile.Parser()
try:
file = open(path, "rb")
except OSError:
return ""

try:
image_data = file.read()
if not image_data:
return ""
pil_parser.feed(image_data)
if pil_parser.image:
mime_type = pil_parser.image.get_format_mimetype()
encoded_string = base64.b64encode(image_data)
return f"data:{mime_type:s};base64, {encoded_string.decode('utf-8'):s}"
return ""
finally:
file.close()
def svg_to_datauri(path: Path | str):
"""Return the data URI string of an SVG image."""
return str(DataURI.from_file(path))
2 changes: 1 addition & 1 deletion src/app/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ dependencies = [
"celery[redis]==5.4.0",
"Jinja2==3.1.4",
"jinja2-simple-tags==0.6.1",
"pillow==10.4.0",
"psycopg[binary]==3.2.1",
"pydantic_settings==2.4.0",
"python-datauri==2.2.0",
"fastapi[standard]==0.112.0",
"pymysql==1.1.1",
"sentry-sdk==2.12.0",
Expand Down
24 changes: 11 additions & 13 deletions src/mail/mjml/warning_email.mjml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<mj-wrapper css-class="wrapper" padding="20px 40px 40px 40px">
<mj-section>
<mj-column>
<mj-image src="{% base64_static 'images/logo_fun.png' %}" width="200px" align="left" alt="Logo de France Université Numérique" />
<mj-image src="{% svg_static 'images/logo-fr.svg' %}" width="200px" align="left" alt="Logo de France Université Numérique" />
</mj-column>
</mj-section>
<mj-section mj-class="bg--blue-100" padding="20px 40px 0 40px">
Expand All @@ -17,23 +17,21 @@
<mj-section mj-class="bg--blue-100" border-radius="6px 6px 0 0" padding="20px 40px 20px 40px">
<mj-column>
<mj-text padding="0">
<p>
{{ fullname }},<br/>
Vous ne vous êtes pas connectés sur fun-mooc.fr depuis longtemps.
<br/>
Malheureusement, sans action de votre part et
conformément à notre politique de protection des données, nous procéderons
à la suppression de votre compte dans 30 jours.
<br/>
<br/>
Si vous souhaitez conservez votre compte, veuillez vous connecter à la plateforme.
</p>
{{ fullname }},<br/>
Vous ne vous êtes pas connectés sur fun-mooc.fr depuis longtemps.
<br/>
Malheureusement, sans action de votre part et
conformément à notre politique de protection des données, nous procéderons
à la suppression de votre compte dans 30 jours.
<br/>
<br/>
Si vous souhaitez conservez votre compte, veuillez vous connecter à la plateforme.
</mj-text>
</mj-column>
</mj-section>
<mj-section mj-class="bg--blue-100" border-radius="0 0 6px 6px" padding="0 50px 30px 50px">
<mj-column>
<mj-button background-color="#055FD2" color="white" href="https://lms.fun-mooc.fr/login">
<mj-button background-color="#055FD2" color="white" href="{{ site.login_url }}">
Se connecter
</mj-button>
</mj-column>
Expand Down
43 changes: 21 additions & 22 deletions src/mail/package.json
Original file line number Diff line number Diff line change
@@ -1,23 +1,22 @@
{
"name": "mail_mjml",
"version": "2.5.1",
"description": "An util to generate html and text jinja templates from mjml templates",
"type": "module",
"dependencies": {
"@html-to/text-cli": "0.5.4",
"mjml": "4.15.3"
},
"private": true,
"scripts": {
"build-mjml-to-html": "./bin/mjml-to-html",
"build-html-to-plain-text": "./bin/html-to-plain-text",
"build": "yarn build-mjml-to-html; yarn build-html-to-plain-text;"
},
"volta": {
"node": "16.15.1"
},
"repository": "https://github.com/openfun/mork",
"author": "France Université Numérique",
"license": "MIT"
}

"name": "mail_mjml",
"version": "2.5.1",
"description": "An util to generate html and text jinja templates from mjml templates",
"type": "module",
"dependencies": {
"@html-to/text-cli": "0.5.4",
"mjml": "4.15.3"
},
"private": true,
"scripts": {
"build-mjml-to-html": "./bin/mjml-to-html",
"build-html-to-plain-text": "./bin/html-to-plain-text",
"build": "yarn build-mjml-to-html; yarn build-html-to-plain-text;"
},
"volta": {
"node": "16.15.1"
},
"repository": "https://github.com/openfun/mork",
"author": "France Université Numérique",
"license": "MIT"
}

0 comments on commit b49a7ad

Please sign in to comment.