diff --git a/src/main/java/net/siegeln/cameleer/saas/vendor/EmailConnectorService.java b/src/main/java/net/siegeln/cameleer/saas/vendor/EmailConnectorService.java index 90d07cb..b8fbffa 100644 --- a/src/main/java/net/siegeln/cameleer/saas/vendor/EmailConnectorService.java +++ b/src/main/java/net/siegeln/cameleer/saas/vendor/EmailConnectorService.java @@ -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 buildSmtpConfig(SmtpConfig smtp) { var config = new HashMap(); @@ -165,26 +184,26 @@ public class EmailConnectorService { Map.of( "usageType", "Register", "contentType", "text/html", - "subject", "Verify your email for Cameleer", - "content", "
Cameleer

Enter this code to verify your email and create your account:

{{code}}

This code expires in 10 minutes. If you did not request this, you can safely ignore this email.

" + "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", "
Cameleer

Your sign-in verification code:

{{code}}

This code expires in 10 minutes.

" + "content", loadTemplate("sign-in.html") ), Map.of( "usageType", "ForgotPassword", "contentType", "text/html", "subject", "Reset your Cameleer password", - "content", "
Cameleer

Enter this code to reset your password:

{{code}}

This code expires in 10 minutes. If you did not request a password reset, you can safely ignore this email.

" + "content", loadTemplate("forgot-password.html") ), Map.of( "usageType", "Generic", "contentType", "text/html", "subject", "Your Cameleer verification code", - "content", "
Cameleer

Your verification code:

{{code}}

This code expires in 10 minutes.

" + "content", loadTemplate("generic.html") ) )); return config; diff --git a/src/test/java/net/siegeln/cameleer/saas/vendor/EmailTemplateLoadingTest.java b/src/test/java/net/siegeln/cameleer/saas/vendor/EmailTemplateLoadingTest.java new file mode 100644 index 0000000..8fd650d --- /dev/null +++ b/src/test/java/net/siegeln/cameleer/saas/vendor/EmailTemplateLoadingTest.java @@ -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"); + } + } +}