본문 바로가기

Java

JavaMail API : 자바로 웹에서 파일첨부 메일보내기

JavaMail API

프로젝트 중 폼태그 부분에서 파일을 첨부해 메일을 보내는 기능이 필요했다. 

조건

- 이용자에게 추가 액션을 요구하지 않고 폼태그 안에서 메일을 전송할 것.

- 파일을 첨부하면 저장과정 없이 다이렉트로 메일이 전송되게 할 것.

- 빠르고 전송이 정확할 것.


JavaMail API에 Google SMTP 서버를 이용하기로 했다. 

Google SMTP 서버를 이용하면 지메일을 이용해 검색/색인이 쉽고, 스팸으로 차단당할 가능성이 줄어들고 무엇보다 무료다. 

Setup

먼저 아래 JavaMail jar파일을 다운받고, 빌드패스에 추가한다.

javax.mail.jar

Maven을 이용한다면 아래 코드 복붙.

<!-- https://mvnrepository.com/artifact/javax.mail/mail -->
<dependency>
    <groupId>javax.mail</groupId>
    <artifactId>mail</artifactId>
    <version>1.4</version>
</dependency>
 
cs
만약 Java 버전이 6 이하라면 javax.activation 패키지를 제공하는 JAF(JavaBeans Activation Framework) 확장이 필요하다. 난 Java8 버전이라 패스. (링크)

HTML

간단하게 제목, 본문, 첨부파일로 이루어진 페이지. 
Submit 버튼을 누르면 WebSendMail 서블릿의 doPost()로 데이터를 전송하며 
첨부파일을 다루어야 하므로 enctypemultipart/form-data로 선언한다.
<form action="WebSendMail" method="post" enctype="multipart/form-data">
<table border="1" cellpadding="0" cellspacing="0">
<tr>
   <td>Title</td>
   <td><input type="text" name="subject" size="40"></td>
</tr>
<tr>
   <td>Content</td>
   <td><textarea name="body" rows="10" cols="40"></textarea></td>
</tr>
<tr>
   <td>File</td>
   <td><input type="file" name="attachment"></td>
</tr>
<tr>
   <td colspan="2"><input type="submit" value="Submit"></td>
</tr>
</table>
</form>
cs

화면은 아래와 같다.

javax.activation.DataSource

파일첨부를 위해 DataSource 인터페이스를 상속해 구현한 클래스.
import java.io.*;
import javax.activation.*;
 
public class ByteArrayDataSource implements DataSource{
 
     byte[] bytes;
       String contentType;
       String name;
 
       ByteArrayDataSource(byte[] bytes, String contentType, String name) {
          this.bytes = bytes;
          if(contentType == null)
             this.contentType = "application/octet-stream";
          else
             this.contentType = contentType;
          this.name = name;
       }
 
       @Override
       public String getContentType() {
          return contentType;
       }
       
       @Override
       public InputStream getInputStream() {
          // 가장 마지막의 CR/LF를 없앤다.
          return new ByteArrayInputStream(bytes,0,bytes.length - 2);
       }
       
       @Override
       public String getName() {
          return name;
       }
       
       @Override
       public OutputStream getOutputStream() throws IOException {
          throw new FileNotFoundException();
       }
}
cs

getInputStream() 메서드에서 마지막 2글자를 빼는 이유는 웹브라우저가 웹서버에 multipart/form-data 인코딩 타입을 사용해 데이터를 전송하면 각 필드 마지막에 "\r\n"을 추가하기 때문이다.

doPost()

public class WebSendMail extends HttpServlet {
 
    String to = "실제로 메일을 받을 주소";
 
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        if (request.getContentType().startsWith("multipart/form-data")) {
            try {
                HashMap data = getMailData(request, response);
                sendMail(data);
 
                ServletContext sc = getServletContext();
                RequestDispatcher rd = sc.getRequestDispatcher("/thankyou.html");
                rd.forward(request, response);
            } catch (MessagingException ex) {
                throw new ServletException(ex);
            }
        } else {
            response.sendError(HttpServletResponse.SC_NOT_FOUND);
        }
    }
cs

POST방식으로 데이터를 전송했으므로 doPost() 메소드가 실행되며 

이용자가 전송한 데이터타입을 검사하여 multipart/form-data 라면 getMailData() 메서드로 이용자가 위에서 입력한 데이터를 읽어오고 그 데이터를 sendMail() 메서드로 실제 메일로 전송한다. 

성공적으로 제출되면 thankyou 페이지로 이동하도록 했다. 

getMailData()

getMailData()는 리턴타입이 HashMap으로 form태그 각각의 
name속성이 키 값, 
입력값이 밸류값이 된다.
private HashMap getMailData(HttpServletRequest request,
                            HttpServletResponse response)
        throws IOException, ServletException, MessagingException {
   String boundary = request.getHeader("Content-Type");
   int pos = boundary.indexOf('=');
   boundary = boundary.substring(pos + 1);
   boundary = "--" + boundary;
   ServletInputStream in = request.getInputStream();
   byte[] bytes = new byte[512];
   int state = 0;
   ByteArrayOutputStream buffer = new ByteArrayOutputStream();
   String name = null, value = null
          filename = null, contentType = null;
   HashMap mailData = new HashMap();
   
   int i = in.readLine(bytes,0,512);
   while(-1 != i) {
      String st = new String(bytes, 0, i);
      if(st.startsWith(boundary)) {
         state = 0;
         if(null != name) {
            if(value != null)
               // -2 to remove CR/LF
               mailData.put(name, value.substring(0, value.length() - 2));
            else if(buffer.size() > 2) {
               MimeBodyPart bodyPart = new MimeBodyPart();
               DataSource ds = new ByteArrayDataSource(
                  buffer.toByteArray(),
                  contentType, filename);
               bodyPart.setDataHandler(new DataHandler(ds));
               bodyPart.setDisposition(
                    "attachment; filename=\"" + filename + "\"");
               bodyPart.setFileName(filename);
               mailData.put(name, bodyPart);
            }
            name = null;
            value = null;
            filename = null;
            contentType = null;
            buffer = new ByteArrayOutputStream();
         }
      } else if(st.startsWith("Content-Disposition: form-data"&& state == 0) {
         StringTokenizer tokenizer = new StringTokenizer(st,";=\"");
         while(tokenizer.hasMoreTokens()) {
            String token = tokenizer.nextToken();
            if(token.startsWith(" name")) {
               name = tokenizer.nextToken();
               state = 2;
            } else if(token.startsWith(" filename")) {
               filename = tokenizer.nextToken();
               StringTokenizer ftokenizer = new StringTokenizer(filename,"\\/:");
               filename = ftokenizer.nextToken();
               while(ftokenizer.hasMoreTokens())
                  filename = ftokenizer.nextToken();
               state = 1;
               break;
            }
         }
      } else if(st.startsWith("Content-Type"&& state == 1) {
         pos = st.indexOf(":");
         // + 2 to remove the space
         // - 2 to remove CR/LF
         contentType = st.substring(pos + 2,st.length() - 2);
      } else if(st.equals("\r\n"&& state == 1)
         state = 3;
      else if(st.equals("\r\n"&& state == 2)
         state = 4;
      else if(state == 4)
         value = value == null ? st : value + st;
      else if(state == 3)
         buffer.write(bytes,0,i);
      i = in.readLine(bytes,0,512);
   }
   return mailData;
}
cs


스트림으로 데이터를 읽어와 MimeBodyPart 객체에 저장한다.


만약 서버에 업로드되어있는 파일을 직접 가져와 전송하거나 할 때는 아래와 같이 한다.

messageBodyPart = new MimeBodyPart();
String filename = "/home/file.txt";
DataSource source = new FileDataSource(filename);
messageBodyPart.setDataHandler(new DataHandler(source));
messageBodyPart.setFileName(filename);
multipart.addBodyPart(messageBodyPart);
cs

sendMail()

sendMail() 메서드는 MimeMessage 클래스를 이용해 이메일을 작성한 후 메일을 전송한다. 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
  private void sendMail(HashMap mailData) throws MessagingException {
        System.setProperty("mail.smtp.starttls.enable""true"); // gmail은 무조건 true 고정
        System.setProperty("mail.smtp.auth""true"); // gmail은 무조건 true 고정
        System.setProperty("mail.smtp.host""smtp.gmail.com"); // smtp 서버 주소
        System.setProperty("mail.smtp.port""587"); // gmail 포트
        //구글 인증
        Authenticator auth = new MyAuthentication();
        Message msg = new MimeMessage(Session.getDefaultInstance(System.getProperties(), auth));
        //받는사람
        InternetAddress[] tos = InternetAddress.parse(to);
        msg.setRecipients(Message.RecipientType.TO, tos);
        //한글을 위한 인코딩
        msg.setHeader("Content-Type""text/plain; charset=UTF-8");
        //제목
        msg.setSubject((String)mailData.get("subject"));
        msg.setSentDate(new Date());
 
        //첨부파일이 없으면 내용만 전송
        if(null == mailData.get("attachment")){
                 msg.setText((String)mailData.get("body"));
          } else {
            //첨부파일이 있으면
            BodyPart body = new MimeBodyPart();
              BodyPart attachment = (BodyPart)mailData.get("attachment");
              body.setText((String)mailData.get("body"));
              MimeMultipart multipart = new MimeMultipart();
              multipart.addBodyPart(body);
              multipart.addBodyPart(attachment);
              msg.setContent(multipart, "text/plain; charset=UTF-8");
           }
        //전송
        Transport.send(msg);
    }
cs

Authentication

마지막으로 구글의 SMTP 서버인증을 위한..
    class MyAuthentication extends Authenticator {
 
        private PasswordAuthentication pa;
        private String id;
        private String pw;
 
        private MyAuthentication() {
 
            id = "abcdefghi"// 구글 ID
            pw = "password"// 구글 비밀번호
            pa = new PasswordAuthentication(id, pw);
        }
 
        // 시스템에서 사용하는 인증정보
        public PasswordAuthentication getPasswordAuthentication() {
            return pa;
        }
    }
cs

결과

전송에 성공해 Thank you 페이지를 띄웠고


첨부파일을 포함한 메일도 잘 도착했다. (입력폼을 좀 더 늘려서 테스트함.)

참고1

실행파일(.exe)이나 .jar 따위의 파일을 첨부하면 메일을 전송하는 과정에서 보안문제로 거부당한다.

참고2

구글의 포터블 SMTP 서버 하루 메일 제한량은 99통이다. 24시간마다 한도는 초기화된다. 

참고3

구글계정 보안문제로 메일이 전송되지 않을 수 있다. 
구글의 보안 수준이 낮은 앱의 액세스로 들어가 "사용함" 으로 설정을 변경해주면 된다.


포스팅에 사용한 코드전문은 아래 깃헙에서.

https://github.com/SaintSilver/java-mail