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.)

설명

  1. application.yml 의 파일의 세팅은 그대로 두고, .env파일을 디렉토리에 추가하여 개인 정보를 담아 사용

  2. .gitignore에 application.yml을 제외시키고 .env 파일을 추가하였음

  3. .env 파일을 사용하기 위해서는 application.yml과 build.gradle의 설정이 필요한데 이메일 인증 API를 구현하면서 해당 설정을 추가해두었고 아래 사진을 참고하면 됨!

  4. 결론적으로 깃에 올라와있는 파일에 .env파일만 추가하면 됨!

  5. 이메일 인증을 위한 SMTP 설정 방법 : https://m.blog.naver.com/monsterkn/221333152250

  6. SMTP 설정 후 401Unauthorized가 뜬다면 개인 SMTP 관련 사용하는 메일의 2단계 인증(2 step-verification) 내용을 해제해보면 될 것 같음

.env 파일 (사진 참조 및 하단 양식 참조)

Untitled

EMAIL_ID= 실제_이메일@naver.com

EMAIL_PASSWORD= 실제비밀번호!

SENDER_EMAIL= 보내는이메일@naver.com (여기선 우리가 SMTP 설정을 하므로 우리 개인의 이메일이 해당 역할을 대체함)

DB_URL= 실제_DB_URL

DB_USERNAME= 실제_DB_이름

DB_PASSWORD= 실제_DB_비밀번호

application.yml (참조용)