feat: add EmailConnectorService for Logto email connector management
This commit is contained in:
186
src/main/java/net/siegeln/cameleer/saas/vendor/EmailConnectorService.java
vendored
Normal file
186
src/main/java/net/siegeln/cameleer/saas/vendor/EmailConnectorService.java
vendored
Normal file
@@ -0,0 +1,186 @@
|
||||
package net.siegeln.cameleer.saas.vendor;
|
||||
|
||||
import net.siegeln.cameleer.saas.identity.LogtoManagementClient;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
public class EmailConnectorService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(EmailConnectorService.class);
|
||||
private static final String SMTP_FACTORY_ID = "simple-mail-transfer-protocol";
|
||||
|
||||
private final LogtoManagementClient logtoClient;
|
||||
|
||||
public EmailConnectorService(LogtoManagementClient logtoClient) {
|
||||
this.logtoClient = logtoClient;
|
||||
}
|
||||
|
||||
public record SmtpConfig(String host, int port, String username, String password, String fromEmail) {}
|
||||
|
||||
public record EmailConnectorStatus(
|
||||
String connectorId,
|
||||
String factoryId,
|
||||
String host,
|
||||
int port,
|
||||
String username,
|
||||
String fromEmail,
|
||||
boolean registrationEnabled
|
||||
) {}
|
||||
|
||||
/** Get the current email connector config, or null if none is configured. */
|
||||
@SuppressWarnings("unchecked")
|
||||
public EmailConnectorStatus getEmailConnector() {
|
||||
var connectors = logtoClient.listConnectors();
|
||||
var emailConnector = connectors.stream()
|
||||
.filter(c -> "Email".equals(c.get("type")))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
|
||||
if (emailConnector == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var config = (Map<String, Object>) emailConnector.getOrDefault("config", Map.of());
|
||||
var auth = (Map<String, Object>) config.getOrDefault("auth", Map.of());
|
||||
String host = String.valueOf(config.getOrDefault("host", ""));
|
||||
int port = config.containsKey("port") ? ((Number) config.get("port")).intValue() : 587;
|
||||
String username = String.valueOf(auth.getOrDefault("user", ""));
|
||||
String fromEmail = String.valueOf(config.getOrDefault("fromEmail", ""));
|
||||
|
||||
boolean registrationEnabled = isRegistrationEnabled();
|
||||
|
||||
return new EmailConnectorStatus(
|
||||
String.valueOf(emailConnector.get("id")),
|
||||
String.valueOf(emailConnector.get("connectorId")),
|
||||
host, port, username, fromEmail, registrationEnabled
|
||||
);
|
||||
}
|
||||
|
||||
/** Create or update the SMTP email connector. Returns the connector status. */
|
||||
public EmailConnectorStatus saveSmtpConnector(SmtpConfig smtp, Boolean registrationEnabled) {
|
||||
var connectorConfig = buildSmtpConfig(smtp);
|
||||
|
||||
// Check if an email connector already exists
|
||||
var existing = getEmailConnector();
|
||||
if (existing != null) {
|
||||
logtoClient.updateConnector(existing.connectorId(), connectorConfig);
|
||||
log.info("Updated SMTP email connector: {}", existing.connectorId());
|
||||
} else {
|
||||
var result = logtoClient.createConnector(SMTP_FACTORY_ID, connectorConfig);
|
||||
log.info("Created SMTP email connector: {}", result != null ? result.get("id") : "unknown");
|
||||
}
|
||||
|
||||
// Handle registration toggle
|
||||
boolean enableReg = registrationEnabled != null ? registrationEnabled : (existing == null);
|
||||
setRegistrationEnabled(enableReg);
|
||||
|
||||
return getEmailConnector();
|
||||
}
|
||||
|
||||
/** Delete the email connector and disable registration. */
|
||||
public void deleteEmailConnector() {
|
||||
var existing = getEmailConnector();
|
||||
if (existing != null) {
|
||||
logtoClient.deleteConnector(existing.connectorId());
|
||||
setRegistrationEnabled(false);
|
||||
log.info("Deleted email connector: {}", existing.connectorId());
|
||||
}
|
||||
}
|
||||
|
||||
/** Send a test email through the configured connector. */
|
||||
public void sendTestEmail(String toEmail) {
|
||||
var existing = getEmailConnector();
|
||||
if (existing == null) {
|
||||
throw new IllegalStateException("No email connector configured");
|
||||
}
|
||||
// Re-read the full config from Logto to pass to the test endpoint
|
||||
var connectors = logtoClient.listConnectors();
|
||||
@SuppressWarnings("unchecked")
|
||||
var emailConnector = connectors.stream()
|
||||
.filter(c -> "Email".equals(c.get("type")))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IllegalStateException("Email connector not found"));
|
||||
@SuppressWarnings("unchecked")
|
||||
var config = (Map<String, Object>) emailConnector.getOrDefault("config", Map.of());
|
||||
logtoClient.testConnector(existing.factoryId(), toEmail, config);
|
||||
}
|
||||
|
||||
/** Set registration mode on the Logto sign-in experience. */
|
||||
public void setRegistrationEnabled(boolean enabled) {
|
||||
if (enabled) {
|
||||
logtoClient.updateSignInExperience(Map.of(
|
||||
"signInMode", "SignInAndRegister",
|
||||
"signUp", Map.of(
|
||||
"identifiers", List.of("email"),
|
||||
"password", true,
|
||||
"verify", true
|
||||
),
|
||||
"signIn", Map.of(
|
||||
"methods", List.of(
|
||||
Map.of("identifier", "email", "password", true, "verificationCode", false, "isPasswordPrimary", true),
|
||||
Map.of("identifier", "username", "password", true, "verificationCode", false, "isPasswordPrimary", true)
|
||||
)
|
||||
)
|
||||
));
|
||||
} else {
|
||||
logtoClient.updateSignInExperience(Map.of(
|
||||
"signInMode", "SignIn",
|
||||
"signIn", Map.of(
|
||||
"methods", List.of(
|
||||
Map.of("identifier", "username", "password", true, "verificationCode", false, "isPasswordPrimary", true)
|
||||
)
|
||||
)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/** Check if registration is currently enabled in Logto. */
|
||||
@SuppressWarnings("unchecked")
|
||||
private boolean isRegistrationEnabled() {
|
||||
var signInExp = logtoClient.getSignInExperience();
|
||||
if (signInExp == null) return false;
|
||||
return "SignInAndRegister".equals(signInExp.get("signInMode"));
|
||||
}
|
||||
|
||||
/** 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", "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>"
|
||||
),
|
||||
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>"
|
||||
),
|
||||
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>"
|
||||
),
|
||||
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>"
|
||||
)
|
||||
));
|
||||
return config;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user