emails: Replace Premailer with CSS-inline.

Primary goal of library replacement is improving execution speed.
This commit should not affect the functionality of the system
or make any changes to it.
This commit is contained in:
Daniil Fadeev 2023-04-03 19:30:03 +04:00 committed by Tim Abbott
parent dceb49011a
commit 5c6f842b28
8 changed files with 62 additions and 65 deletions

View File

@ -131,7 +131,7 @@ limited CSS support and generally require us to inject any CSS we're
using in the emails into the email as inline styles. And then you
also need both plain-text and HTML emails. We solve these problems
using a combination of the
[premailer](https://github.com/peterbe/premailer) library and having
[css-inline](https://github.com/Stranger6667/css-inline) library and having
two copies of each email (plain-text and HTML).
So for each email, there are two source templates: the `.txt` version
@ -139,7 +139,7 @@ So for each email, there are two source templates: the `.txt` version
`.txt` version is used directly; while the `.source.html` template is
processed by `scripts/setup/inline_email_css.py` (generating a `.html` template
under `templates/zerver/emails/compiled`); that tool (powered by
`premailer`) injects the CSS we use for styling our emails
`css-inline`) injects the CSS we use for styling our emails
(`templates/zerver/emails/email.css`) into the templates inline.
What this means is that when you're editing emails, **you need to run

View File

@ -56,7 +56,7 @@ html2text
https://github.com/zulip/talon/archive/137ea31ca506069f9a8bbddde0d0174f395a6893.zip#egg=talon-core==1.6.0.zulip1&subdirectory=talon-core
# Needed for inlining the CSS in emails
premailer
css-inline
# Needed for JWT-based auth
PyJWT

View File

@ -155,10 +155,6 @@ bracex==2.3.post1 \
--hash=sha256:351b7f20d56fb9ea91f9b9e9e7664db466eb234188c175fd943f8f755c807e73 \
--hash=sha256:e7b23fc8b2cd06d3dec0692baabecb249dda94e06a617901ff03a6c56fd71693
# via wcmatch
cachetools==5.3.0 \
--hash=sha256:13dfddc7b8df938c21a940dfa6557ce6e94a2f1cdfa58eb90c805721d58f2c14 \
--hash=sha256:429e1a1e845c008ea6c85aa35d4b98b65d6a9763eeef3e37e92728a12d1de9d4
# via premailer
cairocffi==1.4.0 \
--hash=sha256:509339b32ccd8d7b00c2204c32736cde78db53a32e6a162d312478d25626cd9a
# via cairosvg
@ -474,22 +470,40 @@ cryptography==39.0.2 \
# social-auth-core
# types-pyopenssl
# types-redis
css-inline==0.8.7 \
--hash=sha256:0e87293717865bca00bbc2046864e33a421468129a8a8fe938abf2397c4a8e26 \
--hash=sha256:20ee0894e7a72434750799fced0c7404fed19f3d0538c7fb3ff61d4efacd473d \
--hash=sha256:2bf6053352d053d32a2294510416c704e5b62325a8214dca5c764455e318948c \
--hash=sha256:3604d7af3df90681a5a31d2a1438bd237ae1ba171f2ea1cb62824f4909238a63 \
--hash=sha256:36a392ed87f840621838e63937e151d236a62cfdba2760f503273330691ea3d8 \
--hash=sha256:39a90bd53272ee68a4d7909dc9b03240b296c50249b964cb253faf361c90e9dd \
--hash=sha256:51cc996cfce5fd10aa5e41569c664cded198f30f4706e699e97942893aa9e7f9 \
--hash=sha256:5b08837acd1fe60a8f8cbd44dfce88dda1676aca47eeb51bf512c02e90803b77 \
--hash=sha256:6579c9ea5e288e644bd78dbfd3d2bae9c33eb11e3f02d2c61cf4b5683c8f97da \
--hash=sha256:6803c2fb2064330de1761a44ae86ab6c9b78d1761c36ebdd8919346e595d702d \
--hash=sha256:68dac7010c27624627f7df9be12888b9129fd658804f52f8feac25f7d4766050 \
--hash=sha256:6c0f02ddc5b694520d0fb8db7961e703120a373e516a74cfa6c8303b1c131e42 \
--hash=sha256:6e0afb35e17888b2ddd8efee738f1f68ae569615cb32b66427381372cab2d6b2 \
--hash=sha256:773a150ba085d73ea8a4f27f562dcf90a7bc8dcecf0d1867b660f22d036c6a6a \
--hash=sha256:7e5d5aebf87bcacc6bd3f548e0648f4e1b26d4b50e7239df234a42cb1374c039 \
--hash=sha256:819bf4c331e59da07d824529027c00ec618c2f0b1d498f43e5281988339ac082 \
--hash=sha256:84e2b2c5c7c16b5ff546f9fae53e7f0d24bc63e9de6a2a655809c3698ad9f9e2 \
--hash=sha256:c5910202e7583f0d1b6aea34d63cd378e28808f8bcf210a9c961cb26ccc5ed25 \
--hash=sha256:ea7d4313eed1de7def6d2e9c4780f11e1faed3800f2e2ad4de47dc15ba7a40da \
--hash=sha256:f0757b71ae23d4131d7ca274bb35d41f5faa41b88eed9090df3ab409689d055d \
--hash=sha256:f3b5dc09f6de78fa9f7152e701fdfa2ed827a7c0543a6746ba9879e8731c6da5
# via -r requirements/common.in
cssselect==1.2.0 \
--hash=sha256:666b19839cfaddb9ce9d36bfe4c969132c647b92fc9088c4e23f786b30f1b3dc \
--hash=sha256:da1885f0c10b60c03ed5eccbb6b68d6eff248d91976fcde348f395d54c9fd35e
# via
# parsel
# premailer
# scrapy
# talon-core
cssselect2==0.7.0 \
--hash=sha256:1ccd984dab89fc68955043aca4e1b03e0cf29cad9880f6e28e3ba7a74b14aa5a \
--hash=sha256:fd23a65bfd444595913f02fc71f6b286c29261e354c41d722ca7a261a49b5969
# via cairosvg
cssutils==2.6.0 \
--hash=sha256:30c72f3a5c5951a11151640600aae7b3bf10e4c0d5c87f5bc505c2cd4a26e0c2 \
--hash=sha256:f7dcd23c1cec909fdf3630de346e1413b7b2555936dec14ba2ebb9913bf0818e
# via premailer
dataclasses-json==0.5.7 \
--hash=sha256:bc285b5f892094c3a53d558858a88553dd6a61a11ab1a8128a0e554385dcc5dd \
--hash=sha256:c2c11bc8214fbf709ffc369d11446ff6945254a7f09128154a7620613d8fda90
@ -1063,7 +1077,6 @@ lxml==4.6.5 \
# via
# -r requirements/common.in
# parsel
# premailer
# pyoembed
# python3-saml
# scrapy
@ -1463,10 +1476,6 @@ polib==1.2.0 \
--hash=sha256:1c77ee1b81feb31df9bca258cbc58db1bbb32d10214b173882452c73af06d62d \
--hash=sha256:f3ef94aefed6e183e342a8a269ae1fc4742ba193186ad76f175938621dbfc26b
# via -r requirements/common.in
premailer==3.10.0 \
--hash=sha256:021b8196364d7df96d04f9ade51b794d0b77bcc19e998321c515633a2273be1a \
--hash=sha256:d1875a8411f5dc92b53ef9f193db6c0f879dc378d618e0ad292723e388bfe4c2
# via -r requirements/common.in
prompt-toolkit==3.0.38 \
--hash=sha256:23ac5d50538a9a38c8bde05fecb47d0b403ecd0662857a86f886f798563d5b9b \
--hash=sha256:45ea77a2f7c60418850331366c81cf6b5b9cf4c7fd34616f733c5427e6abbb1f
@ -1948,7 +1957,6 @@ requests[security]==2.28.2 \
# -r requirements/common.in
# matrix-client
# moto
# premailer
# pyoembed
# python-digitalocean
# python-gcm

View File

@ -84,10 +84,6 @@ botocore==1.29.84 \
# via
# boto3
# s3transfer
cachetools==5.3.0 \
--hash=sha256:13dfddc7b8df938c21a940dfa6557ce6e94a2f1cdfa58eb90c805721d58f2c14 \
--hash=sha256:429e1a1e845c008ea6c85aa35d4b98b65d6a9763eeef3e37e92728a12d1de9d4
# via premailer
cchardet==2.1.7 \
--hash=sha256:0b859069bbb9d27c78a2c9eb997e6f4b738db2d7039a03f8792b4058d61d1109 \
--hash=sha256:228d2533987c450f39acf7548f474dd6814c446e9d6bd228e8f1d9a2d210f10b \
@ -322,16 +318,33 @@ cryptography==39.0.2 \
# -r requirements/common.in
# pyopenssl
# social-auth-core
css-inline==0.8.7 \
--hash=sha256:0e87293717865bca00bbc2046864e33a421468129a8a8fe938abf2397c4a8e26 \
--hash=sha256:20ee0894e7a72434750799fced0c7404fed19f3d0538c7fb3ff61d4efacd473d \
--hash=sha256:2bf6053352d053d32a2294510416c704e5b62325a8214dca5c764455e318948c \
--hash=sha256:3604d7af3df90681a5a31d2a1438bd237ae1ba171f2ea1cb62824f4909238a63 \
--hash=sha256:36a392ed87f840621838e63937e151d236a62cfdba2760f503273330691ea3d8 \
--hash=sha256:39a90bd53272ee68a4d7909dc9b03240b296c50249b964cb253faf361c90e9dd \
--hash=sha256:51cc996cfce5fd10aa5e41569c664cded198f30f4706e699e97942893aa9e7f9 \
--hash=sha256:5b08837acd1fe60a8f8cbd44dfce88dda1676aca47eeb51bf512c02e90803b77 \
--hash=sha256:6579c9ea5e288e644bd78dbfd3d2bae9c33eb11e3f02d2c61cf4b5683c8f97da \
--hash=sha256:6803c2fb2064330de1761a44ae86ab6c9b78d1761c36ebdd8919346e595d702d \
--hash=sha256:68dac7010c27624627f7df9be12888b9129fd658804f52f8feac25f7d4766050 \
--hash=sha256:6c0f02ddc5b694520d0fb8db7961e703120a373e516a74cfa6c8303b1c131e42 \
--hash=sha256:6e0afb35e17888b2ddd8efee738f1f68ae569615cb32b66427381372cab2d6b2 \
--hash=sha256:773a150ba085d73ea8a4f27f562dcf90a7bc8dcecf0d1867b660f22d036c6a6a \
--hash=sha256:7e5d5aebf87bcacc6bd3f548e0648f4e1b26d4b50e7239df234a42cb1374c039 \
--hash=sha256:819bf4c331e59da07d824529027c00ec618c2f0b1d498f43e5281988339ac082 \
--hash=sha256:84e2b2c5c7c16b5ff546f9fae53e7f0d24bc63e9de6a2a655809c3698ad9f9e2 \
--hash=sha256:c5910202e7583f0d1b6aea34d63cd378e28808f8bcf210a9c961cb26ccc5ed25 \
--hash=sha256:ea7d4313eed1de7def6d2e9c4780f11e1faed3800f2e2ad4de47dc15ba7a40da \
--hash=sha256:f0757b71ae23d4131d7ca274bb35d41f5faa41b88eed9090df3ab409689d055d \
--hash=sha256:f3b5dc09f6de78fa9f7152e701fdfa2ed827a7c0543a6746ba9879e8731c6da5
# via -r requirements/common.in
cssselect==1.2.0 \
--hash=sha256:666b19839cfaddb9ce9d36bfe4c969132c647b92fc9088c4e23f786b30f1b3dc \
--hash=sha256:da1885f0c10b60c03ed5eccbb6b68d6eff248d91976fcde348f395d54c9fd35e
# via
# premailer
# talon-core
cssutils==2.6.0 \
--hash=sha256:30c72f3a5c5951a11151640600aae7b3bf10e4c0d5c87f5bc505c2cd4a26e0c2 \
--hash=sha256:f7dcd23c1cec909fdf3630de346e1413b7b2555936dec14ba2ebb9913bf0818e
# via premailer
# via talon-core
decorator==5.1.1 \
--hash=sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330 \
--hash=sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186
@ -747,7 +760,6 @@ lxml==4.6.5 \
--hash=sha256:ff44de36772b05c2eb74f2b4b6d1ae29b8f41ed5506310ce1258d44826ee38c1
# via
# -r requirements/common.in
# premailer
# pyoembed
# python3-saml
# social-auth-core
@ -1022,10 +1034,6 @@ polib==1.2.0 \
--hash=sha256:1c77ee1b81feb31df9bca258cbc58db1bbb32d10214b173882452c73af06d62d \
--hash=sha256:f3ef94aefed6e183e342a8a269ae1fc4742ba193186ad76f175938621dbfc26b
# via -r requirements/common.in
premailer==3.10.0 \
--hash=sha256:021b8196364d7df96d04f9ade51b794d0b77bcc19e998321c515633a2273be1a \
--hash=sha256:d1875a8411f5dc92b53ef9f193db6c0f879dc378d618e0ad292723e388bfe4c2
# via -r requirements/common.in
prompt-toolkit==3.0.38 \
--hash=sha256:23ac5d50538a9a38c8bde05fecb47d0b403ecd0662857a86f886f798563d5b9b \
--hash=sha256:45ea77a2f7c60418850331366c81cf6b5b9cf4c7fd34616f733c5427e6abbb1f
@ -1425,7 +1433,6 @@ requests[security]==2.28.2 \
# via
# -r requirements/common.in
# matrix-client
# premailer
# pyoembed
# python-gcm
# python-twitter

View File

@ -2,36 +2,20 @@
import os
from typing import Set
from cssutils import profile
from cssutils.profiles import Profiles, macros, properties
from premailer import Premailer
import css_inline
ZULIP_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../../")
EMAIL_TEMPLATES_PATH = os.path.join(ZULIP_PATH, "templates", "zerver", "emails")
CSS_SOURCE_PATH = os.path.join(EMAIL_TEMPLATES_PATH, "email.css")
def configure_cssutils() -> None:
# These properties are not supported by cssutils by default and will
# result in warnings when premailer package is run.
properties[Profiles.CSS_LEVEL_2]["-ms-interpolation-mode"] = r"none|bicubic|nearest-neighbor"
properties[Profiles.CSS_LEVEL_2]["-ms-text-size-adjust"] = r"none|auto|{percentage}"
properties[Profiles.CSS_LEVEL_2]["mso-table-lspace"] = r"0|{num}(pt)"
properties[Profiles.CSS_LEVEL_2]["mso-table-rspace"] = r"0|{num}(pt)"
properties[Profiles.CSS_LEVEL_2]["-webkit-text-size-adjust"] = r"none|auto|{percentage}"
properties[Profiles.CSS_LEVEL_2]["mso-hide"] = "all"
properties[Profiles.CSS_LEVEL_2]["pointer-events"] = (
r"auto|none|visiblePainted|"
r"visibleFill|visibleStroke|"
r"visible|painted|fill|stroke|all|inherit"
)
profile.addProfiles(
[(Profiles.CSS_LEVEL_2, properties[Profiles.CSS_LEVEL_2], macros[Profiles.CSS_LEVEL_2])]
)
def get_inliner_instance() -> css_inline.CSSInliner:
with open(CSS_SOURCE_PATH) as file:
content = file.read()
return css_inline.CSSInliner(extra_css=content)
configure_cssutils()
inliner = get_inliner_instance()
def inline_template(template_source_name: str) -> None:
@ -45,13 +29,11 @@ def inline_template(template_source_name: str) -> None:
with open(template_path) as template_source_file:
template_str = template_source_file.read()
output = Premailer(
template_str, external_styles=[CSS_SOURCE_PATH], allow_loading_external_files=True
).transform()
output = inliner.inline(template_str)
output = escape_jinja2_characters(output)
# Premailer.transform will try to complete the DOM tree,
# Inline method of css-inline will try to complete the DOM tree,
# adding html, head, and body tags if they aren't there.
# While this is correct for the email_base_default template,
# it is wrong for the other templates that extend this
@ -83,7 +65,7 @@ def escape_jinja2_characters(text: str) -> str:
def strip_unnecessary_tags(text: str) -> str:
end_block = "</body>\n</html>"
end_block = "</body></html>"
start_block = "{% extends"
start = text.find(start_block)
end = text.rfind(end_block)

View File

@ -673,7 +673,7 @@ html_rules: List["Rule"] = [
"web/templates/single_message.hbs",
# Old-style email templates need to use inline style
# attributes; it should be possible to clean these up
# when we convert these templates to use premailer.
# when we convert these templates to use css-inline.
"templates/zerver/emails/email_base_messages.html",
# Email log templates; should clean up.
"templates/zerver/email.html",

View File

@ -48,4 +48,4 @@ API_FEATURE_LEVEL = 169
# historical commits sharing the same major version, in which case a
# minor version bump suffices.
PROVISION_VERSION = (227, 1)
PROVISION_VERSION = (228, 0)

View File

@ -987,7 +987,7 @@ earl-test@zulip.com""",
# field, because we've included that field inside the mailto:
# link for the sender.
self.assertIn(
'<a href="mailto:hamlet@zulip.com" style="color:#5f5ec7; text-decoration:underline">&lt;/a&gt; https://www.google.com (hamlet@zulip.com)</a> wants',
'<a href="mailto:hamlet@zulip.com" style="color: #5f5ec7;text-decoration: underline;">&lt;/a&gt; https://www.google.com (hamlet@zulip.com)</a> wants',
body,
)