Java & Spring

[Spring Mail] 존재하지 않는 메일은 어떻게 확인하지?

ju_young 2024. 3. 19. 14:37
728x90

이메일의 형식은 spring validation 등을 사용하여 확인할 수 있다. 다음과 같이 말이다.

@NotBlank
@Email
String email

그렇다면 만약 존재하지 않는 메일일 경우 전송에 실패하게 될 것인데 서버에서는 어떻게 이 사실을 알아 낼 수 있을까?

메일 전송

먼저 메일 전송을 수행해보기위해 spring mail을 사용해볼 것이다.

Dependency

implementation 'org.springframework.boot:spring-boot-starter-mail'

spring mail는 jakarta.mail에 의존한다.

Properties

application.yml 에도 다음과 같이 속성 값을 설정해준다.

spring:
  mail:  
    host: smtp.gmail.com  
    port: 587  
    username: ${MAIL_USERNAME}  
    password: ${MAIL_PASSWORD}  
    properties:  
      mail:  
        smtp:  
          auth: true # 사용자 인증 시도 여부  
          timeout: 5000  
          starttls.enable: true # TLS 활성화 여부
  • 메일 전송을 위해 SMTP 프로토콜이 사용되고 포트는 587이 사용된다.
  • username은 발신자 이메일을 의미한다.
  • password는 gmail에서 추가한 앱 비밀번호이다.

Gmail 앱 비밀번호 추가

Gmail을 사용하여 메일을 전송하려면 일단 앱 비밀번호를 추가해야한다.

 

위와 같은 경로로 들어가고 마지막에 앱 이름을 입력하기만 하면 비밀번호가 자동으로 생성된다. 그리고 이 생성된 비밀번호를 Properties에 사용하면된다.

Service

이제 설정을 다했으니 메일 전송을 위한 코드를 작성해보자.

@Service  
@RequiredArgsConstructor  
public class EmailService {
    private final JavaMailSender javaMailSender;

    public void sendEmail(String toEmail, String subject, String text) {  
        MimeMessage mimeMessage = javaMailSender.createMimeMessage();  
        try {  
            MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMessage, false, "UTF-8");  
            mimeMessageHelper.setTo(toEmail);  
            mimeMessageHelper.setSubject(subject);  
            mimeMessageHelper.setText(text);  
            javaMailSender.send(mimeMessage);  
        } catch (Exception e) {  
            throw new RuntimeException(e);  
        }  
    }
}
  • toEmail: 수신자 이메일
  • subject: 메일 제목
  • text: 본문

발신자, 프로토콜, 포트 등은 이미 properties에 설정해주었기때문에 자동으로 지정된다.

 

이제 메일 전송이 가능하게 되었다.

반송되는 메일

어...?

 

test@gmil.com 처럼 이메일 형식은 맞지만 존재하지 않는 메일 일 경우 Exception, 로그 등 서버에서는 아무것도 나타나지 않았다. 몇 초 후 위처럼 Mail Delivery System으로부터 반송 메일이 오는 것이다.

 

먼저 spring mail이 하는 역할이 무엇인지 알아야한다.


spring mail은 jakarta.mail을 사용하는데 이 라이브러리는 메일 서버에 전송해달라는 요청만해 줄 뿐 실제로 메일을 전송하지는 않는다. 즉, 메일 서버가 대신 메일을 보내주기 때문에 서버 입장에서는 잘 보내졌는지 확인할 수가 없는 것이다.

 

그래서 메일이 잘 보내졌는지 확인하기위해 코드를 작성해보고자 한다.

Gmail Settings

메일보관함(INBOX)를 확인하기 위해서는 POP3 또는 IMAP 프로토콜을 사용할 수 있다.

 

POP3를 사용할 경우, 그리고 Gmail을 사용할 경우 메일 설정에서 설정한 시점 이후의 메일을 가져온다던가 읽은 메일을 삭제 혹은 읽음 처리를 하는 등 다양한 처리를 할 수 있다.

 

반면 IMAP는 동기화를 한다. 다시 말해 INBOX에 있는 모든 메일을 읽어들인다. 또한 치명적인 단점은 메일을 하나씩 순서대로 읽을 경우 오름차순으로 읽는다. 가장 오래된 메일부터 읽게되는 것이다.

 

따라서 POP3 프로토콜을 사용하여 메일보관함에 반송된 메일이 있는지 확인해볼 것이다.

 

properties

pop3.mail:  
  host: pop.gmail.com  
  port: 995  
  protocol: pop3s  
  folder: INBOX  
  username: ${MAIL_USERNAME}  
  password: ${MAIL_PASSWORD}  
  until-time: 300 # 설정한 시간 전까지 메일까지 확인
  • protocol: jakarta.mail에서는 ssl을 적용한 pop3 프로토콜을 pop3s라고 한다.
  • folder: 읽을 메일 보관함 이름

Service

@Service  
@RequiredArgsConstructor  
public class EmailService {
    private final Pop3Properties pop3Properties;  
    private static final String FAILED_RECIPIENTS_HEADER = "X-Failed-Recipients";

    public boolean isCompleteSentEmail(String email) {  
        try {  
            Thread.sleep(5000); // 메일이 반송되기까지 기다릴 시간  
            Folder emailFolder = getEmailFolder();  
            Instant untilTime = Instant.now().minusSeconds(pop3Properties.untilTime());  
            for (Message message : emailFolder.getMessages()) {  
                // 지정한 시간(limitedTime) 안에 수신한 메일만 확인  
                if (untilTime.isBefore(message.getSentDate().toInstant())) {  
                    // 받는 사람 이메일 확인  
                    Optional.ofNullable(message.getHeader(FAILED_RECIPIENTS_HEADER))  
                            .ifPresent(recipients -> {  
                                if (recipients[0].equals(email)) {  
                                    try {  
                                        // 반송된 이메일 삭제  
                                        message.setFlag(Flags.Flag.DELETED, true);  
                                        emailFolder.close(true);  
                                    } catch (MessagingException e) {  
                                        throw new MailException(ErrorCode.CANT_GET_MAIL, e);  
                                    }  
                                }  
                            });  
                    if (!emailFolder.isOpen()) {  
                        return false;  
                    }  
                }  
            }  
        } catch (Exception e) {  
            throw new MailException(ErrorCode.CANT_GET_MAIL, e);  
        }  
        return true;  
    }

    private Folder getEmailFolder() {  
        try {  
            Properties properties = new Properties();  
            properties.put("mail.pop3.host", pop3Properties.host());  
            properties.put("mail.pop3.port", pop3Properties.port());  
            properties.put("mail.pop3.starttls.enable", "true");  
            Session emailSession = Session.getDefaultInstance(properties);  

            Store store = emailSession.getStore(pop3Properties.protocol());  
            store.connect(pop3Properties.host(), pop3Properties.username(), pop3Properties.password());  

            Folder emailFolder = store.getFolder(pop3Properties.folder());  
            emailFolder.open(Folder.READ_WRITE);  
            return emailFolder;  
        } catch (Exception e) {  
            throw new MailException(ErrorCode.CANT_GET_FOLDER, e);  
        }  
    }
}
  • 메일을 전송하고 바로 메일 보관함을 확인하게되면 나중에 메일이 반송될 수 있기때문에 확인할 수 없게된다. 따라서 Thread.sleep으로 반송 메일을 기다려주도록 했다.
  • 반송된 메일이 있을 경우 삭제해주고 false를 반환한다. 없다면 true를 반환한다.

위 코드의 문제는 반송된 메일이 있는지 확인하기 오래걸린다는 것이다. 메일이 반송되기까지 걸리는 시간이 일정하지 않아 더 늦게 반송되어 확인이 안될 수도 있다.

 

spring retry를 사용해서 반송 또는 메일 전송에 실패했을 경우를 판단해서 재요청할 수 있으면 좋겠지만 판단하는 것 자체가 문제이다... 그리고 재요청한다고 해도 계속 실패하면 결국 실패했다는 사실을 알려야한다. 결국 메일 서버를 따로 만들어야하는 건가....

 

[reference]
https://okky.kr/questions/407411

728x90