feat: test SMTP connection on save and retain password on edit
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:
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -15,7 +15,7 @@ export interface SmtpConfigRequest {
|
||||
host: string;
|
||||
port: number;
|
||||
username: string;
|
||||
password: string;
|
||||
password?: string;
|
||||
fromEmail: string;
|
||||
registrationEnabled?: boolean;
|
||||
}
|
||||
|
||||
14
ui/src/pages/vendor/EmailConfigPage.tsx
vendored
14
ui/src/pages/vendor/EmailConfigPage.tsx
vendored
@@ -50,8 +50,12 @@ export function EmailConfigPage() {
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
if (!host || !username || !password || !fromEmail) {
|
||||
toast({ title: 'All fields are required', variant: 'error' });
|
||||
if (!host || !username || !fromEmail) {
|
||||
toast({ title: 'Host, username, and from email are required', variant: 'error' });
|
||||
return;
|
||||
}
|
||||
if (!isConfigured && !password) {
|
||||
toast({ title: 'Password is required for new configuration', variant: 'error' });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -59,7 +63,7 @@ export function EmailConfigPage() {
|
||||
host,
|
||||
port: parseInt(port, 10) || 587,
|
||||
username,
|
||||
password,
|
||||
password: password || undefined,
|
||||
fromEmail,
|
||||
});
|
||||
toast({ title: 'Email connector saved', variant: 'success' });
|
||||
@@ -217,10 +221,10 @@ export function EmailConfigPage() {
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="Password *">
|
||||
<FormField label={isConfigured ? 'Password' : 'Password *'}>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder={isConfigured ? 'Enter new password' : 'Password'}
|
||||
placeholder={isConfigured ? 'Leave blank to keep current' : 'Password'}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user