feat: load email templates from classpath with watermark URL resolution
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,14 @@
|
||||
package net.siegeln.cameleer.saas.vendor;
|
||||
|
||||
import net.siegeln.cameleer.saas.identity.LogtoManagementClient;
|
||||
import net.siegeln.cameleer.saas.provisioning.ProvisioningProperties;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -16,9 +20,11 @@ public class EmailConnectorService {
|
||||
private static final String SMTP_FACTORY_ID = "simple-mail-transfer-protocol";
|
||||
|
||||
private final LogtoManagementClient logtoClient;
|
||||
private final ProvisioningProperties provisioningProps;
|
||||
|
||||
public EmailConnectorService(LogtoManagementClient logtoClient) {
|
||||
public EmailConnectorService(LogtoManagementClient logtoClient, ProvisioningProperties provisioningProps) {
|
||||
this.logtoClient = logtoClient;
|
||||
this.provisioningProps = provisioningProps;
|
||||
}
|
||||
|
||||
public record SmtpConfig(String host, int port, String username, String password, String fromEmail) {}
|
||||
@@ -154,6 +160,19 @@ public class EmailConnectorService {
|
||||
return "SignInAndRegister".equals(signInExp.get("signInMode"));
|
||||
}
|
||||
|
||||
/** 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>();
|
||||
@@ -165,26 +184,26 @@ public class EmailConnectorService {
|
||||
Map.of(
|
||||
"usageType", "Register",
|
||||
"contentType", "text/html",
|
||||
"subject", "Verify your email for Cameleer",
|
||||
"content", "<div style=\"font-family:sans-serif;max-width:480px;margin:0 auto;padding:24px\"><div style=\"text-align:center;margin-bottom:24px\"><span style=\"font-size:24px;font-weight:700;color:#C6820E\">Cameleer</span></div><p style=\"color:#333;font-size:15px;line-height:1.6\">Enter this code to verify your email and create your account:</p><div style=\"text-align:center;margin:24px 0\"><span style=\"font-size:32px;font-weight:700;letter-spacing:6px;color:#C6820E\">{{code}}</span></div><p style=\"color:#666;font-size:13px\">This code expires in 10 minutes. If you did not request this, you can safely ignore this email.</p></div>"
|
||||
"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", "<div style=\"font-family:sans-serif;max-width:480px;margin:0 auto;padding:24px\"><div style=\"text-align:center;margin-bottom:24px\"><span style=\"font-size:24px;font-weight:700;color:#C6820E\">Cameleer</span></div><p style=\"color:#333;font-size:15px;line-height:1.6\">Your sign-in verification code:</p><div style=\"text-align:center;margin:24px 0\"><span style=\"font-size:32px;font-weight:700;letter-spacing:6px;color:#C6820E\">{{code}}</span></div><p style=\"color:#666;font-size:13px\">This code expires in 10 minutes.</p></div>"
|
||||
"content", loadTemplate("sign-in.html")
|
||||
),
|
||||
Map.of(
|
||||
"usageType", "ForgotPassword",
|
||||
"contentType", "text/html",
|
||||
"subject", "Reset your Cameleer password",
|
||||
"content", "<div style=\"font-family:sans-serif;max-width:480px;margin:0 auto;padding:24px\"><div style=\"text-align:center;margin-bottom:24px\"><span style=\"font-size:24px;font-weight:700;color:#C6820E\">Cameleer</span></div><p style=\"color:#333;font-size:15px;line-height:1.6\">Enter this code to reset your password:</p><div style=\"text-align:center;margin:24px 0\"><span style=\"font-size:32px;font-weight:700;letter-spacing:6px;color:#C6820E\">{{code}}</span></div><p style=\"color:#666;font-size:13px\">This code expires in 10 minutes. If you did not request a password reset, you can safely ignore this email.</p></div>"
|
||||
"content", loadTemplate("forgot-password.html")
|
||||
),
|
||||
Map.of(
|
||||
"usageType", "Generic",
|
||||
"contentType", "text/html",
|
||||
"subject", "Your Cameleer verification code",
|
||||
"content", "<div style=\"font-family:sans-serif;max-width:480px;margin:0 auto;padding:24px\"><div style=\"text-align:center;margin-bottom:24px\"><span style=\"font-size:24px;font-weight:700;color:#C6820E\">Cameleer</span></div><p style=\"color:#333;font-size:15px;line-height:1.6\">Your verification code:</p><div style=\"text-align:center;margin:24px 0\"><span style=\"font-size:32px;font-weight:700;letter-spacing:6px;color:#C6820E\">{{code}}</span></div><p style=\"color:#666;font-size:13px\">This code expires in 10 minutes.</p></div>"
|
||||
"content", loadTemplate("generic.html")
|
||||
)
|
||||
));
|
||||
return config;
|
||||
|
||||
68
src/test/java/net/siegeln/cameleer/saas/vendor/EmailTemplateLoadingTest.java
vendored
Normal file
68
src/test/java/net/siegeln/cameleer/saas/vendor/EmailTemplateLoadingTest.java
vendored
Normal file
@@ -0,0 +1,68 @@
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user