feat: redesign transactional email templates with 2026 best practices - #73
Merged
Merged
Conversation
…tices Replaces bare <p>/<a> string concatenation with a proper table-based, inline-CSS HTML layout (Outlook-safe), a hidden preheader, an accessible CTA button with a raw-link fallback, and a real multipart/alternative plain-text body generated from the same structured EmailContent — so HTML and plain text can't drift apart. Also fixes an HTML-injection gap: display/organization/project names were interpolated raw into the email body; they're now escaped via EmailTemplateRenderer before insertion.
Contributor
Author
|
Real end-to-end verification against a running app + Mailpit (all 5 flows triggered via actual API calls, not just unit tests) confirmed 4/5 templates render correctly — Mailpit's built-in HTML Check also reported 95.54% email-client compatibility across 186 tests (Outlook, Outlook.com, macOS, iOS, Android, webmail). Found and fixed one real bug: |
16 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Researched current (2026) transactional-email best practices and rebuilt every email ReqsAI sends (verification, password reset, org invitation, project invitation, project assignment) — they were bare
<p>/<a>string concatenation with no styling, no plain-text fallback, and no protection against HTML injection from user-controlled values.What changed:
<style>blocks, so inlinestyle=attributes on tables/cells is the only approach that renders consistently everywhere.multipart/alternative— every email now sends an HTML body and a plain-text body generated from the same structured content (newEmailContentrecord +EmailTemplateRenderer), so clients that can't/won't render HTML still get something readable.<script>...</script>would have been injected verbatim. Now escaped viaHtmlUtils.htmlEscapebefore insertion.IAM_EMAIL_VERIFICATION_EXPIRATION/IAM_PASSWORD_RESET_EXPIRATIONdefaults).Bounded context / area: iam
Related issue / US: N/A — hygiene/quality pass, not tied to a ticket
Type of Change
feat— new featurefix— bug fixrefactor— code change without behavior change (the publicEmailNotificationPortcontract is unchanged; only the rendered content and MIME structure improved)test— tests onlydocs— documentation onlybuild/ci— build, dependencies, or CI/CDchore— maintenanceChecklist
develop(notmain)feature/*,bugfix/*, orhotfix/*./gradlew buildpasses locally (compile + tests +verifyModularity)iam.infrastructure.emailSmtpEmailAdapterTest.pemkeys are committedCHANGELOG.mdupdated under[Unreleased]How to Test
./gradlew test --tests "*SmtpEmailAdapterTest*"— covers multipart/alternative structure, HTML-escaping of hostile input, and existing content assertionsreqsai.email.provider=mailpit): trigger any of the 5 email flows (sign-up, password reset, org invite, project invite, direct project assignment) and inspect the rendered message in Mailpit's UI — both the HTML and plain-text tabsNotes / Screenshots (optional)
EmailNotificationPort's public signatures are unchanged — this is entirely internal to the SMTP adapter's rendering, so no other module or caller needed changes.