Project : Java Email + Mail APIs (Email Verification Code)
#Foreword
When joining Team Poject, Team Convention often got me confused. I don't mean to say this is a problem. When implementing, especially new features, I need to focus on implementation first. After it, we can take a test if it runs well.
And then, If the program runs well, we can refactor the code that alines with project's structure. Like exception handling, Team convention, Response Type or anything like that...............................
In addition, when you need to implement new features that you've never tried, you need to focus on making it simple, modularizing it with each function. In doing so, you can try one at a time.
If you focus on clean code or team convention or anything complicated from the first, It's really hard to make progress.
(Below Source code is for my future reference, no explanation, but it's the things that I implemented.)
Implementation without advancement
Configuration
@Configuration
public class MailConfig {
@Bean
public JavaMailSender getJavaMailSender() {
JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
mailSender.setHost("smtp.naver.com");
mailSender.setPort(465);
mailSender.setUsername("yourActual@naver.com");
mailSender.setPassword("yourActualPassword");
Properties props = mailSender.getJavaMailProperties();
props.put("mail.transport.protocol", "smtp");
props.put("mail.smtp.auth", "true");
props.put("mail.smtp.starttls.enable", "true");
props.put("mail.smtp.ssl.enable", "true"); // Enable SSL
props.put("mail.debug", "true");
return mailSender;
}
}
Application.yml
spring:
mail:
sender-email: yourActual@naver.com
datasource:
url: jdbc:mysql://localhost:3306/yourDB
username: yourDB
password: yourDBpw
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
database-platform: org.hibernate.dialect.MySQLDialect
hibernate:
ddl-auto: create
show-sql : true
#.env file config
#config:
#import: optional:file:.env[.properties]
jwt:
secret: c2lsdmVybmluZS10ZWNoLXNwcmluZy1ib290LWp3dC10dXRvcmlhbC1zZWNyZXQtc2lsdmVybmluZS10ZWNoLXNwcmluZy1ib290LWp3dC10dXRvcmlhbC1zZWNyZXQK
token-validity-in-seconds: 86400
cookie:
domain: localhost
Controller
@RestController
@RequestMapping("/api/email")
public class EmailController {
@Autowired
private EmailService emailService;
@PostMapping("/send/code")
public String sendVerificationCode(@RequestBody SendEmailRequest request) {
emailService.sendVerificationCode(request.email());
return "Verification code sent to " + request.email();
}
@GetMapping("/verify/{code}")
public ResponseEntity<?> verifyEmail(@PathVariable String code, @RequestParam String email) {
boolean isVerified = emailService.verifyCode(email, code);
if (isVerified) {
return ResponseEntity.ok().body(Map.of("code", 200));
} else {
return ResponseEntity.status(400).body(Map.of("code", 400, "message", "Invalid verification code or email"));
}
}
}
Service
@Service
public class EmailService {
@Autowired
private JavaMailSender mailSender;
@Value("${spring.mail.sender-email}")
private String senderEmail;
private Map<String, String> verificationCodes = new HashMap<>();
public void sendVerificationCode(String toEmail) {
String verificationCode = generateVerificationCode();
SimpleMailMessage message = new SimpleMailMessage();
message.setTo(toEmail);
message.setSubject("Email Verification Code");
message.setText("Your verification code is: " + verificationCode);
message.setFrom(senderEmail);
mailSender.send(message);
// Normally, you would save the verification code to the database associated with the user
verificationCodes.put(toEmail, verificationCode);
}
public boolean verifyCode(String email, String code) {
String storedCode = verificationCodes.get(email);
return storedCode != null && storedCode.equals(code);
}
private String generateVerificationCode() {
int codeLength = 6;
Random random = new SecureRandom();
StringBuilder sb = new StringBuilder(codeLength);
for (int i = 0; i < codeLength; i++) {
sb.append(random.nextInt(10));
}
return sb.toString();
}
}
Repository - No
Build.gradle dependencies
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
// Security
implementation 'org.springframework.boot:spring-boot-starter-security'
// jwt
implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5'
// Add this !Email Verification
implementation 'org.springframework.boot:spring-boot-starter-mail'
}
Updated Code With Advancement (final)
Controller
import com.FC.SharedOfficePlatform.domain.member.dto.request.SendEmailRequest;
import com.FC.SharedOfficePlatform.domain.member.service.EmailService;
import com.FC.SharedOfficePlatform.global.util.ResponseDTO;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequiredArgsConstructor
@RequestMapping("/email")
public class EmailController {
private final EmailService emailService;
@PostMapping("/send/code")
public ResponseEntity<ResponseDTO<Void>> sendVerificationCode(
@RequestBody SendEmailRequest request) {
emailService.sendVerificationCode(request.email());
return ResponseEntity.ok(ResponseDTO.ok());
}
@GetMapping("/verify/{code}")
public ResponseEntity<ResponseDTO<Void>> verifyEmail(@PathVariable String code, @RequestParam String email) {
emailService.verifyCode(email, code);
return ResponseEntity.ok(ResponseDTO.ok());
}
}
Service
import com.FC.SharedOfficePlatform.domain.member.entity.VerificationCode;
import com.FC.SharedOfficePlatform.domain.member.repository.VerificationCodeRepository;
import com.FC.SharedOfficePlatform.domain.member.exception.EmailSendingException;
import com.FC.SharedOfficePlatform.domain.member.exception.InvalidVerificationCodeException;
import java.security.SecureRandom;
import java.util.Optional;
import java.util.Random;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.mail.MailException;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
@Transactional
public class EmailService {
private final JavaMailSender javaMailSender;
private final VerificationCodeRepository verificationCodeRepository;
@Value("${SENDER_EMAIL}")
private String senderEmail;
public void sendVerificationCode(String toEmail) {
String verificationCode = generateVerificationCode();
SimpleMailMessage message = new SimpleMailMessage();
message.setTo(toEmail);
message.setSubject("Email Verification Code");
message.setText("Your verification code is!: " + verificationCode);
message.setFrom(senderEmail);
try {
javaMailSender.send(message);
VerificationCode code = new VerificationCode(toEmail, verificationCode);
verificationCodeRepository.save(code);
} catch (MailException e) {
throw new EmailSendingException();
}
}
public boolean verifyCode(String email, String code) {
Optional<VerificationCode> storedCode = verificationCodeRepository.findByEmailAndCode(email, code);
if (storedCode.isPresent()) {
verificationCodeRepository.deleteByEmail(email); // Remove the code after successful verification
return true;
}
throw new InvalidVerificationCodeException();
}
private String generateVerificationCode() {
int codeLength = 6;
Random random = new SecureRandom();
StringBuilder sb = new StringBuilder(codeLength);
for (int i = 0; i < codeLength; i++) {
sb.append(random.nextInt(10));
}
return sb.toString();
}
}
Entity ( New Feature -> to save Verification Code, when it's verified it is removed )
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.NoArgsConstructor;
@Entity
@Table(name = "verification_codes")
@NoArgsConstructor
public class VerificationCode {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String email;
private String code;
public VerificationCode(String email, String code) {
this.email = email;
this.code = code;
}
}
Repository
import com.FC.SharedOfficePlatform.domain.member.entity.VerificationCode;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface VerificationCodeRepository extends JpaRepository<VerificationCode, Long> {
Optional<VerificationCode> findByEmailAndCode(String email, String code);
void deleteByEmail(String email);
}
+For .env File with application.yml
Configuration
import java.util.Properties;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.JavaMailSenderImpl;
@Configuration
public class JavaMailConfig {
@Value("${EMAIL_ID}")
private String emailId;
@Value("${EMAIL_PASSWORD}")
private String emailPassword;
@Bean
public JavaMailSender getJavaMailSender() {
JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
mailSender.setHost("smtp.naver.com");
mailSender.setPort(465);
mailSender.setUsername(emailId);
mailSender.setPassword(emailPassword);
Properties props = mailSender.getJavaMailProperties();
props.put("mail.transport.protocol", "smtp");
props.put("mail.smtp.auth", "true");
props.put("mail.smtp.starttls.enable", "true");
props.put("mail.smtp.ssl.enable", "true"); // Enable SSL
props.put("mail.debug", "true");
return mailSender;
}
}
Build.gradle dependencies
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
// Security
implementation 'org.springframework.boot:spring-boot-starter-security'
// jwt
implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5'
// Send Email Verification Code
implementation 'org.springframework.boot:spring-boot-starter-mail'
----------------------
---------------------- // .env added
// .env
implementation 'io.github.cdimascio:java-dotenv:+'
}
application.yml
spring:
mail:
username: ${EMAIL_ID}
password: ${EMAIL_PASSWORD}
sender-email: ${SENDER_EMAIL}
#.env 파일 인식 설정 추가
config:
import: optional:file:.env[.properties]
datasource:
url: ${DB_URL}
username: ${DB_USERNAME}
password: ${DB_PASSWORD}
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
database-platform: org.hibernate.dialect.MySQLDialect
hibernate:
ddl-auto: create
show-sql: true
jwt:
secret: c2lsdmVybmluZS10ZWNoLXNwcmluZy1ib290LWp3dC10dXRvcmlhbC1zZWNyZXQtc2lsdmVybmluZS10ZWNoLXNwcmluZy1ib290LWp3dC10dXRvcmlhbC1zZWNyZXQK
token-validity-in-seconds: 86400
cookie:
domain: localhost
.env File
EMAIL_ID=yourRealEmail@naver.com
EMAIL_PASSWORD=YourRealPassword!@
SENDER_EMAIL=yourRealEmail@naver.com // In this case, the email set SMTP(Mine)
DB_URL=jdbc:mysql://localhost:3306/YourDB
DB_USERNAME=YourDBiD
DB_PASSWORD=YourDBPassword
이메일 인증 및 application.yml - .env 파일 사용법 (document that I used for Team Project.)
설명
application.yml 의 파일의 세팅은 그대로 두고, .env파일을 디렉토리에 추가하여 개인 정보를 담아 사용
.gitignore에 application.yml을 제외시키고 .env 파일을 추가하였음
.env 파일을 사용하기 위해서는 application.yml과 build.gradle의 설정이 필요한데 이메일 인증 API를 구현하면서 해당 설정을 추가해두었고 아래 사진을 참고하면 됨!
결론적으로 깃에 올라와있는 파일에 .env파일만 추가하면 됨!
이메일 인증을 위한 SMTP 설정 방법 : https://m.blog.naver.com/monsterkn/221333152250
SMTP 설정 후 401Unauthorized가 뜬다면 개인 SMTP 관련 사용하는 메일의 2단계 인증(2 step-verification) 내용을 해제해보면 될 것 같음
.env 파일 (사진 참조 및 하단 양식 참조)
EMAIL_ID=
실제_이메일@naver.com
EMAIL_PASSWORD=
실제비밀번호!
SENDER_EMAIL=
보내는이메일@naver.com (여기선 우리가 SMTP 설정을 하므로 우리 개인의 이메일이 해당 역할을 대체함)
DB_URL=
실제_DB_URL
DB_USERNAME=
실제_DB_이름
DB_PASSWORD=
실제_DB_비밀번호