feat: test SMTP connection on save and retain password on edit
All checks were successful
CI / build (push) Successful in 2m20s
CI / docker (push) Successful in 1m36s

Adds testSmtpConnection() that performs EHLO + auth via JavaMailSender
before persisting to Logto — saves fail fast with a clear error if
SMTP credentials are wrong. Password is now optional when editing:
if left blank the backend fetches the existing password from Logto's
connector config, so users can update host/port/fromEmail without
re-entering the password every time.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-26 16:23:42 +02:00
parent 0413a5b882
commit 883e10ba7c
4 changed files with 81 additions and 9 deletions

View File

@@ -33,7 +33,7 @@ public class EmailConnectorController {
@NotBlank String host,
@Min(1) @Max(65535) int port,
@NotBlank String username,
@NotBlank String password,
String password,
@NotBlank @Email String fromEmail,
Boolean registrationEnabled
) {}
@@ -76,11 +76,28 @@ public class EmailConnectorController {
}
@PostMapping
public ResponseEntity<EmailConnectorResponse> save(@Valid @RequestBody SmtpConfigRequest request) {
public ResponseEntity<?> save(@Valid @RequestBody SmtpConfigRequest request) {
// Resolve password: use provided value, or fall back to existing password from Logto
String password = request.password();
if (password == null || password.isBlank()) {
password = emailConnectorService.getExistingPassword();
if (password == null) {
return ResponseEntity.badRequest().body(Map.of("message", "Password is required for new configuration"));
}
}
var smtp = new EmailConnectorService.SmtpConfig(
request.host(), request.port(), request.username(),
request.password(), request.fromEmail()
password, request.fromEmail()
);
// Test SMTP connection before saving
try {
emailConnectorService.testSmtpConnection(smtp);
} catch (IllegalStateException e) {
return ResponseEntity.badRequest().body(Map.of("message", e.getMessage()));
}
var status = emailConnectorService.saveSmtpConnector(smtp, request.registrationEnabled());
return ResponseEntity.ok(EmailConnectorResponse.from(status));
}

View File

@@ -5,6 +5,7 @@ import net.siegeln.cameleer.saas.provisioning.ProvisioningProperties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.ClassPathResource;
import org.springframework.mail.javamail.JavaMailSenderImpl;
import org.springframework.stereotype.Service;
import java.io.IOException;
@@ -12,6 +13,7 @@ import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
@Service
public class EmailConnectorService {
@@ -68,6 +70,55 @@ public class EmailConnectorService {
);
}
/**
* Retrieve the existing SMTP password from Logto, or null if not configured.
* Used to retain the password when the user edits other fields without re-entering it.
*/
@SuppressWarnings("unchecked")
public String getExistingPassword() {
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 pass = String.valueOf(auth.getOrDefault("pass", ""));
return pass.isEmpty() ? null : pass;
}
/**
* Test SMTP connection by performing EHLO + auth. Throws on failure.
*/
public void testSmtpConnection(SmtpConfig smtp) {
var sender = new JavaMailSenderImpl();
sender.setHost(smtp.host());
sender.setPort(smtp.port());
sender.setUsername(smtp.username());
sender.setPassword(smtp.password());
sender.setDefaultEncoding("UTF-8");
Properties props = sender.getJavaMailProperties();
props.put("mail.transport.protocol", "smtp");
props.put("mail.smtp.auth", "true");
if (smtp.port() == 465) {
props.put("mail.smtp.ssl.enable", "true");
} else {
props.put("mail.smtp.starttls.enable", "true");
}
props.put("mail.smtp.timeout", "10000");
props.put("mail.smtp.connectiontimeout", "10000");
try {
sender.testConnection();
log.info("SMTP connection test successful: {}:{}", smtp.host(), smtp.port());
} catch (Exception e) {
log.warn("SMTP connection test failed: {}:{} — {}", smtp.host(), smtp.port(), e.getMessage());
throw new IllegalStateException("SMTP connection failed: " + e.getMessage(), e);
}
}
/** Create or update the SMTP email connector. Returns the connector status. */
public EmailConnectorStatus saveSmtpConnector(SmtpConfig smtp, Boolean registrationEnabled) {
var connectorConfig = buildSmtpConfig(smtp);