Skip to content

Rework EMailTemplate: blocks rendered at the end, one template file per block, shared design tokens #65330

Description

@skjnldsv

After the email redesign for 36 (#65193, #65194, #65312 and the PRs moving mails over), lib/private/Mail/EMailTemplate.php has become hard to change, and hard for customers to customise. Ten PRs in, these are the parts that slowed us down:

Today What it cost
The HTML lives in ~35 PHP heredocs filled with positional vsprintf (%7\$s) Every visual tweak is a careful edit of long strings
Colors, radius and font stack repeated in about 50 inline styles, dark mode as a separate set of rules in <head> The NcNoteCard colors (#65222) and dark mode links touched dozens of places
bodyOpened / bodyListOpened / footerAdded decide where HTML gets appended Headings and the sender block have to close the body by hand
addBodyText($text, $plainText) silently treats $text as raw HTML when $plainText is given That's how the &amp;amp; heading bug happened (#65219)
Right-to-left mirroring is a regex over the finished HTML (#65312) Works, but it's a workaround
Byte-exact whole-email fixtures, plus full HTML copied into app tests (NewUserMailHelperTest) Each style change rewrites 5+ fixtures
Customers customise by copying and patching the protected HTML strings (mail_template_class) Every redesign breaks their templates, see nextcloud-gmbh/email_template_example#10

Note

mail_template_class stays the supported way for customers to customise emails, and gets simpler. The public API (IEMailTemplate, EMailDetails) doesn't change.

Proposal, in four PRs:

  1. Blocks first, render last. Each add*() call appends a small block object, renderHtml() and renderText() walk the list at the end. Each block also knows its plain text. No more open/close state, HTML and text can't drift apart. Pure refactor: the existing fixtures must stay byte-identical.
  2. One template file per block in core/templates/mail/ (layout.php, heading.php, note.php, details.php...), rendered with ITemplateManager and p(), so escaping is the default.
  3. Design tokens defined once (text, muted, borders, backgrounds, note colors, radius, font) in a small EMailStyle, used by the templates and the dark mode rules. Direction is a token too (start/end), which replaces the regex mirroring.
  4. Tests per block: a small fixture per block and direction, a couple of full email snapshots, and app tests asserting template calls instead of comparing HTML.

With that, a customer's mail_template_class can customise at the level it needs, without copying HTML:

Level How Survives a redesign
Branding Theming app: logo, color, name, slogan. No code Yes
Look and feel Override getStyle() and change tokens: font, colors, radius, backgrounds Yes
One block's markup Ship templates/mail/<block>.php in the customer's app, set $templateApp. Lookup falls back to core for every other block Mostly, if the block's parameters stay
Behaviour per email Override add*() methods, use $this->emailId and $this->data, as today Yes

These extension points (getStyle(), the token names, each block template's parameters, the add*() methods) become a documented contract with @since, covered by a test rendering a sample customer subclass.

Existing subclasses that override the old HTML strings get a compatibility layer for one release: an overridden string like $header is still used for its block, with a deprecation warning in the log pointing to the matching template file. It's removed the release after. Subclasses overriding methods need nothing.

Customer material to update along the way: the email_template_example app (one small example per level), the admin manual section on mail_template_class, and the portal article on customised email templates.

Not planned: a CSS inliner library or MJML. Plain PHP tokens are enough and add no dependency.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions