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

MIME Message를 이해하자.

이번 호에서는 지난 호에서 살펴보았던 내용들을 토대로 MIME Message를 어떻게 구성하는지, 또 구성된 MIME Message에서 어떻게 우리가 원하는 데이터를 끄집어내는지에 대해서 자세히 살펴볼 계획이다. 이번 호가 끝나면 MIME Parser를 구현하기 위한 기본 내용을 모두 숙지하게 된다. 이를 기초로 하여 마지막 호인 다음 호에서는 실제로 MIME Parser를 구현해 보도록 하자.

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


1. 제일 간단한 MIME Message

리스트 1의 메일 메시지는 MIME Message이다. 왜? 이유는 단 한가지이다. MIME-Version이란 MIME 헤더가 RFC822 헤더(From, To, Subject)와 함께 들어있기 때문이다.
메일 메시지 헤더에 MIME-Version이란 헤더가 들어있다면 메일 메시지는 MIME Message로 해석이 되어야 한다. 그렇다면 리스트 1의 메일 메시지를 MIME Message로 간주하고 해석해보도록 하자.

리스트 1 : MIME Message 예
From: hanyoul@netsarang.com
To: anyone@somehost.com
Subject: The Simplest MIME Message
MIME-Version: 1.0

Hi! Everybody.
Thank you

MIME Message를 해석하기 위해서 없어서는 안될 MIME 헤더는 2가지가 있다. 바로 Content-Type과 Content-Transfer-Encoding이다. 지난 호를 유심히 살펴본 독자라면 이미 이 두가지 헤더의 의미를 알겠지만 다시 한 번 이 두가지 헤더에 대한 의미를 간략히 살펴보자.
Content-Type 헤더는 MIME Message가 포함하고 있는 내용 혹은 데이터(Content)가 어떤 타입인지를 가리키는 헤더이다. Plain text이나 GIF image등의 타입이 여기에 기술되어 있다. 만약 Content-Type이 텍스트라면 Content-Type에는 부가 정보로 Character Set에 대한 정보가 기술되어 있다.
Content-Transfer-Encoding 헤더는 8bit 데이터를 어떤 방식을 통해 7bit 데이터로 변환시켰는지를 알려주는 헤더이다. Content-Transfer-Encoding 헤더의 값으로는 7bit, 8bit, binary, quoted-printable, base64등이 있다. 위의 값 중에서 7bit, 8bit, binary는 그 어떤 변환도 하지 않음을 말해준다. 그냥 메일을 보낸 시점의 데이터가 변환되지 않고 그대로 메일 메시지에 실려왔음을 말해주는 것이다.
이제 리스트 1의 MIME Message를 다시 한 번 살펴보자. 그러나, MIME-Version이라는 헤더만 있을 뿐, Content-Type이나 Content-Transfer-Encoding 헤더는 보이지 않는다. 그렇다면 MIME Message를 어떻게 해석할 수 있을까. 정답은 간단하다. Content-Type 헤더가 없을 때에는 Content-Type이 plain text라고 간주하면 된다. 또한 이 Content의 character set은 US-ASCII(일반 영자)라고 생각하면 된다. Content-Transfer-Encoding 헤더가 없을 때에는 기본적으로 7bit 인코딩으로 간주된다. 따라서 위의 간단한 MIME Message는 리스트 2와 동일하다.

리스트 2 : 7bit로 간주된 MIME Message
From: hanyoul@netsarang.com
To: anyone@somehost.com
Subject: The Simplest MIME Message
MIME-Version: 1.0
Content-Type: text/plain; charset=us-ascii
Content-Transfer-Encoding: 7but

Hi! Everybody.
Thank you

그러므로 리스트 1의 MIME Message의 Content는 데이터변환이 없는(7bit 인코딩이니까) 영문으로 된 텍스트(text/plian; charset=us-ascii)라는 것을 알 수 있다.

이번엔 조금 복잡한 MIME Message를 살펴보자. 역상 부분이 MIME 헤더라는 것은 이제 너무도 쉽게 알 수 있을 것이다. 리스트 3의 MIME Message에 대한 설명을 보기 전에 독자 여러분이 스스로 MIME Message를 MIME 헤더 정보(역상부분)를 이용하여 해석해 보는 것도 좋을 것이다.

리스트 3 : MIME Message
From: hanyoul@netsarang.com
To: anyone@somehost.com
Subject: JPEG image file
MIME-Version: 1.0
Content-Type: image/jpeg
Content-Transfer-Encoding: base64

/9j/4AAQSkZJRgABAgAAZABkAAD//gASQWRvYmUgSW1hZ2VSZWFkef/sABFEdWNreQABAAQAAAAu
AAD/7gAOQWRvYmUAZMAAAAAB/9sAhAAKBwcHBwcKBwcKDgkICQ4QDAoKDBATDw8QDw8TEg4QDw8Q
DhISFRYXFhUSHR0fHx0dKSkpKSkvLy8vLy8vLy8vAQoJCQoLCg0LCw0QDQ4NEBQODg4OFBcPDxEP
DxcdFRISEhIVHRocFxcXHBogIB0dICAoKCYoKC8vLy8vLy8vLy//wAARCADUARsDASIAAhEBAxEB
/8QAqgAAAgIDAQAAAAAAAAAAAAAAAAECBQMEBgcBAAEFAQEAAAAAAAAAAAAAAAABAgQFBgMHEAAB
AwIFAgQEBAQEBQQDAAABABECIQMxQRIEBVFhcYEiBpGhMhOxwUIU0eFSI/DxFQdicpIzFoKiwlNU
NRcRAAEDAgQCBwYEBQUAAAAAAAEAAgMRBCExQRJxBVFhgZEiMhOhwdFCUhSxckMV8OHxMwZigsIj
JP/aAAwDAQACEQMRAD8A5wFsii2SLhBpV1mk0Q8pae5KwCQne1ROoHA5KEcitjzED0W447x71ebM
kgMa5KW6


아마도 지난 호의 내용과 앞에서 설명한 내용을 잘 이해했다면 무리없이 해석할 수 있을 것이다. 아직 이해가 되지 않는 독자들을 위해 리스트 3의 MIME Message를 해석해보자.

'MIME-Version: 1.0' 이라는 헤더를 통해 우리는 위의 메일을 MIME Message라고 간주해야 한다. Content-Type이 image/jpeg이므로 MIME Message가 포함하고 있는 Content는 jpeg형식의 image 파일이다. image 파일은 binary 파일이므로 8bit 데이터이다. 8bit 데이터를 7bit 데이터로 변환하여 메일을 전송하는 것이 안전하기 때문에(이유는 1회에서 설명하였다.) 7bit로 인코딩했다는 것을 예측할 수 있다. 그러므로 Content-Transfer-Encoding이 무엇인지를 살펴봐야 한다. Content-Transfer-Encoding 헤더는 인코딩 방식이 base64 방식임을 말해주고 있다. 즉, MIME Message가 포함하고 있는 Content는 base64 방식으로 인코딩되어 있기 때문에, 원래의 데이터를 얻기 위해서는 base64 방식에 의거하여 인코딩되 Content를 디코딩해야 한다.

/9j/4AAQSkZ........ 로 시작되는 부분이 바로 Content인데, 이것이 base64 방식으로 인코딩 된 것이다. 이를 지난 호에서 설명한 base64 방식에 의거하여 디코딩하면 원래의 jpeg image를 얻을 수 있는 것이다.

2. Multipart MIME Message

만약에 메일을 통해 위에서 보았던 jpeg image를 보내면서 이 image 파일에 대한 설명을 같은 메일에 함께 보내려면 어떻게 하면 될까? 이럴 때는 Multipart MIME Message를 만들면 된다. 처음 들어보는 말이라고 해서 너무 어렵게 생각할 필요는 없다. Multipart MIME Message란 두 개 이상의 Content를 하나의 MIME Message에 붙여넣는 것이다. 이 때, 각 Content 마다 MIME 헤더를 붙이고 난 후 각 Content를 구분할 수 있는 경계선을 표시해 넣는 것이다. 말로 설명하면 더욱 어려워질 것 같으니, 직접 Multipart MIME Message를 살펴보도록 하자.

리스트 4의 Multipart MIME Message는 image 파일과 image 파일에 대한 설명을 담은 text를 함께 묶어 구성한 것이다.

리스트 4 : Multipart MIME Message
From: hanyoul@netsarang.com
To: anyone@somehost.com
Subject: JPEG image file
MIME-Version: 1.0
Content-Type: multipart/mixed;
boundary="MIME-boundary1--------DC128F5642CA"

--MIME-boundary1--------DC128F5642CA
Content-Type: text/plain; charset=us-ascii

Hi!
I will send you a good image. This image is my favorite picture.

--MIME-boundary1--------DC128F5642CA
Content-Type: image/jpeg
Content-Transfer-Encoding: base64

/9j/4AAQSkZJRgABAgAAZABkAAD//gASQWRvYmUgSW1hZ2VSZWFkef/sABFEdWNreQABAAQAAAAu
AAD/7gAOQWRvYmUAZMAAAAAB/9sAhAAKBwcHBwcKBwcKDgkICQ4QDAoKDBATDw8QDw8TEg4QDw8Q
DhISFRYXFhUSHR0fHx0dKSkpKSkvLy8vLy8vLy8vAQoJCQoLCg0LCw0QDQ4NEBQODg4OFBcPDxEP
DxcdFRISEhIVHRocFxcXHBogIB0dICAoKCYoKC8vLy8vLy8vLy//wAARCADUARsDASIAAhEBAxEB
/8QAqgAAAgIDAQAAAAAAAAAAAAAAAAECBQMEBgcBAAEFAQEAAAAAAAAAAAAAAAABAgQFBgMHEAAB
AwIFAgQEBAQEBQQDAAABABECIQMxQRIEBVFhcYEiBpGhMhOxwUIU0eFSI/DxFQdicpIzFoKiwlNU
NRcRAAEDAgQCBwYEBQUAAAAAAAEAAgMRBCExQRJxBVFhgZEiMhOhwdFCUhSxckMV8OHxMwZigsIj
JP/aAAwDAQACEQMRAD8A5wFsii2SLhBpV1mk0Q8pae5KwCQne1ROoHA5KEcitjzED0W447x71ebM
kgMa5KW6

--MIME-boundary1--------DC128F5642CA--

먼저 Content-Type을 살펴보면 multipart/mixed라는 것을 알 수 있다. 주 카테고리인 multipart는 위의 메일이 Multipart MIME Message 포맷이라는 것을 나타내고 있다. 또한 multipart의 부 카테고리는 Content 간의 관계를 나타내는 것이 일반적인데, 여기서는 부 카테고리가 mixed임을 알 수 있다. mixed는 multipart로 묶여진 Content들이 서로 독립적이지만 Content들의 순서에 의미가 있을 때 사용한다.
Content-Type이 multipart일 때는 Content-Type의 부가정보인 boundary가 무엇보다도 중요하다. Content-Type의 부가정보인 boundary는 Content들을 구분하는 구분경계를 표시하는 것이기 때문이다. boundary 정보가 있어야만 올바르게 Multipart MIME Message를 원래의 데이터들로 파싱하는 것이 가능하다는 것은 당연할 것이다.
boundary는 메일 메시지에 등장하지 않을 만한 문자열로 만들어야 한다. 만약 Content 내용에 boundary에 해당하는 문자열이 들어있어 엉뚱하게 파싱이 되는 것을 막기 위해서이다.
실제로 boundary 문자열을 이용하여 경계를 표시할 때는 하이픈(-)을 덧붙이는 것이 규칙이다. boundary 문자열 앞에 하이픈(-) 두 개를 덧붙여 Content의 경계를 표시한다. 리스트 4의 메일에서 boundary가 "MIME-boundary1--------DC128F5642CA"(따옴표는 제외)이므로 경계를 표시하기 위한 실제 문자열은

"--MIME-boundary1--------DC128F5642CA"(따옴표 제외)

가 된다. 어떤 줄의 첫 시작이 "--MIME-boundary1--------DC128F5642CA"라면 그 다음 줄부터는 새로운 Content이다.
boundary 문자열의 앞뿐만 아니라 뒤에도 하이픈(-)이 붙은 문자열이 존재하는 데, 이는 Multipart MIME Message의 끝을 나타낸다. boundary 문자열의 앞뒤에 하이픈(-)이 두 개씩 붙은 문자열은 그 문자열의 뒤로는 Content가 더 이상 없다는 것을 뜻한다. 위의 메일에서 맨 마지막 줄을 보면 "--MIME-boundary1--------DC128F5642CA--"(따옴표 제외)가 있는데, boundary 문자열인 "MIME-boundary1--------DC128F5642CA"(따옴표 제외)의 앞뒤에 하이픈(-)이 두 개씩 붙어있음을 알 수 있고, 이 문자열을 끝으로 Multipart MIME Message가 끝난다는 것을 알 수 있다.
--boundary 문자열과 그 다음에 나오는 --boundary 문자열(혹은 --boundary-- 문자열) 사이에 있는 것이 하나의 MIME Content라는 것은 수차에 걸쳐 이야기했다. 이 MIME Content는 처음에 살펴본 간단한 MIME Message와 마찬가지로 헤더와 데이터로 구성되어 있다. MIME Content의 헤더가 데이터의 정보를 나타내고 있다는 것을 이제는 쉽게 알 수 있을 것이다. 헤더와 데이터는 하나의 빈 줄로 구분된다. 헤더가 나오고 한 줄이 비어지고 그 다음에 데이터가 나오는 것이다. 위의 메일에는 모두 두 개의 Content가 있는데, 첫 번째 Content의 Content-Type은 text/plain이면 두 번째 Content-Type은 image/jpeg라는 것을 알 수 있다. 또한 위의 첫 번째 Content의 헤더에 Content-Transfer-Encoding이 없으므로 디폴트 값인 7bit 인코딩이 되었음을 알 수 있고, 두 번째 Content는 base64 방식으로 인코딩 되어있음을 알 수 있다.
그런데 여기에서 의문이 생길 수가 있다. 어디가 Content의 마지막인가 이다. 무슨 말인지 의아하겠지만 다음을 한 번 살펴보자. 아래는 위 Multipart MIME Message의 한 부분이다. 첫 번째 Content가 끝나고 두 번째 Content를 구분하기 위한 --boundary 문자열이 나타나는 곳이다.

Hi!
I will send you a good image. This image is my favorite picture.
[CRLF]
--MIME-boundary1--------DC128F5642CA
Content-Type: image/jpeg

역상부분으로 나타낸 [CRLF](줄바꿈)표시는 과연 첫 번째 Content에 포함되는가, 아닌가 하는 질문에 대답해보자.
text 기반의 데이터라면 그것이 그렇게 중요하지 않을 수도 있지만 만약 binary 데이터라면 [CRLF]가 데이터에 포함되는지 포함되지 않는지는 무척 중요한 문제이다.
MIME을 설명하고 있는 RFC문서에서는 --boundary 문자열(혹은 --boundary-- 문자열) 앞에 붙어있는 [CRLF] 문자를 --boundary 문자열(혹은 --boundary-- 문자열)에 속한 것으로 정의하고 있다. 즉 Content 데이터에 포함되지 않는다는 것이다.

3. Multipart Content를 내부에 포함한 Multipart MIME Message

Multipart MIME Message 안에 들어있는 Content가 또 다시 Multipart MIME Message가 될 수도 있다. 계속 재귀적으로 multipart가 중첩된 구조를 가질 수 있다는 것이다.

From: hanyoul@netsarang.com
To: anyone@somehost.com
Subject: JPEG image file
MIME-Version: 1.0
Content-Type: multipart/mixed;
boundary="MIME-boundary1--------DC128F5642CA"

--MIME-boundary1--------DC128F5642CA
Content-Type: text/plain; charset=us-ascii

This message include another multipart message.

--MIME-boundary1--------DC128F5642CA
Content-Type: multipart/mixed;
boundary="MIME-boundary2--------DC128F5642CA"

--MIME-boundary2--------DC128F5642CA
Content-Type: text/plain; charset=us-ascii

Hi!
I will send you a good image. This image is my favorite picture.

--MIME-boundary2--------DC128F5642CA
Content-Type: image/jpeg
Content-Transfer-Encoding: base64

/9j/4AAQSkZJRgABAgAAZABkAAD//gASQWRvYmUgSW1hZ2VSZWFkef/sABFEdWNreQABAAQAAAAu
AAD/7gAOQWRvYmUAZMAAAAAB/9sAhAAKBwcHBwcKBwcKDgkICQ4QDAoKDBATDw8QDw8TEg4QDw8Q
DhISFRYXFhUSHR0fHx0dKSkpKSkvLy8vLy8vLy8vAQoJCQoLCg0LCw0QDQ4NEBQODg4OFBcPDxEP
DxcdFRISEhIVHRocFxcXHBogIB0dICAoKCYoKC8vLy8vLy8vLy//wAARCADUARsDASIAAhEBAxEB
/8QAqgAAAgIDAQAAAAAAAAAAAAAAAAECBQMEBgcBAAEFAQEAAAAAAAAAAAAAAAABAgQFBgMHEAAB
AwIFAgQEBAQEBQQDAAABABECIQMxQRIEBVFhcYEiBpGhMhOxwUIU0eFSI/DxFQdicpIzFoKiwlNU
NRcRAAEDAgQCBwYEBQUAAAAAAAEAAgMRBCExQRJxBVFhgZEiMhOhwdFCUhSxckMV8OHxMwZigsIj
JP/aAAwDAQACEQMRAD8A5wFsii2SLhBpV1mk0Q8pae5KwCQne1ROoHA5KEcitjzED0W447x71ebM
kgMa5KW6

--MIME-boundary2--------DC128F5642CA--

--MIME-boundary1--------DC128F5642CA--

위에서 역상부분이 중첩된 Multipart MIME Message이다. 중첩된 Multipart MIME Message는 새로운 boundary 문자열(MIME-boundary2--------DC128F5642CA)을 가지고 있다. 그러므로 "--MIME-boundary2--------DC128F5642CA--"(따옴표 제외)로 끝날 때까지가 하나의 Content인 것이다.
이런 중첩된 구조는 multipart뿐만이 아니라 multipart를 포함한 메일 메시지 자체(message/rfc822)를 포함할 수도 있다. 이는 메일 서버가 서버 관리자에게 에러를 보고할 때 에러가 난 메일 자체를 포함시킨 에러 보고 메일(Error Reporting Mail)을 보낼 때 자주 사용된다.

4. 눈여겨봐야할 MIME 타입

4.1 multipart/mixed

기본적인 multipart 타입이 바로 multipart/mixed이다. multipart/mixed 타입은 multipart로 묶여진 Content들이 서로 독립적임을 나타낸다. 하지만 Content들이 들어있는 순서에 의미가 있다. 화일을 첨부하여 메일을 보낼 때 주로 사용되는데, 이 때 처음에 들어있는 Content가 메일 본문이고 다음부터 나오는 Content들이 첨부화일이다. 메일 본문과 첨부화일의 순서처럼 순서에 의미가 있는 경우에 사용하는 것이 multipart/mixed 타입이다. 또한 부 카테고리가 어떤 타입인지를 모르는 multipart 타입은 모두 multipart/mixed로 해석한다.

4.2 multipart/alternative

multipart/alternative 타입은 multipart로 묶여진 Content들이 서로 같은 내용을 담고 있지만 표현 형식이 다를 때 사용한다. 예를 들어 메일을 보낼 때, html 형식으로 보내고 싶지만 상대방 메일 클라이언트가 html을 지원하지 않는 프로그램일지도 모른다는 걱정이 들 때 multipart/alternative 타입을 사용한다. 아래의 메일을 보자.

From: hanyoul@netsarang.com
To: anyone@somehost.com
Subject: alternative multipart example
MIME-Version: 1.0
Content-Type: multipart/alternative;
boundary=boundary1234

--boundary1234
Content-Type: text/plain; charset=us-ascii

...여기에는 plain text 형식의 메시지가 들어간다...

--boundary42
Content-Type: text/html

...여기에는 위의 메시지와 같은 내용을 가지고 있지만, 형식이 html인 메시지가 들어간다...

--boundary42--

위의 예에서 살펴보면 같은 내용의 메시지를 text/plain 형식과 text/html 형식으로 만들어서 함께 보냈음을 알 수 있다. 이 때, 받는 사람의 메일 클라이언트가 html을 지원하면 두 번째 Content를 보여주고, html을 지원하지 않는다면 첫 번째 Content를 보여주게 된다. 여기서 하나 더 알아두어야 할 것은 multipart/alternative 타입에서도 Content들 간의 순서가 중요하다는 것이다. 보내는 사람이 더 선호하는 형식을 뒤에 놓게 되어 있다. 위의 예에서 보내는 사람이 text/html 형식을 text/plain 보다 뒤에 배치했기 때문에 받는 사람은 보낸느 사람이 html 형식으로 보기를 바란다는 것을 알 수 있는 것이다. 그러므로 multipart/alternative 타입으로 묶여진 모든 Content들의 형식을 받는 메일 프로그램이 모두 다 알고 있을 때에는 맨 마지막 Content를 보여주면 되는 것이다.

4.3 multipart/digest

multipart/digest 타입은 multipart/mixed 타입과 거의 차이가 없지만. multipart로 묶여진 Content들이 Content-Type 헤더를 갖고 있지 않을 때의 디폴트 타입이 text/plain이 아니라 message/rfc822 타입이라는 차이점이 있다.

4.4 multipart/parallel

multipart/parallel 타입은 multipart/mixed 타입과 똑같은 타입이지만, multipart로 묶여진 Content들 간의 순서에 별 의미가 없다는 것에 차이점이 있다.

4.5 multipart/report

multipart/report 타입은 메일 서버가 메일 에러 보고를 할 때 사용하는 타입이다. 만약 어떤 사용자가 메일을 보냈는데, 받는 사람의 메일 주소가 틀려서(없는 메일 주소일 때 등등) 메일을 보낼 수가 없다면 메일 서버는 메일을 보낸 사용자에게 메일을 보낼 수 없다는 에러 보고 메일을 보내게 된다. 이 때 보내는 에러 보고 메일의 형식은 RFC 1892에서 정의되고 있다. 에러 보고 메일은 2개의 꼭 필요한 Content와 그 외의 부가적인 Content를 multipart로 묶어 구성한다. 첫 번째 Content(꼭 필요함)는 에러 원인에 대한 메시지를 담고 있다. 이 메시지는 사람이 쉽게 읽을 수 있어야 한다. 첫 번째 Content가 multipart/alternative로 구성될 수도 있다. 두 번째 Content(꼭 필요함)는 컴퓨터(메일 처리 프로그램)가 알아보기 쉬운 에러 원인에 대한 메시지이다. 컴퓨터가 쉽게 파싱할 수 있어야 하며 파싱된 정보를 통해 컴퓨터가 에러 원인을 정확히 알 수 있어야 한다. 그래야 자동 에러 처리가 가능하니까. 리스트 5는 multipart/report 의 예이다.

리스트 5 : multipart/report 의 예
Date: Thu, 7 Sep 2000 18:18:35 +0900
From: Mail Delivery Subsystem <MAILER-DAEMON@netsarang.com>
Message-Id: <200009070918.SAB04287@netsarang.com>
To: hanyoul@netsarang.com
MIME-Version: 1.0
Content-Type: multipart/report; report-type=delivery-status;
boundary="SAB04287.968318315/netsarang.com"
Content-Transfer-Encoding: 8bit
Subject: Returned mail: User unknown
Auto-Submitted: auto-generated (failure)

--SAB04287.968318315/netsarang.com

The original message was received at Thu, 7 Sep 2000 18:18:35 +0900
from hanyoul@localhost

----- The following addresses had permanent fatal errors -----
badman@badhost.com

----- Transcript of session follows -----
550 badman@badhost.com... User unknown
554 /dead.letter... cannot open /dead.letter: Permission denied

--SAB04287.968318315/netsarang.com
Content-Type: message/delivery-status

Reporting-MTA: dns; netsarang.com
Arrival-Date: Thu, 7 Sep 2000 18:18:35 +0900

Final-Recipient: RFC822; badman@badhost.com
Action: failed
Status: 5.1.1
Last-Attempt-Date: Thu, 7 Sep 2000 18:18:35 +0900

--SAB04287.968318315/netsarang.com--

4.6 message/external-body

만약 어떤 사람이 20M나 되는 MP3 파일을 메일로 보내려고 한다고 치자. 그런데 받는 사람의 메일 계정 용량이 10M밖에 안된다면 메일을 받지 못할 것이다. 또 메일 계정 용량이 충분하더라도 20M가 되는 메일을 받는 것을 꺼리는 사람도 있을 것이다. 이럴 때 사용할 수 있는 것이 바로 message/external-body 타입이다. message/external-body 타입은 Content를 직접 메일 안에 첨부하는 것이 아니라, 외부에 두고 메일 안에는 그 Content에 대한 정보(URL, 파일명등)만을 포함하는 MIME 타입이다. message/external-body 타입은 RFC 2046에 정의되어 있다. message/external-body 타입은 Content-Type의 부가정보로 access-type이라는 정보를 꼭 포함하고 있어야 한다. access-type은 외부에 있는 Content를 어떠한 방식에 의해 접근해야 할 지를 알려준다. 그 종류로는 FTP, ANON-FTP, TFTP, LOCAL-FILE, MAIL-SERVER등이 있다. 또한 Content-Type의 부가정보로 expiration이라는 정보를 포함할 수도 있다. expiration 정보는 외부에 있는 Content가 유효한 시간을 가리키는 정보이다. 또 size라는 정보 역시 포함될 수 있는데, size는 외부에 있는 Content의 크기를 가리킨다. 각각의 access-type은 그 종류에 따라 각각의 부가 정보를 가질 수 있다. 이에 대한 간단히 살펴보자.
FTP와 TFTP는 name, site 그리고 directory라는 정보를 가지고 있다. name은 Content 파일의 이름을 가리킨다. site는 Content가 있는 곳의 도메인 이름이다. 이 이름은 완전한 도메인 이름의 형식을 가져야 한다. directory는 Content가 있는 디렉토리를 가리킨다. access-type이 FTP(TFTP)라면 site가 가리키는 컴퓨터에 FTP(TFTP) 프로토콜을 이용해 접속하여 directory 아래에 있는 name 파일을 가져오면 된다.
ANON-FTP는 FTP와 유사하지만 ftp로 접근할 수 있는 유저ID와 패스워드를 묻지 않을 경우에 사용한다. 이 때는 anonymous ID와 사용자의 메일주소를 사용하여 접속한다.
LOCAL-FILE은 site라는 부가정보가 가리키는 컴퓨터 안에 name이라는 이름으로 저장되어 있는 Content를 가리킨다.
MAIL-SERVER는 외부에 있는 Content를 메일 서버를 통해 얻어올 수 있다는 것을 말해준다. MAIL-SERVER access-type은 server라는 부가정보와 subject라는 부가정보를 가지고 있다. server는 Content를 얻을 수 있는 메일 서버를 가리킨다. subject라는 부가정보를 가지고 있다라면 server가 가리키는 메일 서버에게 subject가 가리키는 제목으로 메일을 보낼 때 우리가 원하던 Content를 얻을 수 있음을 말해준다.
리스트 6은 message/external-body 타입의 예이다.

리스트 6 : message/external-body 타입의 예
From: hanyoul@netsarang.com
To: anyone@somehost.com
Subject: External-body Type
MIME-Version: 1.0
Content-type: message/external-body;
access-type=local-file;
name="/u/nsb/Me.jpeg"

Content-type: image/jpeg
Content-ID: <id42@guppylake.bellcore.com>
Content-Transfer-Encoding: binary

THIS IS NOT REALLY THE BODY!


4.7 message/partial

message/partial 타입은 용량이 큰 메일을 보낼 때 유용하게 사용할 수 있다. message/partial 타입은 용량이 큰 메일을 여러개로 쪼개어 보내는 방법을 사용할 때 쓰이는 MIME 타입이다. 메일 클라이언트가 이렇게 보내진 메일을 다시 하나로 합치게 하면 원래의 메일이 된다.
message/partial 타입은 id, number, total이라는 부가정보를 가지고 있다. id는 쪼개진 Content를 가리키는 문자열이다. 다시 말해서 id가 같은 메일을 모아야 다시 합칠 수 있다는 말이다. number는 이 메일이 쪼개진 Content의 조각 가운데 몇번째 조각인지를 가리키는 것이고, total은 모두 몇 개의 조각으로 쪼개졌는가를 가리키는 것이다. 리스트 7이 message/partial 타입의 예이다.

[첫번째 메일]

From: hanyoul@netsarang.com
To: anyone@somehost.com
Subject: 쪼개진 메시지 (part 1 of 2)
Message-ID: <id1@host.com>
MIME-Version: 1.0
Content-type: message/partial;
id="ABC@host.com";
number=1;
total=2
Message-ID: <anotherid@foo.com>
Subject: GIF 그림
MIME-Version: 1.0
Content-type: image/gif
Content-transfer-encoding: base64

... GIF 이미지 파일의 첫 번째 조각(총 2개) ....


[두번째 메일]

From: hanyoul@netsarang.com
To: anyone@somehost.com
Subject: 쪼개진 메시지 (part 2 of 2)
MIME-Version: 1.0
Message-ID: <id2@host.com>
Content-type: message/partial;
id="ABC@host.com";
number=2;
total=2

... GIF 이미지 파일의 두 번째 조각(총 2개) ....

[메일 클라이언트에 의해 합쳐진 메일]

From: hanyoul@netsarang.com
To: anyone@somehost.com
Subject: GIF 이미지
Message-ID: <anotherid@foo.com>
MIME-Version: 1.0
Content-type: image/gif
Content-transfer-encoding: base64

... GIF 이미지 파일의 첫 번째 조각(총 2개) ....
... GIF 이미지 파일의 두 번째 조각(총 2개) ....

리스트 7의 역상부분을 살펴보면 첫 번째 쪼개진 메시지의 Content-Type, Content-Transfer-Encoding, Subject들이 합쳐진 메일의 것과 같음을 알 수 있다. message/partial 타입은 원래 메일의 Content-Type, Content-Transfer-Encoding, Subject들을 첫 번째 조개진 메시지에 포함시키고 있음을 알 수 있다.

지금까지 우리는 MIME에 대한 기본적인 내용을 모두 살펴보았다. 이제 이 내용을 가지고 실제로 MIME Parser를 구현하는 일만이 남았다. 앞에서도 이야기했듯이 마지막 호인 다음 호에서는 실제 MIME Parser를 구현해볼 것이다.

5. MIME 참고 자료

RFC 2045 - "Multipurpose Internet Mail Extensions (MIME) Part One: Format of Internet Message Bodies"
RFC 2046 - "Multipurpose Internet Mail Extensions (MIME) Part Two: Media Types"
RFC 2047 - "MIME (Multipurpose Internet Mail Extensions) Part Three: Message Header Extensions for Non-ASCII Text"
RFC 2048 - "Multipurpose Internet Mail Extensions (MIME) Part Four: Registration Procedures"
RFC 2049 - "Multipurpose Internet Mail Extensions (MIME) Part Five: Conformance Criteria and Examples"

반응형
C로 구현하는 MIME Parser (마지막회)

MIME Parser의 실제 구현

이번 호는 연재의 마지막으로 실제 MIME Parser를 구현해볼 것이다. 비록 제한된 지면으로 인해 모든 것을 다 설명할 수는 없지만 MIME Parser의 핵심만큼은 이해할 수 있도록 설명할 것이다. 이번 호의 내용이 쉽게 이해된다면 자신만의 MIME Parser를 구현하는 것은 시간 문제일 것이다. 그럼, MIME Parser를 실제로 구현해보도록 하자.

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


1. MIME Parser의 자료 구조

프로그램을 만들 때, 가장 중요한 것 중의 하나가 자료 구조이다. 자료 구조만 보면 프로그램의 모든 것을 본 것과 같다라는 명언이 있을 정도로 프로그램을 설계할 때 가장 먼저 고려해야 할 것이 자료 구조 설계이다. 우리도 MIME Parser를 위한 자료 구조를 설계하여 보자.
MIME Parser는 stream을 입력 받아 입력 받은 stream을 의미 있는 Mail Message로 만드는(파싱하는) 것을 주된 목표로 삼는다. 그러므로 우리는 Mail Message를 저장할 수 있는 자료 구조를 설계해야 한다.
Mail Message(MIME을 포함하는 Mail Message)는 크게 두 가지 부분으로 나눌 수 있다. 첫 번째는 헤더이고 두 번째는 바디이다. 헤더는 RFC822 헤더를 가리킨다. 헤더에는 Mail의 모든 정보가 들어가 있다. 바디는 한 개 혹은 여러 개의 Content로 구성된다. 물론 아예 컨텐트가 없는 경우도 있을 수 있다. MIME Message는 여러 개의 컨텐트를 가질 수도 있고 아니면 아예 컨텐트가 없을 수도 있기 때문이다. 각각의 컨텐트는 컨텐트의 정보를 가리키는 부분(MIME Header에 해당하는 부분)과 실제 데이터로 구성된다. 이와 같은 특성을 갖고 있는 MIME Message를 저장할 수 있는 자료 구조를 다음과 같이 설계하였다.

[Mail Message]
struct H_MESSAGE
{
struct H_HEADER *m_header; /* header를 가리키는 포인터 */
struct H_BODY *m_body; /* body를 가리키는 포인터 */
};

Mail Message는 위의 구조체에 저장된다. Mail Message가 헤더와 바디로 구성되어 있기 때문에 헤더를 가리키는 포인터와 바디를 가리키는 포인터를 가지고 있다. 이때, H_HEADER, H_BODY는 바로 뒤에 정의하게 될 헤더와 바디를 저장할 수 있는 구조체이다.

[HEADER]
struct H_HEADER
{
char *h_field; /* header field name */
char *h_value; /* header field value */
struct H_HEADER *h_next; /* 다음 header entry를 가리키는 포인터 */
};

RFC822 헤더는 위의 구조체에 저장된다. RFC822 헤더는 콜론(:)으로 구분되는 name-value쌍으로 이루어져 있다. 위에서 h_field는 각 헤더의 이름을 가리키고(Subject, To, X-Mailer등등) h_value는 h_field에 대응되는 값을 가리키고 있다. 또한 위에서 역상으로 표시된 곳을 보면 H_HEADER라는 구조체가 연결 리스트(Linked List)라는 것을 알 수 있다. 즉 name-value 쌍의 연결 구조로서 헤더를 저장할 수 있다는 것이다.

[BODY]
struct H_BODY
{
char *b_c_type; /* body part의 Content-Type */
char *b_c_desc; /* body part의 Content-Description */
char *b_c_encode; /* body part의 Content-Transfer-Encoding */
char *b_c_id; /* body part의 Content-ID */
char *b_charset; /* body part의 charater set */
char *b_filename; /* body part의 실제 파일이름 */
char *b_savename; /* body part가 파일시스템에 저장된 이름 */
struct H_BODY *b_next; /* 다음 body part를 가리키는 포인터 */
struct H_BODY *b_body; /* 중첩된 구조의 multipart를 가리키는 포인터*/
};

MIME Message의 컨텐트들은 위의 구조체에 저장된다. 위의 구조체에서 b_c_type(Content-Type), b_c_desc(Content-Description), b_c_encode(Cotent-Transfer-Encoding), b_c_id(Content-ID), b_charset(Content-Type의 부가 정보인 Character Set), b_filename(Content-Dispositon의 부가 정보인 Filename)은 MIME 헤더에서 나오는 컨텐트에 관한 정보들이다. 그리고 실제 컨텐트의 데이터는 b_savename이 가리키는 파일로 저장될 것이다.
위의 구조체에서 눈여겨 봐야 할 것은 b_next와 b_body이다. b_next는 다음 컨텐트를 가리킨다. 일반적인 컨텐트들이 MIME 형식으로 인코딩 되어 있다면 그 Content들은 b_next를 통해 연결 리스트를 형성할 것이다. 그렇다면 b_body는 어디에 사용할 것인가. b_body는 바로 지난 호에서 살펴본 중첩된 구조의 MIME Message에서 사용할 것이다. 즉 multipart내에 또 다시 multipart가 있을 때, 우리는 새로운 body의 연결 리스트를 b_body에 저장할 것이다.

2. MIME Parser의 알고리즘

MIME Parser의 알고리즘은 의외로 간단하다. 일단 중요한 것은 Mail Message를 한 줄씩 읽어 들인다는 것이다. 모든 프로세스가 한 줄단위로 이루어진다.
MIME Parser는 한 줄씩 읽어 들이면서 헤더를 파싱하고 그 뒤 바디를 파싱한다. gpejdhk 바디는 빈 줄 하나로 구분되기 때문에 한 줄씩 읽어 들이다가 빈 줄을 만나며 헤더가 끝났다는 것을 알 수 있게 되고 그 뒤부터는 바디를 파싱한다.
바디를 파싱하는데 있어서 중요한 것은 Content-Type이다. Content-Type이 Multipart이면 boundary가 구분하는 경계를 찾아서 각각의 Content들을 하나씩 하나씩 파싱하면 된다. 이 때, boundary를 찾는 것 역시 한 줄씩 읽어 들이면서 체크를 하게 된다.

2.1 MIME Parser의 핵심 함수

MIME Mesasge를 파싱하기 위한 핵심 함수는 다음과 같다.

● hParseMessageRFC822()
아래에서 소개될 hMessageHeaderParsing(), hMessageBodyParsing() 함수를 호출하여 Mail Message stream으로부터 헤더와 바디를 파싱하고 파싱된 헤더와 바디를 H_MESSAGE 구조체에 저장한 뒤 저장된 구조체를 리턴하는 함수이다. 이 때, Mail Message stream은 파일로 저장되어 있다고 가정하며, Mail Message가 저장된 파일의 file descriptor를 인수로 넘겨받아 사용하게 된다.

● hMessageHeaderParsing()
hParseMessageRFC822() 함수가 넘겨준 Mail Message stream을 저장하고 있는 file descriptor로부터 헤더를 파싱해 내고 그 결과를 H_HEADER 구조체에 저장한 뒤 구조체를 리턴한다.

● hMessageBodyParsing()
hParseMessageRFC822() 함수가 넘겨준 Mail Message stream을 저장하고 있는 file descriptor(이 때, file descriptor의 파일포인터는 헤더를 파싱하고 난 다음라인을 가리키고 있다.)로부터 바디를 파싱해 내고 그 결과를 H_BODY 구조체에 저장한 뒤 구조체를 리턴한다. 이 때, 아래의 두 함수 hParseMultipartMixed(), hCollect()를 적절하게 사용한다.

● hParseMultipartMixed()
hMessageBodyParsing() 함수가 넘겨준 file descriptor와 boundary(Content-Type의 부가정보인 boundary)를 이용하여 Multipart Content를 파싱하고 파싱된 결과를 H_BODY 구조체에 저장한 뒤 구조체를 리턴한다. 이 때, 리턴된 H_BODY 구조체는 b_body에 저장된다.

● hCollect()
hMessageBodyParsing() 함수가 넘겨준 file descriptor와 boundary(Content-Type의 부가정보인 boundary)를 이용하여 Multipart가 아닌 하나의 Content만을 파싱하고 파싱된 결과를 temporary 파일을 생성하여 저장한다. 이 때, Content-Transfer-Encoding 정보를 이용하여 데이터를 디코딩 한다.

2.2 MIME Parser의 핵심 Source코드

다음은 위에서 설명한 함수들의 소스 코드이다. 소스 코드 중간 중간에 사용하는 함수들에 대해서 짤막하게 설명하고 넘어가도록 하겠다. 이 함수들을 구현하는 것은 독자들의 몫으로 남긴다.

● hReadLineCRLF2LF(int fd, char *buf, int size)
file descriptor의 파일 포인터로부터 한 라인을 읽어 들여 buf에 저장하는 함수이다. 이 때, CRLF는 LF로 변환한다.

● hStrHasSpace(char *str)
str에 space가 있는지 확인하는 함수 있다. space가 있다면 1을 리턴하고 그렇지 않다면 0을 리턴한다.

● hStrLtrim(char *str)
str의 왼쪽 부분(처음 부분)에 있는 white space(공백)을 제거하는 함수이다. 리턴 값은 공백이 없어진 새로운 str의 포인터이다.

● hStrRtrim(char *str)
str의 오른쪽 부분(마지막 부분)에 있는 white space(공백)을 제거하는 함수로서 나머지는 hStrLtrim()과 동일하다.

● hHeaderAddValue(struct H_HEADER **header, char *field, char *value)
H_HEADER 구조체에 새로운 엔트리를 첨가하는 함수이다. 이 때 첨가되는 엔트리의 값은 field, value 쌍이다.

● hHeaderNamedValueCat(struct H_HEADER *header, char *field, char *str)
field가 가리키는 엔트리를 header에서 찾아 str을 h_value에 덧붙인다.

● hHeaderContentTypeGet(struct H_HEADER *header)
header에서 Content-Type의 값을 찾아 리턴한다.

● hHeaderContentEncodingGet(struct H_HEADER *header)
header에서 Content-Transfer-Encoding의 값을 찾아 리턴한다.

● hHeaderCharsetGet(struct H_HEADER *header)
header에서 charset의 값을 찾아 리턴한다.

● hHeaderFilenameGet(struct H_HEADER *header)
header에서 filename의 값을 찾아 리턴한다.

● hStrToLower(char *str)
str을 모두 lower case로 만든다.

● hSavenameGet()
temporary 파일 이름을 만들어 리턴한다.

리스트 1 : struct H_MESSAGE *hParseMessageRFC822(int fd, const char *boundary)
{
struct H_MESSAGE *msg = (struct H_MESSAGE *)NULL;
struct H_HEADER *tempHeader = (struct H_HEADER *)NULL;
struct H_BODY *tempBody = (struct H_BODY *)NULL;

msg = hMessageCreate();

tempHeader = hMessageHeaderParsing(fd);

if(!tempHeader)
return NULL;

tempBody = hMessageBodyParsing(fd, tempHeader, boundary);

if(!tempBody)
return NULL;

msg->m_header = tempHeader;
msg->m_body = tempBody;

return msg;
}

리스트 1의 hMessageCreate()는 H_MESSAGE 구조체의 메모리를 동적으로 할당하는 함수이다. 위의 소스코드에서 msg는 H_MESSAGE 구조체의 포인터로서 리턴할 값이다. 위에서 살펴본대로 msg는 두개의 멤버를 가지고 있는데, 하나는 헤더를 가리키는 m_header이고, 다른 하나는 바디를 가리키는 m_body이다. hMessageHeaderParsing() 함수로 만들어진 H_HEADER 구조체(tempHeader)와 hMessageBodyParsing() 함수로 만들어진 H_BODY 구조체(tempBody)를 msg에 저장하고 msg를 리턴한다.


리스트 2 : struct H_HEADER *hMessageHeaderParsing(int fd)
{
char buf[BUF_SIZE];
char *field = (char *)NULL;
char *value = (char *)NULL;
int islong; /* header가 long임을 나타내는 flag */
struct H_HEADER *retHeader = (struct H_HEADER *)NULL;

/* EOF을 만났다는 것은 파일의 끝 */
if(hReadLineCRLF2LF(fd, buf, BUF_SIZE) == NULL)
return NULL;

while(TRUE)
{
/* LF로 시작되는 행은 header의 끝을 나타낸다. */
if(!strcmp(buf, "\n")) break;

/* 첫 character가 white space이면 long header(RFC822 정의) */
islong = hIsSpace(buf[0]) ? LONG : NOT_LONG;

switch(islong)
{
case NOT_LONG:
field = (char *)strtok(buf , ":");
value = (char *)strtok(NULL, "");

if(hStrHasSpace(field)) break; /* field에 space가 있으면 :쌍이 아니다. */

/* value 맨 앞 character는 space 이다. */
value = hStrLtrim(value);

if(field != NULL && value != NULL)
hHeaderAddValue(&retHeader, field, value);

break;

case LONG:
/* long character 이면 뒤에 삽입한다. */
hHeaderNamedValueCat(retHeader, NULL, buf);
break;
}

if(hReadLineCRLF2LF(fd, buf, BUF_SIZE) == NULL) /* 헤더가 중간에 비정상적으로 끝나버린 경우 */
return retHeader;
}

return retHeader;
}

리스트 2의 함수는 헤더를 파싱하여 H_HEADER 구조체에 저장하고 저장된 H_HEADER 구조체를 리턴하는 함수이다. 그런데 헤더 파싱에는 한가지 유의할 것이 있다. 바로 RFC822 에서 정의하고 있는 long 헤더 때문이다. 다음 예와 같이 long 헤더는 여러 줄에 걸쳐서 나올 수 있다.

[long 헤더의 예]

Subject: 이것은 긴 제목의 예입니다. 제목 역시 헤더에 들어가죠? 얼마나 긴 제목인지 한번 살펴보세요.

앞서 우리는 메일 파싱을 위해 메일을 한 줄씩 읽어 들인다고 했다. 그런데 위에서 살펴 본대로 long 헤더는 여러 줄에 걸쳐 나올 수 있으므로 name, value 쌍인 H_HEADER 구조체에 저장하기 위해서는 여러 줄에 걸쳐 나오는 헤더의 value를 하나로 합쳐서 저장해야 한다. 다행히도 RFC822 문서에는 어떤 줄의 첫 문자가 White Space(공백)이면 이 라인은 바로 위에 나온 헤더 name에 속한다라고 정의하고 있다. 그러므로 우리는 한 줄씩 읽어 들여가며 첫번째 문자가 공백인지를 체크하고 공백이라면 바로 위에 나온 헤더 name에 속한 value에 덧붙이면 된다. 이 때, 사용하는 것이 hHeaderNamedValueCat() 함수이다(리스트 3).

리스트 3 : hHeaderNamedValueCat() 함수
struct H_BODY *hMessageBodyParsing(int fd, const struct H_HEADER *hd, const char *boundary)
{
struct H_BODY *retBody = (struct H_BODY *)NULL;
struct H_BODY *tempBody = (struct H_BODY *)NULL;
struct H_MESSAGE *tempMsg = (struct H_MESSAGE *)NULL;
char buf[BUF_SIZE];
char *content_type = (char *)NULL;
char *content_encoding = (char *)NULL;
char *value = (char *)NULL;
char *decodeValue = (char *)NULL;
char *nested_boundary = (char *)NULL;
char *filename = (char *)NULL;
char *savename = (char *)NULL;
char *charset = (char *)NULL;
int state = 0;
int isend = 0;
int decodeLen = 0;

content_type = hHeaderContentTypeGet(hd);

if(content_type != NULL)
{
content_type = strtok(content_type, ";");
content_type = hStrRtrim(content_type);
}
else /* Content Type이 없으면 디폴트로 text/plain이다. */
content_type = "text/plain";

content_encoding = hHeaderContentEncodingGet(hd); /* 모든 body part에 적용되는 encoding type */

/* body의 Content-Type에 따라 프로세스 결정 */
if (!strcmp(hStrToLower(content_type), "message/rfc822" ))
state = MESSAGE_RFC822;
else if(!strcmp(hStrToLower(content_type), "multipart/mixed" ))
state = MULTIPART_MIXED;
else if(!strcmp(hStrToLower(content_type), "multipart/alternative"))
state = MULTIPART_ALTERNATIVE;
else if(!strcmp(hStrToLower(content_type), "multipart/report"))
state = MULTIPART_REPORT;
else if(!strcmp(hStrToLower(content_type), "multipart/related"))
state = MULTIPART_RELATED;
else
state = REGULAR;

switch(state)
{

case MULTIPART_ALTERNATIVE:
case MULTIPART_REPORT:
case MULTIPART_RELATED:
case MULTIPART_MIXED:

nested_boundary = hHeaderBoundaryGet(hd);

/* boundary가 NULL이면 parsing 할 수 없다. */
if(!nested_boundary)
return NULL;

tempBody = hParseMultipartMixed(fd, nested_boundary);

/* body linked list에 첨가 */
hBodyAddValue(&retBody, content_type, NULL, content_encoding, NULL, NULL, NULL);

retBody->b_body = tempBody;

while(TRUE)
{
if(hReadLineCRLF2LF(fd, buf, BUF_SIZE) == NULL)
break;
}

break;

case MESSAGE_RFC822:
case REGULAR:

savename = hSavenameGet();

hCollect(fd, savename, boundary, content_encoding, &isend);

/* filename과 charset을 header에서 알아낸다. */
filename = hHeaderFilenameGet(hd);
charset = hHeaderCharsetGet(hd);

/* body linked list에 첨가 */
hBodyAddValue(&retBody, content_type, NULL, content_encoding, charset, filename, savename);

break;
}

return retBody;

}

리스트 3의 함수와 아래에서 나올 hParseMultipartMixed() 함수가 이번 호의 핵심 중의 핵심이다. 리스트 3의 함수는 Mail Message의 헤더에서 Content-Type을 구하고 Content-Type에 따라 Content-Type이 multipart이면 MIME decoding을 위해 hParseMultipartMixed() 함수를 호출하고 multipart가 아니면 hCollect() 함수를 호출한다. hParseMultipartMixed(), hCollect() 함수호출에 따라 얻어진 H_BODY 구조체를 리턴하는 것이 이 함수의 역할이다.

리스트 4 : hParseMultipartMixed() 함수
struct H_BODY *hParseMultipartMixed(int fd, const char *boundary)
{
struct H_MESSAGE *tempMsg = (struct H_MESSAGE *)NULL;
struct H_HEADER *tempHeader = (struct H_HEADER *)NULL;
struct H_BODY *retBody = (struct H_BODY *)NULL;
struct H_BODY *tempBody = (struct H_BODY *)NULL;
char buf[BUF_SIZE];
char boundaryn[256];
char boundaryEOFn[256];
char *nested_boundary = (char *)NULL;
char *value = (char *)NULL;
char *decodeValue = (char *)NULL;
char *content_type = (char *)NULL;
char *content_encoding = (char *)NULL;
char *filename = (char *)NULL;
char *savename = (char *)NULL;
char *charset = (char *)NULL;
int isend = 0;
int state = 0;
int decodeLen = 0;

sprintf(boundaryn, "--%s\n", boundary);
sprintf(boundaryEOFn, "--%s--\n", boundary);

/* MIME prologue 제거 */
while(TRUE)
{
if(hReadLineCRLF2LF(fd, buf, BUF_SIZE) == NULL)
return NULL;

if(!strcmp(buf, boundaryn))
break;
}

while(TRUE)
{
tempHeader = hMessageHeaderParsing(fd);

content_type = hHeaderContentTypeGet(tempHeader);

if(content_type != NULL)
{
content_type = strtok(content_type, ";");
content_type = hStrRtrim(content_type);
}
else
content_type = "text/plain";

content_encoding = hHeaderContentEncodingGet(tempHeader); /* 모든 body part에 적용되는 encoding type */

/* body의 Content-Type에 따라 프로세스 결정 */
if (!strcmp(hStrToLower(content_type), "message/rfc822" ))
state = MESSAGE_RFC822;
else if(!strcmp(hStrToLower(content_type), "multipart/mixed" ))
state = MULTIPART_MIXED;
else if(!strcmp(hStrToLower(content_type), "multipart/alternative"))
state = MULTIPART_ALTERNATIVE;
else if(!strcmp(hStrToLower(content_type), "multipart/report"))
state = MULTIPART_REPORT;
else if(!strcmp(hStrToLower(content_type), "multipart/related"))
state = MULTIPART_RELATED;
else
state = REGULAR;

switch(state)
{
case MULTIPART_ALTERNATIVE:
case MULTIPART_REPORT:
case MULTIPART_RELATED:
case MULTIPART_MIXED:

nested_boundary = hHeaderBoundaryGet(tempHeader);

/* boundary가 NULL이면 parsing 할 수 없다. */
if(!boundary)
return NULL;

tempBody = hParseMultipartMixed(fd, nested_boundary);

/* body linked list에 첨가 */
hBodyAddValue(&retBody, content_type, NULL, content_encoding, NULL, NULL, NULL);

retBody->b_body = tempBody;

/* multipart내의 multipart이면 다음 라인은 boundary이거나 boundaryEOF이다. 즉, skip */
while(TRUE)
{
if(hReadLineCRLF2LF(fd, buf, BUF_SIZE) == NULL)
{
isend = 1;
break;
}

if(!strcmp(buf, boundaryn))
{
isend = 0;
break;
}
/* boundaryEOFn을 만나면 파싱이 끝난다. */
else if(!strcmp(buf, boundaryEOFn))
{
isend = 1;
break;
}
}

break;

case MESSAGE_RFC822:

case REGULAR:

savename = hSavenameGet();

hCollect(fd, savename, boundary, content_encoding, &isend);

/* filename과 charset을 header에서 알아낸다. */
filename = hHeaderFilenameGet(tempHeader);
charset = hHeaderCharsetGet(tempHeader);

/* body linked list에 첨가 */
hBodyAddValue(&retBody, content_type, NULL, content_encoding, charset, filename, savename);

break;
}

if(isend == 1) /* end of multipart */
break;

}

return retBody;
}

리스트 4의 함수를 몇 번이고 읽어보게 되면 자연스레 MIME Parsing의 원리를 이해할 수 있을 것이다. 이 함수의 기본 동작은 boundary를 찾는 것이다. Boundary를 찾아서 각각의 Content들을 파싱 해낸다. 파싱 된 각각의 Content들은 H_BODY 구조체에 저장된 채로 연결리스트를 이루게 된다. Content-Type이 Multipart가 아닌 각각의 Content들을 파싱하기 위해서는 hCollect() 함수의 도움을 받는다. 만약 Content가 또 다시 Multipart이면 recursive하게 hParseMultipartMixed() 함수를 호출한다. 이렇게 호출된 hParserMultipartMixed() 함수의 끝내기 조건은 --boundary-를 만났을 때이다. --boundary-가 MIME Message의 끝을 가리키고 있는 경계선이기 때문이다. 위의 함수에서 --boundary-를 만나면 isend 라는 변수를 1로 셋팅하게 되고, while() 루프에서 isend가 1이면 루프를 빠져 나와 H_BODY 구조체를 리턴한다.

리스트 5 : Content를 파싱하는 함수
int hCollect(int fd, const char *filename, const char *boundary, const char *content_encoding, int *isend)
{
int valuelen = 0;
int buflen = 0;
int templen = 0;
char *temp = (char *)NULL;
char boundaryn[256], boundaryEOFn[256];
char buf[BUF_SIZE];
int i;
FILE *fout;

fout = fopen(filename, "w+");

if(!boundary) /* boundary가 없는 경우 */
{
while(TRUE)
{
if(hReadLineCRLF2LF(fd, buf, BUF_SIZE) == NULL)
{
fclose(fout);
*isend = 1;

return 0;
}

temp = hStrDecode(buf, content_encoding, &templen);

for(i=0;i<templen;i++)
fputc((int)temp[i], fout);

if(temp != buf)
free(temp);
}
}
else /* boundary가 있는 경우 */
{
/* boundary set */
sprintf(boundaryn, "--%s\n", boundary);
sprintf(boundaryEOFn, "--%s--\n", boundary);

while(TRUE)
{
if(hReadLineCRLF2LF(fd, buf, BUF_SIZE) == NULL)
{
fclose(fout);

*isend = 1;
return 1;
}

if(!strcmp(buf, boundaryn))
{
fclose(fout);

*isend = 0;
return 0;
}
else if(!strcmp(buf, boundaryEOFn))
{
fclose(fout);

*isend = 1;
return 0;
}

temp = hStrDecode(buf, content_encoding, &templen);

for(i=0;i<templen;i++)
fputc((int)temp[i], fout);

if(temp != buf)
free(temp);
}
}

return 1;
}

리스트 5의 함수는 multipart가 아닌 Content를 파싱하는 함수이다. Boundary가 없을 때는 Content가 multipart 안에 들어 있는 것이 아니기 때문에 Mail Message의 끝까지가 Content 데이터이다. 그렇지 않고 boundary가 존재한다면 파싱하려는 Content는 multipart 안에 속해있는 Content이기 때문에 boundary를 체크 해야 한다. 만약 --boundary-를 만나면 MIME의 끝이기 때문에 isend 값을 1로 셋팅하여 이 함수를 호출한 함수에게 MIME이 끝났음을 알린다. 파싱된 데이터는 hSavenameGet() 함수에 의해 얻어진 임시 파일이름으로 저장된다.

리스트 6 : hSavenameGet() 함수
char *hSavenameGet(void)
{
struct timeval t;
static char savename[512];

gettimeofday(&t, NULL);
sprintf(savename, "%s/%ld.%ld%ld", TMP_DIR, getpid(), t.tv_sec, t.tv_usec);

return savename;
}

리스트 6의 함수는 temporary 파일이름을 리턴하는 함수이다. TMP_DIR는 파싱된 Content들의 데이터가 임시 파일로 저장될 디렉토리를 가리킨다.

마치며

지금까지 간략하게나마 MIME Parser의 핵심부분을 살펴보았다. 잘 이해가 되지 않는다면 지난 호의 내용과 이번 호의 소스 코드를 계속해서 읽어보길 바란다. 조만간 프로그램세계 홈페이지를 통하여 필자의 홈페이지를 공지하며 MIME Parser를 라이브러리 형태로 공개할 예정이니 참고하길 바란다. 지금까지 4회에 걸쳐 필자의 글을 읽어준 독자들에게 다시 한 번 감사의 말씀을 전하며 마칠까 한다.

반응형
자바와 게임의 만남 '로보코드 코리아컵 2007'
- 자바 프로그래밍 언어를 기반으로 제작한 로봇 간의 대결
- 자바개발자 커뮤니티인 JCO와 취업전문포탈인 인크루트 후원

(2007.8.8) 한국IBM(대표 이휘성)은 8월 8일 자바 기반의 프로그래밍 게임 대회인 로보코드 코리아컵 2007 결승전을 개최했으며, 우승은 김동환씨(고려대 신소재공학부)가 차지했다고 발표했다.

지난 2003년에 첫 대회가 개최된 이래 4회째를 맞는 ‘로보코드 코리아컵 2007’은 자바 기반의 프로그래밍 대회로 국내 자바 개발자 커뮤니티 모임인 JCO(JAVA Community Organization)와 인크루트가 후원하고 있다.

로보코드 코리아컵은 올 5월 로봇신청 접수를 시작하여 7월 말에 접수를 마감했으며, 64강전과 32강전을 치르고 오늘 16강전부터 결승전까지 치름으로써 최종 승자가 가려졌다. 준우승은 이종혁씨(경기대 컴퓨터과학과), 3위는 조규현씨(호남대 인터넷소프트웨어학과) 가 각각 수상했다.

로보코드는 지난 2001년 IBM의 개발자인 맷 넬슨이 개발한 게임으로서, 사용자들이 직접 자바를 기반으로 인공지능이 담긴 로봇을 만들어 전투를 벌이게 된다. 예선전에서는 그룹별 전투를 거쳐 최종 점수가 가장 높은 로봇이 승리하게 되며, 64강전부터는 1대1의 토너먼트 방식으로 진행된다. 각 참가자들은 최대 3개까지 로봇을 제출할 수 있다.

로보코드 참가자는 자바 언어의 요소를 사용하여 자신의 로봇을 만들면서 프로그래밍 언어를 익힐 수 있어 재미와 기술을 동시에 얻을 수 있다. 특히 초보자들도 쉽게 배울 수 있도록 로보코드 코리아컵 홈페이지(www.ibm.com/developerworks/kr/robocode)를 통해 개발 방법을 소개하고 있다.

또 로보코드는 오픈소스를 기본 전제로 하고 있어 샘플 로봇뿐 아니라, 등록되어 있는 다른 개발자들이 소스를 다운받아 분석하고 자신의 소스를 업로드하는 과정을 반복하면서 로봇은 점차 진화하게 된다.

한국IBM은 올초 developerWorks 대학생 모니터 요원을 선발하여 대학 내 로보코드 홍보를 강화했으며, 고려대, 서울여대, 목포대, 세종대, 숭실대, 동국대, 전남대 등의 요청으로 출장 강의를 진행하는 등 대학생 개발자들의 큰 호응을 얻었다.

한국IBM 솔루션 파트너 사업부의 계혜실 실장은 "IBM은 오픈 소스를 적극적으로 지원하고 있으며, 로보코드를 통해 자바 언어에 대한 대학생 및 개발자들의 관심과 흥미가 크게 증대되었다고 본다. 향후에도 다양한 오픈 소스 지원 정책과 프로그램을 통해 개발자들에게 많은 기술정보와 커뮤니티 환경을 제공할 예정”이라고 밝혔다.

--------------------------------------------------------------------------------------------------------------------
참고자료

- 로보코드의 특징: 각 로보코드 참가자는 자바 언어의 요소를 사용하여 자신의 로봇을 만들면서 자바가 갖고 있는 상속성, 다형성, 이벤트 처리 및 내부 클래스 다루는 방법을 배우게 된다. 표준 API(Application Program Interface)를 지향하지만, 커스터마이징할 수 있는 이벤트를 갖고 있다. 따라서 개발자들이 창의적일수록 로봇이 전투에서 살아남을 가능성이 높아진다. 특히, 로보코드는 초보자부터 고급 프로그래머에 이르기까지 모든 수준의 개발자들이 참가할 수 있다.

- 로보코드의 기반 솔루션: 로보코드는 이클립스, 웹스피어, DB2, 웹스피어 애플리케이션 디벨로퍼를 사용하여 개발되었으며, 참가자들은 IBM이 제공하는 API를 이용해 쉽게 로봇을 만들 수 있다.

+ Recent posts