[Spring Boot] 이메일 보내기 (2) - 참조(cc), 첨부 파일

2022. 8. 10. 18:02Spring Boot

반응형

 

꽤 오래전에 구글 SMTP 서버를 이용한 Email 전송에 관한 내용을 다룬 적이 있었는데, 생각보다 조회수가 매우 높았다. 아마 이메일 전송이 부가적인 기능으로 다양하게 사용되기 떄문이겠지?

 

새로운 프로젝트에서 Email 서비스를 다시 구현하게 됐는데, 좀 더 세부적인 기능이 추가적으로 필요하여 두 번째 시리즈를 쓰기로 결심했다.

 

이번엔 다중 송신, 다중 참조(CC), 첨부파일 전송 등 다양한 옵션을 사용하여 좀 더 세부적으로 이메일 전송을 구현해보도록 하겠다.

 

++) 이전 프로젝트를 참고하고 오는 것을 추천한다.

https://born2bedeveloper.tistory.com/14

 

[Spring Boot] EMail 보내기

기존 프로젝트를 베타 오픈하기 전에, 피드백을 받는 기능을 추가하면 좋겠다는 생각이 들었다. 일종의 고객센터 같은 느낌? 간단한 입력 form에 작성한 후 보내면, 관리자 계정으로 메일이 오도

born2bedeveloper.tistory.com

 

 

기본적인 개념은 이전 포스팅에서 다뤘기 때문에 구현 위주로 가겠다.

 

 프로젝트 환경구성

 

이전 프로젝트에서 사용한 라이브러리, 설정 외에 더 추가할 것이 생겼다.

 

build.gradle

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

implementation group: 'commons-io', name: 'commons-io', version: '2.11.0'

compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'

commons-io는 첨부 파일 구현에서 편리하게 쓸 메소드를 가져오는 데 쓰인다.
(이 포스팅 에서 파일 업로드, 다운로드 관련으로 유용하게 쓰였음)

 

application.properties

## mail default settings
spring.mail.default-encoding=UTF-8
## mail custom settings
spring.mail.host=smtp.gmail.com
spring.mail.port=587
spring.mail.username=[구글계정]
spring.mail.password=[앱 비밀번호]
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.debug=true
spring.mail.properties.mail.smtp.starttls.enable=true
spring.mail.properties.mail.mime.charset=UTF-8
spring.mail.properties.mail.transport.protocol=smtp

 

잠깐!
기존에는 메일 송신을 위해 보안 수준이 낮은 앱 및 Google계정 을 허용하여 날먹(?)으로 메일 송신이 가능했지만, 이제 더는 해당 방식을 지원하지 않는다.

따라서 2단계 인증을 설정한 후, 앱 비밀번호를 생성하여 해당 비밀번호를 기입해야만이 메일 송신이 가능하다.

그 방법은 이 포스팅 아래 부분에서 자세히 다루고 있으니 참고하자 ㅎㅎ

 

 

 프로젝트 화면 구성 - view

 

필자는 타임리프 뷰 템플릿으로 간단하게 화면을 구성했다.

js부분을 추가해서 여러 명의 수신인과 참조인을 가능하도록 했으니 편하게 가져다 쓰면 된다!

 

textMail.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="ko">
<head>
    <meta charset="UTF-8">
    <title>Send Mail</title>
    <script type="text/javascript" src="/scripts/jquery-ui/jquery.min.js"></script>
	<script type="text/javascript" src="/scripts/common/common-ui.js"></script>
	<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css">
	<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
	<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js"></script>
</head>
<body>
<div class="container">
<div style="float: left; width: 50%;">
    <h1>텍스트 메일 보내기</h1>
 
    <form th:action="@{/mail/send}" method="post" enctype="multipart/form-data">
        <table>
            <tr class="form-group">
                <td>보내는 사람</td>
                <td>
                    <input type="text" class="form-control" name="from" placeholder="이메일 주소를 입력하세요">
                </td>
            </tr>
            <tr id="box" class="form-group">
                <td>받는 사람</td>
                <td>
                    <input type="text" class="form-control" name="address" placeholder="이메일 주소를 입력하세요">
                </td>
                <td>
					<input type="button" class="form-control" value="추가" onclick="add_textbox(this)">
				</td>
            </tr>
            <tr id="box2" class="form-group">        
                <td>참조 메일 주소</td>
                <td>
                    <input type="text" class="form-control" name="ccAddress" placeholder="참조 수신인을 입력하세요">
                </td>
                <td>
					<input type="button" class="form-control" value="추가" onclick="add_textbox2(this)">
				</td>
            </tr>
            <tr class="form-group">
                <td>제목</td>
                <td>
                    <input type="text" class="form-control" name="title" placeholder="제목을 입력하세요">
                </td>
            </tr>
            <tr class="form-group">
                <td>내용</td>
                <td>
                    <textarea class="form-control" name="content" placeholder="보낼 내용을 입력하세요"> </textarea>
                </td>
            </tr>
            <tr class="form-group">
            	<td>첨부 파일 </td>
            	<td>
            		<input type="file" name="file" class="file-input" />
            	</td>
            </tr>
        </table>
        <button class="btn btn-default">발송</button>
    </form>

 </div>
 </div>
</body>
<script>
	const add_textbox = (obj) => {
	    const box = obj.parentElement.parentElement; 
	    const newP = document.createElement("tr");
	
	    newP.innerHTML = "<tr class='form-group'><td>메일 주소</td><td><input type='text' class='form-control' name='address' ></td><td><input type='button' class='form-control' value='삭제' onclick='opt_remove(this)'></td></tr>";
		box.parentNode.insertBefore(newP, box.nextSibling);
	}
	
	const add_textbox2 = (obj) => {
	    const box = obj.parentElement.parentElement; 
	    const newP = document.createElement("tr");

	    newP.innerHTML = "<tr class='form-group'><td>참조 메일 주소</td><td><input type='text' class='form-control' name='ccAddress' ></td><td><input type='button' class='form-control' value='삭제' onclick='opt_remove2(this)'></td></tr>";
		box.parentNode.insertBefore(newP, box.nextSibling);
		}
	
	const opt_remove = (obj) => {
		obj.parentElement.parentElement.parentElement.removeChild(obj.parentElement.parentElement);
	}
	const opt_remove2 = (obj) => {
		obj.parentElement.parentElement.parentElement.removeChild(obj.parentElement.parentElement);
	}
</script>
</html>

form 타입은 꼭 multipart로 지정해야 첨부파일이 잘 넘어간다. 아니면 null처리되니 주의!

 

result.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="ko">
<head>
  <title>Bootstrap Example</title>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css">
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
  <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js"></script>
</head>
<body>

<div class="container">
  <h2>Email 전송 완료.</h2>    
</div>

</body>
</html>

 

 DTO 구현

 

 

view에서 받아올 데이터를 맵핑할 DTO를 구현하자.

 

package com.example.demo.dto;

import java.util.ArrayList;
import java.util.List;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
 
@Getter
@Setter
@NoArgsConstructor
public class MailDto {
	private String from;
    private String[] address;
    private String[] ccAddress;
    private String title;
    private String content;
}

 

 Controller 구현

 

package com.example.demo.controller;

import java.io.IOException;
import java.util.List;

import javax.mail.MessagingException;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;

import com.example.demo.domain.Email;
import com.example.demo.dto.MailDto;
import com.example.demo.service.MailService;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Controller
public class MailController {
	
	private final MailService mailService;
	
	 
    public MailController(MailService mailService) {
        this.mailService = mailService;
    }
	
	@GetMapping("/textMail")
    public String mailSend() {
		return "textMail";
	}
    
    @PostMapping("/mail/send")
    public String sendMail(MailDto mailDto, MultipartFile file) throws MessagingException, IOException {
    	mailService.sendMultipleMessage(mailDto, file);
        System.out.println("메일 전송 완료");
        return "result";
    }

}

첨부파일의 경우 MultipartFile 형식으로 받아온다.

 

 

 Service 구현

 

 

package com.example.demo.service;

import java.io.IOException;
import java.util.HashMap;
import java.util.List;

import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeUtility;

import org.apache.commons.io.IOUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.ClassPathResource;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;

import com.example.demo.dto.MailDto;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Service
@RequiredArgsConstructor 
public class MailService {
 
    private final JavaMailSender emailSender;
    
    public void sendMultipleMessage(MailDto mailDto, MultipartFile file) throws MessagingException, IOException {
    	MimeMessage message = emailSender.createMimeMessage();
        MimeMessageHelper helper = new MimeMessageHelper(message, true);
        //메일 제목 설정
        helper.setSubject(mailDto.getTitle());

        //참조자 설정
        helper.setCc(mailDto.getCcAddress());        
        
        helper.setText(mailDto.getContent(), false);
        
        helper.setFrom(mailDto.getFrom());
        
        //첨부 파일 설정
        String fileName = StringUtils.cleanPath(file.getOriginalFilename());
        
        helper.addAttachment(MimeUtility.encodeText(fileName, "UTF-8", "B"), new ByteArrayResource(IOUtils.toByteArray(file.getInputStream())));
                       
        //수신자 개별 전송       
//        for(String s : mailDto.getAddress()) {
//        	helper.setTo(s);
//        	emailSender.send(message);
//        }
        //수신자 한번에 전송
        helper.setTo(mailDto.getAddress());
        emailSender.send(message);
        log.info("mail multiple send complete.");
   
        
    }

}

기존에 text기반 메일을 보냈을 때와 달리 첨부파일을 담기 위해 MimeMessage 객체를 사용하고 있다. 

 

  • MimeMessageHelperclass
    MIME 메시지를 만들기 위한 클래스
    HTML 레이아웃에서 이미지, 일반적인 메일 첨부 파일 및 텍스트 내용을 지원

 

MIME이란?
Multipurpose Internet Mail Extensions의 약자이다.
기존 UUEncoding 방식은 ASCII(텍스트) 파일만 지원하여 음악, 워드 파일 등의 바이너리 파일을 전송할 수 없음
이러한 방식을 보안하여 나온 인코딩 방식이다.

 

참조, 수신인을 설정할때도 간단하다. setTo와 setCc 메소드를 활용하면 된다.

이 때, 수신인이나 참조인지 다중(여러 명)일 경우와 단일일 경우의 메소드가 둘 다 구현되어 있다.

따라서

A방식 메일 : 수신인[a, b, c, d....] 제목: "제목" 내용 : "내용"

B방식 메일 : 수신인[a]  제목: "제목" 내용: "내용"  ,    수신인[b]  제목: "제목" 내용: "내용"....

 

한 번에 여러 수신인에게 메일을 보내는 A 방식의 경우 setTo에 수신인 리스트를 넣어두고,

각 수신인에게 하나의 메일을 여러개 보내는 B 방식의 경우 setTo에 개별 수신인을 넣어 전송하면 된다.

 

(코드에 주석처리되어있으니 편한대로 사용하시라!)

 

 

 실행 결과

 

이제 프로젝트를 구동하여 메일이 정상적으로 보내지는지 확인해보자.

 

 

완료다! 직접 메일을 확인해보자.

 

 

 

 

참조도 무사히 달렸고, 파일도 잘 첨부되었다!! 

 

 

 

다음 포스팅은 html template를 적용한 이메일 전송을 다뤄볼 예정이다!