프로젝트 환경구성
이전 프로젝트에서 사용한 라이브러리, 설정 외에 더 추가할 것이 생겼다.
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에 개별 수신인을 넣어 전송하면 된다.
(코드에 주석처리되어있으니 편한대로 사용하시라!)
실행 결과
이제 프로젝트를 구동하여 메일이 정상적으로 보내지는지 확인해보자.
완료다! 직접 메일을 확인해보자.
REF: https://born2bedeveloper.tistory.com/68#google_vignette
'Study > SpringBoot' 카테고리의 다른 글
[Spring Boot] dev, prd LogBack 남기기 + AWS EC2에 파일 Mount (2) | 2025.04.15 |
---|---|
@PostConstruct 어노테이션을 통한 스프링 빈(Bean) 초기화 콜백 (0) | 2024.10.22 |
spring boot + AWS RDS 연동(postgreSQL) (0) | 2024.10.11 |
Spring Boot Servlet Filter에서 에러 코드 변경하기 (0) | 2024.04.17 |
MultipartFile을 사용해서 이미지를 로컬에 업로드하기 (1) | 2024.02.16 |
댓글