# Email Template Polish Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Replace inline HTML email templates with polished, branded HTML files loaded from classpath, featuring playful desert/caravan copy, structured card layout with watermark, and proper header/footer. **Architecture:** Extract 4 email templates from `EmailConnectorService.buildSmtpConfig()` into standalone HTML files at `src/main/resources/email-templates/`. Generate a pre-faded watermark PNG served as a static asset. Inject `ProvisioningProperties` to resolve the watermark URL at runtime. **Tech Stack:** Java 21, Spring Boot, ImageMagick (one-time asset generation), HTML email (inline styles only) --- ### File Map | Action | File | Purpose | |--------|------|---------| | Create | `src/main/resources/email-templates/register.html` | Registration verification email | | Create | `src/main/resources/email-templates/sign-in.html` | Sign-in verification email | | Create | `src/main/resources/email-templates/forgot-password.html` | Password reset email | | Create | `src/main/resources/email-templates/generic.html` | Generic verification email | | Create | `src/main/resources/static/assets/email-watermark.png` | Pre-faded logo at 7% opacity | | Modify | `src/main/java/net/siegeln/cameleer/saas/vendor/EmailConnectorService.java` | Load templates from classpath, inject watermark URL | | Modify | `src/main/java/net/siegeln/cameleer/saas/config/SecurityConfig.java:49` | Permit `/assets/**` for unauthenticated email clients | | Create | `src/test/java/net/siegeln/cameleer/saas/vendor/EmailTemplateLoadingTest.java` | Verify templates load and placeholders resolve | --- ### Task 1: Generate the pre-faded watermark PNG **Files:** - Create: `src/main/resources/static/assets/email-watermark.png` - [ ] **Step 1: Generate the faded watermark using ImageMagick** Source the logo from the design-system sibling repo. Apply 7% opacity on a transparent background, output to the static assets directory: ```bash magick "C:/Users/Hendrik/Documents/projects/design-system/assets/cameleer-logo.png" \ -channel A -evaluate Multiply 0.07 +channel \ -resize 320x320 \ "src/main/resources/static/assets/email-watermark.png" ``` If `magick` is not available, use Python Pillow as fallback: ```bash python3 -c " from PIL import Image img = Image.open('C:/Users/Hendrik/Documents/projects/design-system/assets/cameleer-logo.png').convert('RGBA') img = img.resize((320, 320), Image.LANCZOS) r, g, b, a = img.split() a = a.point(lambda x: int(x * 0.07)) img = Image.merge('RGBA', (r, g, b, a)) img.save('src/main/resources/static/assets/email-watermark.png') print('Saved watermark') " ``` - [ ] **Step 2: Verify the file exists and is reasonable size** ```bash ls -la src/main/resources/static/assets/email-watermark.png ``` Expected: File exists, roughly 5-30 KB. - [ ] **Step 3: Commit** ```bash git add src/main/resources/static/assets/email-watermark.png git commit -m "feat: add pre-faded logo watermark for email templates" ``` --- ### Task 2: Permit static assets in SecurityConfig **Files:** - Modify: `src/main/java/net/siegeln/cameleer/saas/config/SecurityConfig.java:49` The watermark image must be loadable by email clients without authentication. The current security config has `.anyRequest().authenticated()` as catch-all, so `/assets/**` needs an explicit permit. - [ ] **Step 1: Add `/assets/**` to the permitAll list** In `SecurityConfig.java`, find the existing line: ```java .requestMatchers("/_app/**", "/favicon.ico", "/favicon.svg", "/logo.svg", "/logo-dark.svg").permitAll() ``` Change it to: ```java .requestMatchers("/_app/**", "/assets/**", "/favicon.ico", "/favicon.svg", "/logo.svg", "/logo-dark.svg").permitAll() ``` - [ ] **Step 2: Commit** ```bash git add src/main/java/net/siegeln/cameleer/saas/config/SecurityConfig.java git commit -m "feat: permit /assets/** for unauthenticated access (email watermark)" ``` --- ### Task 3: Create the 4 HTML email template files **Files:** - Create: `src/main/resources/email-templates/register.html` - Create: `src/main/resources/email-templates/sign-in.html` - Create: `src/main/resources/email-templates/forgot-password.html` - Create: `src/main/resources/email-templates/generic.html` All templates use the same card structure. The `{{code}}` placeholder is Logto's built-in substitution. The `{{watermarkUrl}}` placeholder is replaced by `EmailConnectorService` at runtime. - [ ] **Step 1: Create `register.html`** ```html
Cameleer.io

Welcome to the caravan!

Enter this code to verify your email and claim your spot. The dunes wait for no one.

{{code}}

This code expires in 10 minutes. If you didn't request this, you can safely ignore this email — no camels were harmed.

Questions? Contact your administrator

Cameleer — Apache Camel observability

``` - [ ] **Step 2: Create `sign-in.html`** ```html
Cameleer.io

Back at the oasis already?

Here's your sign-in code. The caravan master is checking credentials.

{{code}}

This code expires in 10 minutes.

Questions? Contact your administrator

Cameleer — Apache Camel observability

``` - [ ] **Step 3: Create `forgot-password.html`** ```html
Cameleer.io

Lost in the dunes?

No worries — enter this code to reset your password and get back on the trail.

{{code}}

This code expires in 10 minutes. If you didn't request a password reset, you can safely ignore this email.

Questions? Contact your administrator

Cameleer — Apache Camel observability

``` - [ ] **Step 4: Create `generic.html`** ```html
Cameleer.io

Quick checkpoint

Here's your verification code. Just making sure it's really you at the reins.

{{code}}

This code expires in 10 minutes.

Questions? Contact your administrator

Cameleer — Apache Camel observability

``` - [ ] **Step 5: Commit** ```bash git add src/main/resources/email-templates/ git commit -m "feat: add branded HTML email templates with desert/caravan copy" ``` --- ### Task 4: Refactor EmailConnectorService to load templates from classpath **Files:** - Modify: `src/main/java/net/siegeln/cameleer/saas/vendor/EmailConnectorService.java` - [ ] **Step 1: Write the failing test** Create `src/test/java/net/siegeln/cameleer/saas/vendor/EmailTemplateLoadingTest.java`: ```java package net.siegeln.cameleer.saas.vendor; import org.junit.jupiter.api.Test; import org.springframework.core.io.ClassPathResource; import java.io.IOException; import java.nio.charset.StandardCharsets; import static org.junit.jupiter.api.Assertions.*; class EmailTemplateLoadingTest { private static final String[] TEMPLATE_FILES = { "email-templates/register.html", "email-templates/sign-in.html", "email-templates/forgot-password.html", "email-templates/generic.html" }; @Test void allTemplateFilesExistOnClasspath() { for (String path : TEMPLATE_FILES) { var resource = new ClassPathResource(path); assertTrue(resource.exists(), "Template file missing: " + path); } } @Test void templatesContainCodePlaceholder() throws IOException { for (String path : TEMPLATE_FILES) { String content = new ClassPathResource(path).getContentAsString(StandardCharsets.UTF_8); assertTrue(content.contains("{{code}}"), path + " must contain {{code}} placeholder"); } } @Test void templatesContainWatermarkPlaceholder() throws IOException { for (String path : TEMPLATE_FILES) { String content = new ClassPathResource(path).getContentAsString(StandardCharsets.UTF_8); assertTrue(content.contains("{{watermarkUrl}}"), path + " must contain {{watermarkUrl}} placeholder"); } } @Test void watermarkPlaceholderIsReplaced() throws IOException { String content = new ClassPathResource("email-templates/register.html") .getContentAsString(StandardCharsets.UTF_8); String resolved = content.replace("{{watermarkUrl}}", "https://example.com/platform/assets/email-watermark.png"); assertFalse(resolved.contains("{{watermarkUrl}}")); assertTrue(resolved.contains("https://example.com/platform/assets/email-watermark.png")); } @Test void templatesContainBrandElements() throws IOException { for (String path : TEMPLATE_FILES) { String content = new ClassPathResource(path).getContentAsString(StandardCharsets.UTF_8); assertTrue(content.contains("Cameleer.io"), path + " must contain Cameleer.io header"); assertTrue(content.contains("Apache Camel observability"), path + " must contain tagline"); assertTrue(content.contains("#C6820E"), path + " must use brand color"); } } } ``` - [ ] **Step 2: Run tests to verify they pass (templates exist from Task 3)** ```bash ./mvnw test -pl . -Dtest=EmailTemplateLoadingTest -Dspring.profiles.active=test ``` Expected: All 5 tests PASS. - [ ] **Step 3: Add `ProvisioningProperties` dependency to `EmailConnectorService`** Replace the constructor and add the template loading logic. The full updated `EmailConnectorService.java`: Change the imports and fields at the top of the class — add `ProvisioningProperties` import and field: ```java import net.siegeln.cameleer.saas.provisioning.ProvisioningProperties; import org.springframework.core.io.ClassPathResource; import java.io.IOException; import java.nio.charset.StandardCharsets; ``` Replace the constructor: ```java private final LogtoManagementClient logtoClient; private final ProvisioningProperties provisioningProps; public EmailConnectorService(LogtoManagementClient logtoClient, ProvisioningProperties provisioningProps) { this.logtoClient = logtoClient; this.provisioningProps = provisioningProps; } ``` Replace the `buildSmtpConfig` method (lines 157-191) with: ```java /** Load an email template from classpath and resolve the watermark URL placeholder. */ private String loadTemplate(String filename) { try { String content = new ClassPathResource("email-templates/" + filename) .getContentAsString(StandardCharsets.UTF_8); String watermarkUrl = provisioningProps.publicProtocol() + "://" + provisioningProps.publicHost() + "/platform/assets/email-watermark.png"; return content.replace("{{watermarkUrl}}", watermarkUrl); } catch (IOException e) { throw new IllegalStateException("Failed to load email template: " + filename, e); } } /** Build the Logto SMTP connector config with Cameleer-branded email templates. */ private Map buildSmtpConfig(SmtpConfig smtp) { var config = new HashMap(); config.put("host", smtp.host()); config.put("port", smtp.port()); config.put("auth", Map.of("user", smtp.username(), "pass", smtp.password())); config.put("fromEmail", smtp.fromEmail()); config.put("templates", List.of( Map.of( "usageType", "Register", "contentType", "text/html", "subject", "Your caravan pass is almost ready", "content", loadTemplate("register.html") ), Map.of( "usageType", "SignIn", "contentType", "text/html", "subject", "Your Cameleer sign-in code", "content", loadTemplate("sign-in.html") ), Map.of( "usageType", "ForgotPassword", "contentType", "text/html", "subject", "Reset your Cameleer password", "content", loadTemplate("forgot-password.html") ), Map.of( "usageType", "Generic", "contentType", "text/html", "subject", "Your Cameleer verification code", "content", loadTemplate("generic.html") ) )); return config; } ``` - [ ] **Step 4: Verify the project compiles** ```bash ./mvnw compile -pl . ``` Expected: BUILD SUCCESS - [ ] **Step 5: Run the template tests again to confirm nothing broke** ```bash ./mvnw test -pl . -Dtest=EmailTemplateLoadingTest -Dspring.profiles.active=test ``` Expected: All 5 tests PASS. - [ ] **Step 6: Commit** ```bash git add src/main/java/net/siegeln/cameleer/saas/vendor/EmailConnectorService.java git add src/test/java/net/siegeln/cameleer/saas/vendor/EmailTemplateLoadingTest.java git commit -m "feat: load email templates from classpath with watermark URL resolution" ``` --- ### Task 5: Run the full test suite **Files:** None (verification only) - [ ] **Step 1: Run all tests** ```bash ./mvnw test -Dspring.profiles.active=test ``` Expected: BUILD SUCCESS, all tests pass. If any existing tests fail due to the new `ProvisioningProperties` constructor parameter on `EmailConnectorService`, they will need their mocks updated — but there are no existing tests for this class. - [ ] **Step 2: Verify the watermark is accessible without auth by checking SecurityConfig** Confirm the `/assets/**` matcher is in the `permitAll()` chain (done in Task 2). With context-path `/platform`, the full public URL will be `https:///platform/assets/email-watermark.png`. - [ ] **Step 3: Final commit if any fixes were needed** Only if test failures required changes: ```bash git add -A git commit -m "fix: resolve test failures from email template refactor" ```