emails: Inline CSS in emails in build_email.

Previously, we had an architecture where CSS inlining for emails was
done at provision time in inline_email_css.py. This was necessary
because the library we were using for this, Premailer, was extremely
slow, and doing the inlining for every outgoing email would have been
prohibitively expensive.

Now that we've migrated to a more modern library that inlines the
small amount of CSS we have into emails nearly instantly, we are able
to remove the complex architecture built to work around Premailer
being slow and just do the CSS inlining as the final step in sending
each individual email.

This has several significant benefits:

* Removes a fiddly provisioning step that made the edit/refresh cycle
  for modifying email templates confusing; there's no longer a CSS
  inlining step that, if you forget to do it, results in your testing a
  stale variant of the email templates.
* Fixes internationalization problems related to translators working
  with pre-CSS-inlined emails, and then Django trying to apply the
  translators to the post-CSS-inlined version.
* Makes the send_custom_email pipeline simpler and easier to improve.

Signed-off-by: Daniil Fadeev <fadeevd@zulip.com>
This commit is contained in:
Daniil Fadeev
2023-04-05 13:19:58 +04:00
committed by Tim Abbott
parent 7202a98438
commit 2f203f4de1
33 changed files with 91 additions and 214 deletions

View File

@@ -13,12 +13,12 @@ with only a few things you need to know to get started.
- All email templates are in `templates/zerver/emails/`. Each email has three
template files: `<template_prefix>.subject.txt`, `<template_prefix>.txt`, and
`<template_prefix>.source.html`. Email templates, along with all other templates
`<template_prefix>.html`. Email templates, along with all other templates
in the `templates/` directory, are Jinja2 templates.
- Most of the CSS and HTML layout for emails is in `email_base.html`. Note
- Most of the CSS and HTML layout for emails is in `email_base_default.html`. Note
that email has to ship with all of its CSS and HTML, so nothing in
`static/` is useful for an email. If you're adding new CSS or HTML for an
email, there's a decent chance it should go in `email_base.html`.
email, there's a decent chance it should go in `email_base_default.html`.
- All email is eventually sent by `zerver.lib.send_email.send_email`. There
are several other functions in `zerver.lib.send_email`, but all of them
eventually call the `send_email` function. The most interesting one is
@@ -104,11 +104,6 @@ email_password = gmail_password
### Notes
- After changing any HTML email or `email_base.html`, you need to run
`scripts/setup/inline_email_css.py` for the changes to be reflected
in the development environment. The script generates files like
`templates/zerver/emails/compiled/<template_prefix>.html`.
- Images won't be displayed in a real email client unless you change
the `base_image_uri` used for emails to a public URL such as
`https://chat.zulip.org/static/images/emails` (image links to
@@ -134,29 +129,18 @@ using a combination of the
[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
(for plain-text format) as well as a `.source.html` template. The
`.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
`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
`scripts/setup/inline_email_css.py`** after making changes to see the changes
take effect. Our tooling automatically runs this as part of
`tools/provision` and production deployments; but you should bump
`PROVISION_VERSION` when making changes to emails that change test
behavior, or other developers will get test failures until they
provision.
So, for each email, there are two source templates: the `.txt` version
(for plain-text format) as well as a `.html` template. The `.txt` version
is used directly, while `.html` is processed by `css-inline`, which injects
the CSS we use for styling our emails (`templates/zerver/emails/email.css`)
into the templates just before sending an email.
While this model is great for the markup side, it isn't ideal for
[translations](../translating/translating.md). The Django
translation system works with exact strings, and having different new
markup can require translators to re-translate strings, which can
result in problems like needing 2 copies of each string (one for
plain-text, one for HTML) and/or needing to re-translate a bunch of
strings after making a CSS tweak. Re-translating these strings is
plain-text, one for HTML). Re-translating these strings is
relatively easy in Transifex, but annoying.
So when writing email templates, we try to translate individual
@@ -166,7 +150,7 @@ translators to not have to deal with multiple versions of each string
in our emails.
One can test whether you did the translating part right by running
`scripts/setup/inline_email_css.py && manage.py makemessages` and then searching
`manage.py makemessages` and then searching
for the strings in `locale/en/LC_MESSAGES/django.po`; if there
are multiple copies or they contain CSS colors, you did it wrong.
@@ -179,5 +163,5 @@ code path for the "you don't have an account email" might not be,
since we might not know what language to use in the second case.
Future work in this space could be to actually generate the plain-text
versions of emails from the `.source.html` markup, so that we don't
versions of emails from the `.html` markup, so that we don't
need to maintain two copies of each email's text.