From e96288f3839378d50b2251ef5662d98c652a3715 Mon Sep 17 00:00:00 2001 From: vimal-tech-starter Date: Sun, 12 Apr 2026 23:53:08 +0530 Subject: [PATCH 1/3] feat: implement async email sending with thread pool and SMTP timeout optimization chore: remove unused Redis configuration and dependencies Signed-off-by: vimal-tech-starter --- Dockerfile | 2 +- pom.xml | 12 ---- .../contactapi/ContactApiApplication.java | 2 + .../contactapi/config/AppProperties.java | 19 +++++ .../contactapi/config/AsyncConfig.java | 22 ++++++ .../controller/EmailTestController.java | 35 +++++++++ .../contactapi/dto/EmailRequest.java | 14 ++++ .../contactapi/service/ContactService.java | 68 ++++++++++++++++-- .../contactapi/service/EmailService.java | 7 ++ .../service/impl/SmtpEmailService.java | 55 ++++++++++++++ src/main/resources/application-prod.yml | 23 +++--- src/main/resources/static/favicon.ico | Bin 0 -> 15406 bytes 12 files changed, 228 insertions(+), 31 deletions(-) create mode 100644 src/main/java/com/vimaltech/contactapi/config/AppProperties.java create mode 100644 src/main/java/com/vimaltech/contactapi/config/AsyncConfig.java create mode 100644 src/main/java/com/vimaltech/contactapi/controller/EmailTestController.java create mode 100644 src/main/java/com/vimaltech/contactapi/dto/EmailRequest.java create mode 100644 src/main/java/com/vimaltech/contactapi/service/EmailService.java create mode 100644 src/main/java/com/vimaltech/contactapi/service/impl/SmtpEmailService.java create mode 100644 src/main/resources/static/favicon.ico diff --git a/Dockerfile b/Dockerfile index 77235ae..a550482 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,7 +10,7 @@ COPY src ./src RUN mvn clean package -DskipTests # ---------- Stage 2: Runtime ---------- -FROM eclipse-temurin:21-jre-alpine +FROM eclipse-temurin:21-jre WORKDIR /app diff --git a/pom.xml b/pom.xml index 3bc10eb..49c424c 100644 --- a/pom.xml +++ b/pom.xml @@ -99,18 +99,6 @@ 8.9.0 - - com.bucket4j - bucket4j-redis - 8.9.0 - - - - - org.springframework.boot - spring-boot-starter-data-redis - - org.springframework.boot spring-boot-starter-actuator diff --git a/src/main/java/com/vimaltech/contactapi/ContactApiApplication.java b/src/main/java/com/vimaltech/contactapi/ContactApiApplication.java index 5c5bfb8..ffcdf8b 100644 --- a/src/main/java/com/vimaltech/contactapi/ContactApiApplication.java +++ b/src/main/java/com/vimaltech/contactapi/ContactApiApplication.java @@ -2,8 +2,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableAsync; @SpringBootApplication +@EnableAsync public class ContactApiApplication { public static void main(String[] args) { diff --git a/src/main/java/com/vimaltech/contactapi/config/AppProperties.java b/src/main/java/com/vimaltech/contactapi/config/AppProperties.java new file mode 100644 index 0000000..1681b69 --- /dev/null +++ b/src/main/java/com/vimaltech/contactapi/config/AppProperties.java @@ -0,0 +1,19 @@ +package com.vimaltech.contactapi.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@ConfigurationProperties(prefix = "app.admin") +@Component +public class AppProperties { + + private String email; + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } +} \ No newline at end of file diff --git a/src/main/java/com/vimaltech/contactapi/config/AsyncConfig.java b/src/main/java/com/vimaltech/contactapi/config/AsyncConfig.java new file mode 100644 index 0000000..eb21bb0 --- /dev/null +++ b/src/main/java/com/vimaltech/contactapi/config/AsyncConfig.java @@ -0,0 +1,22 @@ +package com.vimaltech.contactapi.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +import java.util.concurrent.Executor; + +@Configuration +public class AsyncConfig { + + @Bean(name = "emailExecutor") + public Executor emailExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(3); // baseline threads (good for VPS) + executor.setMaxPoolSize(8); // peak load + executor.setQueueCapacity(100); // queue size + executor.setThreadNamePrefix("EmailThread-"); + executor.initialize(); + return executor; + } +} \ No newline at end of file diff --git a/src/main/java/com/vimaltech/contactapi/controller/EmailTestController.java b/src/main/java/com/vimaltech/contactapi/controller/EmailTestController.java new file mode 100644 index 0000000..1838077 --- /dev/null +++ b/src/main/java/com/vimaltech/contactapi/controller/EmailTestController.java @@ -0,0 +1,35 @@ +package com.vimaltech.contactapi.controller; + +import com.vimaltech.contactapi.config.AppProperties; +import com.vimaltech.contactapi.dto.EmailRequest; +import com.vimaltech.contactapi.service.EmailService; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Profile; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +//@Profile({"dev"}) +//@Profile({"dev", "local"}) +@RestController +@RequestMapping("/test") +@RequiredArgsConstructor +public class EmailTestController { + + private final EmailService emailService; + private final AppProperties appProperties; + + @GetMapping("/email") + public String testEmail() { + + emailService.sendEmail( + EmailRequest.builder() + .to(appProperties.getEmail()) // 👈 CHANGE THIS + .subject("Test Email from VimalTech API") + .body("Email service is working successfully 🚀") + .build() + ); + + return "Email sent successfully"; + } +} \ No newline at end of file diff --git a/src/main/java/com/vimaltech/contactapi/dto/EmailRequest.java b/src/main/java/com/vimaltech/contactapi/dto/EmailRequest.java new file mode 100644 index 0000000..98f174c --- /dev/null +++ b/src/main/java/com/vimaltech/contactapi/dto/EmailRequest.java @@ -0,0 +1,14 @@ +package com.vimaltech.contactapi.dto; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class EmailRequest { + private final String to; + private final String subject; + private final String body; + + private final String replyTo; +} \ No newline at end of file diff --git a/src/main/java/com/vimaltech/contactapi/service/ContactService.java b/src/main/java/com/vimaltech/contactapi/service/ContactService.java index 39bfd97..9c7f000 100644 --- a/src/main/java/com/vimaltech/contactapi/service/ContactService.java +++ b/src/main/java/com/vimaltech/contactapi/service/ContactService.java @@ -1,31 +1,91 @@ package com.vimaltech.contactapi.service; +import com.vimaltech.contactapi.config.AppProperties; import com.vimaltech.contactapi.dto.ContactRequest; +import com.vimaltech.contactapi.dto.EmailRequest; import com.vimaltech.contactapi.entity.ContactInquiry; import com.vimaltech.contactapi.repository.ContactRepository; -import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; -import java.time.LocalDateTime; import java.util.List; @Service -@RequiredArgsConstructor +@Slf4j public class ContactService { private final ContactRepository contactRepository; + private final EmailService emailService; + private final AppProperties appProperties; + + public ContactService(ContactRepository repo, EmailService emailService, AppProperties appProperties) { + this.contactRepository = repo; + this.emailService = emailService; + this.appProperties = appProperties; + } public void processContact(ContactRequest request) { + // ✅ 1. Save to DB (unchanged behavior) ContactInquiry inquiry = ContactInquiry.builder() .name(request.name()) .email(request.email()) .subject(request.subject()) .message(request.message()) - .createdAt(LocalDateTime.now()) .build(); contactRepository.save(inquiry); + + log.info("Contact saved | email={}", request.email()); + + // ✅ 2. Send emails {(non-blocking for business logic), try-catch removed} + // ✅ Fire async emails (NO try-catch) + sendEmails(request); + } + + // ✅ NEW METHOD (clean separation) + private void sendEmails(ContactRequest request) { + + // 📩 Admin Email + EmailRequest adminEmail = EmailRequest.builder() + .to(appProperties.getEmail()) + .subject("New Contact Inquiry: " + + (request.subject() != null ? request.subject() : "No Subject")) + .body(""" + New contact inquiry received: + + Name: %s + Email: %s + Subject: %s + + Message: + %s + """.formatted( + request.name(), + request.email(), + request.subject(), + request.message() + )) + .replyTo(request.email()) + .build(); + + emailService.sendEmail(adminEmail); + + // 📩 User Confirmation Email + EmailRequest userEmail = EmailRequest.builder() + .to(request.email()) + .subject("Thanks for contacting VimalTech") + .body(""" + Hi %s, + + Thank you for reaching out. We have received your message and will respond shortly. + + Best regards, + VimalTech Team + """.formatted(request.name())) + .build(); + + emailService.sendEmail(userEmail); } public List getAllContacts() { diff --git a/src/main/java/com/vimaltech/contactapi/service/EmailService.java b/src/main/java/com/vimaltech/contactapi/service/EmailService.java new file mode 100644 index 0000000..39d7fa3 --- /dev/null +++ b/src/main/java/com/vimaltech/contactapi/service/EmailService.java @@ -0,0 +1,7 @@ +package com.vimaltech.contactapi.service; + +import com.vimaltech.contactapi.dto.EmailRequest; + +public interface EmailService { + void sendEmail(EmailRequest request); +} \ No newline at end of file diff --git a/src/main/java/com/vimaltech/contactapi/service/impl/SmtpEmailService.java b/src/main/java/com/vimaltech/contactapi/service/impl/SmtpEmailService.java new file mode 100644 index 0000000..d9c0906 --- /dev/null +++ b/src/main/java/com/vimaltech/contactapi/service/impl/SmtpEmailService.java @@ -0,0 +1,55 @@ +package com.vimaltech.contactapi.service.impl; + +import com.vimaltech.contactapi.dto.EmailRequest; +import com.vimaltech.contactapi.service.EmailService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.mail.SimpleMailMessage; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; + +@Service +@Slf4j +public class SmtpEmailService implements EmailService { + + private final JavaMailSender mailSender; + private final String from; + + public SmtpEmailService( + JavaMailSender mailSender, + @Value("${app.mail.from}") String from + ) { + this.mailSender = mailSender; + this.from = from; + } + + @Override + @Async("emailExecutor") + public void sendEmail(EmailRequest request) { + try { + log.info("START: Sending email | to={} | thread={}", + request.getTo(), Thread.currentThread().getName()); + + SimpleMailMessage message = new SimpleMailMessage(); + + message.setFrom(from); // 🔥 CRITICAL FIX + message.setTo(request.getTo()); + message.setSubject(request.getSubject()); + message.setText(request.getBody()); + + // ✅ OPTIONAL but IMPORTANT + if (request.getReplyTo() != null && !request.getReplyTo().isBlank()) { + message.setReplyTo(request.getReplyTo().trim()); + } + + mailSender.send(message); + + log.info("SUCCESS: Email sent | to={}", request.getTo()); + + } catch (Exception e) { + // ❗ DO NOT THROW in async + log.error("ERROR: Email failed | to={}", request.getTo(), e); + } + } +} \ No newline at end of file diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 092c430..c725fe8 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -11,18 +11,11 @@ spring: username: ${MAIL_USERNAME} password: ${MAIL_PASSWORD} properties: - mail: - smtp: - auth: true - starttls: - enable: true - - data: - redis: - host: vimaltech-redis - port: 6379 - password: ${SPRING_DATA_REDIS_PASSWORD} - timeout: 2000 + mail.smtp.auth: true + mail.smtp.starttls.enable: true + mail.smtp.connectiontimeout: 5000 + mail.smtp.timeout: 5000 + mail.smtp.writetimeout: 5000 jpa: hibernate: @@ -61,7 +54,7 @@ management: enabled: true # ✅ IMPORTANT group: readiness: - include: db,redis + include: db health: mail: @@ -73,4 +66,6 @@ management: app: mail: - from: ${MAIL_FROM} \ No newline at end of file + from: ${MAIL_FROM:?MAIL_FROM is required} + admin: + email: ${APP_ADMIN_EMAIL:?APP_ADMIN_EMAIL is required} \ No newline at end of file diff --git a/src/main/resources/static/favicon.ico b/src/main/resources/static/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..c1e1f593f41e9b6552906fbe8701460ffd6cc92a GIT binary patch literal 15406 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 Date: Mon, 13 Apr 2026 01:40:06 +0530 Subject: [PATCH 2/3] chore: rerun CI Signed-off-by: vimal-tech-starter From 3102d6d210bcc7bc0b4ecf3fcb195b3a012f6eb1 Mon Sep 17 00:00:00 2001 From: vimal-tech-starter Date: Mon, 13 Apr 2026 02:27:51 +0530 Subject: [PATCH 3/3] fix: add fallback email service for CI Signed-off-by: vimal-tech-starter --- .../service/impl/NoOpEmailService.java | 18 ++++++++++++++++++ .../service/impl/SmtpEmailService.java | 2 ++ src/main/resources/application-test.yml | 14 ++++++++++++++ 3 files changed, 34 insertions(+) create mode 100644 src/main/java/com/vimaltech/contactapi/service/impl/NoOpEmailService.java create mode 100644 src/main/resources/application-test.yml diff --git a/src/main/java/com/vimaltech/contactapi/service/impl/NoOpEmailService.java b/src/main/java/com/vimaltech/contactapi/service/impl/NoOpEmailService.java new file mode 100644 index 0000000..04b1547 --- /dev/null +++ b/src/main/java/com/vimaltech/contactapi/service/impl/NoOpEmailService.java @@ -0,0 +1,18 @@ +package com.vimaltech.contactapi.service.impl; + +import com.vimaltech.contactapi.dto.EmailRequest; +import com.vimaltech.contactapi.service.EmailService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Service; + +@Service +@Primary +@Slf4j +public class NoOpEmailService implements EmailService { + + @Override + public void sendEmail(EmailRequest request) { + log.warn("Email disabled (NoOp) | to={}", request.getTo()); + } +} \ No newline at end of file diff --git a/src/main/java/com/vimaltech/contactapi/service/impl/SmtpEmailService.java b/src/main/java/com/vimaltech/contactapi/service/impl/SmtpEmailService.java index d9c0906..0d3f1bb 100644 --- a/src/main/java/com/vimaltech/contactapi/service/impl/SmtpEmailService.java +++ b/src/main/java/com/vimaltech/contactapi/service/impl/SmtpEmailService.java @@ -4,6 +4,7 @@ import com.vimaltech.contactapi.service.EmailService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.mail.SimpleMailMessage; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.scheduling.annotation.Async; @@ -11,6 +12,7 @@ @Service @Slf4j +@ConditionalOnProperty(name = "spring.mail.host") public class SmtpEmailService implements EmailService { private final JavaMailSender mailSender; diff --git a/src/main/resources/application-test.yml b/src/main/resources/application-test.yml new file mode 100644 index 0000000..3353ec8 --- /dev/null +++ b/src/main/resources/application-test.yml @@ -0,0 +1,14 @@ +# application-test.yml +spring: + mail: + host: localhost + port: 1025 + username: test + password: test + +app: + mail: + from: test@test.com + enabled: false + admin: + email: test@test.com \ No newline at end of file