Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: P4ADEV-1496 send-email-activity #23

Closed
wants to merge 19 commits into from
Closed
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ val commonsCompressVersion = "1.27.1"
val commonsLang3Version = "3.17.0"
val commonsTextVersion = "1.12.0"
val jacksonModuleVersion = "2.18.1"

val jsoupVersion = "1.18.1"

dependencies {
implementation("org.springframework.boot:spring-boot-starter")
Expand All @@ -71,6 +71,11 @@ dependencies {

implementation("com.fasterxml.jackson.module:jackson-module-parameter-names:$jacksonModuleVersion")

// Used for mail
implementation("org.springframework.boot:spring-boot-starter-mail")
implementation("org.springframework.retry:spring-retry")
implementation("org.jsoup:jsoup:$jsoupVersion")

// Testing
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.junit.jupiter:junit-jupiter-api")
Expand Down
6 changes: 6 additions & 0 deletions gradle.lockfile
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,10 @@ org.springframework:spring-core:6.1.14=compileClasspath
org.springframework:spring-expression:6.1.14=compileClasspath
org.springframework:spring-jcl:6.1.14=compileClasspath
org.yaml:snakeyaml:2.0=compileClasspath
org.eclipse.angus:jakarta.mail:2.0.3=compileClasspath
org.springframework:spring-context-support:6.1.14=compileClasspath
org.jsoup:jsoup:1.18.1=compileClasspath
jakarta.activation:jakarta.activation-api:2.1.3=compileClasspath
org.springframework.boot:spring-boot-starter-mail:3.3.5=compileClasspath
org.springframework.retry:spring-retry:2.0.10=compileClasspath
empty=
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
package it.gov.pagopa.payhub.activities.activity.fdr;
import it.gov.pagopa.payhub.activities.dto.fdr.FdRIngestionActivityResult;
package it.gov.pagopa.payhub.activities.activity.paymentsreporting;
import it.gov.pagopa.payhub.activities.dto.reportingflow.FdRIngestionActivityResult;

/**
* Interface for the FdRIngestionActivity.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package it.gov.pagopa.payhub.activities.activity.paymentsreporting;

import it.gov.pagopa.payhub.activities.activity.paymentsreporting.service.IngestionFileHandlerService;
import it.gov.pagopa.payhub.activities.activity.paymentsreporting.service.IngestionFileValidatorService;
import it.gov.pagopa.payhub.activities.activity.paymentsreporting.service.IngestionFlowRetrieverService;
import it.gov.pagopa.payhub.activities.dto.reportingflow.FdRIngestionActivityResult;
import it.gov.pagopa.payhub.activities.dto.reportingflow.IngestionFlowDTO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;

@Slf4j
@Component
public class FdRIngestionActivityImpl implements FdRIngestionActivity {
private final IngestionFlowRetrieverService ingestionFlowRetrieverService;
private final IngestionFileValidatorService ingestionFileValidatorService;
private final IngestionFileHandlerService ingestionFileHandlerService;

public FdRIngestionActivityImpl(IngestionFlowRetrieverService ingestionFlowRetrieverService,
IngestionFileValidatorService ingestionFileValidatorService,
IngestionFileHandlerService ingestionFileHandlerService) {
this.ingestionFlowRetrieverService = ingestionFlowRetrieverService;
this.ingestionFileValidatorService = ingestionFileValidatorService;
this.ingestionFileHandlerService = ingestionFileHandlerService;
}

@Override
public FdRIngestionActivityResult processFile(String ingestionFlowId) {
List<String> iufList = new ArrayList<>();
boolean success = true;

try {
IngestionFlowDTO ingestionFlowDTO = ingestionFlowRetrieverService.getIngestionFlow(Long.valueOf(ingestionFlowId));

ingestionFileValidatorService.validate(ingestionFlowDTO.getFilePathName(), ingestionFlowDTO.getFileName(), ingestionFlowDTO.getRequestTokenCode());

ingestionFileHandlerService.setUpProcess(ingestionFlowDTO.getFilePathName(), ingestionFlowDTO.getFileName());
} catch (Exception e) {
log.error("Error during IngestionActivity flowId {} due to: {}", ingestionFlowId, e.getMessage());
success = false;
}
return new FdRIngestionActivityResult(iufList, success);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package it.gov.pagopa.payhub.activities.activity.fdr;
package it.gov.pagopa.payhub.activities.activity.paymentsreporting;

/**
* Interface for SendEmailIngestionFlowActivity.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package it.gov.pagopa.payhub.activities.activity.paymentsreporting;

import it.gov.pagopa.payhub.activities.activity.paymentsreporting.service.IngestionFlowRetrieverService;
import it.gov.pagopa.payhub.activities.activity.reportingflow.service.AsyncSendMailService;
import it.gov.pagopa.payhub.activities.dao.IngestionFlowDao;
import it.gov.pagopa.payhub.activities.dto.reportingflow.IngestionFlowDTO;
import it.gov.pagopa.payhub.activities.exception.SendMailException;
import it.gov.pagopa.payhub.activities.helper.MailParameterHelper;
import it.gov.pagopa.payhub.activities.model.MailParams;
import lombok.extern.slf4j.Slf4j;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class SendEmailIngestionFlowActivityImpl implements SendEmailIngestionFlowActivity {
private final IngestionFlowRetrieverService ingestionFlowRetrieverService;
private final AsyncSendMailService asyncSendMailService;
private final MailParams mailParams;
private final JavaMailSender javaMailSender;
private final IngestionFlowDao ingestionFlowDao;

public SendEmailIngestionFlowActivityImpl(IngestionFlowRetrieverService ingestionFlowRetrieverService, AsyncSendMailService asyncSendMailService, IngestionFlowDao ingestionFlowDao, MailParams mailParams, JavaMailSender javaMailSender) {
this.ingestionFlowRetrieverService = ingestionFlowRetrieverService;
this.asyncSendMailService = asyncSendMailService;
this.ingestionFlowDao = ingestionFlowDao;
this.mailParams = mailParams;
this.javaMailSender = javaMailSender;
}

/**
* Sends an email based on the process result of the given file ingestionFlow ID.
*
* @param ingestionFlowId the unique identifier of the IngestionFlow record related to the imported file.
* @param success true if the process succeeded, false otherwise.
* @return true if the email was sent successfully, false otherwise.
*/
@Override
public boolean sendEmail(String ingestionFlowId, boolean success) {
// verify if previous operation is success
if (success){
try {
IngestionFlowDTO ingestionFlowDTO = ingestionFlowRetrieverService.getIngestionFlow(Long.valueOf(ingestionFlowId));
if (ingestionFlowDTO!=null) {
mailParams.setIngestionFlowDTO(ingestionFlowDTO);
}
// get e-mail parameters
MailParams params = MailParameterHelper.getMailParams(mailParams);

// send e-mail if there are no errors in parameters
if (params.isSuccess()){
mailParams.setHtmlText(params.getHtmlText());
mailParams.setMailSubject(params.getMailSubject());
mailParams.setIngestionFlowId(ingestionFlowId);
asyncSendMailService.sendMail(javaMailSender, mailParams);
return true;
}
} catch (Exception e) {
throw new SendMailException("Error sending mail for id: "+ingestionFlowId);
}
}
return false;
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package it.gov.pagopa.payhub.activities.activity.fdr;
package it.gov.pagopa.payhub.activities.activity.paymentsreporting;

/**
* Interface for the UpdateIngestionFlowStatusActivity.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package it.gov.pagopa.payhub.activities.activity.reportingflow.service;

import it.gov.pagopa.payhub.activities.exception.SendMailException;
import it.gov.pagopa.payhub.activities.model.MailParams;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ArrayUtils;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.safety.Safelist;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.mail.MailException;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Recover;
import org.springframework.retry.annotation.Retryable;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;

import java.util.concurrent.Executor;

@Service
@Slf4j
public class AsyncSendMailService {
@Value("${async.sendMail.corePoolSize:2}")
private String corePoolSize;
@Value("${async.sendMail.maxPoolSize:10}")
private String maxPoolSize;
@Value("${async.sendMail.queueCapacity:500}")
private String queueCapacity;


@Async("SendMailTaskExecutor")
@Retryable(value = MailException.class, maxAttemptsExpression = "${async.sendMail.retry.maxAttempts}",
backoff = @Backoff(random = true, delayExpression = "${async.sendMail.retry.delay}",
maxDelayExpression = "${async.sendMail.retry.maxDelay}", multiplierExpression = "${async.sendMail.retry.multiplier}"))
public void sendMail(JavaMailSender javaMailSender, MailParams mailParams) {
try {
javaMailSender.send( mimeMessage -> {
MimeMessageHelper message = new MimeMessageHelper(mimeMessage, true, "UTF-8");
message.setFrom(mailParams.getEmailFromAddress(), mailParams.getEmailFromName());
message.setTo(mailParams.getTo());
if(ArrayUtils.isNotEmpty(mailParams.getCc()))
message.setCc(mailParams.getCc());
message.setSubject(mailParams.getMailSubject());
String plainText = Jsoup.clean(mailParams.getHtmlText(), "", Safelist.none(), new Document.OutputSettings().prettyPrint(false));
message.setText(plainText, mailParams.getHtmlText());
log.info("sending mail message");
} );
log.info("MAIL has been send");
}
catch (Exception e) {
log.info("MAIL error");
throw new SendMailException("Error in mail sending");
}
}

@Recover
private void recover(MailException e, String[] to, String[] cc, String subject, String htmlText){
//TODO write fail to db or queue for retry, in case
}

@Bean("SendMailTaskExecutor")
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(Integer.parseInt(corePoolSize));
executor.setMaxPoolSize(Integer.parseInt(maxPoolSize));
executor.setQueueCapacity(Integer.parseInt(queueCapacity));
executor.setThreadNamePrefix("BatchSendMail-");
executor.initialize();
return executor;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package it.gov.pagopa.payhub.activities.activity.paymentsreporting.service;

import it.gov.pagopa.payhub.activities.util.AESUtils;
import it.gov.pagopa.payhub.activities.util.FileUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

@Slf4j
@Service
public class IngestionFileHandlerService {
private static final String TEMPORARY_PATH = "TEMP";

private final String dataCipherPsw;

public IngestionFileHandlerService(@Value("${data-cipher.encrypt-psw}") String dataCipherPsw) {
this.dataCipherPsw = dataCipherPsw;
}

public Path setUpProcess(String relativePath, String filename) throws IOException {
Path relativePathDir = Paths.get(relativePath);
Path encryptedFilePath = relativePathDir.resolve(filename);

// Prepare temporary path
Path temporaryPath = relativePathDir.resolve(TEMPORARY_PATH);
Files.createDirectories(temporaryPath);

// Derive the decrypted file name and path
String filenameNoCipher = filename.replace(AESUtils.CIPHER_EXTENSION, "");
Path temporaryZipFilePath = temporaryPath.resolve(filenameNoCipher);

// Decrypt the file
log.debug("Decrypting file: {}", encryptedFilePath);
AESUtils.decrypt(dataCipherPsw, encryptedFilePath.toFile(), temporaryZipFilePath.toFile());

// Validate ZIP file
log.debug("Validating ZIP file: {}", temporaryZipFilePath);
FileUtils.validateZip(temporaryZipFilePath);

// Unzip the file
String unzippedFilename = filenameNoCipher.replace(".zip", ".xml");
Path outputUnzippedPath = temporaryPath.resolve(unzippedFilename);

log.debug("Unzipping file: {} to {}", temporaryZipFilePath, outputUnzippedPath);
FileUtils.unzipFile(temporaryZipFilePath, outputUnzippedPath);

log.debug("File setup process completed successfully for: {}", filename);
return outputUnzippedPath;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package it.gov.pagopa.payhub.activities.activity.paymentsreporting.service;

import it.gov.pagopa.payhub.activities.exception.InvalidIngestionFileException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.io.IOUtils;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

@Slf4j
@Service
public class IngestionFileValidatorService {

public void validate(String relativePath, String filename, String requestTokenCode) {
Path fileLocation = Paths.get(relativePath, filename);

validateFile(fileLocation);
validateAUTH(fileLocation.toString(), requestTokenCode);
validateMD5(fileLocation.toString());
}

private void validateFile(Path filePath) {
if (!Files.exists(filePath) || !Files.isRegularFile(filePath)) {
throw new InvalidIngestionFileException("File non trovato: " + filePath);
}
}

private void validateMD5(String filename) {
String md5 = filename.replace(".zip", ".md5");
String valueMD5 = readContentFile(md5);
String calculatedMD5 = calculateMd5(filename);
if(!valueMD5.equalsIgnoreCase(calculatedMD5)) {
throw new InvalidIngestionFileException("Error while calculating MD5 file value");
}
}

private void validateAUTH(String filename, String requestToken) {
String auth = filename.replace(".zip", ".auth");
String valueAUTH = readContentFile(auth);
if(!valueAUTH.equalsIgnoreCase(requestToken)) {
throw new InvalidIngestionFileException("Error while calculating MD5 file value");
}
}

private String readContentFile(String filename) {
try {
return Files.readString(Paths.get(filename));
} catch (IOException e) {
throw new InvalidIngestionFileException("Error while reading file: " + filename);
}
}

private String calculateMd5(String filename) {
try (InputStream is = Files.newInputStream(Paths.get(filename))) {
MessageDigest md = MessageDigest.getInstance("MD5");
Dismissed Show dismissed Hide dismissed
DigestInputStream dis = new DigestInputStream(is, md);
IOUtils.toByteArray(dis);
return Hex.encodeHexString(md.digest());
} catch (NoSuchAlgorithmException | IOException e) {
throw new InvalidIngestionFileException("Error while calculating MD5");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package it.gov.pagopa.payhub.activities.activity.paymentsreporting.service;

import it.gov.pagopa.payhub.activities.dao.IngestionFlowDao;
import it.gov.pagopa.payhub.activities.dto.reportingflow.IngestionFlowDTO;
import it.gov.pagopa.payhub.activities.exception.IngestionFlowNotFoundException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.util.Optional;

@Service
@Slf4j
public class IngestionFlowRetrieverService {

private final IngestionFlowDao ingestionFlowDao;

public IngestionFlowRetrieverService(IngestionFlowDao ingestionFlowDao) {
this.ingestionFlowDao = ingestionFlowDao;
}

public IngestionFlowDTO getIngestionFlow(Long ingestionFlowId) {
Optional<IngestionFlowDTO> ingestionFlow = ingestionFlowDao.getIngestionFlow(ingestionFlowId);

return ingestionFlow
.orElseThrow(() -> new IngestionFlowNotFoundException("Cannot found ingestionFlow having id: "+ ingestionFlowId));
}
}
Loading