diff --git a/.github/labeler-config.yml b/.github/labeler-config.yml index a0a634840..bb52c7b85 100644 --- a/.github/labeler-config.yml +++ b/.github/labeler-config.yml @@ -27,18 +27,34 @@ Back End: Security: - changed-files: + - any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/config/interfaces/DatabaseInterface.java' - any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/config/security/**/*' + - any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/controller/api/DatabaseController.java' + - any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/controller/api/EmailController.java' + - any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/controller/api/H2SQLController.java' + - any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/controller/web/AccountWebController.java' + - any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/controller/web/DatabaseWebController.java' + - any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/controller/api/UserController.java' + - any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/model/api/Email.java' + - any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/model/exception/BackupNotFoundException.java' + - any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/model/exception/NoProviderFoundExceptionjava' - any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/model/provider/**/*' - any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/model/AuthenticationType.java' - - any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/model/BackupNotFoundException.java' + - any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/model/ApiKeyAuthenticationToken.java' + - any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/model/AttemptCounter.java' + - any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/model/Authority.java' + - any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/model/PersistentLogin.java' + - any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/model/SessionEntity.java' - any-glob-to-any-file: 'scripts/download-security-jar.sh' - any-glob-to-any-file: '.github/workflows/dependency-review.yml' - any-glob-to-any-file: '.github/workflows/scorecards.yml' API: - changed-files: + - any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/config/OpenApiConfig.java' - any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/controller/web/MetricsController.java' - any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/controller/api/**/*' + - any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/model/api/**/*' - any-glob-to-any-file: 'scripts/png_to_webp.py' - any-glob-to-any-file: 'split_photos.py' - any-glob-to-any-file: '.github/workflows/swagger.yml' diff --git a/.github/workflows/PR-Demo-Comment-with-react.yml b/.github/workflows/PR-Demo-Comment-with-react.yml index 291e6b97a..adb3e33cf 100644 --- a/.github/workflows/PR-Demo-Comment-with-react.yml +++ b/.github/workflows/PR-Demo-Comment-with-react.yml @@ -48,7 +48,7 @@ jobs: # Generate GitHub App token - name: Generate GitHub App Token id: generate-token - uses: actions/create-github-app-token@db3cdf40984fe6fd25ae19ac2bf2f4886ae8d959 # v2.0.5 + uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2.0.6 with: app-id: ${{ secrets.GH_APP_ID }} private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} @@ -135,7 +135,7 @@ jobs: - name: Generate GitHub App Token id: generate-token - uses: actions/create-github-app-token@db3cdf40984fe6fd25ae19ac2bf2f4886ae8d959 # v2.0.5 + uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2.0.6 with: app-id: ${{ secrets.GH_APP_ID }} private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} diff --git a/.github/workflows/licenses-update.yml b/.github/workflows/licenses-update.yml index e05c4b40f..f2ab49f88 100644 --- a/.github/workflows/licenses-update.yml +++ b/.github/workflows/licenses-update.yml @@ -24,7 +24,7 @@ jobs: - name: Generate GitHub App Token id: generate-token - uses: actions/create-github-app-token@db3cdf40984fe6fd25ae19ac2bf2f4886ae8d959 # v2.0.5 + uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2.0.6 with: app-id: ${{ secrets.GH_APP_ID }} private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} diff --git a/.github/workflows/pre_commit.yml b/.github/workflows/pre_commit.yml index 3c121da17..ce10a6c3e 100644 --- a/.github/workflows/pre_commit.yml +++ b/.github/workflows/pre_commit.yml @@ -22,7 +22,7 @@ jobs: - name: Generate GitHub App Token id: generate-token - uses: actions/create-github-app-token@db3cdf40984fe6fd25ae19ac2bf2f4886ae8d959 # v2.0.5 + uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2.0.6 with: app-id: ${{ secrets.GH_APP_ID }} private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} diff --git a/.github/workflows/sync_files.yml b/.github/workflows/sync_files.yml index a9c72d5f0..fe790c65b 100644 --- a/.github/workflows/sync_files.yml +++ b/.github/workflows/sync_files.yml @@ -30,7 +30,7 @@ jobs: - name: Generate GitHub App Token id: generate-token - uses: actions/create-github-app-token@db3cdf40984fe6fd25ae19ac2bf2f4886ae8d959 # v2.0.5 + uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2.0.6 with: app-id: ${{ secrets.GH_APP_ID }} private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} @@ -63,7 +63,7 @@ jobs: - name: Generate GitHub App Token id: generate-token - uses: actions/create-github-app-token@db3cdf40984fe6fd25ae19ac2bf2f4886ae8d959 # v2.0.5 + uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2.0.6 with: app-id: ${{ vars.GH_APP_ID }} private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} diff --git a/build.gradle b/build.gradle index 1162952d9..e91f0a467 100644 --- a/build.gradle +++ b/build.gradle @@ -51,16 +51,20 @@ sourceSets { main { java { if (System.getenv("DOCKER_ENABLE_SECURITY") == "false") { + exclude "stirling/software/SPDF/config/interfaces/DatabaseInterface.java" exclude "stirling/software/SPDF/config/security/**" exclude "stirling/software/SPDF/controller/api/DatabaseController.java" - exclude "stirling/software/SPDF/controller/api/UserController.java" + exclude "stirling/software/SPDF/controller/api/EmailController.java" exclude "stirling/software/SPDF/controller/api/H2SQLCondition.java" + exclude "stirling/software/SPDF/controller/api/UserController.java" exclude "stirling/software/SPDF/controller/web/AccountWebController.java" exclude "stirling/software/SPDF/controller/web/DatabaseWebController.java" + exclude "stirling/software/SPDF/model/api/Email.java" exclude "stirling/software/SPDF/model/ApiKeyAuthenticationToken.java" exclude "stirling/software/SPDF/model/AttemptCounter.java" exclude "stirling/software/SPDF/model/Authority.java" - exclude "stirling/software/SPDF/model/BackupNotFoundException.java" + exclude "stirling/software/SPDF/model/exception/BackupNotFoundException.java" + exclude "stirling/software/SPDF/model/exception/NoProviderFoundException.java" exclude "stirling/software/SPDF/model/PersistentLogin.java" exclude "stirling/software/SPDF/model/SessionEntity.java" exclude "stirling/software/SPDF/model/User.java" @@ -78,16 +82,8 @@ sourceSets { java { if (System.getenv("DOCKER_ENABLE_SECURITY") == "false") { exclude "stirling/software/SPDF/config/security/**" - exclude "stirling/software/SPDF/controller/api/UserControllerTest.java" - exclude "stirling/software/SPDF/controller/api/DatabaseControllerTest.java" - exclude "stirling/software/SPDF/controller/web/AccountWebControllerTest.java" - exclude "stirling/software/SPDF/controller/web/DatabaseWebControllerTest.java" exclude "stirling/software/SPDF/model/ApiKeyAuthenticationTokenTest.java" - exclude "stirling/software/SPDF/model/AttemptCounterTest.java" - exclude "stirling/software/SPDF/model/AuthorityTest.java" - exclude "stirling/software/SPDF/model/PersistentLoginTest.java" - exclude "stirling/software/SPDF/model/SessionEntityTest.java" - exclude "stirling/software/SPDF/model/UserTest.java" + exclude "stirling/software/SPDF/controller/api/EmailControllerTest.java" exclude "stirling/software/SPDF/repository/**" } @@ -459,6 +455,7 @@ dependencies { implementation "org.thymeleaf.extras:thymeleaf-extras-springsecurity5:3.1.3.RELEASE" implementation "org.springframework.boot:spring-boot-starter-data-jpa:$springBootVersion" implementation "org.springframework.boot:spring-boot-starter-oauth2-client:$springBootVersion" + implementation "org.springframework.boot:spring-boot-starter-mail:$springBootVersion" implementation "org.springframework.session:spring-session-core:3.4.3" implementation "org.springframework:spring-jdbc:6.2.6" @@ -506,11 +503,11 @@ dependencies { implementation "com.drewnoakes:metadata-extractor:2.19.0" implementation "commons-io:commons-io:2.19.0" - implementation "org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.6" + implementation "org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.8" //general PDF // https://mvnrepository.com/artifact/com.opencsv/opencsv - implementation ("com.opencsv:opencsv:5.10") + implementation ("com.opencsv:opencsv:5.11") implementation ("org.apache.pdfbox:pdfbox:$pdfboxVersion") implementation "org.apache.pdfbox:preflight:$pdfboxVersion" diff --git a/src/main/java/stirling/software/SPDF/config/security/mail/EmailService.java b/src/main/java/stirling/software/SPDF/config/security/mail/EmailService.java new file mode 100644 index 000000000..8939fbab6 --- /dev/null +++ b/src/main/java/stirling/software/SPDF/config/security/mail/EmailService.java @@ -0,0 +1,63 @@ +package stirling.software.SPDF.config.security.mail; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import jakarta.mail.MessagingException; +import jakarta.mail.internet.MimeMessage; + +import lombok.RequiredArgsConstructor; + +import stirling.software.SPDF.model.ApplicationProperties; +import stirling.software.SPDF.model.api.Email; + +/** + * Service class responsible for sending emails, including those with attachments. It uses + * JavaMailSender to send the email and is designed to handle both the message content and file + * attachments. + */ +@Service +@RequiredArgsConstructor +@ConditionalOnProperty(value = "mail.enabled", havingValue = "true", matchIfMissing = false) +public class EmailService { + + private final JavaMailSender mailSender; + private final ApplicationProperties applicationProperties; + + /** + * Sends an email with an attachment asynchronously. This method is annotated with @Async, which + * means it will be executed asynchronously. + * + * @param email The Email object containing the recipient, subject, body, and file attachment. + * @throws MessagingException If there is an issue with creating or sending the email. + */ + @Async + public void sendEmailWithAttachment(Email email) throws MessagingException { + ApplicationProperties.Mail mailProperties = applicationProperties.getMail(); + MultipartFile file = email.getFileInput(); + + // Creates a MimeMessage to represent the email + MimeMessage message = mailSender.createMimeMessage(); + + // Helper class to set up the message content and attachments + MimeMessageHelper helper = new MimeMessageHelper(message, true); + + // Sets the recipient, subject, body, and sender email + helper.addTo(email.getTo()); + helper.setSubject(email.getSubject()); + helper.setText( + email.getBody(), + true); // The "true" here indicates that the body contains HTML content. + helper.setFrom(mailProperties.getFrom()); + + // Adds the attachment to the email + helper.addAttachment(file.getOriginalFilename(), file); + + // Sends the email via the configured mail sender + mailSender.send(message); + } +} diff --git a/src/main/java/stirling/software/SPDF/config/security/mail/MailConfig.java b/src/main/java/stirling/software/SPDF/config/security/mail/MailConfig.java new file mode 100644 index 000000000..68c2fe35d --- /dev/null +++ b/src/main/java/stirling/software/SPDF/config/security/mail/MailConfig.java @@ -0,0 +1,54 @@ +package stirling.software.SPDF.config.security.mail; + +import java.util.Properties; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.JavaMailSenderImpl; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import stirling.software.SPDF.model.ApplicationProperties; + +/** + * This configuration class provides the JavaMailSender bean, which is used to send emails. It reads + * email server settings from the configuration (ApplicationProperties) and configures the mail + * client (JavaMailSender). + */ +@Configuration +@Slf4j +@AllArgsConstructor +@ConditionalOnProperty(value = "mail.enabled", havingValue = "true", matchIfMissing = false) +public class MailConfig { + + private final ApplicationProperties applicationProperties; + + @Bean + public JavaMailSender javaMailSender() { + + ApplicationProperties.Mail mailProperties = applicationProperties.getMail(); + + // Creates a new instance of JavaMailSenderImpl, which is a Spring implementation + JavaMailSenderImpl mailSender = new JavaMailSenderImpl(); + mailSender.setHost(mailProperties.getHost()); + mailSender.setPort(mailProperties.getPort()); + mailSender.setUsername(mailProperties.getUsername()); + mailSender.setPassword(mailProperties.getPassword()); + mailSender.setDefaultEncoding("UTF-8"); + + // Retrieves the JavaMail properties to configure additional SMTP parameters + Properties props = mailSender.getJavaMailProperties(); + + // Enables SMTP authentication + props.put("mail.smtp.auth", "true"); + + // Enables STARTTLS to encrypt the connection if supported by the SMTP server + props.put("mail.smtp.starttls.enable", "true"); + + // Returns the configured mail sender, ready to send emails + return mailSender; + } +} diff --git a/src/main/java/stirling/software/SPDF/controller/api/AnalysisController.java b/src/main/java/stirling/software/SPDF/controller/api/AnalysisController.java index 75f61e64e..49c5bc81c 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/AnalysisController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/AnalysisController.java @@ -59,7 +59,8 @@ public class AnalysisController { description = "Returns title, author, subject, etc. Input:PDF Output:JSON Type:SISO") public Map getDocumentProperties(@ModelAttribute PDFFile file) throws IOException { - try (PDDocument document = pdfDocumentFactory.load(file.getFileInput())) { + // Load the document in read-only mode to prevent modifications and ensure the integrity of the original file. + try (PDDocument document = pdfDocumentFactory.load(file.getFileInput(), true)) { PDDocumentInformation info = document.getDocumentInformation(); Map properties = new HashMap<>(); properties.put("title", info.getTitle()); diff --git a/src/main/java/stirling/software/SPDF/controller/api/EmailController.java b/src/main/java/stirling/software/SPDF/controller/api/EmailController.java new file mode 100644 index 000000000..3b91368ef --- /dev/null +++ b/src/main/java/stirling/software/SPDF/controller/api/EmailController.java @@ -0,0 +1,57 @@ +package stirling.software.SPDF.controller.api; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import io.swagger.v3.oas.annotations.tags.Tag; + +import jakarta.mail.MessagingException; +import jakarta.validation.Valid; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import stirling.software.SPDF.config.security.mail.EmailService; +import stirling.software.SPDF.model.api.Email; + +/** + * Controller for handling email-related API requests. This controller exposes an endpoint for + * sending emails with attachments. + */ +@RestController +@RequestMapping("/api/v1/general") +@RequiredArgsConstructor +@Slf4j +@Tag(name = "General", description = "General APIs") +@ConditionalOnProperty(value = "mail.enabled", havingValue = "true", matchIfMissing = false) +public class EmailController { + private final EmailService emailService; + + /** + * Endpoint to send an email with an attachment. This method consumes a multipart/form-data + * request containing the email details and attachment. + * + * @param email The Email object containing recipient address, subject, body, and file + * attachment. + * @return ResponseEntity with success or error message. + */ + @PostMapping(consumes = "multipart/form-data", value = "/send-email") + public ResponseEntity sendEmailWithAttachment(@Valid @ModelAttribute Email email) { + try { + // Calls the service to send the email with attachment + emailService.sendEmailWithAttachment(email); + return ResponseEntity.ok("Email sent successfully"); + } catch (MessagingException e) { + // Catches any messaging exception (e.g., invalid email address, SMTP server issues) + String errorMsg = "Failed to send email: " + e.getMessage(); + log.error(errorMsg, e); // Logging the detailed error + // Returns an error response with status 500 (Internal Server Error) + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorMsg); + } + } +} diff --git a/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController.java b/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController.java index 7e1f2601f..58e5b848c 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController.java @@ -1,6 +1,7 @@ package stirling.software.SPDF.controller.api.security; import java.awt.*; +import java.beans.PropertyEditorSupport; import java.io.*; import java.nio.file.Files; import java.security.*; @@ -53,7 +54,10 @@ import org.bouncycastle.operator.OperatorCreationException; import org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo; import org.bouncycastle.pkcs.PKCSException; import org.springframework.core.io.ClassPathResource; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.WebDataBinder; +import org.springframework.web.bind.annotation.InitBinder; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; @@ -82,6 +86,18 @@ public class CertSignController { Security.addProvider(new BouncyCastleProvider()); } + @InitBinder + public void initBinder(WebDataBinder binder) { + binder.registerCustomEditor( + MultipartFile.class, + new PropertyEditorSupport() { + @Override + public void setAsText(String text) throws IllegalArgumentException { + setValue(null); + } + }); + } + private final CustomPDFDocumentFactory pdfDocumentFactory; private static void sign( @@ -103,8 +119,7 @@ public class CertSignController { signature.setLocation(location); signature.setReason(reason); signature.setSignDate(Calendar.getInstance()); - - if (showSignature) { + if (Boolean.TRUE.equals(showSignature)) { SignatureOptions signatureOptions = new SignatureOptions(); signatureOptions.setVisualSignature( instance.createVisibleSignature(doc, signature, pageNumber, showLogo)); @@ -121,13 +136,18 @@ public class CertSignController { } } - @PostMapping(consumes = "multipart/form-data", value = "/cert-sign") + @PostMapping( + consumes = { + MediaType.MULTIPART_FORM_DATA_VALUE, + MediaType.APPLICATION_FORM_URLENCODED_VALUE + }, + value = "/cert-sign") @Operation( summary = "Sign PDF with a Digital Certificate", description = "This endpoint accepts a PDF file, a digital certificate and related" - + " information to sign the PDF. It then returns the digitally signed PDF" - + " file. Input:PDF Output:PDF Type:SISO") + + " information to sign the PDF. It then returns the digitally signed PDF" + + " file. Input:PDF Output:PDF Type:SISO") public ResponseEntity signPDFWithCert(@ModelAttribute SignPDFWithCertRequest request) throws Exception { MultipartFile pdf = request.getFileInput(); @@ -137,12 +157,13 @@ public class CertSignController { MultipartFile p12File = request.getP12File(); MultipartFile jksfile = request.getJksFile(); String password = request.getPassword(); - Boolean showSignature = request.isShowSignature(); + Boolean showSignature = request.getShowSignature(); String reason = request.getReason(); String location = request.getLocation(); String name = request.getName(); - Integer pageNumber = request.getPageNumber() - 1; - Boolean showLogo = request.isShowLogo(); + // Convert 1-indexed page number (user input) to 0-indexed page number (API requirement) + Integer pageNumber = request.getPageNumber() != null ? (request.getPageNumber() - 1) : null; + Boolean showLogo = request.getShowLogo(); if (certType == null) { throw new IllegalArgumentException("Cert type must be provided"); @@ -279,7 +300,7 @@ public class CertSignController { widget.setAppearance(appearance); try (PDPageContentStream cs = new PDPageContentStream(doc, appearanceStream)) { - if (showLogo) { + if (Boolean.TRUE.equals(showLogo)) { cs.saveGraphicsState(); PDExtendedGraphicsState extState = new PDExtendedGraphicsState(); extState.setBlendMode(BlendMode.MULTIPLY); diff --git a/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java b/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java index d42429619..82a17ff2c 100644 --- a/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java +++ b/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java @@ -82,6 +82,8 @@ public class ApplicationProperties { private Metrics metrics = new Metrics(); private AutomaticallyGenerated automaticallyGenerated = new AutomaticallyGenerated(); + private Mail mail = new Mail(); + private Premium premium = new Premium(); private EnterpriseEdition enterpriseEdition = new EnterpriseEdition(); private AutoPipeline autoPipeline = new AutoPipeline(); @@ -420,6 +422,16 @@ public class ApplicationProperties { } } + @Data + public static class Mail { + private boolean enabled; + private String host; + private int port; + private String username; + @ToString.Exclude private String password; + private String from; + } + @Data public static class Premium { private boolean enabled; diff --git a/src/main/java/stirling/software/SPDF/model/api/Email.java b/src/main/java/stirling/software/SPDF/model/api/Email.java new file mode 100644 index 000000000..21b5152e5 --- /dev/null +++ b/src/main/java/stirling/software/SPDF/model/api/Email.java @@ -0,0 +1,39 @@ +package stirling.software.SPDF.model.api; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; + +import io.swagger.v3.oas.annotations.media.Schema; + + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +@ConditionalOnProperty(value = "mail.enabled", havingValue = "true", matchIfMissing = false) +public class Email extends GeneralFile { + + @Schema( + description = "The recipient's email address", + requiredMode = Schema.RequiredMode.REQUIRED, + format = "email") + private String to; + + @Schema( + description = "The subject of the email", + defaultValue = "Stirling Software PDF Notification", + requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String subject; + + @Schema( + description = "The body of the email", + requiredMode = Schema.RequiredMode.NOT_REQUIRED, + defaultValue = + "This message was automatically generated by Stirling-PDF, an innovative" + + " solution from Stirling Software. For more information, visit our website.

Please do" + + " not reply directly to this email.") + private String body; +} diff --git a/src/main/resources/settings.yml.template b/src/main/resources/settings.yml.template index 1c3ee32ae..201f875dd 100644 --- a/src/main/resources/settings.yml.template +++ b/src/main/resources/settings.yml.template @@ -76,6 +76,14 @@ premium: apiKey: '' appId: '' +mail: + enabled: true # set to 'true' to enable sending emails + host: smtp.example.com # SMTP server hostname + port: 587 # SMTP server port + username: '' # SMTP server username + password: '' # SMTP server password + from: '' # sender email address + legal: termsAndConditions: https://www.stirlingpdf.com/terms-and-conditions # URL to the terms and conditions of your application (e.g. https://example.com/terms). Empty string to disable or filename to load from local file in static folder privacyPolicy: https://www.stirlingpdf.com/privacy-policy # URL to the privacy policy of your application (e.g. https://example.com/privacy). Empty string to disable or filename to load from local file in static folder diff --git a/src/main/resources/static/3rdPartyLicenses.json b/src/main/resources/static/3rdPartyLicenses.json index c20671d97..da8718489 100644 --- a/src/main/resources/static/3rdPartyLicenses.json +++ b/src/main/resources/static/3rdPartyLicenses.json @@ -270,7 +270,7 @@ { "moduleName": "com.opencsv:opencsv", "moduleUrl": "http://opencsv.sf.net", - "moduleVersion": "5.10", + "moduleVersion": "5.11", "moduleLicense": "Apache 2", "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" }, @@ -623,21 +623,21 @@ { "moduleName": "io.swagger.core.v3:swagger-annotations-jakarta", "moduleUrl": "https://github.com/swagger-api/swagger-core/modules/swagger-annotations", - "moduleVersion": "2.2.29", + "moduleVersion": "2.2.30", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "io.swagger.core.v3:swagger-core-jakarta", "moduleUrl": "https://github.com/swagger-api/swagger-core/modules/swagger-core", - "moduleVersion": "2.2.29", + "moduleVersion": "2.2.30", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "io.swagger.core.v3:swagger-models-jakarta", "moduleUrl": "https://github.com/swagger-api/swagger-core/modules/swagger-models", - "moduleVersion": "2.2.29", + "moduleVersion": "2.2.30", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, @@ -1011,6 +1011,13 @@ "moduleLicense": "GNU General Public License, version 2 with the GNU Classpath Exception", "moduleLicenseUrl": "https://www.gnu.org/software/classpath/license.html" }, + { + "moduleName": "org.eclipse.angus:jakarta.mail", + "moduleUrl": "https://www.eclipse.org", + "moduleVersion": "2.0.3", + "moduleLicense": "GPL2 w/ CPE", + "moduleLicenseUrl": "https://www.gnu.org/software/classpath/license.html" + }, { "moduleName": "org.eclipse.jetty.ee10.websocket:jetty-ee10-websocket-jakarta-client", "moduleUrl": "https://jetty.org/", @@ -1235,6 +1242,12 @@ "moduleLicense": "GNU Library General Public License v2.1 or later", "moduleLicenseUrl": "https://www.opensource.org/licenses/LGPL-2.1" }, + { + "moduleName": "org.hibernate.validator:hibernate-validator", + "moduleVersion": "8.0.2.Final", + "moduleLicense": "Apache License 2.0", + "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" + }, { "moduleName": "org.jboss.logging:jboss-logging", "moduleUrl": "http://www.jboss.org", @@ -1423,19 +1436,19 @@ }, { "moduleName": "org.springdoc:springdoc-openapi-starter-common", - "moduleVersion": "2.8.6", + "moduleVersion": "2.8.8", "moduleLicense": "The Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt" }, { "moduleName": "org.springdoc:springdoc-openapi-starter-webmvc-api", - "moduleVersion": "2.8.6", + "moduleVersion": "2.8.8", "moduleLicense": "The Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt" }, { "moduleName": "org.springdoc:springdoc-openapi-starter-webmvc-ui", - "moduleVersion": "2.8.6", + "moduleVersion": "2.8.8", "moduleLicense": "The Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt" }, @@ -1523,6 +1536,13 @@ "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, + { + "moduleName": "org.springframework.boot:spring-boot-starter-mail", + "moduleUrl": "https://spring.io/projects/spring-boot", + "moduleVersion": "3.4.5", + "moduleLicense": "Apache License, Version 2.0", + "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" + }, { "moduleName": "org.springframework.boot:spring-boot-starter-oauth2-client", "moduleUrl": "https://spring.io/projects/spring-boot", @@ -1544,6 +1564,13 @@ "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, + { + "moduleName": "org.springframework.boot:spring-boot-starter-validation", + "moduleUrl": "https://spring.io/projects/spring-boot", + "moduleVersion": "3.4.5", + "moduleLicense": "Apache License, Version 2.0", + "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" + }, { "moduleName": "org.springframework.boot:spring-boot-starter-web", "moduleUrl": "https://spring.io/projects/spring-boot", @@ -1656,6 +1683,13 @@ "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, + { + "moduleName": "org.springframework:spring-context-support", + "moduleUrl": "https://github.com/spring-projects/spring-framework", + "moduleVersion": "6.2.6", + "moduleLicense": "Apache License, Version 2.0", + "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" + }, { "moduleName": "org.springframework:spring-core", "moduleUrl": "https://github.com/spring-projects/spring-framework", @@ -1746,7 +1780,7 @@ { "moduleName": "org.webjars:swagger-ui", "moduleUrl": "https://www.webjars.org", - "moduleVersion": "5.20.1", + "moduleVersion": "5.21.0", "moduleLicense": "Apache-2.0" }, { diff --git a/src/main/resources/static/css/general.css b/src/main/resources/static/css/general.css index b49bfb7ee..88e7a1b11 100644 --- a/src/main/resources/static/css/general.css +++ b/src/main/resources/static/css/general.css @@ -1,7 +1,8 @@ #page-container { - min-height: 100vh; + height: 100vh; display: flex; flex-direction: column; + overflow-x: clip; } #content-wrap { diff --git a/src/main/resources/static/css/navbar.css b/src/main/resources/static/css/navbar.css index f9b3f3072..31099cdf2 100644 --- a/src/main/resources/static/css/navbar.css +++ b/src/main/resources/static/css/navbar.css @@ -162,6 +162,7 @@ html[dir="rtl"] .lang-dropdown-item-wrapper { text-wrap: wrap; word-break: break-word; width: 80%; + font-size: large; } .close-icon { @@ -476,6 +477,9 @@ html[dir="rtl"] .dropdown-menu[data-bs-popper] { display: flex; gap: 30px; justify-content: center; + width: 140%; + position: relative; + left: -20%; } .feature-group { diff --git a/src/main/resources/static/js/multitool/PdfContainer.js b/src/main/resources/static/js/multitool/PdfContainer.js index 32c34ba46..90d130b2f 100644 --- a/src/main/resources/static/js/multitool/PdfContainer.js +++ b/src/main/resources/static/js/multitool/PdfContainer.js @@ -462,7 +462,7 @@ class PdfContainer { this.showButton(selectIcon, true); this.showButton(deselectIcon, false); - + checkboxes.forEach((checkbox) => { checkbox.checked = false; @@ -583,7 +583,7 @@ class PdfContainer { const selectIcon = document.getElementById('select-All-Container'); const deselectIcon = document.getElementById('deselect-All-Container'); - + if (window.selectPage) { // Check if selectPage mode is active console.log("Page Select on. Showing buttons"); //Check if no pages are selected @@ -907,7 +907,7 @@ class PdfContainer { if (!allSelected) { this.showButton(document.getElementById('select-All-Container'), true); } - + if (window.selectedPages.length > 0) { this.showButton(document.getElementById('deselect-All-Container'), true); } diff --git a/src/main/resources/static/js/tab-container.js b/src/main/resources/static/js/tab-container.js index b85334c3c..afc441934 100644 --- a/src/main/resources/static/js/tab-container.js +++ b/src/main/resources/static/js/tab-container.js @@ -9,7 +9,7 @@ TabContainer = { tabList.classList.add('tab-buttons'); tabTitles.forEach((title) => { const tabButton = document.createElement('button'); - tabButton.innerHTML = title; + tabButton.textContent = title; tabButton.onclick = (e) => { this.setActiveTab(e.target); }; diff --git a/src/main/resources/templates/fragments/navbar.html b/src/main/resources/templates/fragments/navbar.html index 593d8c213..ca4ced807 100644 --- a/src/main/resources/templates/fragments/navbar.html +++ b/src/main/resources/templates/fragments/navbar.html @@ -1,4 +1,4 @@ -
+
diff --git a/src/main/resources/templates/home.html b/src/main/resources/templates/home.html index 3842c1fe7..bba498db5 100644 --- a/src/main/resources/templates/home.html +++ b/src/main/resources/templates/home.html @@ -7,20 +7,18 @@ -
-
- - - - +
+ +

- + flex-direction: column;" + >

@@ -123,9 +121,10 @@
+ +
-
@@ -222,7 +221,21 @@ window.showSurvey = /*[[${showSurveyFromDocker}]]*/ true - + + diff --git a/src/test/java/stirling/software/SPDF/config/security/mail/EmailServiceTest.java b/src/test/java/stirling/software/SPDF/config/security/mail/EmailServiceTest.java new file mode 100644 index 000000000..63c38e9c3 --- /dev/null +++ b/src/test/java/stirling/software/SPDF/config/security/mail/EmailServiceTest.java @@ -0,0 +1,64 @@ +package stirling.software.SPDF.config.security.mail; + +import static org.mockito.Mockito.*; + +import jakarta.mail.MessagingException; +import jakarta.mail.internet.MimeMessage; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.web.multipart.MultipartFile; +import stirling.software.SPDF.model.ApplicationProperties; +import stirling.software.SPDF.model.api.Email; + +@ExtendWith(MockitoExtension.class) +public class EmailServiceTest { + + @Mock + private JavaMailSender mailSender; + + @Mock + private ApplicationProperties applicationProperties; + + @Mock + private ApplicationProperties.Mail mailProperties; + + @Mock + private MultipartFile fileInput; + + @InjectMocks + private EmailService emailService; + + @Test + void testSendEmailWithAttachment() throws MessagingException { + // Mock the values returned by ApplicationProperties + when(applicationProperties.getMail()).thenReturn(mailProperties); + when(mailProperties.getFrom()).thenReturn("no-reply@stirling-software.com"); + + // Create a mock Email object + Email email = new Email(); + email.setTo("test@example.com"); + email.setSubject("Test Email"); + email.setBody("This is a test email."); + email.setFileInput(fileInput); + + // Mock MultipartFile behavior + when(fileInput.getOriginalFilename()).thenReturn("testFile.txt"); + + // Mock MimeMessage + MimeMessage mimeMessage = mock(MimeMessage.class); + + // Configure mailSender to return the mocked MimeMessage + when(mailSender.createMimeMessage()).thenReturn(mimeMessage); + + // Call the service method + emailService.sendEmailWithAttachment(email); + + // Verify that the email was sent using mailSender + verify(mailSender).send(mimeMessage); + } +} diff --git a/src/test/java/stirling/software/SPDF/controller/api/EmailControllerTest.java b/src/test/java/stirling/software/SPDF/controller/api/EmailControllerTest.java new file mode 100644 index 000000000..25aa89479 --- /dev/null +++ b/src/test/java/stirling/software/SPDF/controller/api/EmailControllerTest.java @@ -0,0 +1,84 @@ +package stirling.software.SPDF.controller.api; + +import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import jakarta.mail.MessagingException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.multipart.MultipartFile; +import stirling.software.SPDF.config.security.mail.EmailService; +import stirling.software.SPDF.model.api.Email; + +@ExtendWith(MockitoExtension.class) +public class EmailControllerTest { + + private MockMvc mockMvc; + + @Mock private EmailService emailService; + + @InjectMocks private EmailController emailController; + + @Mock private MultipartFile fileInput; + + @BeforeEach + void setUp() { + // Set up the MockMvc instance for testing + mockMvc = MockMvcBuilders.standaloneSetup(emailController).build(); + } + + @Test + void testSendEmailWithAttachmentSuccess() throws Exception { + // Create a mock Email object + Email email = new Email(); + email.setTo("test@example.com"); + email.setSubject("Test Email"); + email.setBody("This is a test email."); + email.setFileInput(fileInput); + + // Mock the service to not throw any exception + doNothing().when(emailService).sendEmailWithAttachment(any(Email.class)); + + // Perform the request and verify the response + mockMvc.perform( + multipart("/api/v1/general/send-email") + .file("fileInput", "dummy-content".getBytes()) + .param("to", email.getTo()) + .param("subject", email.getSubject()) + .param("body", email.getBody())) + .andExpect(status().isOk()) + .andExpect(content().string("Email sent successfully")); + } + + @Test + void testSendEmailWithAttachmentFailure() throws Exception { + // Create a mock Email object + Email email = new Email(); + email.setTo("test@example.com"); + email.setSubject("Test Email"); + email.setBody("This is a test email."); + email.setFileInput(fileInput); + + // Mock the service to throw a MessagingException + doThrow(new MessagingException("Failed to send email")) + .when(emailService) + .sendEmailWithAttachment(any(Email.class)); + + // Perform the request and verify the response + mockMvc.perform( + multipart("/api/v1/general/send-email") + .file("fileInput", "dummy-content".getBytes()) + .param("to", email.getTo()) + .param("subject", email.getSubject()) + .param("body", email.getBody())) + .andExpect(status().isInternalServerError()) + .andExpect(content().string("Failed to send email: Failed to send email")); + } +}