From 5c6f842b28f3ad37f094e42def08603e89759a32 Mon Sep 17 00:00:00 2001 From: Daniil Fadeev Date: Mon, 3 Apr 2023 19:30:03 +0400 Subject: [PATCH] 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. --- docs/subsystems/email.md | 4 +-- requirements/common.in | 2 +- requirements/dev.txt | 38 +++++++++++++++++----------- requirements/prod.txt | 41 ++++++++++++++++++------------- scripts/setup/inline_email_css.py | 36 +++++++-------------------- tools/linter_lib/custom_check.py | 2 +- version.py | 2 +- zerver/tests/test_invite.py | 2 +- 8 files changed, 62 insertions(+), 65 deletions(-) diff --git a/docs/subsystems/email.md b/docs/subsystems/email.md index cd6a83e483..0b30457f0d 100644 --- a/docs/subsystems/email.md +++ b/docs/subsystems/email.md @@ -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 diff --git a/requirements/common.in b/requirements/common.in index 90b3bf8d6b..016a3b2b0c 100644 --- a/requirements/common.in +++ b/requirements/common.in @@ -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 diff --git a/requirements/dev.txt b/requirements/dev.txt index 42f6adcf54..51e41b0fa4 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -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 diff --git a/requirements/prod.txt b/requirements/prod.txt index 80d7810a0e..ddc67112ad 100644 --- a/requirements/prod.txt +++ b/requirements/prod.txt @@ -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 diff --git a/scripts/setup/inline_email_css.py b/scripts/setup/inline_email_css.py index 878e2e70df..aca7020bf6 100755 --- a/scripts/setup/inline_email_css.py +++ b/scripts/setup/inline_email_css.py @@ -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 = "\n" + end_block = "" start_block = "{% extends" start = text.find(start_block) end = text.rfind(end_block) diff --git a/tools/linter_lib/custom_check.py b/tools/linter_lib/custom_check.py index f6eb74de8a..6e2b95d8ba 100644 --- a/tools/linter_lib/custom_check.py +++ b/tools/linter_lib/custom_check.py @@ -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", diff --git a/version.py b/version.py index dbb21342de..33cf268e6b 100644 --- a/version.py +++ b/version.py @@ -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) diff --git a/zerver/tests/test_invite.py b/zerver/tests/test_invite.py index 88f558db8c..c825e4cca3 100644 --- a/zerver/tests/test_invite.py +++ b/zerver/tests/test_invite.py @@ -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> https://www.google.com (hamlet@zulip.com) wants', + '</a> https://www.google.com (hamlet@zulip.com) wants', body, )