@senspond

>

개발>백엔드

네이버 이메일 SMTP 프로토콜로 이메일 발송 프로그램 만들기 ( jakarta.mail )

등록일시 : 2024-06-21 (금) 01:23
업데이트 : 2024-06-21 (금) 09:18
오늘 조회수 : 3
총 조회수 : 576

    개인목적으로 자동화에 활용 할 메일발송 프로그램을 Java기반으로 jakarta Mail로 만들어 보며 정리를 해보았습니다.

    안녕하세요. 오랜만에 다시 글을 씁니다.


    예전 회사에서 Java8로 되어있던 프로젝트에 javax.mail 로 고객사에게 자동발송 보고서를 전송하는 프로그램을 만들어 본적이 있었습니다.

    오래전이지만 그 때의 기억을 떠올려 보며 개인목적으로 자동화에 활용 할 메일발송 프로그램을 만들어 보며 정리를 해봤습니다.


    Java에서 이메일 발송 구현하는 두가지 방법

    Java EE / Jakarta EE

    Java EE

    Jakarta EE

    Java Servlet

    javax.servlet

    Jakarta Servlet

    jakarta.servlet

    JavaServer Pages (JSP)

    javax.servlet.jsp

    Jakarta Server Pages

    jakarta.servlet.jsp

    JavaServer Faces (JSF)

    javax.faces

    Jakarta Server Faces

    jakarta.faces

    Java Message Service (JMS)

    javax.jms

    Jakarta Messaging

    jakarta.jms

    Java Persistence API (JPA)

    javax.persistence

    Jakarta Persistence

    jakarta.persistence

    Java Transaction API (JTA)

    javax.transaction

    Jakarta Transaction

    jakarta.transaction

    Enterprise JavaBeans (EJB)

    javax.ejb

    Jakarta Enterprise Beans

    jakarta.ejb

    Java Mail

    javax.mail

    Jakarta Mail

    jakarta.mail


    Java 17 에서는 Jakarta EE 를 사용해야 합니다.


    https://mvnrepository.com/artifact/com.sun.mail/jakarta.mail

    dependencies {
      implementation 'com.sun.mail:jakarta.mail:2.0.1'
      implementation 'jakarta.activation:jakarta.activation-api:2.0.0'
    }



    Spring Boot Starter mail

    만약 SpringBoot를 쓴다면 spring-boot-starter-mail 라는 패키지를 사용해도 됩니다. 좀더 간단하게 구현 할 수 있습니다.



    dependencies {
     implementation 'org.springframework.boot:spring-boot-starter-mail'
    }



    네이버 이메일 STMP 설정하기

    STMP란?

    단순 전자우편 전송 규약을 의미하는 전송 프로토콜입니다.


    네이버 이메일 설정하기

    네이버에 로그인해서 메일로 들어가서 환경설정 탭에서 다음 설정을 하면 됩니다.






    Spring과 Jakarta Mail 로 구현 과정

    이 글에서는 spring-boot-starter-mail 말고 Java 로 구현해본 과정을 상세하게 담아봤습니다.


    메일정보 설정

    NaverEmailConfig란 클래스를 만들고 설정정보를 bean 으로 등록했습니다.

    import jakarta.mail.Authenticator;
    import jakarta.mail.PasswordAuthentication;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.Primary;
    import org.springframework.stereotype.Component;
    
    import java.util.Properties;
    
    @Configuration
    public class NaverEmailConfig {
        private static final String USER_EMAIL = "[이메일주소]";
        private static final String USER_PASSWORD = "[비밀번호]";
    
        @Primary
        @Bean(name = "naverEmailProperties")
        public Properties emailProperties(){
            Properties props = new Properties();
            props.put("mail.username", USER_EMAIL);
            props.put("mail.host", "smtp.naver.com");
            props.put("mail.transport.protocol", "smtp");
            props.put("mail.debug", "true");
            props.put("mail.smtp.ssl.trust","smtp.naver.com");
            props.put("mail.smtp.ssl.enable","true");
            props.put("mail.smtp.auth", "true");
            props.put("mail.smtp.starttls.enable", "true");
            return props;
        }
    
        @Component
        public static class SimpleAuthenticator extends Authenticator {
            @Override
            protected PasswordAuthentication getPasswordAuthentication() {
                // TODO : 보안을 위해 수정필요
                return new PasswordAuthentication(USER_EMAIL, USER_PASSWORD);
            }
        }
    }

    우선은 비밀번호를 하드코딩으로 넣고 만들었지만, 보안상 좋지 않기 때문에 서버에 올리기 전에는 수정이 필요합니다.


    단일 수신자, 참조 발송

    @RequiredArgsConstructor
    @Service
    public class EmailServiceImpl implements EmailService {
    
        @Qualifier("naverEmailProperties")
        private final Properties props;
        private final NaverEmailConfig.SimpleAuthenticator authenticator;
    
        public void sendMail(String title, String body, String toEmail, String ccEmail){
            //  JavaMail 세션은 실제 네트워크 연결 세션과는 다름. 정보를 담고 있는 객체
            Session session = Session.getInstance(props, authenticator);
    
            try {
                Message message = new MimeMessage(session);
                message.setFrom(new InternetAddress(props.getProperty("mail.username")));
    
                message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(toEmail));
                message.setRecipients(Message.RecipientType.CC, InternetAddress.parse(ccEmail));
                message.setSubject(title);
                message.setText(body);
               // Transport 클래스의 정적 send 메소드는 메시지를 보낸 후에 자동으로 연결을 종료
                Transport.send(message);
            } catch (MessagingException e) {
                throw new RuntimeException(e);
            }
        }
    }

    JavaMail의 Session 은 실제 네트워크 연결을 하는 것이 아니라 정보를 담고 있는 객체입니다.

    Transport 클래스의 내부 connect 메소드를 통해 지정된 프로토콜로 네트워크 연결이 되는데,

    send정적 메소드의 내부를 보면 마지막에 연결을 close해 주도록 설계 되어 있어서 별도로 close해주지 않아도 됩니다.


    RecipientType 에는 다음 세 가지가 있습니다.

    • TO : 수신자

    • CC : 참조

    • BCC : 숨은참조


    emailService.sendMail("안녕하세요", "어서옵셔", "senspond@gmail.com", "senshig@naver.com");

    수신자에게 메일이 발송되고 참조로 넣어둔 사람에게도 발송이 된것을 확인할 수 있습니다.



    다중 수신자, 참조 발송

    한번 메일을 발송하는데 수신자가 여려명인 경우를 고려해 봅니다.

    @RequiredArgsConstructor
    @Service
    @Slf4j
    public class EmailServiceImpl implements EmailService {
    
        @Qualifier("naverEmailProperties")
        private final Properties props;
        private final NaverEmailConfig.SimpleAuthenticator authenticator;
    
        private InternetAddress[] getInternetAddress(List<String> emailList){
            List<InternetAddress> addresses = new ArrayList<>();
            for (int i =0; i < emailList.size(); i++){
                try {
                    InternetAddress address = new InternetAddress(emailList.get(i));
                    addresses.add(address);  // AddressException 발생하는 메일은 담기지 않음
                }catch (AddressException ae){
                    log.error(ae.getMessage());
                }
            }
            return addresses.toArray(new InternetAddress[0]);
        }
    
    
        public void sendMail(String title, String body, List toEmail, List ccEmail){
    
            Session session = Session.getInstance(props, authenticator);
            try {
                Message message = new MimeMessage(session);
                message.setFrom(new InternetAddress(props.getProperty("mail.username")));
    
                message.setRecipients(Message.RecipientType.TO, getInternetAddress(toEmail));
    
                if (ccEmail != null && !ccEmail.isEmpty()){
                    message.setRecipients(Message.RecipientType.CC, getInternetAddress(ccEmail));
                }
    
                message.setSubject(title);
                message.setText(body);
    
                Transport.send(message);
            } catch (MessagingException e) {
    		        log.error(e.getMessage());
                throw new RuntimeException(e);
            }
        }
    }


    template으로 디자인 된 이메일 발송

    Thymeleaf, FreeMarker, Velocity 등 Java에서 사용할 수 있는 템플릿 엔진을 사용해서 디자인 된 html을 랜더링하고 발송을 하는 방법을 생각해볼 수 있습니다. 여기서는 handlerbar 엔진을 사용해보겠습니다.

    implementation 'com.github.jknack:handlebars:4.2.1


    디자인 된 템플릿을 하나 만듭니다. 테스트 용으로 만들었습니다.


    resources/templates/email-template1.hbs

    <!DOCTYPE html>
    <html>
    <head>
        <title>{{title}}</title>
        <style>
            .item-wrapper{background: black; color: white;}
        </style>
    </head>
    <body>
        <h1>Hello, {{name}}!</h1>
    
        <p>{{message}}</p>
        <h1>List of items:</h1>
        <div class="item-wrapper">
            <ul>
                {{#each items}}
                    <li>{{this}}</li>
                {{/each}}
            </ul>
        </div>
        <p>Best regards,</p>
        <p>Your {{company}} Team</p>
    </body>
    </html>

    위 핸들바 템플릿과 맵핑 할 수 있는 클래스를 정의합니다.

    @NoArgsConstructor
    @Getter
    @Setter
    @AllArgsConstructor
    @Builder
    public class EmailRendererVo1 {
        private String name;
        private String title;
        private String message;
        private List<String> items;
        private String company;
    }


    이런 템플릿을 용도에 따라 여러개 만들어서 관리하기 용이하도록 enum 클래스로 정의를 했습니다.

    import lombok.AllArgsConstructor;
    import lombok.Getter;
    
    @Getter
    @AllArgsConstructor
    public enum EmailTemplate {
        // test1
        TEST1("email-template1.hbs", EmailRendererVo1.class),
    
        // test2
        TEST2("email-template2.hbs", EmailRendererVo2.class);
    
        private String name;
        private Class renderer;
    }
    


    이메일 템플릿을 불러오고 html String로 랜더링을 하는 기능을 수행하는 EmailTemplateLoader 클래스를 정의합니다.

    @Component
    @RequiredArgsConstructor
    public class EmailTemplateRenderer {
        private final ResourceLoader resourceLoader;
    
        private String loadTemplate(EmailTemplate emailTemplate) throws IOException {
            Resource resource = resourceLoader.getResource("classpath:/templates/" + emailTemplate.getName());
            try (BufferedReader reader = new BufferedReader(
    							new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8))) {
                return reader.lines().collect(Collectors.joining(System.lineSeparator()));
            }
        }
    
        public String render(EmailTemplate emailTemplate, Object object) {
            try {
                String templateStr = loadTemplate(emailTemplate);
                ObjectMapper objectMapper = new ObjectMapper();
                //
                if(object.getClass().equals(emailTemplate.getRenderer())){
                    Map<String, Object> variables = objectMapper.convertValue(object, Map.class);
                    Template template = new Handlebars().compileInline(templateStr);
                    return template.apply(variables);
                }else {
                    throw new RuntimeException("renderer 확인 바람");
                }
            } catch(IOException e) {
                throw new RuntimeException("Could not load email template", e);
            }
        }
    }
    


        @Test
        public void emailTemplate1Tests(){
            List<String> items = new ArrayList<>();
            items.add("맥북 에어 M3");
            items.add("RTX4090");
            items.add("A100");
            EmailRendererVo1 vo = EmailRenderer1.builder()
                    .title("안녕하세요")
                    .name("OOO님")
                    .message("반가워요")
                    .items(items)
                    .company("rgb코딩")
                    .build();
    
        	  String html = emailTemplateRenderer.render(EmailTemplate.TEST1, vo);
            System.out.println(html);
        }


    만든 템플릿과 Java객체가 맵핑되어 잘 랜더링 되는 것을 확인 할 수 있습니다.



    이제 위의 senMail 코드를 수정합니다.

        public void sendMail(String title, String htmlContent, List toEmail, List ccEmail){
    
            //  JavaMail 세션은 실제 네트워크 연결 세션과는 다름. 정보를 담고 있는 객체
            Session session = Session.getInstance(props, authenticator);
            try {
                Message message = new MimeMessage(session);
                message.setFrom(new InternetAddress(props.getProperty("mail.username")));
    
                message.setRecipients(Message.RecipientType.TO, getInternetAddress(toEmail));
    
                if (ccEmail != null && !ccEmail.isEmpty()){
                    message.setRecipients(Message.RecipientType.CC, getInternetAddress(ccEmail));
                }
    
                message.setSubject(title);
    //            message.setText(body);
                message.setContent(htmlContent, "text/html; charset=UTF-8");
    
                // Transport 클래스의 정적 send 메소드는 메시지를 보낸 후에 자동으로 연결을 종료
                Transport.send(message);
            } catch (MessagingException e) {
                log.error(e.getMessage());
                throw new RuntimeException(e);
            }
        }


    디자인이 적용된 템플릿으로 제대로 발송이 되는지 확인해봅니다.

        @Test
        public void emailTemplate1SendTests(){
            List<String> items = new ArrayList<>();
            items.add("맥북 에어 M3");
            items.add("RTX4090");
            items.add("A100");
            EmailRendererVo1 vo = EmailRenderer1.builder()
                    .title("안녕하세요")
                    .name("OOO님")
                    .message("반가워요")
                    .items(items)
                    .company("rgb코딩")
                    .build();
    
            String html = emailTemplateRenderer.render(EmailTemplate.TEST1, vo);
    //        System.out.println(html);
            emailService.sendMail("안녕하세요", html, Arrays.asList("senspond@gmail.com"), null);
    
        }


    다중 첨부파일 발송

    이제 첨부파일을 추가해 볼 차례입니다.

    @NoArgsConstructor
    @Getter
    @AllArgsConstructor
    public class EmailFile {
        private String filename;
        private String filePath;
    }


    
    private void addFileToMultipart(Multipart multipart, List<EmailFile> files){
    
        for(EmailFile file : files){
            try {
    		        Resource resource = resourceLoader.getResource(file.getFilePath()); // 첨부할 파일 가져오기
                DataSource source = new ByteArrayDataSource(resource.getInputStream(), "application/octet-stream");
    
                MimeBodyPart filePart = new MimeBodyPart();
                filePart.setDataHandler(new DataHandler(source));
                filePart.setFileName(file.getFilename());
                multipart.addBodyPart(filePart);
            }catch (Exception e){
                log.error(e.getMessage());
                throw new RuntimeException("첨부 파일 누락으로 예외발생 - 파일경로 확인 바람 ", e);
            }
        }
    }
    
    public void sendMail(String title, String htmlContent, List<String> toEmail, List<String> ccEmail, List<EmailFile> files){
    
        //  JavaMail 세션은 실제 네트워크 연결 세션과는 다름. 정보를 담고 있는 객체
        Session session = Session.getInstance(props, authenticator);
        try {
            Message message = new MimeMessage(session);
    
            message.setFrom(new InternetAddress(props.getProperty("mail.username")));
    
            message.setRecipients(Message.RecipientType.TO, getInternetAddress(toEmail));
    
            if (ccEmail != null && !ccEmail.isEmpty()){
                message.setRecipients(Message.RecipientType.CC, getInternetAddress(ccEmail));
            }
    
            message.setSubject(title);
    //            message.setText(body);
    
            if(files != null && !files.isEmpty()){
                // Multipart 생성. 이것은 이메일의 본문과 첨부 파일을 포함합니다.
                Multipart multipart = new MimeMultipart();
    
                // 본문
                MimeBodyPart mainPart = new MimeBodyPart();
                mainPart.setContent(htmlContent, "text/html; charset=UTF-8");
                multipart.addBodyPart(mainPart);
    
                // 첨부파일
                addFileToMultipart(multipart, files);
    
                message.setContent(multipart);
            }else{
                message.setContent(htmlContent, "text/html; charset=UTF-8");
            }
    
    
            // Transport 클래스의 정적 send 메소드는 메시지를 보낸 후에 자동으로 연결을 종료
            Transport.send(message);
        } catch (MessagingException e) {
            log.error(e.getMessage());
            throw new RuntimeException(e);
        }
    }


    파일첨부를 하려면 Multipart 라는 객체를 사용해야 합니다.


    테스트를 해봅니다.

        @Test
        public void emailTemplate1WithFileTests(){
            List<String> items = new ArrayList<>();
            items.add("맥북 에어 M3");
            items.add("RTX4090");
            items.add("A100");
            EmailRenderer1 renderer = EmailRenderer1.builder()
                    .title("안녕하세요")
                    .name("OOO님")
                    .message("반가워요")
                    .items(items)
                    .company("rgb코딩")
                    .build();
    
            List<EmailFile> files = new ArrayList<>();
            files.add(new EmailFile("이메일 템플릿.hbs", "classpath:templates/email-template1.hbs"));
            files.add(new EmailFile("test_테스트.txt", "classpath:/templates/test.txt"))
            files.add(new EmailFile("로그.log", "file:/Users/senspond/Dev/logs/backend.log"));
            String html = emailTemplateLoader.formatTemplate(EmailTemplate.TEST1, renderer);
    //        System.out.println(html);
            emailService.sendMail("안녕하세요", html, Arrays.asList("senspond@gmail.com"), 
    																							 Arrays.asList("senshig@naver.com"), files);
        }

    classpath 경로안에 있는 파일과 로컬파일 경로에 있는 파일 모두 정상적으로 첨부 되는지 확인해보았습니다.



    senspond

    안녕하세요. Red, Green, Blue 가 만나 새로운 세상을 만들어 나가겠다는 이상을 가진 개발자의 개인공간입니다.

    댓글 ( 0 )

    카테고리내 관련 게시글

    현재글에서 작성자가 발행한 같은 카테고리내 이전, 다음 글들을 보여줍니다

    @senspond

    >

    개발>백엔드

    • 파이썬 백엔드 Fast API gunicorn 으로 구동하기 ( WSGI, ASGI, uvicorn 한계 정리)

      파이썬 백엔드 Fast API gunicorn 으로 구동하기 ( WSGI, ASGI, uvicorn 한계 정리)
        2024-02-21 (수) 07:21
      1. [현재글] 네이버 이메일 SMTP 프로토콜로 이메일 발송 프로그램 만들기 ( jakarta.mail )

        개인목적으로 자동화에 활용 할 메일발송 프로그램을 Java기반으로 jakarta Mail로 만들어 보며 정리를 해보았습니다.
          2024-06-21 (금) 01:23
        1. 파이썬 가상환경, 버전관리, Docker 환경 구성

          파이썬 가상환경 관리, requirements.txt 버전관리 , Docker 환경 구성 등의 내용을 정리해 본 글입니다.
            2024-02-08 (목) 06:22
          1. Java 프로그램으로 Slack에 각 채널 별로 메시지 전송하기

            Slack은 팀 협업 및 커뮤니케이션을 위한 클라우드 기반의 메시징 플랫폼입니다. Java프로그램으로 Slack 메시지 전송 프로그램을 만들어본 것을 정리 해봅니다.
              2024-06-22 (토) 02:07
            1. Java Spring WebFlux 로 ChatGPT OpenAI Streaming API 만들기 / 자바스크립트에서 스트리밍 요청처리

              Java Spring WebFlux 로 ChatGPT OpenAI Streaming API 만들고 자바스크립트에서 스트리밍 요청처리를 하는 방법을 정리해봅니다.
                2024-06-23 (일) 01:35