Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
12 changes: 0 additions & 12 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -99,18 +99,6 @@
<version>8.9.0</version>
</dependency>

<dependency>
<groupId>com.bucket4j</groupId>
<artifactId>bucket4j-redis</artifactId>
<version>8.9.0</version>
</dependency>

<!-- Spring Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
19 changes: 19 additions & 0 deletions src/main/java/com/vimaltech/contactapi/config/AppProperties.java
Original file line number Diff line number Diff line change
@@ -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;
}
}
22 changes: 22 additions & 0 deletions src/main/java/com/vimaltech/contactapi/config/AsyncConfig.java
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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";
}
}
14 changes: 14 additions & 0 deletions src/main/java/com/vimaltech/contactapi/dto/EmailRequest.java
Original file line number Diff line number Diff line change
@@ -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;
}
68 changes: 64 additions & 4 deletions src/main/java/com/vimaltech/contactapi/service/ContactService.java
Original file line number Diff line number Diff line change
@@ -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<ContactInquiry> getAllContacts() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.vimaltech.contactapi.service;

import com.vimaltech.contactapi.dto.EmailRequest;

public interface EmailService {
void sendEmail(EmailRequest request);
}
Original file line number Diff line number Diff line change
@@ -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());
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
23 changes: 9 additions & 14 deletions src/main/resources/application-prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -61,7 +54,7 @@ management:
enabled: true # ✅ IMPORTANT
group:
readiness:
include: db,redis
include: db

health:
mail:
Expand All @@ -73,4 +66,6 @@ management:

app:
mail:
from: ${MAIL_FROM}
from: ${MAIL_FROM:?MAIL_FROM is required}
admin:
email: ${APP_ADMIN_EMAIL:?APP_ADMIN_EMAIL is required}
14 changes: 14 additions & 0 deletions src/main/resources/application-test.yml
Original file line number Diff line number Diff line change
@@ -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
Binary file added src/main/resources/static/favicon.ico
Binary file not shown.
Loading