JavaMail API
프로젝트 중 폼태그 부분에서 파일을 첨부해 메일을 보내는 기능이 필요했다.
조건
- 이용자에게 추가 액션을 요구하지 않고 폼태그 안에서 메일을 전송할 것.
- 파일을 첨부하면 저장과정 없이 다이렉트로 메일이 전송되게 할 것.
- 빠르고 전송이 정확할 것.
JavaMail API에 Google SMTP 서버를 이용하기로 했다.
Google SMTP 서버를 이용하면 지메일을 이용해 검색/색인이 쉽고, 스팸으로 차단당할 가능성이 줄어들고 무엇보다 무료다.
Setup
Maven을 이용한다면 아래 코드 복붙.
<!-- https://mvnrepository.com/artifact/javax.mail/mail --> <dependency> <groupId>javax.mail</groupId> <artifactId>mail</artifactId> <version>1.4</version> </dependency> | cs |
HTML
<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
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()
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()
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
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
참고2
참고3
포스팅에 사용한 코드전문은 아래 깃헙에서.
https://github.com/SaintSilver/java-mail