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/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
new file mode 100644
index 0000000..0d3f1bb
--- /dev/null
+++ b/src/main/java/com/vimaltech/contactapi/service/impl/SmtpEmailService.java
@@ -0,0 +1,57 @@
+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.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.mail.SimpleMailMessage;
+import org.springframework.mail.javamail.JavaMailSender;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Service;
+
+@Service
+@Slf4j
+@ConditionalOnProperty(name = "spring.mail.host")
+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/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
diff --git a/src/main/resources/static/favicon.ico b/src/main/resources/static/favicon.ico
new file mode 100644
index 0000000..c1e1f59
Binary files /dev/null and b/src/main/resources/static/favicon.ico differ