Skip to content

Commit

Permalink
✨(emails) use mjml to generate html and text emails
Browse files Browse the repository at this point in the history
Introducing MJML, a responsive email framework, to reduce the pain of writing
responsive emails.
  • Loading branch information
wilbrdt committed Aug 14, 2024
1 parent 1e41ef6 commit 9bbed79
Show file tree
Hide file tree
Showing 21 changed files with 1,612 additions and 88 deletions.
42 changes: 42 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -199,20 +199,56 @@ jobs:
working_directory: ~/fun
steps:
- checkout
- attach_workspace:
at: ~/fun
- restore_cache:
keys:
- v1-dependencies-{{ .Revision }}
# Create test database
- run: sudo apt-get update
- run: sudo apt-get install postgresql-client
- run: whoami
- run: |
psql \
-d "postgresql://fun@localhost/postgres" \
-c "create database \"test-mork-db\";"
# Generate email templates
- run: make
- run:
name: Run tests
command: ~/.local/bin/pytest

# ---- Email jobs ----
build-mails:
docker:
- image: cimg/node:18.18
auth:
username: $DOCKER_HUB_USER
password: $DOCKER_HUB_PASSWORD
working_directory: ~/fun/src/mail
steps:
- checkout:
path: ~/fun
- restore_cache:
keys:
- v1-mail-dependencies-{{ .Revision }}
# If the yarn.lock file is not up-to-date with the package.json file,
# using the --frozen-lockfile should fail.
- run:
name: Install node dependencies
command: yarn install --frozen-lockfile
- run:
name: Build mails
command: yarn build
- persist_to_workspace:
root: ~/fun
paths:
- src/mork/templates
- save_cache:
paths:
- ./node_modules
key: v1-mail-dependencies-{{ .Revision }}

# ---- Packaging jobs ----
package:
docker:
Expand Down Expand Up @@ -402,6 +438,11 @@ workflows:
filters:
tags:
only: /.*/
# Build mails
- build-mails:
filters:
tags:
only: /.*/

# Docker jobs
#
Expand All @@ -428,6 +469,7 @@ workflows:
- test:
requires:
- build
- build-mails
filters:
tags:
only: /.*/
Expand Down
2 changes: 2 additions & 0 deletions .env.dist
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,5 @@ EMAIL_USE_TLS=False
[email protected]
EMAIL_RATE_LIMIT=100/m
EMAIL_MAX_RETRIES=3
EMAIL_SITE_NAME=FUN
EMAIL_SITE_BASE_URL=https://fun-mooc.fr
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ ENV/
env.bak/
venv.bak/

# npm
node_modules

# Mails
src/mork/templates/

# Logs
*.log

Expand Down
34 changes: 29 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@
SHELL := /bin/bash

# -- Docker
COMPOSE = bin/compose
COMPOSE_EXEC = $(COMPOSE) exec
COMPOSE_RUN = $(COMPOSE) run --rm --no-deps
COMPOSE_RUN_API = $(COMPOSE_RUN) api
COMPOSE = bin/compose
COMPOSE_EXEC = $(COMPOSE) exec
COMPOSE_RUN = $(COMPOSE) run --rm --no-deps
COMPOSE_RUN_API = $(COMPOSE_RUN) api
COMPOSE_RUN_MAIL = $(COMPOSE_RUN) mail-generator

# -- MySQL
EDX_DB_HOST = mysql
Expand All @@ -25,6 +26,8 @@ MORK_TEST_DB_NAME ?= test-mork-db
# -- Celery
MORK_CELERY_SERVER_PORT ?= 5555

# -- Mail
MAIL_YARN = $(COMPOSE_RUN_MAIL) yarn

# ==============================================================================
# RULES
Expand All @@ -49,7 +52,9 @@ bootstrap: \
build \
run \
migrate \
seed-edx-database
seed-edx-database \
mails-install \
mails-build
.PHONY: bootstrap

build: ## build the app containers
Expand Down Expand Up @@ -171,6 +176,25 @@ test: \
.PHONY: test


# -- Mail generator

mails-build: ## Convert mjml files to html and text
@$(MAIL_YARN) build
.PHONY: mails-build

mails-build-html-to-plain-text: ## Convert html files to text
@$(MAIL_YARN) build-html-to-plain-text
.PHONY: mails-build-html-to-plain-text

mails-build-mjml-to-html: ## Convert mjml files to html and text
@$(MAIL_YARN) build-mjml-to-html
.PHONY: mails-build-mjml-to-html

mails-install: ## mail-generator yarn install
@$(MAIL_YARN) install
.PHONY: mails-install


# -- Misc
help:
@grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
Expand Down
9 changes: 9 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,15 @@ services:
ports:
- "1081:1080"

mail-generator:
image: node:18.18
user: "${DOCKER_USER:-1000}"
environment:
HOME: /tmp
volumes:
- ".:/app"
working_dir: /app/src/mail

mysql:
image: mysql:5.7
ports:
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ keywords = ["FastAPI", "Celery", "emails", "Open edX"]
dependencies = [
"alembic==1.13.2",
"celery[redis]==5.4.0",
"Jinja2==3.1.4",
"psycopg[binary]==3.2.1",
"pydantic_settings==2.4.0",
"fastapi[standard]==0.112.0",
Expand Down
22 changes: 22 additions & 0 deletions src/mail/bin/html-to-plain-text
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/usr/bin/env bash
set -eo pipefail
# Run html-to-text to convert all html files to text files
DIR_MAILS="../mork/templates/"

if [ ! -d "${DIR_MAILS}" ]; then
mkdir -p "${DIR_MAILS}";
fi

if [ ! -d "${DIR_MAILS}"html/ ]; then
mkdir -p "${DIR_MAILS}"html/;
exit;
fi

for file in "${DIR_MAILS}"html/*.html;
do html-to-text -j ./html-to-text.config.json < "$file" > "${file%.html}".txt; done;

if [ ! -d "${DIR_MAILS}"text/ ]; then
mkdir -p "${DIR_MAILS}"text/;
fi

mv "${DIR_MAILS}"html/*.txt "${DIR_MAILS}"text/;
9 changes: 9 additions & 0 deletions src/mail/bin/mjml-to-html
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/usr/bin/env bash

# Run mjml command to convert all mjml templates to html files
DIR_MAILS="../mork/templates/html/"

if [ ! -d "${DIR_MAILS}" ]; then
mkdir -p "${DIR_MAILS}";
fi
mjml mjml/*.mjml -o "${DIR_MAILS}";
11 changes: 11 additions & 0 deletions src/mail/html-to-text.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"wordwrap": 600,
"selectors": [
{
"selector": "h1",
"options": {
"uppercase": false
}
}
]
}
7 changes: 7 additions & 0 deletions src/mail/mjml/partial/footer.mjml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<mj-section padding="0">
<mj-column>
<mj-text mj-class="text--small" align="center" padding="20px 20px">
Cet email a été envoyé à {{ email }} par <a href="{{ site.url }}">{{ site.name }}</a>
</mj-text>
</mj-column>
</mj-section>
43 changes: 43 additions & 0 deletions src/mail/mjml/partial/header.mjml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<mj-head>
<mj-title>{{ title }}</mj-title>
<mj-preview>
{{ title }}
</mj-preview>
<mj-attributes>
<mj-font name="Roboto" href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;700;900&display=swap" />
<mj-all
font-family="Roboto, -apple-system, BlinkMacSystemFont, 'Segoe UI', Oxygen, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif"
font-size="16px"
line-height="1.5em"
color="#031963"
/>
<mj-class name="text--small" font-size="0.875rem" />
<mj-class name="bg--blue-100" background-color="#EDF5FA" />
</mj-attributes>
<mj-style>
/* Reset */
h1, h2, h3, h4, h5, h6, p {
margin: 0;
padding: 0;
}

a {
color: inherit;
}
</mj-style>
<mj-style>
/* Global styles */
h1 {
color: #055FD2;
font-size: 2rem;
line-height: 1em;
font-weight: 700;
}

.wrapper {
background: #FFFFFF;
border-radius: 0 0 6px 6px;
box-shadow: 0 0 6px rgba(2 117 180 / 0.3);
}
</mj-style>
</mj-head>
44 changes: 44 additions & 0 deletions src/mail/mjml/warning_email.mjml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<mjml>
<mj-include path="./partial/header.mjml" />
<mj-body mj-class="bg--blue-100">
<mj-wrapper css-class="wrapper" padding="20px 40px 40px 40px">
<mj-section>
<mj-column>
<mj-image src="../static/images/logo_fun.png" width="200px" align="left" alt="Logo de FUN" />
</mj-column>
</mj-section>
<mj-section mj-class="bg--blue-100" padding="20px 40px 0 40px">
<mj-column>
<mj-text align="center" padding="0 0 20px 0">
<h1>Votre compte va être supprimé dans 30 jours.</h1>
</mj-text>
</mj-column>
</mj-section>
<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>
</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">
Se connecter
</mj-button>
</mj-column>
</mj-section>
</mj-wrapper>
<mj-include path="./partial/footer.mjml" />
</mj-body>
</mjml>
23 changes: 23 additions & 0 deletions src/mail/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"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"
}

1 change: 1 addition & 0 deletions src/mail/static
Loading

0 comments on commit 9bbed79

Please sign in to comment.