반응형
C로 구현하는 MIME Parser (1)

메일의 동작 원리와 메일 형식

메일과 MIME에 대해 이해를 하고, 스스로 MIME Parser를 구현해보면 추후에 어떤 MIME 버전이 나오더라도 어렵지 않게 새로운 버전을 자신의 애플리케이션에 적용시킬 수 있을 것이다. 이러한 의도를 가지고 메일과 MIME Parser 구현에 관한 연재를 하고자 한다.

(주)넷사랑컴퓨터 조한열
hanyoul@netsarang.com

인터넷이 등장하면서 가장 많이 사용되어 왔으며 앞으로도 그 확고한 지위를 놓치지 않을 애플리케이션이 바로 전자메일이다. 월드 와이드 웹(WWW)이 급속도로 성장하고 있지만 전자메일을 따라잡을 수는 없을 것이고, 이는 앞으로도 꽤 오랫동안 마찬가지일 것이다. 오히려 전자메일은 웹의 편리성을 자신에게 적용시켜 웹메일이라는 독특한 영역을 개척해왔다. 웹이 발전하면서 생긴 수많은 포탈 사이트 가운데 웹메일 서비스를 제공하지 않는 곳이 없을 정도로 전자메일은 웹의 영역으로 자신을 확대시켜 나가고 있는 것이다.
이처럼 메일이 자신의 영역을 끊임없이 넓혀나가며 인터넷의 핵심 애플리케이션으로서의 확고한 지위를 차지하고 있지만, 메일의 동작원리와 메일 애플리케이션의 구현에 대한 이해를 가지고 있는 사람들은 그다지 많지 않다. 메일 애플리케이션을 구현할 때, 기존에 나와있는 메일 관련 라이브러리들을 사용하면 되지만(그나마 C로 된 라이브러리는 없는 것 같다.) 메일과 MIME에 대한 이해가 없이 단순히 라이브러리만을 사용한다면 추후에 어려움에 봉착할 수도 있을 것이다. 지금 현재는 MIME 버전이 1.0이지만 언제 차기 MIME 버전이 나와 우리를 당황하게 할 지 모르는 일이기 때문이다.

1. 메일 시스템의 구성

메일을 보내고 받으려면 어떠한 프로그램들이 필요한지 한 번 생각해보자. 가장 간단하게 생각한다면 2개의 프로그램이 있으면 가능할 것이다. 하나는 사용자가 메일을 작성하여 보내는 프로그램이고, 다른 하나는 자신에게 온 메일을 받는 프로그램일 것이다. 그러나 곰곰히 생각해보면 이 두 개의 프로그램만으로는 메일을 주고 받을 수 없다는 것을 알 수 있다. 만약 A라는 컴퓨터에 있는 a라는 사용자가 B라는 컴퓨터에 있는 b라는 사용자에게 메일을 보낸다고 가정하자. 위의 두 개의 프로그램만으로 a가 b에게 메일을 보낸다고 할 때, 만약 b가 B 컴퓨터에 접속을 하고 있지 않는다면 a는 메일을 나중(b가 B 컴퓨터에 접속했을 때)에 다시 보내야 한다. 그렇게 하고 싶지 않다면 B 컴퓨터에 프로그램이 하나가 더 있어서 a가 보내온 메일을 보관하고 있다가 b가 B 컴퓨터에 접속했을 때, 보관해온 메일을 b에게 보내주는 역할을 해야 한다.
그러나 이렇게 한다 해도 완벽한 것이 아니다. a가 b에게 메일을 보내려고 하는데 B 컴퓨터가 작동불능 상태에 있다면 어떻게 될까? B 컴퓨터에 있는 메일 보관 프로그램이 동작을 하지 않고 있으므로 메일을 B 컴퓨터가 작동 가능할 때 다시 보내야 한다. 그러면 메일을 다시 보내는 일은 누가 해야 할 것인가? 이러한 상황이라면 메일을 다시 보내는 일은 사용자의 몫이 되고 만다. 그러나 이는 너무나도 불편한 일이다. 만약 A 컴퓨터에 메일 전송만을 담당하는 프로그램이 있어서 a가 보내라고 요구하는 메일을 받아서 보관하고 있다가 B 컴퓨터가 작동 가능할 때에 B 컴퓨터에 있는 메일 보관 프로그램에게 보낼 수 있다면 a는 메일이 B 컴퓨터에 도착했는지의 여부에 대해 일일이 신경 쓰지 않아도 될 것이다. 그 역할은 A 컴퓨터에 있는 메일 전송 프로그램과 B 컴퓨터에 있는 메일 보관 프로그램의 몫이기 때문이다.

위와 같은 모든 경우들을 고려하여 안정적으로 메일을 보낼 수 있도록 메일 시스템이 설계되었다. 메일 시스템의 가장 중요한 요소들은 그림 1과 같다.

그림 1 : 메일 시스템의 구성 요소

(박스)
Mail User Agent(MUA)
사용자가 메일을 보내고 받는데 사용되는 메일 클라이언트 프로그램이다.
Mail Transfer Agent(MTA)
한 컴퓨터에서 다른 컴퓨터로 메일을 전송하는데 사용되는 메일 서버 프로그램이다. MUA가 보내라고 요청한 메일을 다른 컴퓨터로 전송하는 역할을 한다. 또한 다른 컴퓨터에서 사용자에게 보내온 메일을 받는 역할을 한다.
Mail Delivery Agent(MDA)
MTA가 받은 메일을 사용자의 메일 보관함에 집어넣는 역할을 하는 프로그램이다. MDA는 독립적으로 작동하는 프로그램이 아니라 MTA에 의해서 사용되는 프로그램이다.

다시 A 컴퓨터의 a가 B 컴퓨터의 b에게 메일을 보내는 예를 가지고 메일 시스템 구성을 설명해보자.
a는 자신의 MUA로 b에게 보낼 메일을 작성한다. 그리고 a의 MUA에게 메일을 보내라고 명령한다. 그러면 a의 MUA는 A 컴퓨터에 있는 MTA에게 B 컴퓨터에 있는 b에게 메일을 보내라고 요청한다. MUA의 요청을 받은 MTA는 a의 MUA가 보내온 메일을 받아서 B 컴퓨터에 있는 MTA에게 메일을 전송한다. 이 때 B 컴퓨터가 사용가능하지 않다면 A 컴퓨터의 MTA가 a의 메일을 보관하고 있다가 주기적으로 B 컴퓨터의 MTA에게 메일을 전송하려고 시도한다. 그러다가 B 컴퓨터가 작동 가능하게 될 때, B 컴퓨터의 MTA에게 메일을 성공적으로 전송하게 된다. B 컴퓨터에 있는 MTA는 전송 받은 메일을 B 컴퓨터의 MDA에게 전달하고, B 컴퓨터의 MDA는 b의 메일 보관함에 전송 받은 메일을 집어넣는다. 나중에 b가 자신의 MUA를 실행시키게 되면, b의 MUA는 b의 메일 보관함을 살펴보고 새로운 메일이 있으면 b에게 메일을 보여주게 된다.

1.1 메일 전송 프로토콜 - Simple Mail Transfer Protocol(SMTP)
A 컴퓨터와 B 컴퓨터에 있는 MTA는 인터넷을 통해 메일을 전송한다. 네트워크를 이용한 모든 전송에는 송신측과 수신측간에 합의된 규약(Protocol)이 있어야 한다. 송신측에서 보낸 데이터를 수신측에서 이해하지 못한다면 전송은 이미 실패한 것이다. 메일 전송 역시도 당연히 송수신간에 합의된 규약이 있어야 한다. 메일 전송 규약이 바로 Simple Mail Transfer Protocol(SMTP)이다. SMTP는 RFC 821 문서에 자세히 설명되어 있다.

지금부터 SMTP에 대하여 설명할 것이다. 만약 지금부터 설명하는 내용이 잘 이해되지 않더라도 끝까지 읽어보자. 다음 절에서 실제로 SMTP를 사용하여 직접 메일을 보내볼 것인데, 그 때가 되면 지금 설명하는 내용들을 확실히 이해할 수 있을 것이다. 그러니 꾹 참고 읽어나가자.
먼저 SMTP가 동작하는 모습을 한번 살펴보자(리스트1). 모든 유닉스에는 아주 간단한 기능을 수행하는 메일 클라이언트(MUA)인 mail이라는 프로그램이 존재한다. mail에 옵션 -v를 주고 간단한 메일 하나를 hanyoul@netsarang.com에게 보내보자. 옵션 -v는 메일의 전송과정을 자세히 보여주는 옵션이다. 이 때, 보내는 사람은 cho1102@conux.conux.com이다.

리스트 1 : SMTP의 동작
$ mail -v hanyoul@netsarang.com <Enter>
Subject: 메일 테스트입니다. <Enter>
메일 본문입니다. <Enter>
. <Enter>
Cc: <Enter>
hanyoul@netsarang.com... Connecting to netsarang.com. via esmtp...
220 netsarang.com ESMTP Sendmail 8.9.3/8.9.3; Tue, 4 Jul 2000 18:38:25 +0900
>>> EHLO conux
250-netsarang.com Hello IDENT:cho1102@conux.conux.com [210.118.172.101], pleased to meet you
250-EXPN
250-VERB
250-8BITMIME
250-SIZE
250-DSN
250-ONEX
250-ETRN
250-XUSR
250 HELP
>>> MAIL From:<cho1102@conux.conux.com> SIZE=72
250 <cho1102@conux.conux.com>... Sender ok
>>> RCPT To:<hanyoul@netsarang.com>
250 <hanyoul@netsarang.com>... Recipient ok
>>> DATA
354 Enter mail, end with "." on a line by itself
>>> .
250 SAA03728 Message accepted for delivery
hanyoul@netsarang.com... Sent (SAA03728 Message accepted for delivery)
Closing connection to netsarang.com.
>>> QUIT
221 netsarang.com closing connection

Subject와 본문 그리고 Cc(참조)까지 입력을 하고 나면 mail 프로그램은 입력된 메일을 수신자에게 전송한다. 이 때, 메일을 전송하는 과정이 나타날 텐데 이것을 유심히 살펴보면 SMTP의 원리를 알 수 있다.
먼저 "Connecting to netsarang.com. via esmtp..."라는 메시지가 눈에 띌 것이다. 이 메시지가 나타내는 의미는 지금 mail 프로그램이 ESMPT(나중에 설명하겠지만 SMTP의 확장 프로토콜이다.)를 이용하여 netsarang.com에 있는 메일 서버(MTA)와 연결을 한다는 것이다.
수신측(netsarang.com)의 메일 서버(MTA)와 연결이 성공되면 mail 프로그램은 SMTP 명령어를 수신측 MTA에게 전달하고, 수신측 MTA는 송신자가 보낸 명령어에 대한 응답을 보내온다. 명령어와 응답의 주고받음을 통해 메일이 전달되는 것이고 이러한 명령어와 응답의 관계를 기술한 것이 바로 SMTP이다.
위에서 >>> 다음에 나오는 문자열들이 mail 프로그램이 수신측 메일 서버에게 보내는 명령어이다. 수신측 메일 서버는 mail 프로그램이 보낸 각각의 명령어에 대해 응답을 보내오게 되는데, 그 응답은 상태를 나타내는 숫자 값과 문자열로 구성된다. mail 프로그램은 응답의 첫머리에 나타나는 숫자 값만으로도 자신이 보낸 명령에 대한 수신측의 응답상태를 알 수 있다.

2. SMTP 명령어

EHLO [Domain name]
SMTP 세션의 시작. 자신의 도메인 이름을 수신측 MTA에게 알려주는 명령어이다. 수신측 MTA는 이 도메인 이름이 올바른 형식의 도메인 이름인지를 검사한다. 메일 relay를 방지하기 위해서는 송신측의 도메인 이름을 얻어내는 것이 필수적으로 필요하다. EHLO 명령을 사용하지 않으면 수신측에서 메일 수신을 거부할 가능성이 크다. 이는 수신측 MTA의 환경설정에 따라 달라진다.

MAIL FROM: [Email Address]
메일을 보내는 사람을 수신측 MTA에게 알려주는 명령어이다. 수신측 MTA는 이 전자메일 주소가 올바른지를 검사한다. 이 역시 EHLO처럼 올바르지 못한 주소가 입력될 경우 수신을 거부한다.

RCPT TO: [Email Address] 혹은 [User's ID]
메일을 받을 사람을 수신측 MTA에게 알려주는 명령어이다. 메일을 받을 사람이 수신측의 로컬사용자이면 ID만을 입력해도 된다. 만약 수신측 MTA가 메일 relay를 허용하지 않을 경우(수신측의 MTA를 사용하여 다른 컴퓨터에 있는 MTA에 접속하는 것. 이 경우에 수신측 MTA는 다른 컴퓨터로 메일을 중계하게 되는 데, 이를 relay라고 한다.)에 다른 컴퓨터에 있는 사용자의 전자메일 주소를 입력하게 되면 relay를 허용하지 않는다는 메시지와 함께 수신을 거부한다.

DATA
메일을 입력하겠다는 명령어이다. DATA 명령 다음에 입력되는 것은 메일 메시지이다. 새로운 줄 처음에 마침표(.)를 입력하고 엔터(CRLF)를 치면 메일 메시지 입력을 끝낸다.

QUIT
SMTP 세션을 닫는다.

이 외에도 SEND, EXPN, NOOP, SAML, SOML, RSET, VRFY등의 명령어들이 있으나, 설명하고자 하는 내용에서 벗어나므로 자세한 설명은 하지 않겠다. RFC 821 문서를 보면 자세한 설명이 되어있으니 관심있는 독자들은 참고하기 바란다.

telnet netsarang.com 25
그러면 위에서 살펴본 SMTP를 이용하여 실제로 메일을 보내보자. SMTP를 직접 이용하기 위해서는 telnet을 통해 수신측 MTA에 직접 접속하여야 한다. 수신측 MTA는 자신의 네트워크 포트 25번을 열어 놓고 SMTP 연결을 기다리고 있다. 그러므로 우리는 Telnet 클라이언트를 이용하여 수신측 컴퓨터의 25번 포트로 접근하면 SMTP를 사용하여 수신측 MTA에게 메일을 보낼 수 있는 것이다.

유닉스에서 기본적으로 사용되는 telnet 프로그램을 이용해서, 25번 포트로 접근하겠다는 옵션을 주고 netsarang.com(수신측 도메인 이름)에 접속해보자. 명령은 다음과 같다.

$ telnet nesarang.com 25<Enter>

위와 같은 명령으로 netsarang.com에 있는 MTA에 접속을 하게 되면 다음과 같은 메시지가 나타난다.

Trying 210.118.172.100...
Connected to netsarang.com.
Escape character is '^]'.
220 netsarang.com ESMTP Sendmail 8.9.3/8.9.3; Mon, 10 Jul 2000 15:22:27 +0900

netsarang.com의 MTA가 메일을 받을 준비가 되어 있다면 마지막 줄과 같은 메시지가 나타날 것이다. 220이라는 응답상태 값은 OK 사인이다. 위의 메시지를 통해 netsarang.com의 MTA는 Sendmail 버전 8.9.3 임을 알 수 있고, SMTP의 확장 프로토콜인 ESMTP를 지원하고 있음을 알 수 있다. netsarang.com의 MTA는 SMTP 접속을 허락한다는 OK 사인을 보내놓고 SMTP 시작을 기다리고 있는 것이다.

이제 SMTP 명령을 netsarang.com의 MTA에게 내려보자.

EHLO conux.com<Enter>
250-netsarang.com Hello IDENT:hanyoul@conux.conux.com [210.118.172.101], pleased to meet you
250-EXPN
250-VERB
250-8BITMIME
250-SIZE
250-DSN
250-ONEX
250-ETRN
250-XUSR
250 HELP

위에서도 언급했듯이 EHLO는 SMTP의 시작을 알리면서 자신의 도메인 이름을 수신측 MTA에게 전달해주는 것이다. EHLO 명령에 수신측 MTA는 250이라는 응답상태와 함께 자신의 정보를 알려주고 있다. 이 응답의 자세한 의미에 대해서는 그냥 넘어가도 좋다. 단지 conux.com이라는 도메인 이름으로 요청되는 메일 전송에 대해 수신측 MTA가 허락하는 것이라고 생각하면 된다.

만약, 도메인 이름의 형식이 잘못되었다면 수신측 MTA는 501이라는 응답코드와 함께 에러 메시지를 보내온다. 도메인 이름에 마침표(.)대신 쉼표(,)를 사용하여 EHLO 명령을 내렸다면 다음과 같은 에러 메시지를 볼 수 있을 것이다.

EHLO conux,com<Enter>
501 Invalid domain name

EHLO 명령에 대한 응답이 OK였다면 MAIL FROM: 명령을 내려서 메일을 보내려고 하는 사람이 누구인지 수신측 MTA에게 알려야 한다.

MAIL FROM: cho1102@conux.com<Enter>
250 cho1102@conux.com... Sender ok

만약 도메인 이름을 생략한다면 수신측 MTA는 도메인 이름이 필요하다고 투정을 부릴 것이다.

MAIL FROM: cho1102<Enter>
553 cho1102... Domain name required

응답상태 250번의 OK 사인이 떨어지면 RCPT TO: 명령을 통해 누구에게 메일을 보낼 것인지 알려야 한다.

RCPT TO: hanyoul<Enter>
250 hanyoul... Recipient ok

hanyoul 뒤에 도메인 이름이 없기 때문에 이는 수신측 컴퓨터의 로컬 사용자라는 것을 나타낸다. 수신측 MTA는 자신의 로컬 사용자 중에 hanyoul이라는 이름이 있는 지를 살펴보고 hanyoul이라는 사용자가 존재한다면 250번 응답코드와 함께 OK 사인을 보낸다.

RCPT TO: 명령은 다른 명령들과는 다르게 여러 번의 사용도 가능하다. 그러므로 한 개의 메일 메시지를 가지고 여러 명에게 동시에 보내는 일이 가능하게 된다. RCPT TO: 를 여러 번 사용하여 받을 사람을 정해주면 간단히 동시에 여러 명에게 똑같은 메일을 보낼 수 있는 것이다.

RCPT TO: hanyoul<Enter>
250 hanyoul... Recipient ok
RCPT TO: nkkwak<Enter>
250 nkkwak... Recipient ok

만약 메일 받을 사람이 존재하지 않으면 550번 응답코드와 함께 에러 메시지를 보내온다.

RCPT TO: noname<Enter>
550 noname... User unknown

RCPT TO: 를 이용하여 다른 컴퓨터에 있는 사용자에게도 메일을 보낼 수가 있는데, 이럴때는 도메인 이름까지 들어간 전자메일 주소를 사용하면 된다. 아래에서 nownuri.net에 있는 사용자의 이름을 사용하였는데, 이렇게 되면 수신측 MTA는 nownuri.net의 MTA에게 자신이 받은 메일을 중계하게 된다.

RCPT TO: idol110@nownuri.net<Enter>
250 idol110@nownuri.net... Recipient ok

그러나, 수신측 MTA는 자신의 로컬 사용자에게 보내지는 것이 아닌 메일은 수신을 거부할 수가 있다. 악의적인 사용자들이 수신측 MTA를 이용해 스팸메일을 대량으로 발송하는 것이 가능하기 때문에 메일 중계기능(relaying)을 허용하지 않는 MTA가 많다. 이러한 MTA들은 다음과 같은 에러 메시지를 출력한다.

RCPT TO: idol110@nownuri.net<Enter>
550 idol110@nownuri.net... Relaying denied

RCPT TO: 를 사용하여 메일 수신자에 대한 확인까지 이루어졌으면 이제는 메일 메시지를 전송해야 한다. 메일 메시지를 전송하기 위한 명령어는 DATA이다.

DATA<Enter>
354 Enter mail, end with "." on a line by itself

DATA 명령을 받은 수신측 MTA는 메일 메시지를 전송하라고 요청을 한다. 메일 메시지의 끝을 나타낼 때는 마침표(.)를 찍으라는 친절한 안내를 덧붙이고 있다. 그러면 간단한 메일 메시지 하나를 보내보자.

Subject: 메일 테스트입니다.<Enter>
<Enter>
메일 본문입니다.<Enter>
.<Enter>
250 KAA32447 Message accepted for delivery

응답코드 250번과 함께 메일 메시지가 받아들여졌다는 메시지가 출력되면 수신측 MTA가 수신측 MDA를 통해 수신자에게 메일을 보낼 준비가 되었다는 것을 의미한다. 메일 발송은 간단히 성공을 한 것이다.

이제는 SMTP 세션을 끝내면 된다. SMTP 세션을 끝내기 위해서는 QUIT 명령을 사용한다.

QUIT<Enter>
221 netsarang.com closing connection
Connection closed by foreign host.

SMTP를 통해 보내진 메일 메시지는 수신측 MTA에 의해 저장이 된다. 저장된 메일 메시지를 보려면 수신측 MTA가 저장한 파일을 보면 된다. 수신측 MTA가 저장하는 파일의 위치는 MTA 종류에 따라 틀릴 것이다. 가장 대표적인 MTA인 sendmail이 메일 메시지를 저장하는 곳은 /var/spool/mail/ 디렉토리이고, 파일 이름은 수신자의 ID와 같다. 조금 전에 SMTP를 통해 보낸 메일을 이 곳에서 확인해보자. 확인을 하기 위해서는 netsarang.com으로 접속하여 hanyoul이라는 ID로 로그인 해야 함은 당연하겠다.

$ cat /var/spool/mail/hanyoul<Enter>
From cho1102@conux.com Tue Jul 11 10:15:17 2000
Return-Path: <cho1102@conux.com>
Received: from conux.com (IDENT:hanyoul@conux.conux.com [210.118.172.101])
by netsarang.com (8.9.3/8.9.3) with ESMTP id KAA32568
for hanyoul; Tue, 11 Jul 2000 10:14:56 +0900
Date: Tue, 11 Jul 2000 10:14:56 +0900
From: cho1102@conux.com
Message-Id: <200007110114.KAA32568@netsarang.com>
Subject: 메일 테스트입니다.

메일 본문입니다.
표 1 : 메일 본문

표 1은 조금 전에 우리가 보낸 메일 메시지이다. 메일 메시지는 MUA가 이해할 수 있도록 정형화된 포맷을 가지고 있다. 메일 메시지를 보낼 때에 이 포맷에 맞게 보내야 수신측 MUA가 메일 메시지를 이해할 수 있다. 이러한 메일 메시지 포맷을 정의한 것이 RFC 822 문서이다. RFC 822에서 메일 메시지의 포맷을 정의하고 있다는 이유 때문에 메일 메시지를 RFC 822 메시지라고 부른다.

3. RFC 822 메시지의 구성

RFC 822 메시지는 크게 헤더와 본문으로 구성되어 있다. 헤더는 메일 발송에 대한 모든 정보를 알려주는 역할을 하고, 본문은 말 그대로 메일 메시지의 내용을 나타낸다. RFC 822 메시지에서 헤더와 본문의 구분은 빈 줄(CRLF)로 한다. 예로 보여준 위의 메일 메시지에서 헤더는 첫 번째 줄부터 갨ubject: 메일 테스트입니다. 까지로 구성되어 있다. Subject 다음 줄은 빈 줄인데, 이 빈 줄이 헤더와 본문을 구분하는 구분자이다. 그러므로 빈 줄 다음에 나오는 문자들은 모두 메일 본문에 속한다. 그러나 본문은 있을 수도 있고 없을 수도 있다. 헤더만으로 구성된 메일 메시지도 있을 수 있다는 것이다.

3.1 RFC 822 헤더
RFC 822 헤더는 메일에 관한 정보를 담고 있다. RFC 822 헤더는 콜론(:)으로 구분되는 이름-값 쌍으로 이루어져 있다. 예를 들어 갌rom: cho1102@conux.com" 이라는 헤더의 한 구성요소는 긃rom' 이라는 헤더 이름과 꼊ho1102@conux.com' 이라는 헤더 값으로 이루어져 있고, 콜론(:)으로 이름과 값이 구분되어 있다.

헤더 값이 길 경우에는 헤더 값이 여러 줄에 걸쳐 나올 수가 있다. 이를 Long Header(긴 헤더)라고 한다. 만약 헤더의 값이 여러 줄로 표시되어야 할 필요가 있을 때에는 새로운 줄 앞에 스페이스나 탭(White space)을 한 개 이상 집어넣어 Long Header임을 나타낸다. 위의 예에서 Received 헤더는 Long Header이다.

Received: from conux.com (IDENT:hanyoul@conux.conux.com [210.118.172.101])
(한개 이상의 스페이스나 탭) by netsarang.com (8.9.3/8.9.3) with ESMTP id KAA32568
(한개 이상의 스페이스나 탭) for hanyoul; Tue, 11 Jul 2000 10:14:56 +0900

위에서 설명한 헤더의 구성 규칙을 문법으로 나타내면 다음과 같다. 문법을 나타내는 다음과 같은 형식을 잘 모르는 독자도 자세히 살펴보면 쉽게 이해 할 수 있을 것이다. 정 모르겠다면 넘어가도 좋다. 위에서 설명한 내용을 정리한 것에 불과하기 때문이다.

field = field-name : [ field-value ] CRLF
field-name = 1*<컨트롤 문자와 스페이스, 콜론(:)을 제외한 아스키 문자>
field-value = field-value-contents
[CRLF LWSP-chars field-value]
field-value-contents = <CRLF를 제외한 아스키 문자열>

Return-path
Return-path 헤더는 마지막으로 메일을 수신한 MTA에 의해서 붙여지는 헤더이고, 메일을 회신할 주소를 가리킨다. Reply-To 헤더와 같은 역할을 하지만 Return-path 헤더는 MTA에 의해 덧붙여지기 때문에 항상 존재하는 헤더이다.

Received
Received 헤더는 메일을 수신한 MTA마다 덧붙이는 헤더이다. 자신이 받았다고 스탬프를 찍는 것과 같다. 그러므로 Relay 되는 메일은 Received 헤더가 여러 개가 존재한다. Received 헤더를 참고하면 전송되는 메일이 어느 MTA를 언제 거치면서 전송되는 지 알 수 있다.

From
From 헤더는 메일 메시지를 보내는 사람을 가리키는 헤더이다. 원래의 메일 메시지에 From 헤더가 없으면 수신측 MTA가 덧붙인다.

Sender
Sender 헤더는 실제로 메일 메시지를 작성한 사람을 가리키는 헤더이다. From 헤더가 가리키는 사람이 실제로 메일 메시지를 작성한 사람이 아닐 때, Sender 헤더를 사용하여 실제 작성자를 가리키는 것이다.

Reply-To
Reply-To 헤더는 회신할 주소를 가리키는 헤더이다. Reply-To 헤더가 존재할 때에는 mail을 Reply-To 헤더가 가리키는 주소로 회신하여야 한다. 만약 Reply-To 헤더가 존재하지 않는다면 From 헤더가 가리키는 주소로 회신하여야 한다.

Resent-From, Resent-Sender, Resent-Reply-To
Resent- 가 앞에 붙은 Resent-Reply-To, Resent-From, Resent-Sender는 각각 Reply-To, From, Sender 와 같은 의미이다. 단지 Resent- 가 붙은 헤더들이 그렇지 않은 헤더들보다 더욱 최신의 정보라는 것을 나타낼 뿐이다. 이는 Resent- 를 붙인 MTA에 의해 mail이 Forwarding되었음을 나타낸다.

Date
메일이 작성된 시간을 나타낸다. Date 헤더가 원래의 메일 메시지에 들어있지 않다면 송신측 MTA가 Date 헤더를 원래의 메시지에 덧붙인다.

Resent-Date
Date 헤더와 같은 역할을 하지만 좀 더 최신의 정보를 가리킨다.

To
메일을 받을 사람을 가리킨다. 여러 명을 가리킬 수도 있는데, 이 때는 각각의 전자메일 주소를 쉼표(,)로 구분한다.

Resent-To
To 헤더와 같은 역할을 하지만 좀 더 최신의 정보를 가리킨다.

Cc
메일을 참조할 사람을 가리킨다. Cc 헤더가 가리키는 사람에게 메일의 복사본이 보내진다. Cc 역시 To 헤더와 마찬가지로 여러 명을 가리킬 수도 있고, 각각의 전자메일 주소를 쉼표(,)로 구분하면 된다.

Resent-cc
Cc 헤더와 같은 역할을 하지만 좀 더 최신의 정보를 가리킨다.

Bcc
Bcc 헤더는 숨은 참조를 가리킨다. Cc 와 마찬가지로 메일을 참조할 사람을 가리키지만, 원래의 메일을 받는 사람은 자신에게 보내진 메일이 Bcc가 가리키는 사람에게 메일 복사본이 보내진 것을 알 수가 없다. Bcc 역시 여러 명을 가리킬 수 있다.
Resent-bcc
Bcc 헤더와 같은 역할을 하지만 좀 더 최신의 정보를 가리킨다.

Subject
Subject 헤더는 메일의 제목을 나타낸다.

X-로 시작되는 헤더들
X-로 시작되는 헤더들은 RFC 822 표준 헤더가 아니라 사용자나 MUA가 필요에 따라 정의해서 사용하는 헤더들이다.

3.2 RFC 822 헤더에 사용되는 시간 표시방법
RFC 822 헤더 가운데 시간 정보를 표시해야 하는 헤더들이 있다. 대표적인 헤더들로 Date, Received 헤더가 있다. 이러한 헤더들이 시간을 나타낼 때에는 규칙적인 형식으로 나타내야 수신측 MUA가 헤더에서 사용된 시간을 자동적으로 검출해 낼 수 있다. 위의 예에서 Date 헤더를 살펴보자.

Date: Tue, 11 Jul 2000 10:14:56 +0900

위의 예가 전형적인 시간 표시 형식이다. 처음에는 요일을 나타내는 3개의 문자가 온다. (Mon, Tue, Wed, Thu, Fri, Sat, Sun) 그리고 요일 다음에 반드시 쉼표(,)가 있어야 한다. 요일과 쉼표는 함께 생략될 수도 있다. 요일+쉼표 다음에는 일, 월, 연도가 나타나는데 각각은 스페이스로 구분이 된다. 이 때 월을 나타낼 때는 숫자가 아닌 3개의 문자로 나타낸다. (Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec) 또한 연도는 2자리로 표시할 수도 있고 네자리로 표시할 수도 있다. 일, 월, 연도 뒤에는 시간을 나타내는 시, 분, 초가 나타나는데, 각각은 2자리의 숫자로 표시되고, 콜론(:)으로 구분된다. 이 때 초는 생략될 수 있다. 시, 분, 초 다음에는 time zone이 표시된다. time zone이란 지역 시간대를 말한다. 영국의 그리니치 천문대를 중심으로 얼마만큼의 시간이 떨어져 있는지를 나타내는 것이다. 이 때, 그리니치 천문대보다 동쪽에 있으면 (+)이고, 서쪽에 있으면 (-)이다. 우리나라는 일본과 함께 동경 표준시를 사용하고 있는데, 이 시간대는 그리니치 천문대로부터 동쪽으로 9시간 차이가 난다. 그러므로 우리나라의 time zone 표시는 +0900이 된다. time zone을 숫자로 표시하지 않고 기호로 표시할 수도 있는데 이는 다음과 같다.
UT(+0000), GMT(+0000), EST(-0500), EDT(-0400), CST(-0600), CDT(-0500), MST(-0700), MDT(-0600), PST(-0800), PDT(-0700), A(-0100), B(-0200), C(-0300), D(-0400), E(-0500), F(-0600), G(-0700), H(-0800), I(-0900), K(-1000), L(-1100), M(-1200), N(+0100), O(+0200), P(+0300), Q(+0400), R(+0500), S(+0600), T(+0700), U(+0800), V(+0900), W(+1000), X(+1100), Y(+1200)

RFC 822 시간 표준 포맷으로 이루어진 문자열을 시간 구조체로 바꿔주는 함수를 다음과 같이 작성할 수 있다. 아래 함수에서 hStrExplode()라는 함수가 있는데 이 함수는 주어진 문자열을 주어진 구분자로 분리해 문자열의 배열을 리턴하는 함수이다. 시간 표시를 콜론(:)으로 분리해내기 위해 사용했다. 독자여러분이 스스로 한번 작성해보기 바란다.

리스트 2 : RFC 822 시간 표준 포맷으로 이루어진 문자열을 시간 구조체로 바꿔주는 함수
struct tm *hRFC822strToTime(const char *str)

static struct tm t;
char date_str[7][4] = "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat";
char month_str[12][4] = "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec";
char wday[4]; /* Mon, Tue .. Sun */
char year[5]; /* 2000, 2001 .. */
char month[4]; /* Jan, Feb .. Dec */
char day[3]; /* 1..31 */
char timeStr[9]; /* h:m:s */
char zone[40];
char z_hour[3], z_min[3];
int i;
char *temp = (char *)0x00;
char **parseStr = (char **)0x00;

/* 인수가 0x00이면 시스템 시간을 리턴한다. */
if(str == 0x00)
return localtime(time(0x00));

if(isalpha(str[0])) /* str의 첫 문자가 알파벳이면 요일이 들어있다. */
sscanf(str, "%s%s%s%s%s%s", wday, day, month, year, timeStr, zone);
else
sscanf(str, "%s%s%s%s%s", day, month, year, timeStr, zone);

/* 요일 */
if(isalpha(str[0])) /* str의 첫 문자가 알파벳이면 요일이 들어있다. */

wday[3] = 0x00; /* 쉼표(,) 제거 */

for(i=0;i<7;i++)
if(!strcmp(date_str[i], wday))

t.tm_wday = i;
break;



/* 년 */
t.tm_year = (strlen(year) == 4) ? atoi(year) - 1900 : 2000 + atoi(year) - 1900;

/* 월 */
for(i=0;i<12;i++)
if(!strcmp(month_str[i], month))

t.tm_mon = i;
break;


/* 일 */
t.tm_mday = atoi(day);

/* 시간 */
parseStr = hStrExplode(timeStr, ":");

t.tm_hour = atoi(parseStr[0]);
t.tm_min = atoi(parseStr[1]);
t.tm_sec = parseStr[2] != 0x00 ? atoi(parseStr[2]) : 0; /* HH:MM[:SS] */

/* timezone. GNU 버젼에서만 사용가능 */
if(zone[0] == '+' || zone[0] == '-')

strncpy(z_hour, &zone[1], 2);
z_hour[3] = 0x00;
strncpy(z_min, &zone[3], 2);
z_min[3] = 0x00;

t.tm_gmtoff = (atoi(z_hour) * 60 + atoi(z_min)) * 60;

else if(isalpha(zone[0]) && zone[1] == 0x00)

if(zone[0] >= 'A' && zone[0] < 'J')
t.tm_gmtoff = ('A' - zone[0] - 1) * 60 * 60;
else if(zone[0] > 'J' && zone[0] <= 'M')
t.tm_gmtoff = ('A' - zone[0]) * 60 * 60;
else if(zone[0] >= 'N' && zone[0] <= 'Y')
t.tm_gmtoff = (zone[0] - 'N' + 1) * 60 * 60;
else if(zone[0] == 'Z')
t.tm_gmtoff = 0;

else if(!strcmp(zone, "UT") || !strcmp(zone, "GMT"))
t.tm_gmtoff = 0;
else if(!strcmp(zone, "EST"))
t.tm_gmtoff = (-5) * 60 * 60;
else if(!strcmp(zone, "EDT"))
t.tm_gmtoff = (-4) * 60 * 60;
else if(!strcmp(zone, "CST"))
t.tm_gmtoff = (-6) * 60 * 60;
else if(!strcmp(zone, "CDT"))
t.tm_gmtoff = (-5) * 60 * 60;
else if(!strcmp(zone, "MST"))
t.tm_gmtoff = (-7) * 60 * 60;
else if(!strcmp(zone, "MDT"))
t.tm_gmtoff = (-6) * 60 * 60;
else if(!strcmp(zone, "PST"))
t.tm_gmtoff = (-8) * 60 * 60;
else if(!strcmp(zone, "PDT"))
t.tm_gmtoff = (-7) * 60 * 60;
else
t.tm_gmtoff = 0;

return &t;



3.3 RFC 822 헤더에 사용되는 주석 표시방법
RFC 822 헤더에도 주석을 사용할 수 있다. RFC 822 헤더의 주석은 괄호를 사용하여 표시한다. 다음이 그 예이다.

To: hanyoul@netsarang.com (넷사랑 컴퓨터 연구원)

3.4 RFC 822 본문
RFC 822 본문은 RFC 822 헤더에 비해 너무나도 간단하다. RFC 822 헤더와 구분하기 위한 빈 줄(Null Line)의 다음 줄부터 끝 줄까지가 RFC 822 본문이다.

3.5 SMTP와 RFC 822 메시지의 한계
메일 시스템은 미국에서 개발되었고, 또한 메일 시스템은 텍스트의 전송을 그 목적으로 하였다. 이러한 이유로 메일 시스템의 개발 당시에는 영어의 전송만이 고려 대상이었다. 그러므로 아스키 문자 중에서 제어문자와 특수문자를 제외한 대소문자, 숫자, 기호만이 전송의 대상이었다. 즉, 아스키 문자 중에서 최상위 비트(8비트의 가장 첫 번째 비트)가 0인 문자만이 전송의 대상이었던 것이다. 이런 이유로 SMTP는 최상위 비트가 1인 문자의 전송을 고려하지 않았고, 실제로 많은 MTA들이 최상위 비트가 1인 문자는 전송을 허락하지 않았다. 그래서 SMTP를 7bit 전송이라고 이야기한다. 그런데 여기에서 문제가 생겨났다.
7bit 전송시에 영어의 전송은 아무런 문제가 되지 않지만, 영어가 아닌 다른 언어(유럽어, 한국어, 중국어, 일본어)들은 최상위 비트가 1인 문자도 사용하여 자신들의 문자를 표현했기 때문에 기존의 SMTP로는 메일을 전송할 수가 없었던 것이다. 또한 메일의 이용이 증가하면서 메일을 통해 바이너리 파일들을 주고 받으려는 요구들이 늘어나게 되자 7bit 전송의 문제점이 지적되기 시작하였다.

3.6 E/SMTP 의 등장
이와 같은 7bit 전송의 문제점을 극복하기 위해 SMTP의 확장 프로토콜이 등장하게 되었다. 이것이 바로 E/SMTP이다. E/SMTP는 8bit 전송을 허락하는 프로토콜이다. 하지만 지구상의 많은 MTA들이 아직도 SMTP만을 지원하고 있기 때문에 E/SMTP를 통해 메일 메시지를 보내는 데에는 문제가 있었다.

4. MIME의 개발
이를 해결하기 위해 개발된 것이 바로 Multipurpose Internet Mail Extention(MIME)이다. MIME이란 8bit 데이터를 7bit로 바꾸어 전송하는 기법을 말한다. 송신측에서 8bit 데이터를 7bit로 바꾸면 수신측에서 7bit 데이터를 다시 원래의 8bit 데이터로 복원하는 것이다. 이렇게 되면 7bit만을 지원하는 MTA를 통해서 전송되는 메일도 깨지지 않고 전송될 수 있게 된다. MIME은 여기에서 한발 더 나아가 여러 개의 바이너리 파일을 하나의 메일 메시지에 담아서 전송할 수 있도록 해주고 있다.

5. 마치며
이번 호에서는 메일의 동작원리와 메일 시스템의 구성, 그리고 메일 메시지 형식에 대한 대략적인 설명을 하였다. 너무나도 방대한 내용을 한정된 지면에서 설명하려고 하니, 심도깊은 설명이 되지 않은 측면도 있어보인다. 조금 더 심도깊은 이해를 하고 싶은 독자는 직접 RFC 821 문서와 RFC 822 문서를 읽어보기를 강력히 추천한다. 필자의 글이 심도깊은 이해를 위한 첫 길잡이가 되었다면 더 바랄 것이 없다. 다음 회에서는 7bit 기반의 SMTP의 한계로 인해 전송할 수 없었던 바이너리 데이터와 영어가 아닌 언어의 전송을 위해 개발된 Multipurpose Internet Mial Extension(MIME)에 대해서 알아볼 것이다.

+ Recent posts