@senspond
>
개인목적으로 자동화에 활용 할 메일발송 프로그램을 Java기반으로 jakarta Mail로 만들어 보며 정리를 해보았습니다.
안녕하세요. 오랜만에 다시 글을 씁니다.
예전 회사에서 Java8로 되어있던 프로젝트에 javax.mail 로 고객사에게 자동발송 보고서를 전송하는 프로그램을 만들어 본적이 있었습니다.
오래전이지만 그 때의 기억을 떠올려 보며 개인목적으로 자동화에 활용 할 메일발송 프로그램을 만들어 보며 정리를 해봤습니다.
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'
}
만약 SpringBoot를 쓴다면 spring-boot-starter-mail 라는 패키지를 사용해도 됩니다. 좀더 간단하게 구현 할 수 있습니다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-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);
}
}
}
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 경로안에 있는 파일과 로컬파일 경로에 있는 파일 모두 정상적으로 첨부 되는지 확인해보았습니다.
안녕하세요. Red, Green, Blue 가 만나 새로운 세상을 만들어 나가겠다는 이상을 가진 개발자의 개인공간입니다.
현재글에서 작성자가 발행한 같은 카테고리내 이전, 다음 글들을 보여줍니다
@senspond
>