이번 글에서는 DATA까지 구현했다.
이제 서버는 SMTP 클라이언트와 인사하고, 봉투를 받고, 본문을 받은 뒤, 마침표 한 줄로 메일 데이터가 끝났다는 것도 알아볼 수 있다.
여기까지 오면 메일 한 통을 받는 가장 중요한 흐름은 대략 보인다.
하지만 RFC 5321의 최소 구현 목록을 보면 아직 몇 가지 명령이 더 남아 있다.
이번에 추가한 것은 RSET, NOOP, VRFY다.
이 명령들은 MAIL FROM, RCPT TO, DATA처럼 메일을 직접 구성하는 주인공은 아니다.
대신 SMTP 세션 중간에서 상태를 정리하거나, 연결이 살아 있는지 확인하거나, 주소 확인을 요청하는 보조 명령에 가깝다.
RSET: 지금 쓰던 편지는 취소하겠습니다
먼저 RSET이다.
C: RSET
S: 250 OK
RSET은 현재 진행 중인 메일 트랜잭션을 취소하는 명령이다.
예를 들어 클라이언트가 봉투를 만들다가 중간에 이 메일을 더 이상 보내지 않기로 했다고 해보자.
C: MAIL FROM:<alice@example.com>
S: 250 OK
C: RCPT TO:<bob@example.net>
S: 250 OK
여기까지 오면 서버는 현재 트랜잭션 안에 이런 정보를 들고 있다.
봉투 발신자: alice@example.com
봉투 수신자: bob@example.net
그런데 이 상태에서 클라이언트가 RSET을 보내면, 서버는 지금까지 쌓아둔 봉투 정보를 지운다.
C: RSET
S: 250 OK
편지에 비유하면 이런 느낌이다.
방금 쓰던 봉투와 편지는 취소하겠습니다.
연결은 끊지 말고, 새 편지를 다시 시작하겠습니다.
중요한 점은 RSET이 연결 종료 명령은 아니라는 것이다.
서버는 상태만 초기화하고 연결은 유지한다. RFC 5321도 RSET은 현재 메일 트랜잭션을 중단하고 저장된 발신자, 수신자, 메일 데이터를 삭제해야 하지만, 이 명령을 받았다는 이유로 연결을 닫아서는 안 된다고 설명한다.
이번 구현에서는 이렇게 처리했다.
case "RSET":
resetSession()
if err := writeLine("250 OK"); err != nil {
return
}
아직 우리 서버의 상태는 단순하다.
from, rcpt, message를 비우고 다시 명령을 받을 수 있는 상태로 되돌리면 충분하다.
다만 구현하면서 한 가지는 조심해야 한다.
만약 resetSession()이 무조건 상태를 StateReady로 바꾸는 함수라면, EHLO나 HELO를 하기 전에 RSET을 보낸 클라이언트도 바로 MAIL FROM으로 들어갈 수 있게 될 수 있다.
그래서 조금 더 정확히 구현하려면 “세션 전체 초기화”와 “현재 메일 트랜잭션만 초기화”를 나누는 편이 좋다.
예를 들면 이런 식이다.
resetSession := func(keepState bool) {
if !keepState {
state = StateReady
}
from = ""
rcpt = []string{}
message = ""
}
// EHLO/HELO에서
resetSession(false) // state도 변경
// RSET에서
resetSession(true) // state는 유지
지금 단계에서는 큰 문제가 되지 않을 수 있지만, SMTP는 상태가 중요한 프로토콜이기 때문에 이런 작은 차이가 나중에 의미를 갖게 된다.
NOOP: 아무것도 하지 않기
다음은 NOOP이다.
C: NOOP
S: 250 OK
이름 그대로 아무 일도 하지 않는 명령이다.
처음 보면 조금 이상하다.
아무 일도 하지 않을 거라면 왜 명령이 필요할까?
하지만 네트워크 연결에서는 “아무 일도 하지 않았지만, 서버가 아직 살아 있고 응답할 수 있다”는 사실 자체가 의미가 있을 때가 있다.
C: NOOP
S: 250 OK
이 대화는 대략 이렇게 읽을 수 있다.
클라이언트: 아직 거기 있나요?
서버: 네, 있습니다.
NOOP은 현재 트랜잭션의 발신자, 수신자, 본문 데이터에 영향을 주지 않는다. RFC 5321도 NOOP은 매개변수나 이전 명령에 영향을 주지 않으며, 서버가 250 OK를 반환하는 것 외에는 어떤 작업도 지정하지 않는다고 설명한다.
그래서 구현도 가장 단순하다.
case "NOOP":
if err := writeLine("250 OK"); err != nil {
return
}
이 명령은 상태를 바꾸지 않는다.
봉투도 지우지 않는다.
본문도 건드리지 않는다.
그저 응답만 한다.
250 OK
SMTP를 구현하다 보면 이런 명령도 필요하다는 점이 재미있다.
모든 명령이 무언가를 바꾸지는 않는다.
어떤 명령은 아무것도 바꾸지 않기 위해 존재한다.
VRFY: 이 주소가 존재하나요?
마지막은 VRFY다.
C: VRFY bob@example.net
S: 252 Cannot VRFY user, but will accept message and attempt delivery
VRFY는 이름 그대로 verify, 즉 확인을 요청하는 명령이다.
클라이언트가 서버에게 묻는다.
이 사용자를 확인할 수 있나요?
정상적으로 확인할 수 있는 서버라면 250 응답과 함께 메일박스 정보를 돌려줄 수 있다.
하지만 지금 우리가 만든 서버에는 아직 사용자 DB도 없고, 실제 메일박스도 없고, 도메인별 수신자 검증도 없다.
그러니 여기서 250 OK를 보내면 안 된다.
그건 “확인했다”고 말하는 것이기 때문이다.
지금 우리 서버가 할 수 있는 말은 이것에 가깝다.
확인은 못 합니다.
하지만 메시지를 받는 흐름 자체는 시도할 수 있습니다.
그래서 252를 반환한다.
case "VRFY":
if err := writeLine("252 Cannot VRFY user, but will accept message and attempt delivery"); err != nil {
return
}
이 응답은 꽤 정직하다.
서버가 실제로 주소를 확인하지 않았으면서 확인한 척하지 않는다.
RFC 5321도 서버가 실제로 주소를 확인하지 않는 한 VRFY나 EXPN에 대해 250을 반환해서는 안 된다고 설명한다. 주소가 유효해 보이지만 실시간으로 합리적으로 확인할 수 없는 경우에는 252 응답을 사용할 수 있다.
이 부분은 SMTP 구현을 하면서 처음 만나는 “모른다고 말하는 응답”처럼 느껴진다.
성공도 아니고, 실패도 아니다.
그냥 이렇게 말한다.
나는 지금 확인할 수 없다.
이건 실제 서버 구현에서도 중요하다.
확인할 수 없는 것을 확인했다고 말하면 안 된다.
이번에 추가한 코드
이번에 추가한 코드는 아주 짧다.
case "RSET":
resetSession(true)
if err := writeLine("250 OK"); err != nil {
return
}
case "NOOP":
if err := writeLine("250 OK"); err != nil {
return
}
case "VRFY":
if err := writeLine("252 Cannot VRFY user, but will accept message and attempt delivery"); err != nil {
return
}
짧지만 의미는 각각 다르다.
RSET → 현재 메일 트랜잭션을 취소한다.
NOOP → 아무것도 하지 않고 응답만 한다.
VRFY → 사용자를 확인할 수 있는지 묻지만, 지금은 확인하지 못한다고 답한다.
이 셋을 추가하면 현재 서버는 RFC 5321의 최소 구현 명령 목록에 조금 더 가까워진다. 작업 지침서에서도 Phase 1 체크리스트에 RSET, NOOP, VRFY 처리를 포함하고 있다.
부록을 마치며
이번 부록에서 추가한 명령들은 메일을 직접 만들어내지는 않는다.
RSET은 쓰던 메일을 취소한다.
NOOP은 아무것도 하지 않는다.
VRFY는 사용자를 확인할 수 있는지 묻는다.
그래서 이 명령들은 본문 흐름의 주인공은 아니다.
하지만 SMTP 서버가 하나의 대화 상대라면, 이런 말들도 알아들을 수 있어야 한다.
메일을 보내는 말.
메일을 취소하는 말.
아무것도 하지 않고 연결만 확인하는 말.
확인할 수 없는 것을 확인할 수 없다고 말하는 말.
이런 작은 명령들이 모여서 SMTP 세션은 조금 더 서버다운 모양을 갖춘다.
다음 글에서는 이제 로그에만 찍고 있던 메시지를 조금 더 제대로 다뤄보려고 한다.
서버가 메일을 받았다고 말하려면, 그 메일은 어디엔가 남아야 한다.
250 OK라고 말한 뒤에도 사라지지 않도록, 이제 받은 메시지를 저장하는 쪽으로 나아가보자.
'Deep Dive > 기타' 카테고리의 다른 글
| [SMTP] 6. 메일은 어디에 남아야 할까 (0) | 2026.05.07 |
|---|---|
| [SMTP] 5. 본문은 언제 끝났다고 말할까 (0) | 2026.05.07 |
| [SMTP] 4. 봉투와 편지는 다르다. (1) | 2026.05.07 |
| [SMTP] 3. SMTP 서버와 첫 대화: 서버가 먼저 인사한다 (0) | 2026.05.07 |
| [SMTP] 2. SMTP 지도 그리기: 메일은 어디에서 어디로 가는가 (0) | 2026.05.07 |
