2022. 8. 10. 18:02ㆍSpring Boot
꽤 오래전에 구글 SMTP 서버를 이용한 Email 전송에 관한 내용을 다룬 적이 있었는데, 생각보다 조회수가 매우 높았다. 아마 이메일 전송이 부가적인 기능으로 다양하게 사용되기 떄문이겠지?
새로운 프로젝트에서 Email 서비스를 다시 구현하게 됐는데, 좀 더 세부적인 기능이 추가적으로 필요하여 두 번째 시리즈를 쓰기로 결심했다.
이번엔 다중 송신, 다중 참조(CC), 첨부파일 전송 등 다양한 옵션을 사용하여 좀 더 세부적으로 이메일 전송을 구현해보도록 하겠다.
++) 이전 프로젝트를 참고하고 오는 것을 추천한다.
https://born2bedeveloper.tistory.com/14
기본적인 개념은 이전 포스팅에서 다뤘기 때문에 구현 위주로 가겠다.
프로젝트 환경구성
이전 프로젝트에서 사용한 라이브러리, 설정 외에 더 추가할 것이 생겼다.
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를 적용한 이메일 전송을 다뤄볼 예정이다!
'Spring Boot' 카테고리의 다른 글
[Spring Boot] 이메일 보내기 (3) - html 템플릿 적용 (feat. Thymeleaf) (0) | 2022.08.11 |
---|---|
[Spring Boot] Thymeleaf 경로 변경 & 다중 경로 설정 (0) | 2022.08.11 |
[Spring Boot] SMS 전송 - NAVER SMS API 연동 (16) | 2022.08.10 |
[Spring Boot] File Upload & Download (2) - ORACLE DB 연동 (0) | 2022.08.05 |
[Spring Boot] File Upload & Download (1) (2) | 2022.08.05 |