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

MIME과 7bit 인코딩

전자메일을 통해 바이너리 파일을 전송하거나, 다양한 민족국가의 언어를 전송하기 위해서는 8bit 전송이 필수적이다. 이번 호에서는 MIME의 개괄적 이해와 7bit 인코딩에 대한 내용을 알아보도록 하자.

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

1. MIME

MIME이란 그 말뜻 그대로, 다양한 목적을 위해 전자메일 메시지 형식을 확장시킨 것을 말한다. 지난 호에서도 언급했지만 미국에서 군사적인 목적으로 연구 개발되어 사용되던 인터넷이 전세계로 확산되면서 인터넷에 대한 다양한 요구들이 늘어나기 시작했다. 이러한 요구들 가운데에는 물론 전자메일을 그 대상으로 하는 것도 많았다. 그 중 대표적인 것이 전자메일을 통해 바이너리 파일을 주고 받으려는 것이다.
또한 미국이 아닌 다른 여러 나라의 언어를 가지고도 전자메일을 주고 받으려면 MSB(Most Significant Bit - 가장 최상위 비트)가 1인 바이트들도 전자메일을 통해 깨지지 않고 전송이 되어야 한다는 말이다. 그러나 7bit 전송 프로토콜인 SMTP를 기반으로 하는 전자메일은 MSB가 1인 바이트를 0으로 바꾸어 전송하기도 한다. 이처럼 아직도 많은 메일 서버들이 8bit 전송을 깨뜨려버리는 SMTP를 사용하고 있기 때문에 8bit 전송을 위해서는 새로운 방법이 필요하게 되었다. 그 새로운 방법이 바로 MIME이다.

1.1 MIME의 역할
MIME의 역할은 의외로 간단하다. 어떤 정해진 규칙에 따라서 8bit 데이터를 7bit 데이터로 바꾸어 주는 기능과, 바뀌어진 7bit 데이터를 원래의 8bit 데이터로 그대로 복원하는 기능을 제공하는 일이다. 7bit와 8bit 사이의 변환을 정의해 놓은 표준규약 역시 MIME에 포함되어 있다.
MIME의 중요한 역할은 또 하나 있다. 그것은 바로 메일 메시지에 여러 개의 파일들을 첨부해서 보낼 수 있도록 메일 메시지 형식을 정의해 놓은 것이다. 요즘은 흔하게 파일이 첨부된 메일을 받아볼 수 있다. 이것이 모두 MIME의 개발 덕분이다.

1.2 Multi-part MIME message
파일이 첨부된 메일을 Multi-part MIME message라고 한다. 첨부된 각각의 파일은 하나의 부분(part)을 이루고 있고, 이러한 부분들이 모여 하나의 메일 메시지를 형성하기 때문에 Multi-part MIME message라고 한다.

1.3 RFC 822 헤더와 MIME 헤더
지난 호에서 우리는 RFC 822 헤더에 관해 자세히 살펴보았다. RFC 822 헤더는 메일에 관한 정보를 알려주는 핵심적인 역할을 한다. MIME 헤더 역시 RFC 822 헤더와 마찬가지로 메일 형식에 관한 정보를 알려준다. MIME 헤더는 MIME의 규약에 따라 변환된 메일 메시지를 원래의 메시지로 재변환 시키는데 있어서 필요한 정보들을 제공한다. 즉, 어떤 방식으로 7bit 변환이 되었는지와 Multi-part MIME message를 파싱하기 위한 Boundary 정보들을 담고 있는 것이 MIME 헤더이다.
MIME 헤더 역시 RFC 822 헤더와 같은 형식을 가지고 있다. 즉, 이름과 값의 쌍으로 이루어져 있으며, 이름과 값을 구분하는 구분자는 콜론(:)이다.
대부분의 MIME 헤더는 Content-로 시작한다. MIME 헤더에는 다음과 같은 것이 있다.

·MIME-Version
·Content-Type
·Content-Transfer-Encoding
·Content-ID
·Content-Description
·Content-Disposition

<박스>
MIME 헤더의 예(역상부분이 MIME 헤더)
From hanyoul@netsarang.com Fri Jul 7 15:37:22 2000
Received: from HANYOUL (hanyoul.conux.com)
by netsarang.com (8.9.3/8.9.3) with SMTP id PAA20996
for <hanyoul@conux.com>; Fri, 7 Jul 2000 15:37:19 +0900
From: "Cho Hanyoul" <hanyoul@netsarang.com>
To: "=?ks_c_5601-1987?B?wbYgx9G/rQ==?=" <hanyoul@netsarang.com>
Subject: Test
Date: Fri, 7 Aug 2000 15:39:00 +0900
Message-ID: <FPEEJIDNJIB.hanyoul@conux.com>
MIME-Version: 1.0
Content-Type: multipart/mixed;
boundary="----=_NextPart_000_0009_01BFE829.78EE4E90"

This is a multi-part message in MIME format.


2. MIME 헤더 설명

2.1 Content-Type
Content-Type 헤더는 메일 메시지가 담고 있는 데이터가 어떤 종류의 데이터인지를 알려준다. MUA가 MIME을 해석할 때, Content-Type을 보고 이 메시지가 어떤 종류의 데이터인지를 알아야 디스플레이 해줄 수 있을 것이다. 예를 들어 Content-Type이 그림이라면 MUA는 메일 메시지가 담고 있는 데이터가 그림이라는 것을 인식하고 메일 메시지가 담고 있는 데이터를 읽어 들여 그림을 보여줄 것이고, 만약 Content-Type이 소리라면 메일 메시지의 데이터를 읽어 들여 소리를 출력하게 될 것이다. Content-Type은 text/plain, text/html, image/jpeg 등이 있다.

text/plain, image/jpeg 등을 미디어 타입(media type)이라고 한다. Content-Type으로 지정될 수 있는 미디어 타입은 IANA라는 국제기구에 의해 미리 정의되어 있다. 미디어 타입은 주타입과 부타입으로 나누어진다. 주타입은 8개가 있으며 각각의 주타입마다 무수한 부타입이 있다. 미디어 타입은 주타입/부타입의 형식으로 이루어진다. 즉 image/jpeg 이라는 미디어 타입은 image라는 주타입과 jpeg이라는 부타입으로 이루어진 것이다.

주타입 8가지는 다음과 같다.
·text
·image
·audio
·video
·application
·multipart
·message

<박스>
대표적인 Media Type
Text/plain, text/html, text/xml, text/enriched
image/gif, image/jpeg, image/tiff
audio/basic, audio/32kadpcm
video/mpeg, video/quicktime
model/vrml, model/mesh
application/octet-stream, application/zip, application/vnd.ms-excel
multipart/mixed, multipart/alternative
message/rfc822, message/news

Content-Type 헤더는 몇가지 부가 필드를 가질 수도 있다. 이러한 부가 필드들은 세미콜론(;)에 의해 구분된다.
만약 Content-Type이 text 미디어 타입이라면 charset(Character set - 문자세트)이라는 부가 필드를 가질 수 있다. 아래와 같은 Content-Type을 살펴보자.

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

이것은 메일 메시지가 Plain text(평범한 텍스트 데이터)라는 것을 나타내고 있으며 부가적으로 텍스트의 문자세트는 US ASCII, 즉 영문이라는 것을 나타내고 있다.
또한, Content-Type이 multipart 미디어 타입이라면 boundary라는 부가 필드를 가질 수 있다. 이 boundary 필드는 대단히 중요하다. 미디어 타입이 multipart라는 것은 메일 메시지의 데이터가 하나의 단일한 데이터가 아니라 여러 개의 데이터가 모여 하나의 메시지를 구성한 것을 뜻한다. 앞서 언급했던 첨부파일을 포함한 메일이 여기에 속한다. MUA가 multipart로 구성된 메일 메시지를 파싱하여 각각의 데이터로 만들기 위해서는 어디서부터 어디까지가 각각의 데이터인지를 알아야 한다. 이것을 알려주는 것이 바로 boundary라는 부가 필드이다. boundary가 가리키는 문자열이 바로 데이터들을 구분해주는 경계선이 되기 때문에 boundary 부가 필드는 Multipart MIME message를 파싱하는데 없어서는 안 될 중요한 요소이다(multipart message에 대한 자세한 설명은 다음 호에 연재 할 예정이다).

Content-Type: multipart/mixed;
boundary="----=_NextPart_000_0009_01BFE829.78EE4E90"

Content-Transfer-Encoding

Content-Transfer-Encoding 헤더는 굉장히 중요한 MIME 헤더이다. 바로 8bit 데이터를 어떤 방식을 통해 7bit 데이터로 변환시켰는지를 알려주는 헤더이기 때문이다. 그러므로 Content-Transfer-Encoding 헤더의 정보가 잘못되었을 경우에는, 원래의 데이터를 잃게 되고 만다.
Content-Transfer-Encoding 헤더의 값으로는 다음과 같은 것들이 올 수 있다.

·7bit
·8bit
·binary
·quoted-printable
·base64

위의 값 중에서 7bit, 8bit, binary는 그 어떤 변환도 하지 않음을 말해준다. 그냥 메일을 보낸 시점의 데이터가 변환되지 않고 그대로 메일 메시지에 실려왔음을 말해주는 것이다. 7bit는 메일 메시지가 7bit임을 뜻하고, 8bit는 메일 메시지가 8bit 데이터임을 말해준다. binary는 메일 메시지가 text가 아닌 binary 데이터임을 뜻한다.
우리가 눈여겨 봐야 할 것이 바로 quoted-printable과 base64라는 변환 방식이다. 이는 조금 후에 다시 자세히 설명한다. 이번 호에서 설명하고자 하는 핵심적인 내용이 바로 quoted-printable과 base64라는 변환 방식이다.

* Content-Disposition
: Content-Disposition 헤더는 현재의 데이터를 인라인(inline)으로 할 것인지, 아니면 첨부파일(attachment)로 할 것인지에 대한 것을 결정하는 헤더이다. 아직 실험적인 헤더이다.

* Content-Description
: Content-Description 헤더는 메일 메시지가 담고 있는 Content에 대한 설명을 해 놓는 헤더이다. 만약 MIME 이 해석이 되지 않을 때, 위의 필드를 보고 아래의 데이터가 무슨 데이터인지를 알 수 있도록 설명을 달아 놓으면 좋다.

* Content-ID
: Content-ID 헤더는 메일 메시지 외에 다른 Content를 가리킬 때 사용하지만, 잘 사용하지 않는다.

2.2 MIME Encoding
MIME의 역할 중 큰 역할이 바로 8bit 데이터를 7bit로 만드는 것이라고 앞서 언급했었다. 이러한 역할을 MIME Encoding이라고 한다. 앞서도 살펴봤듯이 MIME Encoding에는 두가지 방식이 있다. 바로 Quoted-Printable 방식과 Base64 방식이 그것이다.

2.3 Quoted-Printable Encoding & Decoding
Quoted-Printable Encoding 방식은 인코딩 된 메시지를 디코딩하지 않더라도 ASCII 문자들이 그대로 보일 수 있도록 하는 방식이다. 즉, 영문과 숫자등의 ASCII 7bit 문자들은 그대로 놔두고 8bit 문자만을 인코딩하는 방식이다. 이 때, 8bit 문자를 인코딩하는 방법은 대단히 간단하다. 8bit 문자는 등호(=) 뒤에 8bit 문자의 값을 16진수로 표현하여 써넣으면 된다. 이렇게 되면 모든 문자가 7bit 문자가 되어 메일 메시지 형태로 전송이 가능하다.
Quoted-Printable Encoding 방식은 대부분이 7bit ASCII 문자들이고, 가끔씩 8bit 문자들이 나오는 text 메시지를 인코딩하는데 유리한 방식이다.


2.4 Quoted-Printable Encoding 규칙
다음은 RFC2045에서 정의한 Quoted-Printable Encoding 규칙이다.

[규칙 1] 모든 옥텟(바이트)의 인코딩은 그 값을 16진수로 표현하여 '=' 뒤에 붙이면 된다. 옥텟의 값을 16진수로 표현하는데 사용되는 문자는 0123456789ABCDEF 이며, 대문자만을 사용한다. 예를 들어 십진수로 12인 옥텟(LF)을 Quoted-Printable로 인코딩하면 "=0C"가 되고, 십진수로 61인 옥텟(=)은 "=3D"가 되는 것이다. 뒤에 나오는 규칙 2-5에서 제시되고 있는 인코딩 방법을 사용하지 않는 모든 옥텟은 이 방식으로 인코딩 해야 한다.

[규칙 2] 십진수 33 - 60, 62 - 126의 문자(제어코드와 십진수 61인 '='를 제외한 아스키 값)는 규칙 1에 따라 인코딩 되지 않아도 된다. 즉, 이스케이프 문자인 '='를 제외한 영문자와 숫자등은 그대로 표현된다.

[규칙 3] 십진수 9(TAB)와 32(SPACE)는 절대로 인코딩 된 문자열의 끝에 나타나서는 않된다. 그러므로 문자열의 끝에 있는 Tab이나 Space는 규칙 1에 따라 인코딩 되어야 한다.

[규칙 4] 줄바꿈 문자(컴퓨터에 따라 CR, LF, CRLF로 각기 달리 나타나는 문자)는 CRLF의 형태로 표현되어야 한다.(RFC822 정의)

[규칙 5] Quoted-Printable로 인코딩 된 문자열의 길이는 76자 이상 이어서는 않된다. 이 때 문자열의 길이는 문자열 맨 뒤에 붙는 CRLF는 제외한 나머지 문자열의 길이를 말한다. 인코딩 되었을 때, 76자가 넘는 문자열에는 Soft Line Break를 사용한다. 즉 원래의 문자열에는 영향을 주지 않고 단지 인코딩 된 문자열의 줄바꿈만을 나타내는 문자를 덧붙이고 줄바꿈을 하여, 인코딩 된 문자열이 76자가 넘지 않도록 하는 것이다. Quoted-Printable에서는 '='을 Soft Line Break로 사용한다. Soft Line Break 뒤에는 Tab이나 Space가 나타날 수 있으며 규칙 3에 의한 인코딩 해서는 않된다. 그 이유는 몇몇 MTA들이 전송하는 원래의 문자열에 Tab이나 Space를 붙이기도 하고 빼기도 하는데, 이 때 덧붙여지는 Tab이나 Space는 원래의 데이터가 아니기 때문이다.

위의 규칙을 적용하여 다음과 같은 문자열을 Quoted-Printable 방식으로 인코딩 해보자.

Quoted-Printable Encoding 방식으로 인코딩하고 디코딩하는 함수를 작성해보자. 함수는 hQPencode(), hQPdecode()이다(리스트 1,2).

리스트 1 : hQPencode()
/**********************************************************************/
/* */
/* hQPencode() */
/* */
/* ------------------------------------------------------------------ */
/* desc : QP 방식으로 encode한다. */
/* usage : QPencode(string pointer, line size, ptr of encoded length) */
/* return: if Success, encoded string pointer */
/* else if fail, NULL */
/* */
/**********************************************************************/
char *hQPencode(char *str, int lineSize, int *len)
{
char *encodeStr = (char *)0x00;
char *encodeHex;
int i = 0, j = 0;
int qpSizeCnt = 0;

/* encoding된 문자열은 원래의 문자열에 대해 최대 3배가 된다. */
encodeStr = (char *)malloc(sizeof(char)*(lineSize*3 + 1));

for(i=0;i<lineSize;i++)
{
if(qpSizeCnt >= QP_SIZE - 3)
qpSizeCnt = 0, encodeStr[j++] = 0x0A;

if((str[i] >= 33 && str[i] <= 126) || (str[i] == 0x0A))
{
if(str[i] == 61)
{
encodeHex = hDec2Hex(str[i]);
encodeStr[j++] = 0x3D; /* '=' */
encodeStr[j++] = encodeHex[0];
encodeStr[j++] = encodeHex[1];
qpSizeCnt += 3;
}
else
qpSizeCnt++, encodeStr[j++] = str[i];
}
else if(str[i] == 9 || str[i] == 32)
{
if(str[i+1] == 0x0A || str[i+1] == 0x00) /* 문자열 끝의 Tab, Space */
{
encodeHex = hDec2Hex(str[i]);
encodeStr[j++] = 0x3D; /* '=' */
encodeStr[j++] = encodeHex[0];
encodeStr[j++] = encodeHex[1];
qpSizeCnt += 3;
}
else
qpSizeCnt++, encodeStr[j++] = str[i];
}
else
{
encodeHex = hDec2Hex(str[i]);
encodeStr[j++] = 0x3D; /* '=' */
encodeStr[j++] = encodeHex[0];
encodeStr[j++] = encodeHex[1];
qpSizeCnt += 3;
}
}

encodeStr[j] = 0x00;
return encodeStr;
}


리스트 2 : hQPdecode()
/**********************************************************************/
/* */
/* hQPdecode() */
/* */
/* ------------------------------------------------------------------ */
/* desc : QP 방식으로 encoding된 string을 decode한다. */
/* usage : QPdecode(encoded string pointer) */
/* return: if Success, decoded string pointer */
/* else if fail, NULL */
/* */
/**********************************************************************/
char *hQPdecode(char *encodeStr, int *len)
{
char *decodeStr;
char hex[3];
char ch;
int dec;
int spaceAdded = 0;
int i = 0, j = 0;

decodeStr = (char *)malloc(sizeof(char) * (strlen(encodeStr) + 1));

if(decodeStr == 0x00)
return 0x00;

while(encodeStr[i] != '\0')
{
if(encodeStr[i] == 0x3D) /* QP ESC seqeunce, '=' */
{
ch = encodeStr[++i]; /* white space를 체크하기 위해 미리 내다봄 */

while(ch == 0x09 || ch == 0x20) /* '=' 다음에 따라오는 character가 Tab, space이면 건너뛴다. */
spaceAdded = 1, ch = encodeStr[++i];

if(spaceAdded == 1)
{
spaceAdded = 0;
continue;
}

if(ch == 0x0A) /* '=' 다음에 LF가 있으면 soft line break임. encoded QP string은 한 라인에 76 characters만 허용 */
{
i++;
continue;
}

hex[0] = encodeStr[i++];
hex[1] = encodeStr[i++];
hex[2] = '\0';

dec = hHex2Dec(hex);

if(dec < 0) /* decoding error */
{
/* error 발생시 그대로 출력하기 위해 메시지 복원 */
decodeStr[j++] = 0x3D; /* '=' */
decodeStr[j++] = hex[0];
decodeStr[j++] = hex[1];
}
else
decodeStr[j++] = dec;
}
else if(encodeStr[i] > 0x7E) /* encoding error */
i++; /* ignore that character */
else
decodeStr[j++] = encodeStr[i++];
}

decodeStr[j] = '\0';

if(len != 0x00)
*len = j;

return decodeStr;
}

3. Base64 Encoding & Decoding

Base64 Encoding 방식은 바이너리 파일을 메일을 통해서 보내거나, Quoted-Printable 인코딩 방식이 부적합한 모든 메시지에 적용하는 방식이다. Base64 Encoding 방식은 이론적으로 무척 간단하다. 간단히 말해서 Base64 방식은 원래의 데이터 3바이트를 6bit씩 나누어 4바이트로 만드는 방식을 말한다. 3바이트, 즉 24bit는 6bit씩 나누면 4개가 나온다. 이렇게 해서 나온 4개의 6bit 값을 다음의 변환 테이블에서 각각 문자로 변환하는 것이 Base64 Encoding 방식의 핵심이다.
표 1은 Base64 Encoding 방식에서 사용되는 변환 테이블이다.

6bit 값
변환값
6bit값
변환값
6bit 값
변환값
6bit값
변환값
0
A
16
Q
32
g
48
w
1
B
17
R
33
h
49
x
2
C
18
S
34
i
50
y
3
D
19
T
35
j
51
z
4
E
20
U
36
k
52
0
5
F
21
V
37
l
53
1
6
G
22
W
38
m
54
2
7
H
23
X
39
n
55
3
8
I
24
Y
40
o
56
4
9
J
25
Z
41
p
57
5
10
K
26
a
42
q
58
6
11
L
27
b
43
r
59
7
12
M
28
c
44
s
60
8
13
N
29
d
45
t
61
9
14
O
30
e
46
u
62
+
15
P
31
f
47
v
63
/
표 1 : Base64 Encoding 방식에서 사용되는 변환 테이블

하지만 실제는 데이터가 3바이트씩 나누어 떨어지는 것이 아니기 때문에 인코딩 된 메시지에 등호(=)를 가지고 패딩을 한다.
실제 예를 통해 Base64 Encoding 방식을 이해해보도록 하자.

10바이트의 변환되기 전의 문자열이 있고, 그것을 이진수로 표현하면 다음과 같다.
0100101011100100100011010110001001011101
이것을 6bit씩으로 나누어보자.
010010 101110 010010 001101 011000 100101 1101
맨 뒤의 숫자는 6bit가 되지 않으므로 뒤에 0을 붙여 6bit로 만든다.
010010 101110 010010 001101 011000 100101 110100
위의 6bit 숫자들을 10진수로 표현하면 다음과 같다.
18 46 18 13 24 37 52
이들 10진수를 위의 변환 테이블을 이용해서 문자로 바꾸어 보자.
S u S N Y l 0
위의 문자열의 길이가 4로 나누어 떨어지도록 문자열의 뒤에 등호(=)를 붙인다(패딩).
S u S N Y l 0 =
이렇게 해서 인코딩된 마지막 결과는 SuSNYl0= 이 된다.

다음에는 Base64 방식으로 인코딩, 디코딩을 하는 함수인 hBASE64encode(), hBASE64decode() 함수를 작성해보도록 하자.

리스트 3 : hBASE64encode()
/**********************************************************************/
/* */
/* hBASE64encode() */
/* */
/* ------------------------------------------------------------------ */
/* desc : 문자열을 BASE64 방식으로 encoding 한다. */
/* usage : BASE64encode(string pointer) */
/* return: if Success, encoded string pointer */
/* else if fail, NULL */
/* */
/**********************************************************************/
char *hBASE64encode(char *str, int lineSize, int *len)
{
char *encodeStr = (char *)0x00;
int i = 0, j = 0;
int count = 0;
int ch;
int base64SizeCnt = 0;

if(str == 0x00)
return 0x00;

encodeStr = (char *)malloc(sizeof(char)*(lineSize*2));

while(TRUE)
{
switch(count++)
{
case 0:
if(i < lineSize)
ch = (str[i] & 0xFC) >> 2;
else
ch = -1;
break;

case 1:
if(i < lineSize)
if(i+1 < lineSize)
ch = ((str[i] & 0x03) << 4) | ((str[i+1] & 0xF0) >> 4);
else
ch = ((str[i] & 0x03) << 4);
else
ch = -1;
i++;
break;

case 2:
if(i < lineSize)
if(i+1 < lineSize)
ch = ((str[i] & 0x0F) << 2) | ((str[i+1] & 0xC0) >> 6);
else
ch = ((str[i] & 0x0F) << 2);
else
ch = -1;
i++;
break;

case 3:
if(i < lineSize)
ch = (str[i] & 0x3F);
else
ch = -1;
i++;
count = 0;
break;
}


if(ch >= 0 && ch <= 25) /* Upper Case Alphabet */
encodeStr[j++] = 'A' + ch;
else if(ch >= 26 && ch <= 51) /* Lower Case Alphabet */
encodeStr[j++] = 'a' + ch - 26;
else if(ch >= 52 && ch <= 61) /* Digit */
encodeStr[j++] = '0' + ch - 52;
else if(ch == 62)
encodeStr[j++] = '+';
else if(ch == 63)
encodeStr[j++] = '/';
else if(ch == -1)
encodeStr[j++] = '='; /* padding */

base64SizeCnt++;

if(j%4 == 0)
{
if(base64SizeCnt == BASE64_SIZE)
base64SizeCnt = 0, encodeStr[j++] = 0x0A; /* soft break */

if(i >= lineSize)
break;
}
}

encodeStr[j] = 0x00;

if(len != 0x00)
*len = j;

return encodeStr;
}

리스트 4 : hBASE64decode()
/**********************************************************************/
/* */
/* hBASE64decode() */
/* */
/* ------------------------------------------------------------------ */
/* desc : BASE64 방식으로 encoding된 string을 decode한다. */
/* usage : BASE64decode(encoded string pointer) */
/* return: if Success, decoded string pointer */
/* else if fail, NULL */
/* */
/**********************************************************************/
char *hBASE64decode(char *encodeStr, int *len)
{
char *decodeStr;
long btmp = 0; /* 4byte (decoding 용) */
int i = 0, j = 0;
int count = 0;
int padCount = 0;

/* decoded string을 위한 메모리를 할당한다. */
/* 실제로는 encodeStr length의 3/4 만큼만 잡으면 된다. */
decodeStr = (char *)malloc(sizeof(char) * strlen(encodeStr));

if(decodeStr == 0x00)
return 0x00;

while(encodeStr[i] != '\0')
{
if(isupper(encodeStr[i]))
btmp = (btmp << 6) | (encodeStr[i] - 'A' ); /* 대문자는 0 - 25까지 */
else if(islower(encodeStr[i]))
btmp = (btmp << 6) | (encodeStr[i] - 'a' + 0x1A); /* 소문자는 26(0x1A) - 51까지 */
else if(isdigit(encodeStr[i]))
btmp = (btmp << 6) | (encodeStr[i] - '0' + 0x34); /* 숫자는 52(0x34) - 61까지 */
else if(encodeStr[i] == '+')
btmp = (btmp << 6) | 0x3E; /* '+'는 62(0x3E) */
else if(encodeStr[i] == '/')
btmp = (btmp << 6) | 0x3F; /* '/'는 63(0x3F) */
else if(encodeStr[i] == '=')
padCount++, btmp = (btmp << 6); /* '='는 pad */
else
btmp = (btmp << 6); /* encoding error */

if(++count >= 4) /* 한 transaction이 끝났으면 */
{

decodeStr[j++] = (char)((btmp & 0x00FF0000) >> 16);
decodeStr[j++] = (char)((btmp & 0x0000FF00) >> 8);
decodeStr[j++] = (char)((btmp & 0x000000FF) );

count = 0;
btmp = 0;

if(encodeStr[i+1] == 0x0A) /* soft linebreak */
i++;
}

i++;
}

decodeStr[j - padCount] = '\0';

if(len != 0x00)
*len = j - padCount;

return decodeStr;
}

/**********************************************************************/
/* */
/* hHex2Dec() */
/* */
/* ------------------------------------------------------------------ */
/* desc : hex를 dec로 전환 */
/* usage : hHex2Dec(hex number e.g. "3D" or "3d") */
/* return: if Success, dec number */
/* else if fail, -1 */
/* */
/**********************************************************************/
int hHex2Dec(char *str)
{
int dec = 0;
int byte;
int i;

for(i=0;i<2;i++)
{
if(str[i] >= '0' && str[i] <= '9')
byte = str[i] - '0';
else if(str[i] >= 'A' && str[i] <= 'F')
byte = str[i] - 'A' + 10;
else if(str[i] >= 'a' && str[i] <= 'f')
byte = str[i] - 'a' + 10;
else
byte = -1;

if(byte < 0)
return -1;

dec += (i == 0) ? byte << 4 : byte;
}

return dec;
}

/**********************************************************************/
/* */
/* hDec2Hex() */
/* */
/* ------------------------------------------------------------------ */
/* desc : dec를 hex로 전환 */
/* usage : hDec2Hex(dec number) */
/* return: if Success, str represented hex number */
/* else if fail, NULL */
/* */
/**********************************************************************/
char *hDec2Hex(int dec)
{
static char hex[3];
int i;
int ch;

for(i=0;i<2;i++)
{
if(i == 0)
ch = (dec & 0xF0) >> 4;
else if(i == 1)
ch = (dec & 0x0F);

if(ch >= 10)
hex[i] = 'A' + ch - 10;
else
hex[i] = '0' + ch;
}

hex[i] = 0x00;

return &hex[0];
}

이번호에는 MIME의 전반부에 해당하는 MIME Encoding에 대하여 살펴보았다. 다음 호에는 MIME 메시지를 구성(Compose)하고 구성된 MIME 메시지를 해석(Parse)하는 방법에 대하여 알아보도록 하겠다.
반응형
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"

+ Recent posts