docs: add email template polish spec, plan, and update GitNexus index
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
<!-- gitnexus:start -->
|
||||
# GitNexus — Code Intelligence
|
||||
|
||||
This project is indexed by GitNexus as **cameleer-saas** (2838 symbols, 6037 relationships, 239 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
|
||||
This project is indexed by GitNexus as **cameleer-saas** (3004 symbols, 6307 relationships, 253 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
|
||||
|
||||
> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first.
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ PostgreSQL (Flyway): `src/main/resources/db/migration/`
|
||||
<!-- gitnexus:start -->
|
||||
# GitNexus — Code Intelligence
|
||||
|
||||
This project is indexed by GitNexus as **cameleer-saas** (2881 symbols, 6138 relationships, 243 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
|
||||
This project is indexed by GitNexus as **cameleer-saas** (3004 symbols, 6307 relationships, 253 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
|
||||
|
||||
> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first.
|
||||
|
||||
|
||||
449
docs/superpowers/plans/2026-04-26-email-template-polish-plan.md
Normal file
449
docs/superpowers/plans/2026-04-26-email-template-polish-plan.md
Normal file
@@ -0,0 +1,449 @@
|
||||
# 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
|
||||
<div style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;max-width:480px;margin:0 auto;background:#ffffff;border-radius:8px;overflow:hidden;border:1px solid #e8e0d4;">
|
||||
<div style="background:#C6820E;padding:20px 24px;text-align:center;">
|
||||
<span style="font-size:22px;font-weight:700;color:#ffffff;letter-spacing:0.5px;">Cameleer.io</span>
|
||||
</div>
|
||||
<div style="padding:32px 24px 24px;position:relative;overflow:hidden;">
|
||||
<img src="{{watermarkUrl}}" width="320" height="320" style="position:absolute;top:-30px;right:-50px;width:320px;height:320px;opacity:0.07;pointer-events:none;" alt="" />
|
||||
<div style="position:relative;">
|
||||
<p style="color:#1a1a1a;font-size:16px;font-weight:600;margin:0 0 8px;">Welcome to the caravan!</p>
|
||||
<p style="color:#444;font-size:14px;line-height:1.6;margin:0 0 24px;">Enter this code to verify your email and claim your spot. The dunes wait for no one.</p>
|
||||
<div style="text-align:center;margin:0 0 24px;">
|
||||
<div style="display:inline-block;background:#FDF6EC;border:2px solid #C6820E;border-radius:8px;padding:16px 32px;">
|
||||
<span style="font-size:32px;font-weight:700;letter-spacing:8px;color:#C6820E;font-family:'Courier New',Courier,monospace;">{{code}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<p style="color:#888;font-size:13px;line-height:1.5;margin:0;">This code expires in 10 minutes. If you didn't request this, you can safely ignore this email — no camels were harmed.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div style="border-top:1px solid #e8e0d4;padding:16px 24px;text-align:center;">
|
||||
<p style="color:#999;font-size:12px;margin:0;">Questions? Contact your administrator</p>
|
||||
<p style="color:#bbb;font-size:11px;margin:6px 0 0;">Cameleer — Apache Camel observability</p>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Create `sign-in.html`**
|
||||
|
||||
```html
|
||||
<div style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;max-width:480px;margin:0 auto;background:#ffffff;border-radius:8px;overflow:hidden;border:1px solid #e8e0d4;">
|
||||
<div style="background:#C6820E;padding:20px 24px;text-align:center;">
|
||||
<span style="font-size:22px;font-weight:700;color:#ffffff;letter-spacing:0.5px;">Cameleer.io</span>
|
||||
</div>
|
||||
<div style="padding:32px 24px 24px;position:relative;overflow:hidden;">
|
||||
<img src="{{watermarkUrl}}" width="320" height="320" style="position:absolute;top:-30px;right:-50px;width:320px;height:320px;opacity:0.07;pointer-events:none;" alt="" />
|
||||
<div style="position:relative;">
|
||||
<p style="color:#1a1a1a;font-size:16px;font-weight:600;margin:0 0 8px;">Back at the oasis already?</p>
|
||||
<p style="color:#444;font-size:14px;line-height:1.6;margin:0 0 24px;">Here's your sign-in code. The caravan master is checking credentials.</p>
|
||||
<div style="text-align:center;margin:0 0 24px;">
|
||||
<div style="display:inline-block;background:#FDF6EC;border:2px solid #C6820E;border-radius:8px;padding:16px 32px;">
|
||||
<span style="font-size:32px;font-weight:700;letter-spacing:8px;color:#C6820E;font-family:'Courier New',Courier,monospace;">{{code}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<p style="color:#888;font-size:13px;line-height:1.5;margin:0;">This code expires in 10 minutes.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div style="border-top:1px solid #e8e0d4;padding:16px 24px;text-align:center;">
|
||||
<p style="color:#999;font-size:12px;margin:0;">Questions? Contact your administrator</p>
|
||||
<p style="color:#bbb;font-size:11px;margin:6px 0 0;">Cameleer — Apache Camel observability</p>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Create `forgot-password.html`**
|
||||
|
||||
```html
|
||||
<div style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;max-width:480px;margin:0 auto;background:#ffffff;border-radius:8px;overflow:hidden;border:1px solid #e8e0d4;">
|
||||
<div style="background:#C6820E;padding:20px 24px;text-align:center;">
|
||||
<span style="font-size:22px;font-weight:700;color:#ffffff;letter-spacing:0.5px;">Cameleer.io</span>
|
||||
</div>
|
||||
<div style="padding:32px 24px 24px;position:relative;overflow:hidden;">
|
||||
<img src="{{watermarkUrl}}" width="320" height="320" style="position:absolute;top:-30px;right:-50px;width:320px;height:320px;opacity:0.07;pointer-events:none;" alt="" />
|
||||
<div style="position:relative;">
|
||||
<p style="color:#1a1a1a;font-size:16px;font-weight:600;margin:0 0 8px;">Lost in the dunes?</p>
|
||||
<p style="color:#444;font-size:14px;line-height:1.6;margin:0 0 24px;">No worries — enter this code to reset your password and get back on the trail.</p>
|
||||
<div style="text-align:center;margin:0 0 24px;">
|
||||
<div style="display:inline-block;background:#FDF6EC;border:2px solid #C6820E;border-radius:8px;padding:16px 32px;">
|
||||
<span style="font-size:32px;font-weight:700;letter-spacing:8px;color:#C6820E;font-family:'Courier New',Courier,monospace;">{{code}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<p style="color:#888;font-size:13px;line-height:1.5;margin:0;">This code expires in 10 minutes. If you didn't request a password reset, you can safely ignore this email.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div style="border-top:1px solid #e8e0d4;padding:16px 24px;text-align:center;">
|
||||
<p style="color:#999;font-size:12px;margin:0;">Questions? Contact your administrator</p>
|
||||
<p style="color:#bbb;font-size:11px;margin:6px 0 0;">Cameleer — Apache Camel observability</p>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Create `generic.html`**
|
||||
|
||||
```html
|
||||
<div style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;max-width:480px;margin:0 auto;background:#ffffff;border-radius:8px;overflow:hidden;border:1px solid #e8e0d4;">
|
||||
<div style="background:#C6820E;padding:20px 24px;text-align:center;">
|
||||
<span style="font-size:22px;font-weight:700;color:#ffffff;letter-spacing:0.5px;">Cameleer.io</span>
|
||||
</div>
|
||||
<div style="padding:32px 24px 24px;position:relative;overflow:hidden;">
|
||||
<img src="{{watermarkUrl}}" width="320" height="320" style="position:absolute;top:-30px;right:-50px;width:320px;height:320px;opacity:0.07;pointer-events:none;" alt="" />
|
||||
<div style="position:relative;">
|
||||
<p style="color:#1a1a1a;font-size:16px;font-weight:600;margin:0 0 8px;">Quick checkpoint</p>
|
||||
<p style="color:#444;font-size:14px;line-height:1.6;margin:0 0 24px;">Here's your verification code. Just making sure it's really you at the reins.</p>
|
||||
<div style="text-align:center;margin:0 0 24px;">
|
||||
<div style="display:inline-block;background:#FDF6EC;border:2px solid #C6820E;border-radius:8px;padding:16px 32px;">
|
||||
<span style="font-size:32px;font-weight:700;letter-spacing:8px;color:#C6820E;font-family:'Courier New',Courier,monospace;">{{code}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<p style="color:#888;font-size:13px;line-height:1.5;margin:0;">This code expires in 10 minutes.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div style="border-top:1px solid #e8e0d4;padding:16px 24px;text-align:center;">
|
||||
<p style="color:#999;font-size:12px;margin:0;">Questions? Contact your administrator</p>
|
||||
<p style="color:#bbb;font-size:11px;margin:6px 0 0;">Cameleer — Apache Camel observability</p>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
- [ ] **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<String, Object> buildSmtpConfig(SmtpConfig smtp) {
|
||||
var config = new HashMap<String, Object>();
|
||||
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://<host>/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"
|
||||
```
|
||||
@@ -0,0 +1,108 @@
|
||||
# Email Template Polish
|
||||
|
||||
Polish the 4 Logto SMTP connector email templates with branded visuals, playful desert/caravan copy, and extract them from inline Java strings to standalone HTML files.
|
||||
|
||||
## Current State
|
||||
|
||||
All 4 email templates are hardcoded as inline HTML strings in `EmailConnectorService.buildSmtpConfig()` (lines 164-189). They share a minimal structure: centered text "Cameleer" wordmark in `#C6820E`, a one-line message, a large verification code, and a small expiry note. No logo, no footer, no personality.
|
||||
|
||||
## Design Decisions
|
||||
|
||||
- **Tone:** Playful desert/caravan voice matching the sign-in page personality
|
||||
- **Layout:** Structured card — amber header bar, white body with watermark, footer with separator
|
||||
- **Footer:** Help link ("Questions? Contact your administrator") + tagline ("Cameleer — Apache Camel observability")
|
||||
- **Header:** Text-only "Cameleer.io" centered on amber `#C6820E` bar, no logo image
|
||||
- **Watermark:** Cameleer logo at ~7% opacity, oversized, positioned top-right showing compass + cameleer + camel head. For production: pre-faded PNG hosted at a public URL to avoid CSS opacity issues in Outlook desktop.
|
||||
- **Storage:** External HTML template files, not inline Java strings
|
||||
|
||||
## Template Structure
|
||||
|
||||
All 4 templates share the same layout, differing only in subject, headline, body copy, and safety note.
|
||||
|
||||
```
|
||||
+------------------------------------------+
|
||||
| [amber #C6820E header bar] |
|
||||
| Cameleer.io (white) |
|
||||
+------------------------------------------+
|
||||
| |
|
||||
| Headline (bold, 16px) [watermark |
|
||||
| Body text (14px, 1.6lh) logo at |
|
||||
| 7% opacity|
|
||||
| +-------------------+ |
|
||||
| | VERIFICATION CODE | |
|
||||
| | cream pill, amber | |
|
||||
| | border, monospace | |
|
||||
| +-------------------+ |
|
||||
| |
|
||||
| Expiry/safety note (13px, muted) |
|
||||
| |
|
||||
+------------------------------------------+
|
||||
| Questions? Contact your administrator |
|
||||
| Cameleer — Apache Camel observability |
|
||||
+------------------------------------------+
|
||||
```
|
||||
|
||||
## Copy
|
||||
|
||||
| Type | Subject | Headline | Body | Safety note |
|
||||
|------|---------|----------|------|-------------|
|
||||
| Register | Your caravan pass is almost ready | Welcome to the caravan! | Enter this code to verify your email and claim your spot. The dunes wait for no one. | This code expires in 10 minutes. If you didn't request this, you can safely ignore this email — no camels were harmed. |
|
||||
| SignIn | Your Cameleer sign-in code | Back at the oasis already? | Here's your sign-in code. The caravan master is checking credentials. | This code expires in 10 minutes. |
|
||||
| ForgotPassword | Reset your Cameleer password | Lost in the dunes? | No worries — enter this code to reset your password and get back on the trail. | This code expires in 10 minutes. If you didn't request a password reset, you can safely ignore this email. |
|
||||
| Generic | Your Cameleer verification code | Quick checkpoint | Here's your verification code. Just making sure it's really you at the reins. | This code expires in 10 minutes. |
|
||||
|
||||
## Visual Specifications
|
||||
|
||||
- **Font stack:** `-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif`
|
||||
- **Card max-width:** 480px, no fixed width (shrinks naturally on mobile)
|
||||
- **Card border:** `1px solid #e8e0d4`
|
||||
- **Header:** `background: #C6820E`, padding 20px 24px, text centered, 22px bold white
|
||||
- **Code pill:** `background: #FDF6EC`, `border: 2px solid #C6820E`, border-radius 8px, padding 16px 32px, 32px bold monospace with 8px letter-spacing in `#C6820E`
|
||||
- **Watermark:** Absolutely positioned `top:-30px; right:-50px`, 320x320px, 7% opacity, clipped by `overflow: hidden` on body container
|
||||
- **Footer separator:** `1px solid #e8e0d4`
|
||||
- **Footer text:** "Questions?" at 12px `#999`, tagline at 11px `#bbb`
|
||||
|
||||
## Responsiveness
|
||||
|
||||
No media queries needed. The single-column layout works from ~280px up:
|
||||
- Card uses `max-width: 480px` with no fixed width
|
||||
- Code pill uses `display: inline-block`, wraps within container
|
||||
- All sizes in px (email clients handle rem/em inconsistently)
|
||||
- Watermark clipped by `overflow: hidden`, never causes horizontal scroll
|
||||
|
||||
## File Structure
|
||||
|
||||
### Template files
|
||||
|
||||
Create 4 HTML template files at `src/main/resources/email-templates/`:
|
||||
|
||||
```
|
||||
src/main/resources/email-templates/
|
||||
register.html
|
||||
sign-in.html
|
||||
forgot-password.html
|
||||
generic.html
|
||||
```
|
||||
|
||||
Each file is a complete HTML email body (inline styles, self-contained). The verification code placeholder uses Logto's `{{code}}` syntax.
|
||||
|
||||
### Watermark image
|
||||
|
||||
Create a pre-faded PNG of the Cameleer logo at 7% opacity on a transparent background. Source the logo from `design-system/assets/cameleer-logo.png` and generate the faded version using ImageMagick or similar (one-time step, committed to the repo).
|
||||
|
||||
Place the image at `src/main/resources/static/platform/assets/email-watermark.png`. Spring Boot serves `/platform/assets/**` as static resources automatically. The template files use a placeholder `{{watermarkUrl}}` that `EmailConnectorService` replaces with `https://<PUBLIC_HOST>/platform/assets/email-watermark.png` at runtime.
|
||||
|
||||
### Java changes
|
||||
|
||||
`EmailConnectorService.buildSmtpConfig()`:
|
||||
- Read each template file from classpath (`src/main/resources/email-templates/*.html`) at startup or on first use
|
||||
- Replace `{{watermarkUrl}}` with the configured public host URL
|
||||
- Pass the HTML content as the `content` field in each Logto template config
|
||||
- Keep subjects in Java (they're short strings, no benefit from externalizing)
|
||||
|
||||
## Email Client Compatibility
|
||||
|
||||
- **Gmail, Apple Mail, Outlook.com:** Full support — opacity, absolute positioning, border-radius all work
|
||||
- **Outlook desktop (Word renderer):** CSS `opacity` is ignored. The pre-faded watermark PNG solves this — the transparency is baked into the image itself, not applied via CSS. Absolute positioning is supported via VML fallback that Outlook generates.
|
||||
- **No CSS classes:** Everything uses inline styles (email clients strip `<style>` blocks inconsistently)
|
||||
- **No external fonts:** System font stack only
|
||||
Reference in New Issue
Block a user