메일 구현은 해보았으나 SMTP 를 정확히 이해하지 못한 거 같아 글을 작성하게 되었다.
이메일은 어떻게 전송될까?
인터넷 이메일 시스템 구조를 볼 때 크게 3개의 요소로 구분할 수 있다.
1. User Agent
사용자 장치이며, 메일을 작성하거나 읽기 등의 기능을 수행한다.
일반적으로 컴퓨터나 스마트폰 등을 생각하면 된다.
2. Mail Server
메일을 송수신을 제어하고, 다수 사용자들의 메일박스를 관리한다.
구글이나 네이버와 같은 메일 서버들을 칭한다.
메일 서버는 메일 전송을 위한 출력큐 (outgoing message queue) 와, 메일을 관리하는 메일박스 (mail box) 를 지닌다.
3. SMTP (Simple Mail Transfer Protocol)
메일을 전송할 때 사용되는 프로토콜이다.
- TCP 기반의 프로토콜이다.
- TCP 기반이라는것은 신뢰성을 보장한다는 것이다. 정확히 보내지거나, 아니거나, 그 결과를 명확하게 사용자가 알 수 있다.
- 서버 - 클라이언트 형태의 프로토콜이다.
- ASCII 문자만을 메세지로 보낼 수 있다. (이 부분은 아래에서 더 상세하게 다뤄보자.)
4. Mail Access Protocol
메일 데이터에 접근할때 사용되는 프로토콜이다.
전통적으로는 POP, IMAP이 있으며 웹메일 형태에서는 HTTP 를 주로 사용한다.
메일 전송의 대략적인 흐름을 살펴보자.
처음으로, 컴퓨터 또는 스마트폰과 같은 기기에서 송신자가 메일을 작성한다.
작성하게 되면 송신자 메일서버의 출력큐 (outgoing message queue) 에 메일이 저장이 된다.
이후, SMTP 를 통하여 수신자의 메일 서버로 메일을 전송하게 된다.
(이 때 전송 불가시 30분 단위로 재전송을 시도하고 설정한 시간이 time out이 된다면 송신자에게 알려준다.)
정상적으로 전송이 된다면, 수신자 메일서버의 메일박스 (mailbox) 에 저장된다.
이후 수신자의 기기에서 Mail Access Protocol 을 통하여 메일을 읽고 관리한다.
SMTP 흐름과 구조
그러면 SMTP 는 내부적으로 어떻게 메일을 주고 받을까?
우선 두 서버간의 TCP Connection 을 진행한다.
정상적으로 연결이 된다면 수신서버에서 서비스를 제공할 수 있음을 알려준다.
이후 송신서버에서 자신의 메일 도메인을 알려주고, 메일 전송이 진행된다.
전송할 데이터를 모두 보냈다면, 송신서버에서 끝을 알리고, 수신서버도 연결을 해제한다.
참고로 SMTP 에는 전용 명령어가 있다. HELLO, MAIL FROM, RCPT TO, DATA, QUIT .. 이런 명령어를 통해서 도메인을 알려주거나, 송수신자를 지정하거나, 데이터 전송을 알려주는 등 행동을 명시할 수 있다.
(명령어에 대한 자세한 설명은 https://mailtrap.io/blog/smtp-commands-and-responses/ 를 확인해보자.)
메일 전송 부분 (위에서 SEND MAIL이라 표현한 부분) 을 상세히 살펴보자.
메일을 전송하는데에 있어서는 기본적인 포맷이 존재하고, envelope (봉투) / message (내용) 로 구성된다.
envelope 부분은 말 그대로 편지의 봉투같은 존재이다. 송신자와 수신자 정보가 담겨있으며 이 정보를 가지고 SMTP 를 통하여 메일을 전송하게 된다.
이 과정에서도 SMTP 명령어를 활용하는데, envelope 부분에서는 대표적으로 MAIL FROM 과 RCPT TO 가 있다.
MAIL FROM 은 송신자의 메일 주소를 설정하는 명령어이며, RCPT TO 는 수신자의 메일 주소를 설정하는 명령어이다.
message 부분은 수신자에게 보여질 내용이다. 헤더부분은 제목이나 송수신자에 대한 정보를 지니고 있으며 바디부분은 메일의 본문이다. DATA 명령어로 이제 message 부분을 전송한다는것을 알리고, 헤더부분과 바디부분은 공백 한 줄로 구분한다. 송신측에서 ' . ' 를 보냄으로써 message 부분 전송이 끝났음을 알려준다.
통신의 결과는 위와 같은 형태로 나타나게 된다.
※ 참고로 본문 내용은 오직 ASCII 코드의 데이터만 허용된다!
SMTP를 규정할때는 아스키 문자의 데이터만 보낼것이라고 생각했다.
그러나 시간이 지남으로써 다양한 언어와 미디어 등을 보내게 됨으로써
MIME 형태로 데이터를 인코딩 하여 보내게 되었다.
구현 (Java)
자바는 javax.mail 패키지에서 SMTP 를 지원한다. 위 내용과 더불어 기본적인 SMTP 를 이해하고 나면 공식문서를 참조해서 구현하는것은 어렵지 않다. 공식문서
import javax.mail.*;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import java.util.Properties;
public class Main {
public static void main(String[] args) {
final String email = "sender@naver.com";
final String to = "receipent@gmail.com";
final String password = "password";
Properties props = new Properties();
props.put("mail.smtp.host", "smtp.naver.com"); // smtp mail server 를 설정한다.
props.put("mail.smtp.port", 465); // SMTP 서버 포트 설정
// props.put("mail.smtp.starttls.enable", "true"); // TLS 암호화
props.put("mail.smtp.ssl.enable", "true"); // SSL 암호화
props.put("mail.smtp.auth", true); // SMTP 서버에 인증이 필요함을 나타낸다.
// 인증을 통해 세션을 가져옴.
Session session = Session.getDefaultInstance(props, new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(email, password);
}
});
try {
MimeMessage message = new MimeMessage(session);
message.setFrom(new InternetAddress(email)); // 발신자 설정
message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(to)); // 수신자 설정
// 제목 설정
message.setSubject("메일 전송 테스트중입니다.");
message.setText("안녕하세요, 이것은 JavaMail API를 사용한 테스트 이메일입니다.");
Transport.send(message);
} catch (MessagingException e) {
throw new RuntimeException();
}
}
}
smtp 호스트와 포트, 관련 정보들을 Properties에 세팅하고 이후 계정 인증을 진행한다.
이후 송신자 수신자를 지정하고 제목 및 내용을 설정하고 전송을 한다.
구현은 공식문서를 참조해서 SMTP 스펙을 그대로 코드로 옮겨둔거이기에 크게 어려운 부분은 없다.
참고로, 나는 보낼때 네이버 메일을 이용했는데 해당 계정 설정에서 SMTP 를 허용해줘야 네이버 메일서버에서 전송이 가능해진다.
메일함 설정으로 들어가서 SMTP 를 사용으로 바꿔준다.
추가적으로 이 페이지에서 SMTP 서버명과 보안 연결에 따른 port 도 알려주니 메일을 보낼때 참조하면 된다.
메일 서비스를 제공하는 웹 메일 서버들은 대부분 위와 비슷한 프로세스를 거쳐야 메일 전송이 가능하다.
참조 (Oracle SMTP)
참고로 Java 뿐 아니라 Database 에서도 SMTP 구현을 지원하기도 한다.
대표적으로 Oracle 에서는 UTL_TCP, UTL_SMTP 로 구현이 가능하다. (공식문서)
공식문서에서 제공되는 코드는 아래와 같다.
DECLARE
c UTL_SMTP.CONNECTION;
PROCEDURE send_header(name IN VARCHAR2, header IN VARCHAR2) AS
BEGIN
UTL_SMTP.WRITE_DATA(c, name || ': ' || header || UTL_TCP.CRLF);
END;
BEGIN
c := UTL_SMTP.OPEN_CONNECTION('smtp-server.acme.com');
UTL_SMTP.HELO(c, 'foo.com');
UTL_SMTP.MAIL(c, 'sender@foo.com');
UTL_SMTP.RCPT(c, 'recipient@foo.com');
UTL_SMTP.OPEN_DATA(c);
send_header('From', '"Sender" <sender@foo.com>');
send_header('To', '"Recipient" <recipient@foo.com>');
send_header('Subject', 'Hello');
UTL_SMTP.WRITE_DATA(c, UTL_TCP.CRLF || 'Hello, world!');
UTL_SMTP.CLOSE_DATA(c);
UTL_SMTP.QUIT(c);
EXCEPTION
WHEN utl_smtp.transient_error OR utl_smtp.permanent_error THEN
BEGIN
UTL_SMTP.QUIT(c);
EXCEPTION
WHEN UTL_SMTP.TRANSIENT_ERROR OR UTL_SMTP.PERMANENT_ERROR THEN
NULL; -- When the SMTP server is down or unavailable, we don't have
-- a connection to the server. The QUIT call raises an
-- exception that we can ignore.
END;
raise_application_error(-20000,
'Failed to send mail due to the following error: ' || sqlerrm);
END;
PL/SQL 이 익숙하지 않으면 어려울 수 있지만..
결국 SMTP 스펙에 따라 코드가 작성되는것을 볼 수 있다. (호스트 설정, 송수신자 설정, 제목 및 내용 설정)
자바랑 조금 다른 재밌는 부분은 데이터의 끝은 CRLF 로 표현하는데 자바에서는 메소드를 호출하면 내부적으로 처리해주지만 여기서는 직접 명시해줘야 하는것! 어떻게 보면 SMTP 와 완전히 1:1 매핑되는 구현 형태라서 이해하기엔 더 좋은 거 같다.
참조
https://taehyeki.tistory.com/155
https://www.youtube.com/watch?v=maNnhEw4bzk
https://byeong9935.tistory.com/22
https://www.perplexity.ai/search/smtp-eseo-meil-jeonsong-bubun-pXNmpp7oT5una2bHerIIfA
'개발' 카테고리의 다른 글
Base64 와 Base64Url Safe (0) | 2025.04.01 |
---|---|
[Oracle / Tibero ] Synonym 이슈 / Synonym이란? (0) | 2024.09.21 |
OAuth 회원가입, 로그인 유지를 어떻게 해야할까? (0) | 2023.11.06 |
Nest JS 프로젝트 배포 자동화 하기 (3) - 스크립트 작성 (0) | 2023.11.05 |
Termius 로 ec2 ssh 접속하기 (0) | 2023.11.02 |