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