diff --git a/server/application-server/compose.yaml b/server/application-server/compose.yaml index 0461fbc9..9b79c3ba 100644 --- a/server/application-server/compose.yaml +++ b/server/application-server/compose.yaml @@ -32,6 +32,16 @@ services: networks: - app-network + postfix: + image: ghcr.io/ls1admin/postfix:latest + container_name: hephaestus-postfix + restart: unless-stopped + volumes: + - ./postfix-config:/config # See https://github.com/ls1admin/postfix-container-tum-mailrelay/tree/main for details + hostname: hephaestus.aet.cit.tum.de + networks: + - app-network + networks: app-network: driver: bridge diff --git a/server/application-server/pom.xml b/server/application-server/pom.xml index cc1c5e84..12bc9622 100644 --- a/server/application-server/pom.xml +++ b/server/application-server/pom.xml @@ -92,6 +92,14 @@ runtime true + + org.springframework.boot + spring-boot-starter-mail + + + org.springframework.boot + spring-boot-starter-thymeleaf + org.postgresql postgresql diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/user/User.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/user/User.java index 679f430c..e0e9f747 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/user/User.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/user/User.java @@ -99,6 +99,9 @@ public class User extends BaseGitServiceEntity { @ToString.Exclude private Set reviewComments = new HashSet<>(); + @NonNull + private boolean notificationsEnabled = false; + // Current ranking points for the leaderboard leagues private int leaguePoints; diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/notification/MailBuilder.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/notification/MailBuilder.java new file mode 100644 index 00000000..9cf99025 --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/notification/MailBuilder.java @@ -0,0 +1,121 @@ +package de.tum.in.www1.hephaestus.notification; + +import de.tum.in.www1.hephaestus.gitprovider.user.User; +import de.tum.in.www1.hephaestus.gitprovider.user.UserInfoDTO; +import jakarta.activation.DataHandler; +import jakarta.activation.FileDataSource; +import jakarta.mail.*; +import jakarta.mail.internet.InternetAddress; +import jakarta.mail.internet.MimeBodyPart; +import jakarta.mail.internet.MimeMessage; +import jakarta.mail.internet.MimeMultipart; +import jakarta.mail.util.ByteArrayDataSource; +import lombok.Getter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.mail.javamail.JavaMailSender; +import org.thymeleaf.context.Context; + +import java.util.*; + +public class MailBuilder { + private static final Logger log = LoggerFactory.getLogger(MailBuilder.class); + private final MailConfig config; + + private final List primarySenders; + private final List primaryRecipients; + + @Getter + private final String subject; + + @Getter + private final String template; + + @Getter + private final Map variables; + + @Getter + private final Set notificationNames; + + public MailBuilder(MailConfig config, String subject, String template) { + this.config = config; + + this.primarySenders = new ArrayList<>(); + this.primaryRecipients = new ArrayList<>(); + + this.subject = subject; + this.template = template; + + this.variables = new HashMap<>(); + this.variables.put("config", config.getConfigDto()); + + this.notificationNames = new HashSet<>(); + } + + public MailBuilder addNotificationName(String name) { + notificationNames.add(name); + + return this; + } + + public MailBuilder addPrimarySender(User user) { + this.primarySenders.add(user); + + return this; + } + + public MailBuilder addPrimaryRecipient(User user) { + if (primaryRecipients.contains(user)) { + return this; + } + + primaryRecipients.add(user); + + return this; + } + + + public void send(JavaMailSender mailSender) { + List toRecipients = new ArrayList<>(); + + for (User recipient : primaryRecipients) { + if (!recipient.isNotificationsEnabled()) { + continue; + } + toRecipients.add(recipient); + } + + for (User recipient : toRecipients) { + try { + MimeMessage message = mailSender.createMimeMessage(); + + message.setFrom("ThesisManagement <" + config.getSender().getAddress() + ">"); + message.setSender(config.getSender()); + + message.addRecipient(Message.RecipientType.TO, new InternetAddress(recipient.getEmail())); + + Context templateContext = new Context(); + templateContext.setVariables(this.variables); + templateContext.setVariable("recipient", UserInfoDTO.fromUser(recipient)); + + message.setSubject(subject); + + Multipart messageContent = new MimeMultipart(); + + BodyPart messageBody = new MimeBodyPart(); + messageBody.setContent(config.getTemplateEngine().process(template, templateContext), "text/html; charset=utf-8"); + messageContent.addBodyPart(messageBody); + + message.setContent(messageContent); + + if (config.isEnabled()) { + mailSender.send(message); + } else { + log.info("Sending Mail (postfix disabled)\n{}", messageBody.getContent()); + } + } catch (Exception exception) { + log.warn("Failed to send email", exception); + } + } + } +} diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/notification/MailConfig.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/notification/MailConfig.java new file mode 100644 index 00000000..2ef03c85 --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/notification/MailConfig.java @@ -0,0 +1,60 @@ +package de.tum.in.www1.hephaestus.notification; + +import jakarta.mail.internet.AddressException; +import jakarta.mail.internet.InternetAddress; +import lombok.Getter; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.thymeleaf.TemplateEngine; + +import java.util.*; + +@Component +public class MailConfig { + + private final Boolean enabled; + + @Getter + private final String clientHost; + + @Getter + private final InternetAddress sender; + + @Getter + private final String signature; + + @Getter + private final TemplateEngine templateEngine; + + @Autowired + public MailConfig( + @Value("${hephaestus.mail.sender}") InternetAddress sender, + @Value("${hephaestus.mail.enabled}") boolean enabled, + @Value("${hephaestus.mail.signature}") String mailSignature, + @Value("${hephaestus.client.host}") String clientHost, + TemplateEngine templateEngine + ) { + this.enabled = enabled; + this.sender = sender; + this.signature = mailSignature; + this.clientHost = clientHost; + this.templateEngine = templateEngine; + } + + public boolean isEnabled() { + return enabled; + } + + public record MailConfigDto( + String signature, + String clientHost + ) {} + + public MailConfigDto getConfigDto() { + return new MailConfigDto( + Objects.requireNonNullElse(signature, ""), + Objects.requireNonNullElse(getClientHost(), "") + ); + } +} diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/notification/MailService.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/notification/MailService.java new file mode 100644 index 00000000..a3ac0df1 --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/notification/MailService.java @@ -0,0 +1,29 @@ +package de.tum.in.www1.hephaestus.notification; + +import de.tum.in.www1.hephaestus.gitprovider.user.User; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class MailService { + + private final JavaMailSender javaMailSender; + + private final MailConfig mailConfig; + + @Autowired + public MailService(JavaMailSender javaMailSender, MailConfig mailConfig) { + this.javaMailSender = javaMailSender; + this.mailConfig = mailConfig; + } + + public void sendBadPracticesDetectedEmail(User user, List badPractices) { + MailBuilder mailBuilder = new MailBuilder(mailConfig, "Bad Practices Detected", "bad-practices-detected"); + mailBuilder + .addPrimaryRecipient(user) + .send(javaMailSender); + } +} diff --git a/server/application-server/src/main/resources/application.yml b/server/application-server/src/main/resources/application.yml index 1968ef06..d2762ea8 100644 --- a/server/application-server/src/main/resources/application.yml +++ b/server/application-server/src/main/resources/application.yml @@ -29,6 +29,23 @@ spring: jwt: issuer-uri: http://localhost:8081/realms/hephaestus + thymeleaf: + prefix: ${MAIL_TEMPLATE_FOLDER:classpath:/mail-templates/} + suffix: .html + + mail: + host: ${POSTFIX_HOST:localhost} + port: ${POSTFIX_PORT:25} + username: ${POSTFIX_USERNAME:} + password: ${POSTFIX_PASSWORD:} + properties: + mail: + transport: + protocol: smtp + smtp: + starttls: + enable: true + springdoc: default-produces-media-type: application/json @@ -55,6 +72,10 @@ hephaestus: contributors: # Eviction rate in milliseconds (1 hour) evict-rate: 3600000 + mail: + enabled: ${MAIL_ENABLED:true} + sender: ${MAIL_SENDER:test@hephaestus.ase.cit.tum.de} + signature: ${MAIL_SIGNATURE:} keycloak: url: http://localhost:8081 diff --git a/server/application-server/src/main/resources/mail-templates/bad-practices-detected.html b/server/application-server/src/main/resources/mail-templates/bad-practices-detected.html new file mode 100644 index 00000000..d8335126 --- /dev/null +++ b/server/application-server/src/main/resources/mail-templates/bad-practices-detected.html @@ -0,0 +1,3 @@ +

+ Bad practice detected. +

\ No newline at end of file