feat: well designed transactional e-mails
This PR introduces tooling for writing HTML emails using React Email, that is then compiled to Thymeleaf templates the backend can consume.
Features
- Familiar React syntax, that'll get compiled to ugly (but email-friendly) HTML
- Helpers to easily make conditionals and for-loops based on variables set during final render before sending
- Live preview & hot-reload, with some linting and miscellaneous convenience tools from React Email
- i18n-ready: uses ICU4J to render strings with proper localization when rendering the email to send
- Supports FormatJS's XML tags rendering. Allows for convenient use of advanced markup inside i18n strings
- Tolgee CLI fully configured and integrated, with a custom extractor to automagically sync strings
- Templates can differ based on whether the email is being sent from Tolgee Cloud or not
- This is detected by the presence of billing capabilities
The email design isn't the one made in #2710, but rather the first draft I originally made when first prototyping with this more than a year ago. (quick note: the design has a different footer based on whether it's sent from Cloud or Self-Hosted). This is why I'm marking this as "chore" more than "feat"; it doesn't bring any feature to the product yet.
I added exports to the package.json as a potential way to have additional email template workspaces in ee and billing: those would be able to import core parts like the layout and essential components/parts, while also defining their own component/parts separately, providing good isolation of them and keeping clear legal boundaries between Tolgee OSS, Tolgee EE, and proprietary Tolgee Cloud licenses.
I included documentation about how to use it, in email/HACKING.md. Gradle config has been updated to build emails as part of the backend build process.
Closes #2707
Summary by CodeRabbit
-
New Features
- Full email system: React-based templates, layouts, components, localization, image/resource handling, build/export tooling, and centralized template-rendered sending with subject extraction and global variables.
-
Bug Fixes
- Improved email delivery/verification stability by adding retry/wait handling in tests and flows.
-
Tests
- New unit/integration tests for email rendering, ICU formatting, and security; added email template test harness.
-
Documentation
- Comprehensive guide for creating and testing email templates.
-
Chores
- CI/workflow and build updates to integrate email build steps, packaging, and artifacts.
Walkthrough
Adds a full email subsystem (React Email templates, extractor, i18n, Tailwind, docs), integrates email build into Gradle and CI, introduces Kotlin EmailService/engine/resolver, adapts senders/tests for async email delivery, modernizes Gradle tasks, and updates E2E email tooling and fixtures.
Changes
| Cohort / File(s) | Summary |
|---|---|
Backend — Email core backend/data/src/main/kotlin/io/tolgee/email/* |
New EmailService, EmailTemplateEngine, EmailMessageResolver, EmailGlobalVariablesProvider and EmailTemplateConfig for Thymeleaf-based email rendering, ICU handling and global variables. |
Backend — Email usage & DTOs backend/data/src/main/kotlin/io/tolgee/component/email/*, backend/data/src/main/kotlin/io/tolgee/dtos/misc/EmailParams.kt, backend/data/src/main/kotlin/io/tolgee/configuration/tolgee/TolgeeProperties.kt |
TolgeeEmailSender now delegates to EmailService; EmailParams fields changed (nullable text, header, locale, templateName, recipientName); TolgeeProperties adds backEndUrl; some senders updated to accept UserAccount and email body/header text adjusted. |
Backend — Resolver / i18n plumbing backend/data/src/main/kotlin/io/tolgee/email/EmailMessageResolver.kt, backend/data/src/main/kotlin/io/tolgee/email/EmailTemplateEngine.kt, backend/data/src/main/resources/I18n_en.properties |
New message resolver post-processes ICU/XML fragments into Thymeleaf fragments; engine wiring added; notification template simplified to use dynamic placeholder. |
Backend — Tests & fixtures backend/data/src/test/kotlin/io/tolgee/email/*, backend/data/src/test/resources/email-i18n-test/*, backend/testing/.../EmailTestUtil.kt, backend/app/src/test/... |
Unit tests for EmailService and EmailGlobalVariablesProvider; test i18n resources; EmailTestUtil sets backEndUrl; many tests updated to use retry/wait wrappers for email assertions. |
Frontend — Email package (React + tooling) email/**/*, email/.config/extractor.ts, email/.config/tolgeerc.json, email/package.json, email/tsconfig.json, email/tailwind.config.ts, email/HACKING.md |
New email monorepo package: React Email components (If, For, Var, ImgResource, LocalizedText, translate, atoms, layouts), templates (default, registration, tests), extractor for i18n keys, Tolgee CLI config, lint/format/tailwind and documentation. |
Gradle — email integration & build scripts gradle/email.gradle, build.gradle, gradle/*.gradle, settings.gradle, gradle.properties |
New gradle/email.gradle tasks (installEmailDeps, buildEmails, buildTestEmails, copyEmailTemplates, copyEmailLocaleData, ...); large migration to lazy task registration (tasks.named / tasks.register), repo/catalog additions (jitpack/vaadinJson) and gradle.properties tuning. |
CI / Actions .github/actions/*, .github/workflows/* |
Composite actions updated with source/target dir inputs and Docker options; upload/download backend build actions include email artifact; test workflows add email-code-checks job and SKIP_EMAIL_BUILD flags; release workflow updated to metadata + docker/build-push-action; some workflows removed. |
E2E / Cypress & SMTP e2e/cypress/common/apiCalls/common.ts, e2e/cypress/e2e/**, e2e/docker-compose.yml |
E2E email API refactor to per-email retrieval primitives (getLatestEmail, getEmail, fetchEmails); tests clear emails before runs; fake SMTP image switched to Mailpit and port updated. |
Misc — modernizations & formatting backend/app/src/main/kotlin/io/tolgee/Application.kt, backend/data/src/main/kotlin/io/tolgee/configuration/HibernateConfig.kt, DEVELOPMENT.md, .gitmodules, various tests |
Exclude Thymeleaf auto-config, add Hibernate trace profile, add email HACKING reference in docs, remove demos submodule entry, and multiple stylistic/indentation updates across tests and build scripts. |
Sequence Diagram(s)
sequenceDiagram
participant Caller as Application / Sender
participant Tolgee as TolgeeEmailSender
participant Service as EmailService
participant Engine as EmailTemplateEngine (Thymeleaf)
participant Mailer as JavaMailSender
Caller->>Tolgee: request send (EmailParams / template)
Tolgee->>Service: sendEmailTemplate(recipient, template, locale, props...)
Service->>Engine: process(template, context + global vars)
Engine-->>Service: rendered HTML
Service->>Mailer: send MimeMessage (attachments, bcc, replyTo)
Mailer-->>Service: sent / error
Note right of Service: method annotated @Async — returns immediately
sequenceDiagram
participant NPM as npm (email package)
participant Extractor as extractor.ts
participant Gradle as gradle/email.gradle
participant Backend as backend build
NPM->>NPM: build/export templates (out/html)
NPM->>Extractor: extract i18n keys from TSX
Extractor-->>Gradle: i18n files & out/html produced
Gradle->>Gradle: copyEmailTemplates & copyEmailLocaleData -> build/generated/resources
Gradle-->>Backend: processResources includes generated templates/resources
Estimated code review effort
🎯 4 (Complex) | ⏱️ ~75 minutes
Areas needing extra attention:
- EmailService: template rendering, subject extraction regex, attachment handling, async behavior and error paths.
- EmailMessageResolver.postProcessMessage: regex/XML-to-fragment transformation edge cases.
- gradle/email.gradle tasks and SKIP_EMAIL_BUILD gating (inputs/outputs, up-to-date behavior).
- E2E email API changes and Mailpit integration (types, port, tests relying on HTML parsing).
Possibly related PRs
- tolgee/tolgee-platform#3126 — modifies the same GitHub Actions composite actions and CI workflow artifacts; strongly related to action YAML edits here.
- tolgee/tolgee-platform#3204 — touches mail-related dependencies and wiring that may intersect with the new EmailService and mail runtime.
Suggested reviewers
- JanCizmar
Poem
🐇 I stitched templates with tiny hops of code,
Thymeleaf and React along the bunny road.
Gradle wakes tasks when emails need to bloom,
Mailpit catches letters, safe from doom.
A rabbit cheers: templates now find home!
Pre-merge checks and finishing touches
❌ Failed checks (2 warnings)
| Check name | Status | Explanation | Resolution |
|---|---|---|---|
| Out of Scope Changes Check | ⚠️ Warning | The changeset includes two modifications that appear unrelated to the email infrastructure objectives: removal of the flagmoji submodule entry from .gitmodules, and addition of a new HibernateConfig.kt file implementing QueryStackTraceLogger for Hibernate SQL debugging under the hibernate-trace profile. These changes are not mentioned in the PR objectives, linked issues, or described functionality for the email feature and would benefit from clarification on their necessity or inclusion in a separate refactoring PR. | The author should clarify whether the flagmoji submodule removal and HibernateConfig additions are intentional inclusions for this PR or accidentally included changes. If these changes are not necessary for the email feature implementation, they should be removed from this PR and submitted as separate changes. If they are supporting infrastructure (e.g., for profiling email rendering performance), this should be documented in the PR description for reviewers. |
| Docstring Coverage | ⚠️ Warning | Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. | You can run @coderabbitai generate docstrings to improve docstring coverage. |
✅ Passed checks (3 passed)
| Check name | Status | Explanation |
|---|---|---|
| Description Check | ✅ Passed | Check skipped - CodeRabbit’s high-level summary is enabled. |
| Title Check | ✅ Passed | The pull request title "feat: well designed transactional e-mails" is a clear, concise, single sentence that accurately summarizes the main change in the changeset. The title directly refers to the primary objective of implementing React Email-based transactional email infrastructure with Thymeleaf compilation, live preview tooling, localization support, and conditional/iterative rendering capabilities. The title is specific and meaningful enough that a teammate scanning the history would understand the significant infrastructure addition being introduced. |
| Linked Issues Check | ✅ Passed | The code changes address all primary objectives from linked issue #2707. The implementation handles FormatJS-style pseudo-XML tag processing through the translation system (translate.ts supporting tag rendering and composition), renders newlines as elements via the addBrToTranslations function, enforces HTML security through proper escaping with ${#strings.escapeXml(variable)} in Var.ts and throughout the template rendering pipeline, and provides global variables via the EmailGlobalVariablesProvider component that supplies instance context and cloud detection to templates. All checked requirements from the linked issue are demonstrably implemented in the code changes. |
✨ Finishing touches
- [ ] 📝 Generate docstrings
🧪 Generate unit tests (beta)
- [ ] Create PR with unit tests
- [ ] Post copyable unit tests in a comment
- [ ] Commit unit tests in branch
cynthia/react-email
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.
Comment @coderabbitai help to get the list of available commands and usage tips.
I assume Snyk is unhappy about my addition of JitPack repository. 😅
Requesting review from both Jan and Stepan, as there were some wishes expressed in #2710 😃
Hi ^^
I had a quick look through the Snyk report: It looks like the new dependency com.github.transferwise:spring-icu:0.3.0 is pulling in org.springframework:spring-beans:@5.2.5.RELEASE which seems to be connected to a CVE (https://www.cve.org/CVERecord?id=CVE-2022-22965). According to Snyk, it is fixed in org.springframework:[email protected], @5.3.18.
Bleh, this explains that. I guess I'll just replace it with a custom impl, it seems there's barely any lib that does it, and it should be simple enough... Plus we already pull icu4j so it's just a matter of connecting the bits together... 😮💨
Thanks for the info 👍🏻
Yee, or we might be able to pin the spring-beans library to a newer version. It's not like the spring-icu library will care if we use spring-beans:@5.2.5 or @5.2.20 with it. :3
Well doing it manually implies copy/pasting an entire class from Spring, so here we are with some exclusions to get rid of the problems 😅
I also tweaked (quite a bit) the Gradle scripts, using lazy constructs and fiddling with some flags. It doesn't seem to have an impact on CI, but I assume it'll start doing some wonders as soon as cache gets warm (PR is read-only cache). It has significantly improved build times on my machine.
I adjusted GHA tasks here so they can be used in other repositories, and improved Docker image build (namely: better image tagging with support for vX, vX.Y, vX.Y.Z tags, and publishing provenance attestations, which aligns with best practices wrt supply chain security and integrity validation)
@dkrizan can we close this one? Looks like you're working on it in separate PR, right?
@dkrizan can we close this one? Looks like you're working on it in separate PR, right?
thats right, this is the PR, which you closed so I am reopening it.