Compare commits

...

2 Commits

Author SHA1 Message Date
Andrei Isvoran
7702335893 email service 2023-09-04 11:41:05 +03:00
Andrei Isvoran
3c733ddc8c email service 2023-09-01 15:00:09 +03:00
6 changed files with 278 additions and 30 deletions

View File

@ -100,6 +100,8 @@ dependencies {
implementation("commons-validator:commons-validator:1.7")
implementation("org.springframework.boot:spring-boot-configuration-processor")
implementation("com.iqser.red.commons:storage-commons:2.22.0")
implementation("jakarta.mail:jakarta.mail-api:2.1.2")
implementation("org.eclipse.angus:angus-mail:2.0.2")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.springframework.cloud:spring-cloud-starter-openfeign")
testImplementation("org.projectlombok:lombok")
@ -111,8 +113,8 @@ dependencies {
testImplementation("org.springframework.amqp:spring-rabbit-test")
testImplementation("org.testcontainers:postgresql:1.18.3")
testImplementation("com.github.dasniko:testcontainers-keycloak:2.5.0")
testImplementation("org.testcontainers:testcontainers:1.18.3")
testImplementation("org.testcontainers:junit-jupiter:1.18.3")
testImplementation("org.testcontainers:testcontainers:1.19.0")
testImplementation("org.testcontainers:junit-jupiter:1.19.0")
testAnnotationProcessor("org.projectlombok:lombok")
}

View File

@ -6,11 +6,12 @@ import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import com.knecon.fforesight.tenantusermanagement.model.SMTPConfiguration;
import com.knecon.fforesight.tenantusermanagement.model.SMTPResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
@ -22,6 +23,7 @@ public interface SMTPConfigurationResource {
String TEST_PATH = "/test";
String TEST_EMAIL = "testEmail";
@ResponseBody
@ResponseStatus(value = HttpStatus.OK)
@ -42,7 +44,7 @@ public interface SMTPConfigurationResource {
@PostMapping(value = SMTP_PATH + TEST_PATH, consumes = MediaType.APPLICATION_JSON_VALUE)
@Operation(summary = "Test SMTP Settings to KeyCloak")
@ApiResponses(value = {@ApiResponse(responseCode = "200", description = "SMTP Configuration is valid."), @ApiResponse(responseCode = "400", description = "SMTP test failed.")})
void testSMTPConfiguration(@RequestBody SMTPConfiguration smtpConfigurationModel);
SMTPResponse testSMTPConfiguration(@RequestBody SMTPConfiguration smtpConfigurationModel, @RequestParam(value = TEST_EMAIL, required = false) String testEmail);
@ResponseStatus(value = HttpStatus.NO_CONTENT)

View File

@ -6,8 +6,10 @@ import static com.knecon.fforesight.tenantusermanagement.permissions.UserManagem
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.fasterxml.jackson.databind.ObjectMapper;
@ -16,6 +18,8 @@ import com.knecon.fforesight.tenantcommons.TenantContext;
import com.knecon.fforesight.tenantusermanagement.api.external.PublicResource;
import com.knecon.fforesight.tenantusermanagement.api.external.SMTPConfigurationResource;
import com.knecon.fforesight.tenantusermanagement.model.SMTPConfiguration;
import com.knecon.fforesight.tenantusermanagement.model.SMTPResponse;
import com.knecon.fforesight.tenantusermanagement.service.EmailService;
import com.knecon.fforesight.tenantusermanagement.service.RealmService;
import lombok.RequiredArgsConstructor;
@ -28,9 +32,12 @@ import lombok.extern.slf4j.Slf4j;
public class SMTPConfigurationController implements SMTPConfigurationResource, PublicResource {
private final static String SMTP_PASSWORD_KEY = "FFORESIGHT_SMTP_PASSWORD";
private final static String DEFAULT_PASSWORD = "**********";
private final RealmService realmService;
private final ObjectMapper objectMapper;
private final EncryptionDecryptionService encryptionDecryptionService;
private final EmailService emailService;
@Override
@ -59,28 +66,23 @@ public class SMTPConfigurationController implements SMTPConfigurationResource, P
}
private Map<String, String> convertSMTPConfigurationModelToMap(SMTPConfiguration smtpConfigurationModel) {
Map<String, Object> propertiesMap = objectMapper.convertValue(smtpConfigurationModel, Map.class);
Map<String, String> stringPropertiesMap = new HashMap<>();
propertiesMap.forEach((key, value) -> {
if (value != null) {
stringPropertiesMap.put(key, value.toString());
} else {
stringPropertiesMap.put(key, "");
}
});
return stringPropertiesMap;
}
@SneakyThrows
@Override
@PreAuthorize("hasAuthority('" + WRITE_SMTP_CONFIGURATION + "')")
public void testSMTPConfiguration(@RequestBody SMTPConfiguration smtpConfigurationModel) {
public SMTPResponse testSMTPConfiguration(@RequestBody SMTPConfiguration smtpConfiguration, @RequestParam(value = TEST_EMAIL, required = false) String testEmail) {
var propertiesMap = convertSMTPConfigurationModelToMap(smtpConfigurationModel);
realmService.realm(TenantContext.getTenantId()).testSMTPConnection(propertiesMap);
String targetEmail;
if (StringUtils.isBlank(testEmail)) {
// will send e-mail to self in case testEmail is not set
targetEmail = smtpConfiguration.getFrom();
} else {
targetEmail = testEmail;
}
updatePassword(smtpConfiguration);
smtpConfiguration.setPassword(encryptionDecryptionService.decrypt(smtpConfiguration.getPassword()));
return emailService.send(convertSMTPConfigurationModelToMap(smtpConfiguration), targetEmail, "Redaction Test message", "This is a test message");
}
@ -95,4 +97,32 @@ public class SMTPConfigurationController implements SMTPConfigurationResource, P
}
private Map<String, String> convertSMTPConfigurationModelToMap(SMTPConfiguration smtpConfigurationModel) {
Map<String, Object> propertiesMap = objectMapper.convertValue(smtpConfigurationModel, Map.class);
Map<String, String> stringPropertiesMap = new HashMap<>();
propertiesMap.forEach((key, value) -> {
if (value != null) {
stringPropertiesMap.put(key, value.toString());
} else {
stringPropertiesMap.put(key, "");
}
});
return stringPropertiesMap;
}
private void updatePassword(SMTPConfiguration smtpConfiguration) {
if (DEFAULT_PASSWORD.equals(smtpConfiguration.getPassword())) {
try {
var password = realmService.realm(TenantContext.getTenantId()).toRepresentation().getAttributesOrEmpty().get(SMTP_PASSWORD_KEY);
smtpConfiguration.setPassword(password);
} catch (Exception e) {
log.info("No current SMTP Config exists", e);
}
} else {
smtpConfiguration.setPassword(encryptionDecryptionService.encrypt(smtpConfiguration.getPassword()));
}
}
}

View File

@ -0,0 +1,22 @@
package com.knecon.fforesight.tenantusermanagement.model;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Schema(description = "Object containing a simplified version of the SMTP test connection response.")
public class SMTPResponse {
@Schema(description = "Parameter containing status code of the response.")
private int statusCode;
@Schema(description = "Parameter containing the reason phrase of the response.")
private String reasonPhrase;
}

View File

@ -0,0 +1,169 @@
package com.knecon.fforesight.tenantusermanagement.service;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.Map;
import java.util.Properties;
import javax.net.ssl.SSLContext;
import javax.ws.rs.BadRequestException;
import org.springframework.stereotype.Service;
import com.knecon.fforesight.tenantusermanagement.model.SMTPResponse;
import jakarta.mail.Address;
import jakarta.mail.Message;
import jakarta.mail.MessagingException;
import jakarta.mail.Multipart;
import jakarta.mail.Session;
import jakarta.mail.Transport;
import jakarta.mail.internet.AddressException;
import jakarta.mail.internet.InternetAddress;
import jakarta.mail.internet.MimeBodyPart;
import jakarta.mail.internet.MimeMessage;
import jakarta.mail.internet.MimeMultipart;
import jakarta.mail.internet.MimeUtility;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service
@RequiredArgsConstructor
public class EmailService {
public SMTPResponse send(Map<String, String> config, String address, String subject, String textBody) {
Transport transport = null;
try {
Properties props = new Properties();
if (config.containsKey("host")) {
props.setProperty("mail.smtp.host", config.get("host"));
}
boolean auth = "true".equals(config.get("auth"));
boolean ssl = "true".equals(config.get("ssl"));
boolean starttls = "true".equals(config.get("starttls"));
if (config.containsKey("port") && config.get("port") != null) {
props.setProperty("mail.smtp.port", config.get("port"));
}
if (auth) {
props.setProperty("mail.smtp.auth", "true");
}
if (ssl) {
props.setProperty("mail.smtp.ssl.enable", "true");
}
if (starttls) {
props.setProperty("mail.smtp.starttls.enable", "true");
}
if (ssl || starttls) {
props.put("mail.smtp.ssl.protocols", getSupportedSslProtocols());
}
props.setProperty("mail.smtp.timeout", "10000");
props.setProperty("mail.smtp.connectiontimeout", "10000");
String from = config.get("from");
String fromDisplayName = config.get("fromDisplayName");
String replyTo = config.get("replyTo");
String replyToDisplayName = config.get("replyToDisplayName");
String envelopeFrom = config.get("envelopeFrom");
Session session = Session.getInstance(props);
Multipart multipart = new MimeMultipart("alternative");
MimeBodyPart textPart = new MimeBodyPart();
textPart.setText(textBody, "UTF-8");
multipart.addBodyPart(textPart);
Message msg = new MimeMessage(session);
msg.setFrom(toInternetAddress(from, fromDisplayName));
msg.setReplyTo(new Address[]{toInternetAddress(from, fromDisplayName)});
if (isNotBlank(replyTo)) {
msg.setReplyTo(new Address[]{toInternetAddress(replyTo, replyToDisplayName)});
}
if (isNotBlank(envelopeFrom)) {
props.setProperty("mail.smtp.from", envelopeFrom);
}
msg.setHeader("To", address);
msg.setSubject(MimeUtility.encodeText(subject, StandardCharsets.UTF_8.name(), null));
msg.setContent(multipart);
msg.saveChanges();
msg.setSentDate(new Date());
transport = session.getTransport("smtp");
if (auth) {
transport.connect(config.get("user"), config.get("password"));
} else {
transport.connect();
}
transport.sendMessage(msg, new InternetAddress[]{new InternetAddress(address)});
return SMTPResponse.builder()
.statusCode(200)
.build();
} catch (Exception e) {
return SMTPResponse.builder()
.statusCode(400)
.reasonPhrase(e.getMessage())
.build();
} finally {
if (transport != null) {
try {
transport.close();
} catch (MessagingException e) {
log.warn("Failed to close transport", e);
}
}
}
}
protected InternetAddress toInternetAddress(String email, String displayName) throws UnsupportedEncodingException, AddressException, BadRequestException {
if (email == null || "".equals(email.trim())) {
throw new BadRequestException("Please provide a valid address");
}
if (displayName == null || "".equals(displayName.trim())) {
return new InternetAddress(email);
}
return new InternetAddress(email, displayName, "utf-8");
}
private String getSupportedSslProtocols() {
try {
String[] protocols = SSLContext.getDefault().getSupportedSSLParameters().getProtocols();
if (protocols != null) {
return String.join(" ", protocols);
}
} catch (Exception e) {
log.warn("Failed to get list of supported SSL protocols", e);
}
return null;
}
public boolean isNotBlank(String str) {
return str != null && !"".equals(str.trim());
}
}

View File

@ -27,14 +27,7 @@ public class SMTPConfigurationTest extends AbstractTenantUserManagementIntegrati
System.out.println(e.getMessage());
}
SMTPConfiguration newConfig = new SMTPConfiguration();
newConfig.setAuth(true);
newConfig.setFrom("from@knecon.com");
newConfig.setHost("test.knecon.com");
newConfig.setPassword("secret");
newConfig.setUser("user");
newConfig.setStarttls(true);
newConfig.setSsl(false);
SMTPConfiguration newConfig = provideTestSMTPConfiguration();
smtpConfigurationClient.updateSMTPConfiguration(newConfig);
var currentSMTPConfiguration = smtpConfigurationClient.getCurrentSMTPConfiguration();
@ -50,4 +43,34 @@ public class SMTPConfigurationTest extends AbstractTenantUserManagementIntegrati
}
@Test
public void testSMTPConnection() {
TenantContext.setTenantId(AbstractTenantUserManagementIntegrationTest.TEST_TENANT_ID);
SMTPConfiguration smtpConfiguration = provideTestSMTPConfiguration();
var response = smtpConfigurationClient.testSMTPConfiguration(smtpConfiguration, "");
// Fails because we are not using a smtp config
assertThat(response.getStatusCode()).isEqualTo(400);
assertThat(response.getReasonPhrase()).isEqualTo("Couldn't connect to host, port: test.knecon.com, 25; timeout 10000");
TenantContext.clear();
}
private SMTPConfiguration provideTestSMTPConfiguration() {
SMTPConfiguration smtpConfiguration = new SMTPConfiguration();
smtpConfiguration.setAuth(true);
smtpConfiguration.setFrom("from@knecon.com");
smtpConfiguration.setHost("test.knecon.com");
smtpConfiguration.setPassword("secret");
smtpConfiguration.setUser("user");
smtpConfiguration.setStarttls(true);
smtpConfiguration.setSsl(false);
return smtpConfiguration;
}
}