사전 SMTP
프로토콜

SMTP

gabury1고친 사람 github-actions[bot]

SMTP 는 메일 한 통을 받는 쪽 메일 서버까지 실어 나르는 프로토콜입니다. 보내는 주소와 받는 주소를 먼저 알립니다. 그다음에 본문을 넘깁니다. 서버는 명령 한 줄마다 숫자 코드로 답합니다. 메일함에 쌓인 메일을 꺼내 읽는 일은 하지 않습니다.

쉽고 빠른 이해

SMTP 는 메일 한 통을 상대 메일 서버까지 배달하는 약속입니다. 보내는 프로그램이 서버에 접속해 누가 보내고 누구에게 가는지를 먼저 말하고, 그다음에 본문을 넘깁니다.

이게 없으면 메일 프로그램마다 배달 방식이 달라 서로 메일을 주고받지 못합니다. 세상의 메일 서버가 모두 같은 말로 이야기하게 만드는 것이 이 프로토콜의 일입니다.

어떻게 도는가:

  1. 보내는 쪽이 받는 쪽 서버에 접속하면 서버가 먼저 인사를 건넵니다
  2. 보내는 주소와 받는 주소를 한 줄씩 알리고, 서버는 줄마다 받아들일지 숫자로 답합니다
  3. 본문을 넘긴 뒤 점 하나만 있는 줄로 끝을 알리면 서버가 접수합니다
  4. 지금 못 보낸 메일은 버리지 않고 쌓아 뒀다가 나중에 다시 보냅니다

대가도 있습니다. 접수했다는 답은 서버가 맡았다는 뜻이지 사람에게 닿았다는 뜻이 아닙니다. 그리고 보내는 사람이 진짜인지 확인하는 장치가 프로토콜 안에 없습니다. 그래서 주소를 속이는 일을 SMTP 혼자서는 막지 못합니다.

상세

SMTP(Simple Mail Transfer Protocol, 단순 메일 전송 프로토콜)는 메일 한 통을 한 서버에서 다른 서버로 옮기는 프로토콜입니다. 목표는 메일을 믿을 수 있게, 효율적으로 옮기는 것입니다. 현행 규격 문서는 RFC(Request for Comments) 5321 입니다.

여기서 다루는 것은 다섯입니다. SMTP 가 메일의 길에서 맡는 구간, 메일 한 통이 무엇으로 이루어지나, 시스템이 맡는 네 역할, 받는 서버를 찾는 순서, 제출과 중계가 갈린 이유입니다.

메일이 지나가는 길에서 SMTP 가 맡는 구간

SMTP 의 대화에는 두 편이 있습니다. 보낼 메일을 가진 쪽이 SMTP 클라이언트입니다. 받는 쪽은 SMTP 서버입니다. 클라이언트가 서버에 양방향 전송 채널을 열고 명령을 보내면 서버가 명령마다 응답을 돌려줍니다.

클라이언트의 책임은 둘 중 하나입니다. 메일을 하나 이상의 서버에 넘기거나, 못 넘겼다고 알리는 것입니다.

SMTP 가 채널에 요구하는 것은 하나뿐입니다. 데이터가 빠짐없이, 보낸 순서대로 도착하는 스트림이면 됩니다. 현행 규격은 TCP(Transmission Control Protocol, 전송 제어 프로토콜) 위에서 도는 경우를 다룹니다. 다른 전송 위에서도 돌 수 있습니다.

SMTP 라는 공통 약속이 있어서 만든 곳이 서로 다른 메일 서버끼리도 메일을 넘길 수 있습니다. 보내는 쪽과 받는 쪽이 같은 네트워크에 붙어 있지 않아도 됩니다. 두 네트워크에 다 닿는 중계 서버를 거쳐 넘기면 됩니다.

메일 한 통은 대개 서버를 여럿 거칩니다. 사람이 쓰는 메일 프로그램을 MUA(Message User Agent, 메일 사용자 에이전트)라고 부릅니다. MUA 가 새 메일을 처음 맡기는 서버가 MSA(Message Submission Agent, 메시지 제출 에이전트)입니다. 메일을 서버에서 서버로 넘기는 쪽은 MTA(Message Transfer Agent, 메시지 전송 에이전트)입니다.

SMTP 는 이 가운데 맡기는 구간과 넘기는 구간을 맡습니다. 사슬의 마지막 서버는 메일을 메시지 저장소에 넣어 둡니다. 받는 사람의 MUA 가 거기서 꺼내 읽는 일은 POP3(Post Office Protocol 3, 우체국 프로토콜 3)나 IMAP(Internet Message Access Protocol, 인터넷 메시지 접근 프로토콜)이 합니다.

flowchart TD
    A["보내는 사람의 MUA"] -->|"SMTP · 제출"| B["MSA"]
    B -->|"SMTP · 전송"| C["중계 MTA"]
    C -->|"SMTP · 전송"| D["마지막 서버"]
    D -->|"메일함에 넣는다"| E["메시지 저장소"]
    E -->|"POP3 · IMAP"| F["받는 사람의 MUA"]

그림의 화살표 다섯 가운데 위의 셋이 SMTP 입니다. 중계 MTA 는 없을 수도 있고 여럿일 수도 있습니다. 아래 둘은 SMTP 가 아닙니다. 넷째는 마지막 서버가 메일을 메시지 저장소에 넣는 과정입니다. 맨 아래는 받는 사람이 꺼내 읽는 구간입니다.

봉투와 내용

SMTP 가 나르는 것은 메일 객체 하나입니다. 메일 객체는 봉투(envelope)와 내용(content)으로 이루어집니다.

봉투는 SMTP 명령으로 오갑니다. 보내는 주소 하나와 받는 주소 하나 이상이 들어갑니다. 보내는 주소는 반환 경로(reverse-path)라고 부릅니다. 오류 보고는 이 주소로 갑니다. 받는 주소는 전달 경로(forward-path)입니다. 서버가 지원한다고 알린 확장을 쓰기로 하면 그 확장이 쓰는 값이 봉투에 덧붙기도 합니다.

내용은 DATA 명령 뒤에 넘어갑니다. 내용은 다시 헤더 섹션과 본문으로 나뉩니다. 헤더 섹션은 From: · To: · Subject: 같은 헤더 필드의 모음입니다. 그 문법은 인터넷 메시지 형식 규격이 정합니다. 본문에 구조가 있으면 MIME(Multipurpose Internet Mail Extensions, 다목적 인터넷 메일 확장)이 그 구조를 정합니다.

flowchart TD
    O["메일 객체"] --> ENV
    O --> CON
    subgraph ENV["봉투 · 명령으로 오간다"]
        M["MAIL · 반환 경로 하나"]
        R["RCPT · 전달 경로 하나 이상"]
    end
    subgraph CON["내용 · DATA 뒤에 넘어간다"]
        H["헤더 섹션 · From · To · Subject"]
        B["본문"]
    end

배달은 봉투를 보고 이뤄집니다. 그래서 봉투의 보내는 주소와 헤더의 From: 은 같은 값이 아니어도 됩니다. 둘이 다른 것이 정당한 경우도 있습니다. 한 사람이 다른 사람을 대신해 보내는 메일, 오류 답장을 따로 정한 주소로 받으려는 메일이 그렇습니다.

시스템이 맡는 네 역할

SMTP 를 쓰는 시스템은 메일의 길에서 맡는 역할에 따라 넷으로 나뉩니다. 앞 소절의 MUA · MSA · MTA 와는 다른 축의 가름입니다. 프로그램의 종류가 아니라 그 시스템이 메일을 나르면서 맡는 몫으로 나눈 것입니다.

아래 표에 나오는 전송 환경(transport environment)은 메일을 나르는 규칙이 한 벌로 통하는 테두리를 말합니다. 인터넷 메일이 그런 테두리 하나입니다. 테두리가 다르면 프로토콜이나 메시지의 뜻이 달라서, 건너 넘길 때 메시지를 바꿔야 할 수 있습니다.

역할 하는 일
발신 (originating) 메일을 인터넷이나 전송 환경에 처음 들여보낸다
배달 (delivery) 전송 환경에서 메일을 받아 MUA 에 넘기거나, MUA 가 나중에 읽을 메시지 저장소에 넣는다
중계 (relay) SMTP 클라이언트에게서 받은 메일을 다음 SMTP 서버로 넘긴다. 메시지 데이터는 고치지 않고 추적 정보만 더한다
게이트웨이 (gateway) 한 전송 환경에서 받은 메일을 다른 전송 환경으로 넘긴다. 양쪽 환경이 달라 메시지를 바꿔야 할 수 있다

중계 서버는 메일을 받고 나면 스스로 SMTP 클라이언트가 되어 다음 서버에 접속합니다. 중계 서버가 더하는 추적 정보는 자기 몫의 Received: 헤더 필드입니다. 그것을 더하는 일과 메일이 서버 사이를 맴도는지 살피는 일 말고는 헤더나 본문을 들여다보지도 고치지도 않습니다(MUST NOT).

주소를 고쳐 쓰는 방화벽은 양쪽이 다 SMTP 여도 게이트웨이로 칩니다. 중계에는 허락되지 않는 변환을 하기 때문입니다.

받는 서버를 찾는 순서

보내는 쪽이 아는 것은 받는 주소의 도메인 이름뿐입니다. 이 이름으로 접속할 서버를 찾는 데 DNS(Domain Name System, 도메인 이름 시스템)를 씁니다. 도메인마다 MX(Mail eXchanger, 메일 교환기) 레코드를 둘 수 있습니다. MX 레코드에는 그 도메인의 메일을 받아 줄 서버 이름과 우선순위 숫자가 적혀 있습니다.

조회 결과에 따라 이렇게 갈립니다.

flowchart TD
    Q["받는 도메인의 MX 를 조회한다"] --> R{"결과"}
    R -->|"도메인이 없다"| ERR["오류로 보고한다"]
    R -->|"일시 오류"| QUE["큐에 넣고 나중에 다시"]
    R -->|"MX 가 없다"| IMP["도메인 자신을 가리키는 우선순위 0 의 MX 로 친다"]
    R -->|"MX 가 있다"| SORT["숫자가 작은 것부터 늘어놓는다"]
    IMP --> ADDR["서버 이름의 주소를 조회해 차례로 접속한다"]
    SORT --> ADDR

MX 레코드가 하나도 없으면 그 도메인 이름 자신을 가리키는 MX 가 우선순위 0 으로 하나 있는 것처럼 다룹니다. 이것을 암묵 MX 라고 부릅니다. 반대로 MX 가 하나라도 있으면 MX 를 거치지 않고 그 이름의 주소 레코드를 바로 쓰면 안 됩니다. 주소 레코드는 그 이름이 가리키는 기계의 IP(Internet Protocol, 인터넷 프로토콜) 주소를 담은 DNS 레코드입니다. A 와 AAAA 가 그것입니다. MX 가 있는데 쓸 수 있는 것이 하나도 없으면 오류입니다.

우선순위 숫자는 작을수록 먼저입니다. 숫자가 같은 MX 가 여럿이고 그중 하나를 고를 뚜렷한 이유가 없으면, 보내는 쪽이 순서를 무작위로 섞습니다(MUST). 한 조직의 여러 메일 서버에 부하를 나누려는 것입니다.

MX 에 적힌 서버 이름을 다시 조회하면 주소 레코드가 나옵니다. 보내는 쪽은 이렇게 얻은 주소 목록을 앞에서부터 차례로 시도합니다. 시도할 주소 수에 상한을 둘 수는 있지만 적어도 둘은 시도하는 것이 좋습니다(SHOULD).

중계하는 서버가 같은 MX 목록을 조회할 때는 규칙이 하나 더 붙습니다. 목록을 우선순위로 늘어놓고, 자기가 메일 거래에서 알려진 이름이나 주소가 들어 있는 줄을 찾습니다. 찾으면 그 줄과 그보다 큰 숫자의 줄을 전부 후보에서 버려야 합니다(MUST). 남는 줄이 없으면 오류입니다. 그 메일은 배달 못 함으로 돌려보냅니다(MUST).

제출과 중계가 갈린 이유

SMTP 는 원래 다 만들어진 메일을 경로를 따라 옮겨 배달하는 전송 프로토콜로 정의됐습니다. 그런데 MUA 가 새 메일을 처음 들여보낼 때도 SMTP 를 씁니다. 이 일을 제출(submission)이라고 부릅니다. 서버끼리 넘기는 전송(transfer)과는 가릅니다.

가른 까닭은 셋입니다.

  • 인증 — 서버끼리의 중계에는 대개 인증을 걸지 않습니다. 누구와도 제약 없이 메일을 주고받게 하려는 것입니다. 반면 제출 서버에는 자기가 들여보내는 메일에 책임을 지라는 기대가 점점 커졌습니다. 그래서 제출에는 인증이 필요해졌습니다
  • 스팸 차단 — 웜이나 바이러스에 걸린 기계가 대량의 스팸을 뿜는 일이 흔해졌습니다. 많은 곳이 표준 SMTP 포트인 25 번으로 나가는 트래픽을 막습니다. 모든 제출은 제출 서버로 몰아넣습니다
  • 덜 된 메일 채우기 — 제출되는 메일은 덜 만들어져 있기도 합니다. Date 헤더가 없거나 도메인 이름이 완전하지 않은 식입니다. 이걸 첫 서버 뒤의 MTA 가 고치면 해가 된다는 것이 드러났습니다. 그런 손질은 표준이 정한 MTA 의 일 밖으로 봅니다. 그래서 채우는 일은 처음 받는 제출 서버가 합니다
통로 포트 누가 쓰나 인증
전송 (중계) 25 서버끼리 대개 걸지 않는다
제출 587 MUA → MSA 기본으로 요구한다
제출 (접속하자마자 암호화) 465 MUA → MSA 587 과 같다

587 번 포트로 들어온 메일은 제출로 정의됩니다. 제출 서버는 인증되지 않은 세션의 MAIL 명령에 기본으로 오류를 돌려줘야 합니다(MUST). 보호된 내부망 안처럼 다른 방법으로 이미 인증된 경우만 예외입니다. 사정이 있으면 한 사이트가 25 번 포트를 제출용으로 쓸 수도 있습니다(MAY).

465 번 포트는 접속하자마자 TLS(Transport Layer Security, 전송 계층 보안) 핸드셰이크부터 시작하는 제출 통로입니다. 이 방식을 암묵적 TLS 라고 부릅니다. 587 번에서는 평문으로 접속한 뒤 STARTTLS 명령으로 감쌉니다.

지금 널리 깔려 있는 것은 587 번의 STARTTLS 쪽입니다. 메일 프로그램이 쓰는 프로토콜은 시간을 두고 암묵적 TLS 로 옮겨 가는 것이 바람직합니다. 다만 제출에 암호화가 최대한 쓰이게 하려면 여러 해의 과도기 동안은 두 방식을 다 구현하는 것이 좋습니다(SHOULD). 양쪽 구현이 올바르고 제출 전에 TLS 협상 성공을 요구하도록 설정돼 있다면 두 방식의 보안 성질에는 뚜렷한 차이가 없습니다.

교환 순서

SMTP 클라이언트와 서버가 한 연결 안에서 무엇을 어떤 차례로 주고받는지가 이 절의 내용입니다. 대상은 메일 한 통을 넘기는 세션 하나입니다. 명령마다 어떤 응답이 오고, 그 뒤에 무엇이 허용되는지를 따라갑니다.

먼저 줄의 꼴입니다. 명령은 모두 명령어 낱말로 시작합니다. 응답은 모두 숫자 세 개로 된 코드로 시작합니다. 줄은 <CRLF>(캐리지 리턴과 라인 피드, 줄바꿈 글자 두 개)로 끝납니다. 받는 쪽은 이 끝을 받을 때까지 아무것도 하지 않습니다.

대화는 일부러 한 번에 하나씩 갑니다. 명령 하나를 보내면 응답 하나가 옵니다. 그 응답을 본 뒤에 다음 명령을 보냅니다. 양쪽이 합의한 확장(명령 파이프라이닝 같은)이 있을 때만 이 규칙이 느슨해집니다.

한 세션의 차례

보내는 주소를 알리는 데서 내용의 끝까지, 메일 한 통을 넘기는 명령 한 벌을 거래(mail transaction)라고 부릅니다. 연결 하나에서 열리는 대화 전체는 세션입니다. 세션 하나에 거래가 여럿 들어갈 수 있습니다.

정상인 세션 하나는 이렇게 흐릅니다.

  1. 클라이언트가 연결을 엽니다. 서버가 먼저 220 인사를 보냅니다
  2. 클라이언트가 EHLO 에 자기 도메인 이름을 실어 보냅니다. 서버는 대개 여러 줄의 250 으로 답합니다. 첫 줄에 자기 이름이, 다음 줄부터 자기가 지원하는 확장의 키워드가 한 줄에 하나씩 옵니다
  3. MAIL FROM:<반환 경로> 로 새 거래를 엽니다. 받아들이면 250 입니다
  4. RCPT TO:<전달 경로> 를 받는 사람마다 한 번씩 보냅니다. 한 사람마다 250 이나 거절이 따로 옵니다
  5. DATA 를 보내면 서버가 354 로 답합니다. 클라이언트가 내용을 줄줄이 보냅니다. 끝은 점 하나만 있는 줄로 알립니다. 서버가 받아 저장하면 250 입니다
  6. 다른 메일이 있으면 3 으로 돌아갑니다. 없으면 QUIT 을 보냅니다. 서버는 221 로 답한 뒤 연결을 닫습니다
sequenceDiagram
    participant 클라이언트 as SMTP 클라이언트
    participant 서버 as SMTP 서버
    클라이언트->>서버: 연결을 연다
    서버-->>클라이언트: 220 · 준비됐다
    클라이언트->>서버: EHLO 도메인
    서버-->>클라이언트: 250 · 확장 키워드 목록
    클라이언트->>서버: MAIL FROM 반환 경로
    서버-->>클라이언트: 250
    loop 받는 사람마다
        클라이언트->>서버: RCPT TO 전달 경로
        서버-->>클라이언트: 250 또는 거절
    end
    클라이언트->>서버: DATA
    서버-->>클라이언트: 354
    클라이언트->>서버: 내용 · 점 하나만 있는 줄
    서버-->>클라이언트: 250
    클라이언트->>서버: QUIT
    서버-->>클라이언트: 221 · 연결을 닫는다

받는 사람이 여럿이어도 한 서버로 가는 몫이면 내용은 한 번만 보냅니다. MAIL · RCPT · RCPT · … · DATA 순서로 보냅니다. 받는 사람마다 MAIL 부터 다시 하지 않는 것이 좋습니다(SHOULD).

EHLO 에 서버가 "모르는 명령"이라고 답하면 클라이언트는 옛 인사인 HELO 로 물러설 수 있어야 합니다(SHOULD). HELO 에는 확장 목록이 붙지 않습니다.

서버가 세션 자체를 받기 싫으면 첫 인사로 220 대신 554 를 보낼 수 있습니다. 그래도 서버는 클라이언트의 QUIT 을 기다린 뒤에 연결을 닫습니다. 그 사이에 오는 명령에는 503 으로 답하기를 권합니다(SHOULD).

명령의 순서 규칙

메일 거래는 MAIL 하나, RCPT 하나 이상, DATA 하나로 이루어지고 반드시 이 순서입니다. 순서를 어기면 이렇게 됩니다.

상황 어떻게 되나
EHLO(또는 HELO) 없이 거래를 시작한다 금지다. 거래 앞에 반드시 인사가 와야 한다
거래가 열려 있는데 MAIL 을 또 보낸다 금지다. 앞 거래가 DATA 로 끝났거나 취소됐을 때만 보낸다
MAIL 없이 RCPT 를 보낸다 503 Bad sequence of commands
MAIL 의 인자를 받아들일 수 없다 501 · 서버 상태는 그대로
처리할 수 없을 만큼 순서가 어긋났다 503 · 서버 상태는 그대로

거래를 처음으로 되돌리는 명령은 RSET 입니다. 저장해 둔 보내는 주소, 받는 주소, 내용을 전부 버리고 250 으로 답합니다. 연결은 닫지 않습니다. 세션 중간에 EHLO 를 다시 보내도 RSET 과 똑같이 초기화됩니다.

메일을 나르는 거래와 상관없는 명령은 세션 어디서나 쓸 수 있습니다. 세션을 열지 않고도 보낼 수 있습니다. RSET 이 그중 하나입니다. 서버는 EHLO 를 아직 못 받았어도 이런 명령을 503 없이 정상으로 처리하는 것이 좋습니다(SHOULD). 클라이언트 쪽은 그래도 EHLO 로 세션을 열고 나서 보내는 것이 좋습니다(SHOULD). 세션의 마지막 명령은 언제나 QUIT 이어야 합니다.

stateDiagram-v2
    state "거래 없음" as 거래없음
    state "보낸 이 받음" as 보낸이받음
    state "받는 이 있음" as 받는이있음
    state "내용 받는 중" as 내용받는중
    [*] --> 거래없음: 220 뒤 EHLO 에 250
    거래없음 --> 보낸이받음: MAIL 에 250
    보낸이받음 --> 받는이있음: RCPT 에 250
    받는이있음 --> 받는이있음: RCPT 에 250
    받는이있음 --> 내용받는중: DATA 에 354
    내용받는중 --> 거래없음: 점 한 줄 뒤 250
    보낸이받음 --> 거래없음: RSET
    받는이있음 --> 거래없음: RSET
    거래없음 --> [*]: QUIT 에 221

그림은 서버가 거래를 어디까지 받았는지를 따라간 것입니다. 받아들인 받는 사람이 하나도 없는데 DATA 가 오면, 서버는 503 이나 554 로 답할 수 있습니다(MAY). 클라이언트는 354 를 받기 전에는 내용을 보내면 안 됩니다(MUST NOT). RSET 은 어느 단계에서든 「거래 없음」으로 되돌립니다.

본문의 끝과 점 하나 더 붙이기

DATA 뒤로는 내용이 줄줄이 넘어갑니다. 서버는 어디까지가 내용인지 알아야 다시 명령을 받을 수 있습니다. 끝 표시는 점 하나만 있는 줄입니다. 줄바꿈 글자까지 적으면 <CRLF>.<CRLF> 입니다.

그러면 사용자가 쓴 본문에 점 하나로 된 줄이 있을 때 문제가 됩니다. 그대로 보내면 서버가 거기서 메일이 끝났다고 읽습니다.

그래서 양쪽이 규칙 하나씩을 지킵니다.

  • 클라이언트는 본문 한 줄을 보내기 전에 첫 글자를 봅니다. 점이면 맨 앞에 점을 하나 더 붙입니다
  • 서버는 받은 줄이 점 하나뿐이면 끝 표시로 읽습니다. 첫 글자가 점이고 뒤에 글자가 더 있으면 첫 점을 지웁니다
flowchart TD
    L["보낼 줄"] --> C1{"첫 글자가 점인가"}
    C1 -->|"아니다"| SEND["그대로 보낸다"]
    C1 -->|"점이다"| ADD["맨 앞에 점을 하나 더 붙여 보낸다"]
    ADD --> S1{"서버가 받은 줄"}
    SEND --> S1
    S1 -->|"점 하나뿐"| FIN["내용의 끝으로 읽는다"]
    S1 -->|"점 + 글자"| DEL["첫 점을 지우고 본문으로 넣는다"]

그러면 본문은 손대지 않은 모습으로 도착합니다. 끝 표시는 끝 표시로만 읽힙니다.

STARTTLS 로 연결을 감쌀 때

SMTP 연결은 평문으로 시작합니다. EHLO 응답의 확장 목록에 STARTTLS 가 있으면 클라이언트가 같은 이름의 명령을 보내 그 연결을 TLS 로 감쌀 수 있습니다. 명령에는 인자가 없습니다.

서버의 답은 셋 중 하나입니다. 220 은 TLS 를 시작하자는 뜻입니다. 501 은 인자를 붙였다는 문법 오류입니다. 454 는 지금은 일시적인 사정으로 TLS 를 못 쓴다는 뜻입니다. 그때 세션을 이어갈지는 클라이언트가 자기 정책으로 정합니다.

sequenceDiagram
    participant 클라이언트 as SMTP 클라이언트
    participant 서버 as SMTP 서버
    클라이언트->>서버: EHLO
    서버-->>클라이언트: 250 · STARTTLS 가 든 확장 목록
    클라이언트->>서버: STARTTLS
    서버-->>클라이언트: 220
    Note over 클라이언트,서버: TLS 핸드셰이크 · 끝나면 초기 상태로 돌아간다
    클라이언트->>서버: EHLO 를 다시 보낸다
    서버-->>클라이언트: 250 · 새 확장 목록

220 을 받은 클라이언트는 다른 명령보다 먼저 TLS 협상을 시작해야 합니다(MUST). 핸드셰이크가 끝나면 SMTP 는 220 인사 직후의 초기 상태로 돌아갑니다.

이때 양쪽은 그 전에 상대에게서 얻은 지식을 버립니다(MUST). 서버는 EHLO 인자 같은 것을, 클라이언트는 확장 목록 같은 것을 버립니다. TLS 이전의 대화는 평문이라 중간에서 누가 고쳤을 수 있기 때문입니다. 그래서 클라이언트는 첫 명령으로 EHLO 를 다시 보내기를 권합니다(SHOULD).

TLS 뒤의 확장 목록은 앞의 것과 다를 수 있습니다(MAY). 핸드셰이크에서 알맞은 클라이언트 인증서를 받기 전에는 어떤 인증 방식의 지원을 광고하지 않으려는 서버가 있기 때문입니다. 서버는 이 목록에 STARTTLS 를 다시 넣으면 안 됩니다(MUST NOT).

AUTH 로 인증할 때

제출 서버는 MAIL 앞에서 인증을 요구합니다. 인증 명령은 AUTH 입니다. 인증 방식은 SASL(Simple Authentication and Security Layer, 단순 인증 보안 계층)이라는 틀의 메커니즘 이름으로 고릅니다. 서버가 지원하는 메커니즘은 EHLO 응답의 AUTH 줄에 나옵니다.

교환은 서버의 질문과 클라이언트의 답이 번갈아 오가는 모양입니다.

  • 클라이언트가 AUTH 메커니즘 을 보냅니다. 첫 답을 같은 줄에 실을 수도 있습니다
  • 서버가 질문할 것이 있으면 334 와 base64 로 인코딩한 문자열을 보냅니다
  • 클라이언트는 base64 로 인코딩한 한 줄로 답합니다. 그만두려면 * 한 글자를 보냅니다. 서버는 501 로 답합니다
  • 성공하면 235 입니다

AUTH 는 한 세션에 한 번만 성공할 수 있습니다. 성공한 뒤에 또 보내거나 거래 도중에 보내면 503 입니다. 고른 메커니즘이 쓸 수 없는 것이면 504 입니다 — 서버가 모르는 방식이거나, 암호화 계층을 요구하는 방식인데 그 계층이 아직 없는 경우입니다.

예시

줄 맨 앞의 C: 는 클라이언트가 보낸 줄, S: 는 서버가 보낸 줄입니다. 예시는 셋입니다. 첫째는 서버 둘이 직접 만난 거래입니다. 둘째는 중계 서버를 하나 거친 거래입니다. 셋째는 제출 통로에서 로그인하는 교환입니다.

받는 사람 하나가 없는 거래

bar.com 의 Smith 가 foo.com 의 Jones · Green · Brown 세 사람에게 메일을 보냅니다. bar.com 이 foo.com 에 바로 접속합니다. foo.com 에는 Green 의 메일함이 없습니다.

S: 220 foo.com Simple Mail Transfer Service Ready
C: EHLO bar.com
S: 250-foo.com greets bar.com
S: 250-8BITMIME
S: 250-SIZE
S: 250-DSN
S: 250 HELP
C: MAIL FROM:<Smith@bar.com>
S: 250 OK
C: RCPT TO:<Jones@foo.com>
S: 250 OK
C: RCPT TO:<Green@foo.com>
S: 550 No such user here
C: RCPT TO:<Brown@foo.com>
S: 250 OK
C: DATA
S: 354 Start mail input; end with <CRLF>.<CRLF>
C: Blah blah blah...
C: ...etc. etc. etc.
C: .
S: 250 OK
C: QUIT
S: 221 foo.com Service closing transmission channel

EHLO 의 답은 여러 줄입니다. 마지막 줄만 250 뒤에 공백이 옵니다. 나머지 줄은 250- 처럼 하이픈이 옵니다. 클라이언트는 공백이 온 줄을 보고 응답이 끝났음을 압니다. 첫 줄에는 서버 자기 이름이 옵니다. 그 아래 줄마다 확장 키워드가 하나씩 옵니다. 여기서는 8BITMIME · SIZE · DSN · HELP 가 이 서버가 지원하는 확장의 키워드입니다. 마지막 키워드인 HELP 는 공백이 온 마지막 줄에 실려 있습니다.

RCPT 세 번 가운데 Green 의 것만 550 으로 거절됐습니다. 거래는 거기서 멈추지 않고 Brown 으로 이어집니다. DATA 는 한 번만 오갑니다. 내용도 한 번만 넘어갑니다.

끝의 250 OK 로 foo.com 은 저장해 둔 받는 사람, 곧 Jones 와 Brown 몫의 책임을 넘겨받습니다. Green 몫은 RCPT 단계에서 이미 거절됐으므로 foo.com 이 맡은 적이 없습니다.

중계 서버를 거치는 한 통

bar.com 의 John Q. Public(주소 [email protected])이 xyz.com 의 Jones 에게 메일을 보냅니다. 첫 단계에서 bar.com 은 xyz.com 의 MX 를 조회합니다. xyz.com 이 가장 앞선 우선순위이고 foo.com 이 그보다 뒤입니다.

bar.com 은 xyz.com 에 접속하려다 실패합니다. 그래서 다음 순위인 foo.com 에 접속해 메일을 넘깁니다. 이때의 거래는 앞 예시와 같은 꼴입니다.

둘째 단계에서 foo.com 이 같은 MX 를 조회합니다. 자기 자신과 자기보다 뒤 순위인 것은 후보에서 버려야 하므로 xyz.com 에 접속합니다. 이번에는 성공합니다. 그 거래의 가운데를 옮기면 이렇습니다.

C: MAIL FROM:<JQP@bar.com>
S: 250 OK
C: RCPT TO:<Jones@XYZ.COM>
S: 250 OK
C: DATA
S: 354 Start mail input; end with <CRLF>.<CRLF>
C: Received: from bar.com by foo.com ; Thu, 21 May 1998
C:     05:33:29 -0700
C: Date: Thu, 21 May 1998 05:33:22 -0700
C: From: John Q. Public <JQP@bar.com>
C: Subject:  The Next Meeting of the Board
C: To: Jones@xyz.com
C:
C: Bill:
C: The next meeting of the board of directors will be
C: on Tuesday.
C:                         John.
C: .
S: 250 OK

DATA 뒤 첫 두 줄이 foo.com 이 더한 Received: 줄입니다. bar.com 에게서 받아 foo.com 이 넘긴다는 것과 그 시각이 적혀 있습니다. 그 아래 Date: 부터가 bar.com 에서 온 내용입니다. 중계 서버가 메시지에 손대도 되는 범위는 이렇게 추적 정보를 더하는 것까지입니다.

MAIL FROM 의 반환 경로도 처음 보낸 [email protected] 그대로입니다. 그래서 xyz.com 에서 배달이 실패하면 통지는 foo.com 이 아니라 John Q. Public 에게 갑니다.

STARTTLS 로 감싼 뒤 AUTH PLAIN 으로 인증하는 교환

클라이언트가 연결을 TLS 로 감싼 뒤 PLAIN 메커니즘으로 인증하는 교환입니다. 첫 답을 AUTH 줄에 함께 실었습니다.

  ... 연결과 서버의 첫 인사는 줄였습니다 ...
C: EHLO client.example.com
S: 250-smtp.example.com Hello client.example.com
S: 250-AUTH GSSAPI DIGEST-MD5
S: 250-ENHANCEDSTATUSCODES
S: 250 STARTTLS
C: STARTTLS
S: 220 Ready to start TLS
  ... TLS negotiation proceeds, further commands
      protected by TLS layer ...
C: EHLO client.example.com
S: 250-smtp.example.com Hello client.example.com
S: 250 AUTH GSSAPI DIGEST-MD5 PLAIN
C: AUTH PLAIN dGVzdAB0ZXN0ADEyMzQ=
S: 235 2.7.0 Authentication successful

TLS 전의 AUTH 줄에는 PLAIN 이 없습니다. TLS 뒤에 다시 보낸 EHLO 의 답에는 PLAIN 이 더해졌습니다. 핸드셰이크 뒤의 확장 목록이 달라질 수 있다는 것을 이 두 줄이 보여 줍니다. 앞 절에서 본 대로, 핸드셰이크 전에는 어떤 인증 방식의 지원을 광고하지 않으려는 서버가 있습니다.

AUTH PLAIN 뒤의 문자열이 base64 로 인코딩한 첫 답입니다. 이 문자열에 무엇을 담는지는 고른 방식인 PLAIN 이 정합니다. 그 내용은 이 편의 자료 범위 밖입니다(미확인). 서버는 더 묻지 않고 235 로 성공을 알립니다. 그 뒤의 2.7.0 은 확장 상태 코드입니다. 확장 상태 코드를 지원하는 서버가 붙이는 값입니다. 확장 상태 코드는 아래 「실패」에서 다룹니다.

실패

SMTP 교환이 어떻게 깨지고 그때 무엇이 돌아오는지가 이 절의 내용입니다. 대상은 서버의 응답 코드와, 보내는 쪽이 못 보낸 메일입니다. 코드를 읽는 법부터 시작해 큐 · 반송 · 루프 · 타임아웃까지 갑니다.

응답 코드 읽는 법

응답 코드는 숫자 세 개입니다. 첫 숫자가 결과의 큰 갈래를 정합니다. 단순한 클라이언트도 첫 숫자만 보면 다음에 할 일을 고를 수 있습니다.

첫 숫자 이름 뜻 클라이언트가 할 일
2 Positive Completion 요청한 일을 마쳤다 다음 요청으로 간다
3 Positive Intermediate 받아들였지만 정보가 더 와야 한다 이어서 보낸다. DATA 의 354 가 이것이다
4 Transient Negative Completion 받아들이지 않았다. 일시적인 사정이다 나중에 다시 시도하기를 권한다(SHOULD)
5 Permanent Negative Completion 받아들이지 않았다. 영구적인 사정이다 같은 요청을 같은 차례로 되풀이하지 않기를 권한다(SHOULD NOT)

4 와 5 를 가르는 어림 기준이 있습니다. 명령을 한 글자도 안 바꾸고 다시 보냈을 때 성공할 수 있으면 4 입니다. 다만 5 로 끝난 요청도 사람이 고치면 풀릴 수 있습니다. 주소 철자를 바로잡거나 계정 상태를 바꾼 뒤에 다시 보내는 경우입니다.

둘째 숫자는 오류의 종류를 대략 가릅니다. 아래 표의 x 와 z 는 아무 숫자든 올 수 있는 칸입니다. 뒤에 나오는 2yz · 4yz · 5yz 의 y 와 z 도 같습니다.

둘째 숫자 갈래
x0z 문법 · 어느 기능에도 안 맞는 명령 · 구현되지 않은 명령
x1z 정보 요청에 대한 답
x2z 전송 채널에 대한 답
x3z · x4z 정해지지 않았다
x5z 받는 쪽 메일 시스템의 상태

클라이언트는 되도록 첫 숫자만 해석하는 것이 좋습니다(SHOULD). 모르는 코드가 오면 첫 숫자로 해석할 준비가 돼 있어야 합니다(MUST). 서버는 세 숫자가 아니거나 2~5 로 시작하지 않는 코드를 보내면 안 됩니다(MUST NOT).

자주 만나는 값

코드 뜻 주로 오는 때
421 서비스를 쓸 수 없다. 전송 채널을 닫는다 서버가 내려가야 할 때. 어느 명령에든 올 수 있다
450 메일함을 지금 쓸 수 없다(사용 중 · 정책상 일시 차단) RCPT · DATA 뒤
451 처리 중 로컬 오류로 중단했다 MAIL · RCPT · DATA 뒤
452 시스템 저장 공간이 모자란다 MAIL · RCPT · DATA 뒤
500 문법 오류 · 모르는 명령 · 명령 줄이 너무 길다 어느 명령에든
501 인자 문법 오류 어느 명령에든
503 명령 순서가 틀렸다 RCPT · DATA 등
530 인증이 필요하다 인증 없이 MAIL 등을 보낼 때
550 메일함을 쓸 수 없다(없음 · 접근 불가 · 정책상 거절) RCPT 가 대표적이다
552 저장 할당량을 넘었다 MAIL · RCPT · DATA 뒤
553 메일함 이름이 허용되지 않는다(문법이 틀림 등) MAIL · RCPT
554 거래 실패. 접속 첫 응답이면 「여기엔 SMTP 서비스가 없다」 접속 직후 · DATA

550 은 RCPT 에서 받는 주소를 모를 때 흔히 옵니다. 중계를 정책상 거절할 때도 550 을 쓰기를 권합니다(SHOULD).

DATA 끝의 답이 정하는 책임

내용 끝의 점 한 줄 뒤에 오는 답은 메일이 지금 누구 손에 있는지를 정합니다.

답 서버 클라이언트
2yz 책임을 넘겨받는다. 아래 「보장과 가정」 손을 뗀다
4yz 그 메일을 배달하려고 다시 시도하면 안 된다(MUST NOT) 책임이 남는다. 사용자에게 돌려주거나 큐에 다시 넣는다
5yz 그 메일을 배달하려고 다시 시도하면 안 된다(MUST NOT) 책임이 남는다. 사람이 메일과 응답을 보고 손대기 전에는 같은 서버로 다시 보내지 않기를 권한다

보낸 사람은 일시 실패가 되돌아오는 것도 배달 실패로 읽을 수 있어야 합니다(SHOULD). 영구 실패와 똑같이 읽습니다.

연결이 끊기거나 서버가 닫힐 때

서버가 정상 운영 중에 일부러 연결을 닫아도 되는 경우는 셋뿐입니다. QUIT 에 221 로 답한 뒤, 서비스를 내려야 해서 421 을 보낸 뒤, 그리고 클라이언트의 명령을 기다리다 타임아웃이 난 뒤입니다.

모르는 명령을 받았다고 연결을 닫는 서버는 규격을 어긴 것입니다. 모르는 명령에는 500 으로 답하고 다음 명령을 기다립니다.

연결이 갑자기 끊기거나 리셋되는 일도 있습니다. 클라이언트는 이것을 451 을 받은 것처럼 다루는 것이 좋습니다(SHOULD). 곧 일시 실패입니다. 나중에 다시 보낼 메일입니다.

큐와 재시도

바로 보내지 못한 메일은 큐에 넣고 주기적으로 다시 시도해야 합니다(MUST). 큐에 넣는 것은 메시지만이 아니고 봉투 정보까지입니다. 받는 도메인의 DNS 조회가 일시 오류를 낼 때도 같은 큐로 갑니다.

책임을 넘겨받은 서버가 다음 서버로 보낼 때의 흐름은 이렇습니다.

flowchart TD
    A["다음 서버에 보낸다"] --> B{"결과"}
    B -->|"2yz"| OK["끝"]
    B -->|"4yz · 연결 실패"| Q["큐에 둔다"]
    Q --> W["30분 이상 기다렸다가 다시"]
    W --> A
    Q -->|"포기 기한을 넘겼다"| N["보낸 사람에게 실패 통지"]
    B -->|"5yz"| N

재시도 간격과 포기 기한은 권장값이고 둘 다 상한이 아니라 하한입니다.

값 권장
한 목적지에 실패한 뒤 다시 시도하기까지 대개 30분 이상(SHOULD). 실패 이유를 알 수 있으면 간격을 바꿔 가는 전략이 도움이 된다
포기 기한 대개 적어도 4~5일은 필요하다
경험상의 시도 횟수 큐에 든 첫 한 시간에 두 번, 그 뒤로는 두세 시간에 한 번

재시도 알고리즘의 값은 설정으로 바꿀 수 있어야 합니다(MUST). 실패 통지 같은 메일에는 일반 메일보다 재시도 횟수 상한을 짧게 잡는 것이 맞을 수 있습니다(MAY).

클라이언트는 닿지 않는 호스트의 목록을 따로 들고 있는 것이 좋습니다(SHOULD). 같은 호스트로 가는 메일이 수백 통 쌓여 있으면 한 통씩 타임아웃을 기다리다 긴 지연이 생기기 때문입니다. 다만 서버의 거절 응답을 캐시할 때는 매우 조심해야 합니다(SHOULD). MAIL 에 받은 5yz 는 캐시하면 안 됩니다(MUST NOT).

반송 통지와 널 반환 경로

메일을 받아들인 뒤에 배달이 실패하면, 받아들인 서버는 실패 통지 메일을 만들어 보내야 합니다(MUST). 받는 사람은 원래 메일의 봉투에 있던 반환 경로입니다.

이 통지 자신의 봉투는 반환 경로를 비워서 보냅니다(MUST). MAIL FROM:<> 모양입니다. 이것을 널 반환 경로라고 부릅니다. 원래 메일의 반환 경로가 비어 있었다면 통지를 보내면 안 됩니다(MUST NOT).

비우는 까닭은 루프를 막는 것입니다. 통지가 배달에 실패해도 그에 대한 통지가 또 생기지 않습니다.

flowchart TD
    F["받아들인 메일의 배달이 실패했다"] --> Q{"원래 메일의 반환 경로가 비었나"}
    Q -->|"비었다"| NO["통지를 보내지 않는다"]
    Q -->|"주소가 있다"| SEND["MAIL FROM 을 비워 그 주소로 통지를 보낸다"]
    SEND --> F2["그 통지도 배달에 실패했다"]
    F2 --> STOP["반환 경로가 비어 있으니 통지가 더 생기지 않는다"]

통지를 나르다 생긴 문제에 대해 통지를 보내는 일은 금지돼 있습니다(MUST NOT). 자동으로 메일을 처리하는 프로그램도 널 반환 경로로 온 메일에는 답장하지 않기를 권합니다(SHOULD NOT).

통지의 형식은 DSN(Delivery Status Notification, 배달 상태 통지)이 정합니다. DSN 은 맨 위 내용 형식이 multipart/report 인 MIME 메시지입니다.

순서 담는 것
첫째 사람이 읽는 설명
둘째 message/delivery-status 형식의 기계가 읽는 보고
셋째 (있으면) 원래 메일이나 그 일부

DSN 은 실패만 알리지 않습니다. 배달 지연, 배달 성공, DSN 을 모르는 환경으로 넘어간 것도 알릴 수 있습니다.

DSN 의 기계가 읽는 보고에는 확장 상태 코드가 들어갑니다. SMTP 의 세 숫자 코드로는 배달 보고에 쓸 만한 값이 열두 개 남짓입니다. 그마저 한 값이 여러 오류를 겹쳐 뜻합니다. 그래서 따로 만든 체계입니다. 표기는 class.subject.detail 이고 숫자 셋을 점으로 잇습니다.

칸 뜻
class 2 성공 · 4 지속되는 일시 실패 · 5 영구 실패
subject 문제가 났을 법한 곳
detail 정확한 오류 조건

5.1.1 이 대표적입니다. 주소가 가리키는 메일함이 없다는 뜻입니다. 인터넷 메일 이름이면 @ 왼쪽이 틀렸다는 뜻입니다. 영구 실패에만 씁니다.

메일 루프

설정이 꼬이면 메일이 서버 사이를 끝없이 돌 수 있습니다. 서버는 단순한 루프를 찾아내 멈추는 장치를 반드시 갖춰야 합니다(MUST).

메일 헤더의 Received: 줄 개수를 세는 방법이 쓰입니다. 중계 서버가 거칠 때마다 한 줄씩 붙으므로 줄이 너무 많으면 루프로 봅니다. 이 방법은 효과가 있다고 드러났지만 최적인 경우는 드뭅니다. 이 방법을 쓰는 서버는 거절 기준을 크게 잡는 것이 좋습니다(SHOULD). 그 기준은 대개 적어도 100 줄입니다.

명령마다 기다리는 시간

클라이언트는 타임아웃을 반드시 둬야 합니다(MUST). 타임아웃은 거래 전체가 아니라 명령마다 따로 잽니다(MUST). 내용을 보낼 때는 버퍼 한 덩이마다 따로 재므로, 전체 시간은 메시지 크기에 비례합니다.

아래는 명령마다 둘 최소값으로 권하는 것입니다(SHOULD).

기다리는 것 최소
첫 220 인사 5분
MAIL 의 답 5분
RCPT 의 답 5분
DATA 에 대한 354 2분
내용 한 덩이를 보내고 나서 3분
점 한 줄 뒤의 250 10분
서버가 다음 명령을 기다릴 때 5분

점 한 줄 뒤를 가장 길게 잡는 데에는 이유가 있습니다. 서버는 이때 대개 메일을 메일함에 넣는 처리를 합니다. 이 시점에 클라이언트가 섣불리 타임아웃을 낸다고 해 봅시다. 서버는 이미 책임을 넘겨받았습니다. 그런데 클라이언트는 실패로 알고 다시 보냅니다. 같은 메일이 여러 통 배달됩니다. 서버도 이 응답에 걸리는 시간을 최소로 줄이도록 애써야 합니다(MUST).

보장과 가정

SMTP 가 무엇을 약속하고 그 약속이 무엇 위에 서 있는지를 짝으로 적은 절입니다. 대상은 DATA 끝의 250 과 그 둘레의 가정 다섯입니다. 가정마다 그것이 깨지면 무엇이 무너지는지를 함께 적습니다.

250 은 책임을 넘겨받았다는 뜻

보장 — 내용 끝에 서버가 250 을 보내면 메일에 대한 책임이 정식으로 서버에게 넘어갑니다. 서버는 그 메일을 배달하거나, 배달하지 못했다고 제대로 알릴 책임을 집니다(MUST). 일시 실패면 적당한 횟수만큼 재시도합니다. 영구 실패거나 재시도가 다 실패하면 MAIL 명령에 있던 주소로 통지합니다.

250 은 받는 사람이 메일을 읽었다는 뜻도, 메일함에 닿았다는 뜻도 아닙니다. 책임이 다음 손에 넘어갔다는 뜻입니다.

가정 — 서버가 그 책임을 진지하게 집니다. 나중에 기계가 죽었다거나 예측할 수 있는 자원 부족 같은 하찮은 이유로 메일을 잃으면 안 됩니다(MUST NOT).

깨질 때 — 받아들인 뒤의 배달 실패 가운데 어떤 것은 피할 수 없습니다. 받는 서버가 DNS 의 일시 오류 때문에 받는 주소를 다 확인하지 못했거나, 받는 사람이 메일링 리스트이거나, 자기는 중계일 뿐이라 최종 서버에 바로 닿지 못하는 경우입니다.

그때 서버는 실패 통지 메일을 만들어 보냅니다. 그 통지는 봉투의 반환 경로로 갑니다. 반환 경로가 비어 있으면 통지가 없습니다. 반환 경로가 엉뚱한 주소면 통지도 엉뚱한 곳으로 갑니다.

믿을 만한 연결이라는 가정

가정 — SMTP 는 데이터가 빠짐없이, 순서대로 오는 채널만 요구합니다. 대화가 한 번에 하나씩 짝을 지어 가는 것도, 점 한 줄이 내용의 끝이라는 것도 채널이 그만큼 믿을 만하다는 전제 위에 서 있습니다.

깨질 때 — 거래 도중에 연결이 끊기거나 리셋되면 클라이언트는 그 거래를 451 을 받은 것처럼 다루는 것이 좋습니다(SHOULD). 그러면 그 메일은 큐로 돌아가 나중에 다시 나갑니다. 위험한 대목은 점 한 줄 뒤의 250 을 기다리는 동안입니다. 여기서 섣불리 타임아웃이 나면 서버는 이미 책임을 넘겨받았는데 클라이언트는 다시 보내서, 같은 메일이 여러 통 배달됩니다.

보낸 사람을 확인하지 않는다

보장하지 않는 것 — SMTP 는 보내는 사람이 밝힌 주소를 확인하지 않습니다. 조금만 아는 사용자도 받는 서버나 중계 서버와 직접 대화해 다른 곳에서 온 것처럼 보이는 메일을 만들 수 있습니다. 전송 단계에서 SMTP 메일은 인증할 수도, 무결성을 검사할 수도 없습니다. 실패 통지에 쓸 반환 경로를 검증하는 일도 SMTP 규격 밖입니다.

왜 막지 않았나 — 봉투의 반환 경로와 헤더의 From: 에 자기 것이 아닌 주소를 못 넣게 하면 정당한 쓰임이 막힙니다. 다른 사람을 대신해 보내는 메일, 답장을 따로 정한 주소로 받는 메일, 한 메일을 여러 호스트의 받는 사람에게 보내는 경우입니다.

밖에서 메우는 장치 — 그래서 보낸 사람을 확인하는 일은 SMTP 위에 따로 얹혔습니다. 세 장치 모두 DNS 에 정책이나 키를 올려 두는 방식입니다.

장치 확인하는 것
SPF(Sender Policy Framework, 발신자 정책 프레임워크) 이 도메인 이름을 MAIL FROM 이나 HELO 에 써도 되는 호스트인가
DKIM(DomainKeys Identified Mail) 이 메일에 책임을 지는 도메인이 암호 서명을 붙였나
DMARC(Domain-based Message Authentication, Reporting, and Conformance) 헤더 From: 의 도메인에 대해 SPF 와 DKIM 결과를 묶고, 검증에 실패한 메일을 어떻게 다룰지를 도메인 주인이 요청한다

전송 단계에서 인증을 대주는 확장과 설정은 위조가 쉬운 이 사정을 어느 정도 낫게 합니다. 다만 대개는 서버 하나가 다음 서버 하나를 확인하는 데까지입니다. 중계 서버의 사슬 전체나 사용자를 인증하지는 않습니다. 메일 자체의 진위를 지키려면 메일 본문에 디지털 서명을 붙이는 종단 간 방식이 필요합니다.

7비트 ASCII 라는 가정

가정 — 확장 없는 SMTP 는 7비트 전송만 합니다. 명령과 응답은 ASCII(American Standard Code for Information Interchange, 미국 정보 교환 표준 부호) 글자입니다. 이 글자 집합을 US-ASCII(United States ASCII, 미국판 ASCII)라고도 부릅니다. 내용도 이 128 글자로 이루어진다고 전제합니다.

한 글자는 한 바이트에 담깁니다. 쓰이지 않는 최상위 비트는 0 으로 비워 둡니다. 맞는 확장을 협상하지 않은 클라이언트는 그 최상위 비트를 쓰는 메시지를 보내면 안 됩니다(MUST NOT).

깨질 때 — 이 규칙을 어긴 메시지를 받은 서버는 최상위 비트를 지우거나 메시지를 거절할 수 있습니다(MAY). 중계 서버는 받은 내용이 올바르다고 보고, 봉투가 허락하는 한 들여다보지 않고 넘기는 것이 좋습니다(SHOULD).

메시지에는 본문이 7비트인지 8비트 MIME 인지 알리는 내용 표시가 붙습니다. 이 표시가 실제 내용과 다를 때가 있습니다. 그때 지나는 길이 그 실제 내용을 받아들이지 못하면 받는 사람에게 심하게 깨진 메시지가 배달될 수 있습니다. 마지막 배달을 맡은 서버는 그런 메시지를 거절하거나 배달 못 함으로 돌려보낼 수 있습니다(MAY).

풀어 주는 확장 — 확장 두 개가 이 가정을 넓힙니다. 둘 다 서버가 EHLO 응답에 키워드를 광고할 때만 쓸 수 있습니다.

확장 넓히는 것 남는 제약
8BITMIME 본문에 8비트 바이트를 담은 MIME 메시지. MAIL 에 BODY=8BITMIME 을 단다 서버는 여전히 줄 길이를 1000 옥텟 이상으로 제한할 수 있다. 그래서 인코딩하지 않은 바이너리를 보내는 수단은 아니다
SMTPUTF8 국제화된 메일 주소와 국제화된 헤더 —

상대가 8BITMIME 을 광고하지 않으면 클라이언트는 US-ASCII 밖의 글자를 담은 내용을 어떤 경우에도 보내면 안 됩니다. 길은 둘입니다. 정보를 잃지 않게 MIME 전송 인코딩을 써서 올바른 7비트 MIME 으로 바꾸거나, 영구 실패로 다룹니다.

확장 없이는 헤더 필드가 언제나 US-ASCII 로 인코딩됩니다. 헤더 값에 다른 글자를 담을 때는 그 값을 US-ASCII 로 인코딩하는 MIME 규칙을 씁니다.

암호화는 한 구간만 가린다

보장 — STARTTLS 로 감싼 연결은 그 클라이언트와 서버 한 쌍 사이의 대화를 가립니다.

가정 — 양쪽이 TLS 협상 결과를 보고, 받아들일 만한 수준의 인증과 기밀성이 이뤄졌는지 확인합니다. 무엇을 받아들일 만하다고 볼지는 각 구현이 자기 쪽에서 정합니다. 규격은 그것을 정해 주지 않습니다. 기밀성이 없게 끝났거나, 쓰인 알고리즘이나 키 길이가 충분히 세지 않다고 보이거나, 인증이 어느 한쪽에 충분하지 않으면 클라이언트는 QUIT 으로 세션을 바로 끝낼 수 있고 서버는 더 이상 명령을 받지 않을 수 있습니다. 이 확인을 건너뛰면 TLS 를 쓴 의미가 없어집니다.

깨질 때 — 한 쌍을 가려도 사슬 전체가 가려지지는 않습니다. SMTP 는 종단 간 방식이 아닙니다. 서버 한 쌍 사이에 TLS 를 더해도 보내는 사람의 MUA 에서 받는 사람까지가 가려지는 것은 아닙니다. 메일 한 통이 서버를 셋 이상 거치면 그중 한 쌍이 TLS 를 쓴다고 사슬 전체가 가려지지도 않습니다.

flowchart TD
    A["보내는 사람의 MUA"] -->|"구간 1 · TLS 여부를 이 쌍이 정한다"| B["MSA"]
    B -->|"구간 2 · 따로 정한다"| C["중계 MTA"]
    C -->|"구간 3 · 따로 정한다"| D["마지막 서버"]

중간에서 끼어드는 공격도 있습니다. 서버 응답에서 250 STARTTLS 줄을 지우면 클라이언트는 TLS 를 시작하려 들지 않습니다. 이것을 막으려면 정해 둔 호스트에 대해서는 TLS 협상이 성공해야만 메시지를 넘기도록 설정할 수 있어야 합니다(MUST). 가능할 때만 TLS 를 쓰는 선택지도 함께 두기를 권합니다(SHOULD).

반면 MX 레코드에 올라 있는 25 번 포트의 서버는 자기가 맡은 도메인의 메일을 받아 배달하는 일에 STARTTLS 를 요구하면 안 됩니다(MUST NOT). 요구하면 TLS 를 못 쓰는 상대와 메일을 주고받을 수 없게 되기 때문입니다.

중계는 열어 두고 제출은 인증한다

보장하지 않는 것 — 어느 서버든 메일을 받아 준다는 약속은 없습니다. 서버를 두고 있는 곳이 보기에 운영상·기술상 이유가 되는 것이면 서버는 무엇으로든 메일 받기를 거절할 수 있습니다. 이것은 오래 굳은 원칙입니다. 다만 거절할 권리를 지나치게 쓰면 어디서나 메일이 닿는다는 인터넷의 강점이 위태로워집니다. 그래서 받을 트래픽을 가려 받기로 한 곳은 상당한 주의를 들여 균형을 지키는 것이 좋습니다(SHOULD).

가정 — 서버끼리 넘길 때는 인증을 거치는 일이 흔하지 않습니다. 제약 없이 주고받게 하려는 것입니다. 그렇게 열어 둔 서버가 다른 도메인 앞으로 가는 메일까지 아무에게나 넘겨주지는 않는다는 것도 가정입니다. 서버는 다른 호스트로 가는 메일을 받지 않을 수 있습니다(MAY). 기능이 모자란 클라이언트는 인터넷의 아무 SMTP 서버나 중계에 쓸 수 있다고 여기면 안 됩니다(MUST NOT).

깨질 때 — 아무 발신자의 메일을 아무 목적지로나 넘겨주는 서버를 실무에서는 오픈 릴레이라고 부릅니다. 이런 중계는 메일의 진짜 출처를 숨기려는 데 쓰였습니다. 그래서 중계를 아는 출처에만 허락하는 사이트가 생겼습니다. 구현에는 이런 걸러 내기 기능이 있는 것이 좋습니다(SHOULD). 정책을 들어 거절할 때는 550 을 쓰는 것이 좋습니다(SHOULD).

제출 쪽은 반대로 묶습니다. 받는 통로는 열고 내보내는 통로는 잠그는 짝입니다 — 인증 없는 세션의 제출은 530 으로 막습니다.

관련 항목

SMTP 한 번의 대화에 오가는 명령

EHLO · HELO · MAIL · RCPT · DATA · QUIT · RSET · NOOP · VRFY · EXPN · HELP · STARTTLS · SMTP AUTH

SMTP 서버가 돌려주는 응답 코드

상태 코드 · SMTP 220 · SMTP 221 · SMTP 250 · SMTP 354 · SMTP 421 · SMTP 450 · SMTP 451 · SMTP 500 · SMTP 503 · SMTP 530 · SMTP 550 · SMTP 554 · 확장 상태 코드

SMTP 가 나르는 메일 객체를 이루는 부분

봉투 · 인터넷 메시지 형식 · 헤더 · Received 헤더 · Return-Path · 메시지 아이디 · 첨부 파일 · 숨은참조

SMTP 주소에 쓰이는 이름

메일 주소 · reverse-path · forward-path · 로컬 파트 · 도메인 이름 · 널 반환 경로 · postmaster

SMTP 로 메일을 주고받는 역할

MUA · MSA · MTA · 메일 중계 · 메일 게이트웨이 · 메시지 저장소

메일을 메일함에서 꺼내 오는 프로토콜

IMAP · POP3 · JMAP · 메일박스 · 웹메일

SMTP 가 올라타는 아래 계층과 이름 조회

TCP · DNS · MX 레코드 · 암묵 MX · TLS · 포트 번호

SMTP 위에 얹히는 서비스 확장

ESMTP · STARTTLS · SMTP AUTH · SASL · 8BITMIME · SMTPUTF8 · SIZE · DSN · PIPELINING · ENHANCEDSTATUSCODES

메일 본문에 글자와 첨부를 담는 규칙

MIME · base64 · quoted-printable · ASCII · UTF-8 · multipart/report

제출 통로를 이루는 규격과 포트

메시지 제출 · 포트 587 · 포트 465 · 포트 25 · 암묵적 TLS

배달이 실패할 때 생기는 메일과 장애

반송 메일 · 배달 상태 통지 · 메일 큐 · 메일 루프 · 블랙리스트 · 배달 지연 알림 · 중복 배달

보내는 쪽이 진짜인지 확인하는 장치

SPF · DKIM · DMARC · 발신자 위조 · 메일 서명 · PGP

SMTP 를 통해 퍼지는 악용과 그 대응

스팸 · 피싱 · 오픈 릴레이 · 그레이리스팅 · 백스캐터

SMTP 를 구현한 메일 서버와 발송 대행 서비스

Postfix · Sendmail · Exim · Microsoft Exchange · Amazon SES · SendGrid · Mailgun

SMTP 를 직접 다루는 도구와 라이브러리

telnet · swaks · MailHog · smtplib · JavaMail · Nodemailer

SMTP 가 속하는 상위 분류

전자메일 · 프로토콜 · 응용 계층 · 인터넷 프로토콜 스위트 · 네트워크

다른 이름: Simple Mail Transfer Protocol · 단순 메일 전송 프로토콜 · 메일 전송 프로토콜