반응형

구조체를 선언할때 보통 아래와 같은식으로 한다.


#pragma pack(push, 1)

  구조체 정의

#pragma pack(pop)



왜냐하면 Win32에서는 기본형이 4Byte이므로 4Byte씩 처리하는것이 가장 빠르다.

그래서 win32에서는 데이타 정렬((Data alignment))을 기본적으로 4Byte씩 하도록 되어있다.


struct A

{

    char a;

    short b;

};

위와 같이 struct가 정의된경우 struct A를 sizeof(A) 하면 얼마일까?


char a  =  1Byte  , short b = 2Byte 이므로 sizeof(A) = 3Byte가 나올까?

그냥하면 3Byte가 아닌 8Byte가 나온다.


위 구조체의 데이타를 같은 구조체에 copy하는데는 문제없겠지만

socket이나 serial로 전송하거나 한다면 문제가 발생할 것이다.


이럴때 데이타정렬을 원하는 size로 하게 하려면

#pragma pack( 1)을 하면 된다.


#pragma pack(push, 1) 

struct A

{

    char a;

    short b;

};

#pragma pack(pop)



#pragma pack(push)  //이전 데이타 정렬 보관

#pragma pack(1)       // 1Byte씩 데이타정렬 설정

...

#pragma pack(pop)   //이전 데이타 정렬 복구



어쩌다 한번씩 깜빡하고 이문제로 해맬때 있다.

혹 모르는 분을 위해..

그리고 스스로 기억을위해 정리해본다.


델파이에서는 packed 키워드를 이용하여 정렬한다.


type
  // Declare an unpacked record
  TDefaultRecord = Record
    name1   : string[4];
    floater : single;
    name2   : char;
    int     : Integer;
  end;

  // Declare a packed record
  TPackedRecord = Packed Record
    name1   : string[4];
    floater : single;
    name2   : char;
    int     : Integer;
  end;

 

참조

http://www.delphibasics.co.uk/RTL.asp?Name=Packed

  1. Favicon of http://ikpil.com BlogIcon 최익필 2008.09.24 13:59

    MSVC2005 에서 초기 구조체의 경우 4바이트가 나옵니다.
    기본적으로 컴파일러에서 8바이트 정렬을 하며, 8바이트보다 작은 바이트 데이터형이 있을경우, 그 작은 바이트로 정렬하는 규칙이 있다고 알고 있습니다.

    다른 컴파일러는 어떻게 할지 모르겠네요^^

  2. 2011.10.28 11:20

    A 구조체는 8바이트가 아니라 4바이트죠. 디폴트가 4Byte 정렬이라고 직접 말씀하셨으면서...

반응형
 

컴파일러는 사용자가 작성한 코드를 컴파일하기에 앞서 전처리문에서 정의해 놓은 문장들을 먼저 처리한다.

종류로는 #include, #define, #if, #error, #line, #pragma 등이 있다.
이것은 방대한 소스 코드를 지우지 않고 활성화와 비활성화하는 데에 가장 많이 이용된다.
즉, 기존에 있는 소스 코드를 건드리지 않고 부분적인 컴파일을 하는 것이다.

어떤 C 컴파일러는 전처리문의 첫 문자 #이 항상 그 라인의 첫 문자이어야 한다.

ANSI 표준에 따른 C의 전처리문의 종류
- 파일 처리를 위한 전처리문 : #include
- 형태 정의를 위한 전처리문 : #define, #undef
- 조건 처리를 위한 전처리문 : #if, #elif, #ifdef, #elif defined(), #ifndef, #else, #endif
- 에러 처리를 위한 전처리문 : #error
- 디버깅을 위한 전처리문 : #line
- 컴파일 옵션 처리를 위한 전처리문 : #pragma

조건 처리를 위한 전처리문은 어떤 조건에 대해 검사를 하고 그 결과를 참(0이 아닌 값) 또는 거짓(0)으로 돌려준다.
#if : ...이 참이라면
#ifdef : ...이 정의되어 있다면
#else : #if나 #ifdef에 대응된다.
#elif : else + if의 의미
#elif defined() : else + ifdef의 의미
#endif : #if, #ifdef, #ifndef이 끝났음을 알린다.

#include
헤더 파일과 같은 외부 파일을 읽어서 포함시키고자 할 때 사용된다. 이때의 파일은 이진 파일(Binary file)이 아닌 C의 소스 파일과 같은 형태의 일반 문서 파일을 말한다:
#include <stdio.h>        /* 이 위치에 stdio.h라는 파일을 포함시킨다. */
#include "text.h"           /* 이 위치에 text.h라는 파일을 포함시킨다. */

<...>을 사용할 때와 ...을 사용할 때의 차이점은 <...>은 컴파일러의 표준 포함 파일 디렉토리(또는 사용자가 별도로 지정해 준)에서 파일을 찾는 것을 기본으로 한다. 그리고 ...을 사용했을 때는 현재의 디렉토리를 기본으로 파일을 찾게 된다. 아예 디렉토리를 같이 지정할 수도 있다:
#include <C:\MYDIR\MYHEAD.H>
#include "C:\MYDIR\MYHEAD.H"

#define
상수 값을 지정하기 위한 예약어로 구문의 상수로 치환한다. 또한 #define은 함수 역활과 비슷하게 아래와 같이 쓰일 수 있다:
#define SUM(x) ((x) = (x) + (x))

#define으로 정의할 수 있는 것은 숫자만이 아니다:
#define MYNAME "Young Hee"

이렇게 #define으로 정의된 것은 일반적인 변수와는 다르다. 그 차이는 명백하다:
#define MYNAME "Turbo"
char my_name[] = "Turbo"

MYNAME은 전처리문으로 my_name은 문자형 배열 변수로 정의되었다:
printf(MYNAME);
printf(MYNAME);
printf(my_name);
printf(my_name);

이것을 전처리한 상태는 다음과 같이 될 것이다:
printf("Turbo");
printf("Turbo");
printf(my_name);
printf(my_name);

이런 결과에서 우리가 유추해 볼 수 있는 것은 전처리 명령을 사용했을 경우 "Turbo"라는 동일한 동작에 대해서 두개의 똑같은 문자열이 사용됐고, 변수를 사용했을 경우에는 하나의 문자열을 가지고 두번을 사용하고 있다는 것이다. 결과적으로 이런 경우에는 전처리문을 사용했을 경우 메모리 낭비를 가져 온다는 것을 알 수 있다.

#undef
#define으로 이미 정의된 매크로를 무효화한다:
#define ADD(a, b) (a + b)
#undef ADD(a, b)

앞으로 사용되는 ADD(...)는 undefined symbol이 되어 에러 처리된다.

#if ~ #endif
#if 구문은 if랑 아주 비슷하다. 이것은 어떠한 구문을 컴파일 할지 안할지를 지정할 수 있다:
#define A 1

#if A
source code ...
#endif

위 source code 부분은 컴파일이 된다. if문에서와 같이 참, 거짓을 구분하여 컴파일이 된다. 위에서 A값은 1 즉 0보다 큰 수이기 때문에 참인 것이다. 직접 아래와 같이 하면 거짓이기 때문에 source code 부분은 컴파일이 되지 않는다:
#if 0
source code ...
#endif

#if A == 2
source code 2 ...
#elif A == 3
source code 3 ...
#else
source code 1 ...
#endif

#ifdef ~ #endif
컴파일 할 때
#define MYDEF                /* MYDEF는 값은 가지지 않았지만 어쨋든 정의는 되었다 */

#ifdef YOURDEF              /* 만약 YOURDEF가 정의되어 있다면... */
#define BASE 10             /* BASE == 10 */
#elif defined MYDEF                    /* 그외에 MYDEF가 정의되었다면... */
#define BASE 2               /* BASE == 2 */
#endif

BASE는 상수 2로 치환되어 전처리기가 컴파일러에게 넘겨준다.

#ifndef __헤더명_H__ ~ #endif
헤더 파일이 겹치는 것을 막기 위한 일종의 매크로이다. 예를 들어, 헤더 파일에 어떤 클래스의 인터페이스 선언을 넣었다고 하자. 이 클래스 인터페이스에서 다른 파일의 프로토타입이 필요해서 다른 A 파일을 include 하고 있는데 이 헤더 파일을 include 하는 파일에서 A라는 헤더 파일을 이미 include 하고 있다면 두번 define한 것이 된다. 그러면 SYNTEX 에러가 난다. 그래서 그런 것을 막는 방법의 하나로 #ifndef을 사용한다. 이전에 include되어 있으면 #endif 쪽으로 점프해 버려 결국 한번 선언되는 것이다:
#include  <stdio.h>    ------ (a)
#include  <stdio.h>    ------ (b)

이렇게 두번 썼다고 하자. 그런데 앞에서 이미 include 했는데 밑에 또 한다면 문제가 된다. 컴파일러가 검사해야할 코드량도 많아진다. 그래서 stdio.h에는
#ifndef __STDIO_H__
#define __STDIO_H__
...
#endif
가 선언되어 있다. 만약 __STDIO_H__가 선언되어 있지 않다면 선언한다는 뜻이다. 그 뒤 (b)에서는 이미 (a)쪽에서 __STDIO_H__ 을 선언한 상태이기 때문에 전처리기 쪽에서 무시해버린다. 그러므로 컴파일러는 (a)만 검사한다.

#defined
define이 여러 개 되어 있는지를 검사할 때 쓴다. 이것은 여러 개를 동시에 검사 할 수 있다:
#if defined A || defined B
#if (defined A) || (defined B)
#if defined(A) || defined(B)


#ifdef와 #if defined의 차이
#ifdef은 정의가 되어 있는지를 테스트 하기 때문에 한번에 여러 개를 사용할 수 없다:
#ifdef name

여러 개가 정의되어 있는지를 테스트하기 위해서 #if defined를 사용할 수 있다:
#if defined(MACRO1) || defined(MACRO2)

#if는 ||로 중첩해서 사용할 수 있다(형식이 #if expression이므로 C 표현이 올 수 있다):
#if (MACRO1) || (MACRO2)

#error
소스 라인에 직접 에러 메세지를 출력한다. 전처리기가 #error 문을 만나면 그 즉시 컴파일을 중단하고 다음과 같은 에러 메시지를 출력한다:
ERROR : XXXXX.c ########: Error directive: 내용
- XXXXX.c --> 현재 컴파일 중인 파일 명
- ####### --> 전처리기가 #error 문을 만난 시점에서의 행 번호(헤더 포함)

#ifdef __LARGE__
#error This program must be compiled in LARGE memory model!
#endif

이 내용은 만일 프로그램이 LARGE 모델이 아니라면 "#error" 뒤에 표시된 메세지를 출력하고 컴파일을 중지하게 된다.

#line
이 명령은 소스 코드의 행 번호를 지정하기 위한 것으로 주로 컴파일러에 의해 미리 정의된 __LINE__과 함께 사용된다.
__LINE__과 __FILE__을 각각 행 번호와 파일 명으로 변경한다:
#include <stdio.h>
#define DEBUG

void main(void)
{
        int count = 100;

        #line 100               /* 다음 줄번호를 100으로 설정한다 */
                                   /* <-- 이 줄의 번호가 100이다 */
        #ifdef DEBUG        /* <-- 이 줄의 번호가 101이다 */
        printf("line:%d, count = %d\n", __LINE__, count);
        #endif

        count = count * count - 56;
        #ifdef DEBUG
        printf("line:%d, count = %d\n", __LINE__, count);
        #endif
        count = count / 2 + 48;
        #ifdef DEBUG
        printf("line:%d, count = %d\n", __LINE__, count);
        #endif
}

#pragma
컴파일 옵션의 지정. 컴파일러 작성자에 의해서 정의된 다양한 명령을 컴파일러에게 제공하기 위해 사용되는 지시어이다. 컴파일러의 여러 가지 옵션을 명령행에서가 아닌 코드에서 직접 설정한다. #pragma는 함수의 바로 앞에 오며 그 함수에만 영향을 준다.
Turbo C는 9개의 #pragma 문(warn, inline, saveregs, exit, argsused, hdrfile, hdrstop, option, startup)을 지원하고 있다:

#pragma inline
컴파일할 때 어셈블러를 통해서 하도록 지시한다. 즉, 인라인 어셈블리 코드가 프로그램에 있음을 알려준다(명령행 상에서 '-B' 옵션).

#pragma saveregs
이 홉션은 휴즈 메모리 모델에 대해 컴파일된 함수에게 모든 레지스터를 저장하도록 한다.

#pragma warn
이 지시어는 Turbo C에게 경고 메시지 옵션을 무시하도록 한다.

#pragma warn -par
이는 매개 변수(parAMETER)가 사용되지 않았다고 경고(warnING)를 내지 못하도록 한다. 이와 반대되는 표현은
#pragma warn +par

경고의 내용 앞에 (+)를 사용하면 경고를 낼 수 있도록 설정하고 (-)를 사용하면 경고를 내지 못하도록 하는 것은 모든 경고에 대해 동일하다. 명령 행에서는 "-wxxx"로 경고를 설정하고 "-w-xxx"로 경고를 해제한다. 경고의 종류는 무척 많은데 자주 사용되는 것을 아래에 나타냈다. 모든 것을 알고 싶다면 컴파일러 User's Guide의 명령행 컴파일러 부분을 참고하기 바란다:
par : 전해진 파라미터가 사용되지 않음
rvl : void 형이 아닌 함수에서 리턴 값이 없음
aus : 변수에 값을 할당했으나 사용하지 않았음
voi : void 형 함수에서 리턴 값이 사용되었음
sig : 부호 비트(MSB)가 고려되지 않은 형 변환(type-castion)에서 부호 비트를 소실할 수 있음

Standard C pre-defined symbols

__FILE__ a string that holds the path/name of the compiled file
__LINE__ an integer that holds the number of the current line number
__DATE__ a string(Mmm dd yyyy) that holds the current system date
__TIME__ a string(hh:mm:ss) that holds the current system time
__STDC__ defined as the value '1' if the compiler conforms with the ANSI C standard
__cplusplus determines if your compiler is in C or C++ mode. Usually used in headers


#include <stdio.h>

void main(void)
{  
        printf("The path/name of this file is %s\n", __FILE__);  
        printf("The current line is %d\n", __LINE__);  
        printf("The current system date is %s\n", __DATE__);  
        printf("The current system time is %s\n", __TIME__);  

        #ifdef __STDC__  
        printf("The compiler conforms with the ANSI C standard\n");  
        #else  
        printf("The compiler doesn't conform with the ANSI C standard\n");  
        #endif  

        #ifdef __cplusplus  
        printf("The compiler is working with C++\n");  
        #else  
        printf("The compiler is working with C\n");  
        #endif 


프로그래머들 마다 코딩 스타일(암시적 약속)이 있다. 보통 매크로, const 변수는 대문자로 적는 것이 원칙이다. 매크로 함수와 일반 함수, 매크로 대상체(object-like macro)와 일반 변수를 구분하기 쉽게 해주는 것이기 때문이다.

#define STDIO_H_
왜 뒤에 _를 붙였을까? 이것도 하나의 암시적 약속이다. 컴파일러 제작 회사는 매크로를 정의할 때 사용자들과 이름이 충돌이 나지 않게 하기 위해서 대부분 _를 뒤어 덧붙인다. 또한 _를 하나 혹은 두 개 연속으로 시작하는 것은 컴파일러 내부에서 사용하는 매크로라는 성격이 강하다. 물론 강제적인 뜻은 없으며 단지 관습상 그렇다. 왜 이것이 관습이 되었나 하면 보통 매크로 변수 이름이나 함수 이름을 지을 때 뒤에 _를 붙이지 않기 때문이다. 그래서 함수 제작자들이 _를 단골로 붙였다.

<출처 : http://cafe.naver.com/devctrl.cafe?iframe_url=/ArticleRead.nhn%3Farticleid=468 >

반응형
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)에 대해서 알아볼 것이다.

  1. Favicon of http://www.cyworld.com/withcycle BlogIcon namo 2008.05.19 23:06

    EHLO가 아니라 HELO가 아니였을까요?

    • Favicon of http://pmguda.com BlogIcon 성군 2008.05.20 13:03

      Extended SMTP [ehlo]
      Extended SMTP (ESMTP), sometimes referred to as Enhanced SMTP, is a definition of protocol extensions to the Simple Mail Transfer Protocol standard. The extension format was defined in RFC 1869...

반응형
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"

반응형
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회에 걸쳐 필자의 글을 읽어준 독자들에게 다시 한 번 감사의 말씀을 전하며 마칠까 한다.

반응형

POP3 구현 하기

우리는 POP3 를 이용하여 메일 서버로부터 메일을 가져 올 수 있습니다 . POP3 환경에서 메일은 메일 서버에 존재 하며 일반적인 클라이언트 프로그램은 메일 서버로 접속 한 다음 메일 메시지를 서버에서 클라이언트로 복사 합니다 . 일반적으로 클라이언트가 POP3 서버의 TCP 110 번 포트로 연결 되면서 POP 세션이 이루어 지면서 클라이언트가 서버에 접속을 성공 하면 POP3 서버는 접속 축하 메시지를 돌려 준다 . 그러면 다음 명령어를 수행 할 수 있으며 클라이언트와 서버가 서로 응답을 주고 받은 다음 연결이 종료 되면서 세션도 끝나게 되는 것이다 .

----------

POP 명령

----------

일반적인 명령어 규약은 다음과 같습니다 .

명령어는 CRLF 시퀀스로 종료 된다 .

키워드는 공백 문자로 구분 된다 .

키워드는 3~4 글자로 이루어져 있다 .

응답은 최대 512 자까지 된다 .

“+OK” 는 긍정적인 응답을 가리 킨다 .

“-ERR” 은 부정적인 응답이나 오류가 발생 한 경우를 나타낸다 .

필수명령

설명

USER [name]

서버에 접속하는 사용자 명

PASS [password[

서버에 접속하는 사용자의 비밀번호

QUIT

현재 세션의 종료

DELE [msg]

서버에서 메일을 삭제

RSET

현재 세션의 모든 변경 사항을 취소

STAT

서버에 존재하는 메시지의 개수를 돌려 준다 .

RETR [msg]

메시지의 컨텐츠를 가져 온다

LIST [msg]

파라미터로 넘어 오는 메시지에 대한 정보를 돌려 준다 . 예를들면 크기를 바이트 단위로 돌려준다 . 파라미터가 없다면 모든 메시지의 목록과 그 크기를 돌려 준다 .

NOOP

서버와 긍적적인 응답을 주고 받는 것 이외에 아무런 작업도 수행하지 않는다 .

TOP [msg] [n]

서버는 메시지의 헤더와 본문을 구분해 주는 빈 줄 , 그리고 메시지의 본문이 몇 개의 행으로 이루어 졌는지 알려 준다 . [msg] 에서 원하는 메시지의 번호를 지정하며 [n] 에서 가져오려는 메시지의 상위 행의 개수를 지정 한다 .

UIDL [msg]

인자가 주어 졌으면 서버는 지정된 메시지에 대한 정보를 담고 있는 행을 긍정적인 응답과 함께 표시 한다 . 이를 선택된 메시지에 대한 “ 고유 id” 목록이라 한다 . 메시지의 고유 id 는 서버에서 독자적으로 지정하는 문자열로 0x21 로부터 0x7e 까지 문자들 중의 하나로 이루어 진다 . 이는 메시지를 고유 하게 식별하기 위해 사용 한다 .

////////////////////////////////////////////////////////////////////////////////////
본문

사용자가 쉘 계정이 있는 호스트에 직접 접속하여 메일을 읽지 않고
자신의 PC에서 바로 로컬 메일 리더(유도라나 넷스케이프 ,outlook outlook express 등등)를 이용하여
자신의 메일을 다운로드 받아서 보여주는 것을 정의한 프로토콜이다.
POP2와 PO3과 함께 널리 사용된다.
POP2는 RFC 937에 정의되어 있으며 POP3는 RFC 1725에 정의되어 있다.
POP2는 포트번호 109를 사용하며 POP3는 포트번호 110을 사용한다.
이들은 다른 명령어를 사용하지만 기본적으로 같은 기능을 수행한다.

POP 프로토콜은 사용자의 로그인 이름과 패스워드를 식별하고,
서버로부터 사용자의 로컬 메일 리더(local mail reader)로 사용자의 메일을 이동시킨다.
POP2는 SMTP와 같이 단순한 요구/응답 프로토콜이다.
HELO 명령은 검색중인 메일박스의 계정을 위해서 사용자의 이름과 패스워드를 제공한다.
HELO 명령에 대한 응답으로 서버는 메일박스에 메시지의 수를 보낸다.
READ 명령에 의해 메일을 읽기 시작한다.
RETR 명령은 현재 메시지의 전체 텍스트를 검색한다.
ACKD는 메시지의 수신 확인 명령으로 서버로부터 해당 메시지를 삭제한다.
각각의 메일에 대한 수신 확인 후에 서버는 새로운 메시지에 바이트의 수를 보낸다.
만약 바이트가 0이라면 더이상 검색할 메시지가 없다라는 것을 가리키며
클라이언트는 QUIT 명령에 의해 세션을 종료한다.

POP3에 대한 명령은 POP2와 완전히 다르지만 유사한 기능을 수행한다.
POP3의 USER 명령은 사용자의 계정 이름을 제공하고 PASS 명령은 사용자의 패스워드를 제공한다.
STAT 명령은 읽지 않은 메시지의 수와 바이트 크기를 출력한다.
RETR 명령은 해당 번호의 메시지를 검색한다.
DELE 명령은 해당 번호의 메시지를 삭제한다.
LAST 명령은 가장 최근에 접근된 메시지의 번호를 출력한다.
LIST 명령은 모든 메시지나 해당 메시지의 크기를 출력한다.
QUIT 명령은 세션을 종료한다.

반응형

이메일 전송시에는 일반 문자 그대로 전송하지 않고
일반적으로 8bit 이나 base64 방식으로 인코딩해서 전송을 합니다.

8bit 문자열은 imap_8bit() 함수를 이용해서 8bit 문자열로 인코딩할 수 있고 quoted_printable_decode() 함수를 이용해서 디코딩할 수 있습니다.

base64 는 base64_encode() 함수로 인코딩하고 base64_decode() 함수로 디코드합니다.

그런데 이메일 인코딩시에는 한가지 규칙이 있습니다.
질문에 문자들을 예를 들면...

=?EUC-KR?B?xde9usau?=

위 내용에서 앞, 뒤에 =?, ?= 는 시작과 끝을 나타내는 것이며 그 사이에 문자열을 ? 로 구분하였을 때...
EUC-KR 은 문자 형식을 말해주며 B 는 Base64 를 뜻 합니다. 만약 Q 라면 Quoted 문자열이 됩니다.
그리고 나머지가 실제 인코딩된 문자열이 되는 것이죠.

그래서 질문에 있는 문자열은 base64 로 인코딩된 문자열이고 실제 인코딩된 문자열인 'xde9usau' 부분을 base64_decode() 함수를 이용해서 디코드하면 '테스트' 라는 문자열이 됩니다.

반응형
/*------ Base64 Encoding Table ------*/
static const char MimeBase64[] = {
    'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
    'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
    'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
    'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
    'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',
    'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
    'w', 'x', 'y', 'z', '0', '1', '2', '3',
    '4', '5', '6', '7', '8', '9', '+', '/'
};

/*------ Base64 Decoding Table ------*/
static int DecodeMimeBase64[256] = {
    -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,  /* 00-0F */
    -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,  /* 10-1F */
    -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,62,-1,-1,-1,63,  /* 20-2F */
    52,53,54,55,56,57,58,59,60,61,-1,-1,-1,-1,-1,-1,  /* 30-3F */
    -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13,14,  /* 40-4F */
    15,16,17,18,19,20,21,22,23,24,25,-1,-1,-1,-1,-1,  /* 50-5F */
    -1,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,  /* 60-6F */
    41,42,43,44,45,46,47,48,49,50,51,-1,-1,-1,-1,-1,  /* 70-7F */
    -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,  /* 80-8F */
    -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,  /* 90-9F */
    -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,  /* A0-AF */
    -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,  /* B0-BF */
    -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,  /* C0-CF */
    -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,  /* D0-DF */
    -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,  /* E0-EF */
    -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1   /* F0-FF */
    };

int base64_decode(char *text, unsigned char *dst, int numBytes )
{
  const char* cp;
  int space_idx = 0, phase;
  int d, prev_d = 0;
  unsigned char c;

    space_idx = 0;
    phase = 0;

    for ( cp = text; *cp != '\0'; ++cp ) {
        d = DecodeMimeBase64[(int) *cp];
        if ( d != -1 ) {
            switch ( phase ) {
                case 0:
                    ++phase;
                    break;
                case 1:
                    c = ( ( prev_d << 2 ) | ( ( d & 0x30 ) >> 4 ) );
                    if ( space_idx < numBytes )
                        dst[space_idx++] = c;
                    ++phase;
                    break;
                case 2:
                    c = ( ( ( prev_d & 0xf ) << 4 ) | ( ( d & 0x3c ) >> 2 ) );
                    if ( space_idx < numBytes )
                        dst[space_idx++] = c;
                    ++phase;
                    break;
                case 3:
                    c = ( ( ( prev_d & 0x03 ) << 6 ) | d );
                    if ( space_idx < numBytes )
                        dst[space_idx++] = c;
                    phase = 0;
                    break;
            }
            prev_d = d;
        }
    }

    return space_idx;

}

int base64_encode(char *text, int numBytes, char **encodedText)
{
  unsigned char input[3]  = {0,0,0};
  unsigned char output[4] = {0,0,0,0};
  int   index, i, j, size;
  char *p, *plen;

  plen           = text + numBytes - 1;
  size           = (4 * (numBytes / 3)) + (numBytes % 3? 4 : 0) + 1;
  (*encodedText) = malloc(size);
  j              = 0;

    for  (i = 0, p = text;p <= plen; i++, p++) {
        index = i % 3;
        input[index] = *p;

        if (index == 2 || p == plen) {
            output[0] = ((input[0] & 0xFC) >> 2);
            output[1] = ((input[0] & 0x3) << 4) | ((input[1] & 0xF0) >> 4);
            output[2] = ((input[1] & 0xF) << 2) | ((input[2] & 0xC0) >> 6);
            output[3] = (input[2] & 0x3F);

            (*encodedText)[j++] = MimeBase64[output[0]];
            (*encodedText)[j++] = MimeBase64[output[1]];
            (*encodedText)[j++] = index == 0? '=' : MimeBase64[output[2]];
            (*encodedText)[j++] = index <  2? '=' : MimeBase64[output[3]];

            input[0] = input[1] = input[2] = 0;
        }
    }

    (*encodedText)[j] = '\0';

    return 0;
}
반응형

realloc

동적으로 할당된 메모리를 재할당합니다.

Declaration

void *realloc( void *memblock, size_t size )

Return value

성공 - 새로운 메모리 영역을 가리키는 포인터 반환
실패 - NULL 포인터 반환


Parameters

memblock - NULL 포인터 또는 이전에 할당한 메모리 영역을 가리키는 포인터
size     - 새로 할당받을 메모리 크기(단위는 바이트)


Detail descriptions

realloc()를 번역할 때 "재할당" 또는 "다시 할당"이라고 하는데 많이 어색합니다. 다시 할당하는 것이 아니라 할당된 메모리 영역의 크기를 변경, 대부분은 확장하는 것입니다. 정확하게 이름을 짓는다면 extendalloc()정도가 좋겠습니다. 굳이 "재할당(realloc)"이라고 이름 지은 것은 대부분 새로운 메모리를 할당받기 때문입니다.

 

realloc()에 전달되는 메모리 영역의 크기는 이전에 할당됐던 영역보다 클 수도 있고 작을 수도 있습니다. 대부분 메모리가 부족했기 때문에 다시 할당 받는 것이지만, 작은 경우가 있을 수도 있습니다. 그러나, 작은 경우라면 메모리를 새로 할당할 이유가 없습니다. 메모리가 부족한 경우에 인접해 있는 연속된 구간을 확보할 수 있다면, 다시 말해 현 상태에서 늘릴 수 있는 영역이 있다면 새로 할당하지 않아도 됩니다. 그러나, 대부분은 그렇지가 못하므로 기존 메모리를 해제하고 다른 위치에 할당하게 됩니다. [C 언어 표준에서는 어떻게 할당해야 한다는 것에 대해서는 언급하지 않습니다. 이 모든 상황은 구현에 따라 달라질 수 있습니다.]

메모리를 새로운 위치에 할당하려면 아래 순서에 맞게 작업해야 합니다.

  1. 새로운 위치에 지정한 크기만큼 메모리를 할당합니다.
  2. 이전 영역에 있던 내용을 새로운 영역에 복사합니다.
  3. 이전 영역의 메모리를 해제합니다.

realloc()는 이와 같이 동작합니다. 새로운 영역을 할당받더라도 현재 내용을 잃어버리는 일은 절대 없습니다. realloc()를 흉내낸 코드가 "Example codes" 항목에 있습니다. 참고하기 바랍니다.

그러나, 이와 같은 방식에는 엄청난 결함이 있습니다. realloc()는 새로운 위치에 할당될 수 있기 때문에, 이전 영역을 가리키는 모든 포인터 변수를 새로운 주소로 변경해야 합니다. 그렇게 할 수 없다면 해제된 영역을 가리키는 포인터가 언젠가는 문제를 일으킬 것입니다. 동적으로 할당한 메모리가 있고 여러 개의 포인터가 공유하고 있다면, 절대 realloc()를 사용해서는 안됩니다. 악명높은 덜 떨어진 포인터, 댕글링(dangling) 포인터가 탄생합니다.

realloc()는 어떤 경우를 실패라고 생각할까요? malloc()라면 할당할 메모리가 부족한 경우에 NULL 포인터를 반환합니다. 첫 번째는 확장에 실패한 경우입니다. realloc()의 목적은 메모리를 확장해서 옮기는 것인데, 현재 위치에서뿐만 아니라 새로운 위치로도 옮겨가지 못하면 NULL 포인터를 반환합니다. 이때 이전 영역에 있던 내용은 그대로 남아 있습니다. 두 번째는 새롭게 할당할 크기를 0으로 지정한 경우입니다. 크기가 0이라는 것은 새로운 할당으로 간주하지 않고, 이전에 사용하던 영역의 해제로 봅니다. free() 함수를 호출하는 셈입니다. 사용하던 영역은 해제되어 다음 번 동적 할당에서 사용할 수 있게 되고, NULL 포인터를 반환합니다. NULL 포인터의 반환이 너무 당연한 것이 할당받은 메모리가 없으므로 가리킬 곳도 없습니다. 그러나, 메모리를 해제하기 위해 realloc()를 사용해서는 안됩니다. free()로 해제할 때 가장 분명한 코드가 나오기 때문에 가독성에서 너무 좋지 않은 행동입니다.

memblock 매개 변수에 NULL 포인터를 전달하는 것은 malloc()를 호출하는 것과 같습니다. 사용중인 메모리가 없기 때문에 해제할 메모리도 없고, 결국 할당만 일어나게 되므로 malloc() 호출과 같습니다.

realloc()가 성공할 때의 반환값이 두 가지가 있습니다. 첫 번째는 현재 위치에서 확장에 성공한 경우로, memblock 매개 변수에 전달된 주소와 같습니다. 두 번째는 연속된 메모리가 없어서 새로운 위치에 할당된 경우로 memblock 매개 변수와 다른 주소입니다. 주의해야 할 것이 있다면 현재 위치에서 확장될 수 있다고 생각하는 것입니다. 함수 이름이 주는 모호함 때문에 자칫 엄청난 실수를 할 수 있습니다.

간혹 realloc()가 필요한지 의아할 때가 있습니다. 메모리가 부족한 상황을 만나기 전에 리스트(list) 자료구조를 사용하거나 엄청난 크기로 할당한 다음 나누어 쓰는 방법이 있습니다. 동적으로 할당한 메모리를 여러 개의 포인터 변수가 가리키는 상황이 생길 수 있다면, 지금 얘기한 방법들을 진지하게 고민해야 할 것입니다.


Remarks

메모리 동적 할당 함수인 calloc(), malloc(), realloc()를 비교한 표가 malloc()에 있습니다. 참고하기 바랍니다.


Header files

<stdlib.h>   <malloc.h>


Example codes

  1. realloc()로 메모리를 "확장"하거나 "축소"할 때 발생하는 포인터 변화를 보여줍니다.
  2. 먼저 NULL 포인터를 전달해서 메모리를 할당한 다음, 사용중인 크기보다 두 배 크게 "확장"하고, 최초 크기보다 두 배 작게 "축소"해서 결과를 비교합니다.
  3. 출력 결과를 보면 "확장"할 때는 반환된 주소가 사용중인 주소와 다르지만, "축소"할 때는 사용중인 주소와 같습니다. "축소"할 때는 사용중인 메모리를 재활용하고 있음을 알 수 있습니다.
  4. 그러나, C 언어 표준에서는 재활용할 것을 요구하지 않습니다. 컴파일러를 구현하는 회사측에서 어떻게 구현할지 결정할 따름입니다.
  5. free()는 한번만 사용했습니다. realloc()를 세 번 호출했지만, 두 번째 호출부터는 내부적으로 free()를 호출하기 때문에 realloc() 호출 횟수만큼 free()를 호출해서는 안됩니다.

#include <stdio.h>
#include <stdlib.h>

void main()
{
    int* array = NULL;
    const int size = 5;

    array = realloc( NULL, size*sizeof(int) );

    printf( "[확장]\n" );
    printf( "old - %d\n", array );
    array = realloc( array, size*sizeof(int)*2 );
    printf( "cur - %d\n", array );

    printf( "\n[축소]\n" );
    printf( "old - %d\n", array );
    array = realloc( array, size*sizeof(int)/2 );
    printf( "cur - %d\n", array );

    free( array );
}

[출력 결과]
[확장]
old - 4397872
cur - 4397776

[축소]
old - 4397776
cur - 4397776


  1. realloc()의 기본 사용법을 보여줍니다.
  2. NULL 포인터를 전달해서 malloc()처럼 사용하는 방법과 사용중인 메모리를 두 배로 확장했을 때의 결과를 보여줍니다.
  3. malloc()처럼 바이트 단위로 동작하기 때문에 요소 개수와 요소 크기를 곱해서 전달합니다.
  4. 출력 결과를 보면, 사용중인 메모리를 20바이트에서 40바이트로 확장하면 사용중인 영역이 확장되는 것이 아니라 새로운 영역에 내용을 복사합니다. 사용중인 영역의 내용은 손상없이 복사되지만, 확장된 영역은 초기화되지 않고 쓰레기로 채워집니다.
  5. 동적 배열을 하나만 사용하고 크기도 20바이트를 확장했을 뿐인데, 새로운 주소를 반환한다는 것은 사용중인 영역의 연속된 구간을 할당하는 것이 얼마나 어려운지 보여줍니다. 출력 결과에서 []로 감싼 숫자가 할당받은 주소인데 서로 다릅니다. 절대 사용중인 주소를 계속해서 사용할 수 있을 거라고 기대해선 안됩니다.
  6. MultiArray()는 배열을 초기화하는 함수로, 0부터 시작해서 multi의 배수로 채웁니다.

#include <stdio.h>
#include <stdlib.h>

void MultiArray( int* array, int size, int multi );
void ShowArray( int* array, int size );

void main()
{
    int* array = NULL;
    int  size = 5;

    array = realloc( NULL, size*sizeof(int) );

    MultiArray( array, size, 3 );
    ShowArray( array, size );

    size *= 2;
    array = realloc( array, size*sizeof(int) );

    ShowArray( array, size );

    free( array );
}

void MultiArray( int* array, int size, int multi )
{
    int i;
    for( i = 0; i < size; i++ )
        array[i] = i*multi;
}

void ShowArray( int* array, int size )
{
    int i;
    printf( "[%d]", array );

    for( i = 0; i < size; i++ )
        printf( " %d", array[i] );

    printf( "\n" );
}

[출력 결과]
[4397776] 0 3 6 9 12
[4397680] 0 3 6 9 12 -842150451 -842150451 -842150451 -842150451 -842150451


  1. 사용중인 메모리의 크기를 늘려주는 realloc()를 만들어 봅니다. ExtendArray() 내부에서 calloc()로 새로 만든 메모리를 초기화합니다. 프로그램은 0보다 작거나 같은 정수를 입력하면 종료합니다.
  2. realloc()와 똑같이 만드는 것은 불가능합니다. 간단하게 realloc()의 특징일 수 있는 것만을 일부 구현합니다.
  3. realloc()를 호출하는 상황이 네 가지 있습니다. 이들 네 가지는 "최초 할당"과 "축소", "확장", "변화 없음"으로 부를 수 있습니다.
  4. "최초 할당"은 사용중인 메모리 주소 대신 NULL 포인터가 전달되는 경우로, 사용중인 메모리가 없다는 뜻이므로 별도의 코드없이 calloc()의 호출 결과를 반환합니다.
  5. "축소"와 "변화 없음"은 메모리 크기를 줄이는 것인데, 굳이 사용중인 메모리를 해제할 필요없이 기존에 사용하던 메모리를 재활용합니다. 크기를 줄이는 과정에서 남는 메모리를 해제할 수 있는 방법이 없으므로, 사용중이던 메모리를 해제할 때까지 그대로 둡니다. 일종의 낭비라고 생각할 수도 있겠습니다. ExtendArray() 내부적으로 메모리에 변화가 없으므로 매개 변수로 전달된 oldblock을 그대로 반환합니다.
  6. "확장"일 때 realloc()를 가장 많이 사용합니다. 연속된 메모리가 있다고 보장할 수 없기 때문에, 요청한 메모리를 새롭게 할당합니다. 사용중인 메모리의 내용을 새로운 메모리로 옮기고, 사용중인 메모리를 해제합니다.
  7. calloc()를 사용했으므로 "확장"된 메모리는 모두 0으로 초기화됩니다. 마지막 출력 결과를 보면, 요소가 3개에서 6개로 확장되었고, 확장된 요소에는 0이 들어갑니다. calloc()를 사용해서 할당했다는 증거입니다. 실제의 realloc()는 "확장" 메모리를 초기화하지 않는데, malloc()을 사용하는 것 같습니다.
  8. 출력 결과를 분석하기가 조금 어렵습니다. 첫 번째 결과는 "최초 할당"이고, 두 번째 결과는 "확장"이고 FillArray()를 호출했기 때문에 새로운 내용으로 채워졌습니다. 세 번째와 네 번째 결과는 "축소"이고, "축소"는 사용중이던 메모리를 해제하지 않기 때문에 이전 결과와 똑같이 나옵니다.
  9. FillArray()는 메모리를 확장할 때만 호출합니다. "축소"에서 사용중인 메모리를 유지한다는 것을 보여주기 위해 FillArray()는 "확장"일 때만 호출합니다. 그러나, "확장"에 대한 결과도 보여줘야 하므로 반복문을 벗어나면 무조건 2배로 확장합니다. 확장 메모리가 0으로 초기화된다는 것을 보여주기 위해, 이번에는 FillArray()를 호출하지 않습니다.
  10. 의도적으로 calloc()를 사용했지만, 메모리를 초기화하지 않는 malloc()를 사용해도 괜찮습니다.
  11. FillArray()와 ShowArray()는 배열을 난수로 채우고 화면에 출력하는 함수들입니다. 대단할 것이 없는 함수이므로 설명은 생략합니다.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void* ExtendArray( void* p, int cur, int want, int datasize );
void FillArray( int* array, int size );
void ShowArray( int* array, int size );

void main()
{
    int* array = NULL;
    int  want, use = 0;

    while( 1 )
    {
        printf( "개수 : " );
        scanf( "%d", &want );

        if( want <= 0 )
            break;

        array = ExtendArray( array, use, want, sizeof(int) );

        if( want > use )
            FillArray( array, want );

        use = want;
        ShowArray( array, use );
    }

    printf( "[calloc() 확인]\n" );

    want = use * 2;
    array = ExtendArray( array, use, want, sizeof(int) );
    ShowArray( array, want );
}

void* ExtendArray( void* oldblock, int count, int want, int datasize )
{
    void* block;

    if( oldblock == NULL )                 // 최초 할당
        return calloc( want, datasize );

    if( count >= want )                    // 축소, 변화 없음
        block = oldblock;
    else                                   // 확장
    {
        block = calloc( want, datasize );
        memcpy( block, oldblock, count*datasize );

        free( oldblock );
    }

    return block;
}

void FillArray( int* array, int size )
{
    int i;
    for( i = 0; i < size; i++ )
        array[i] = rand()%100;
}

void ShowArray( int* array, int size )
{
    int i;
    for( i = 0; i < size; i++ )
        printf( "%d ", array[i] );

    printf( "\n" );
}


[출력 결과]
개수 : 4
41 67 34 0
개수 : 9
69 24 78 58 62 64 5 45 81
개수 : 7
69 24 78 58 62 64 5
개수 : 3
69 24 78
개수 : 0
[calloc() 확인]
69 24 78 0 0 0

  1. Favicon of http://www.psycholian.com BlogIcon psycholian 2010.05.14 02:12

    안녕하세요. c 언어의 동적 배열 할당에 대해서 공부를 하다가 방문하게 되었습니다.^^
    동적 배열 할당에 대한 상세한 내용 덕분에 궁금했던 부분을 많이 채워갑니다.^^
    그런데 질문이 하나 있습니다.
    위 포스팅 내용 중(realloc함수를 사용한 예제코드)상단에 보시면, realloc() 함수를 통해 3번에 걸쳐서 메모리를 동적으로 할당받으셨잖아요? 처음에 NULL 포인터를 매개변수로 malloc 함수와 똑같은 방식으로 할당을 받는 부분은 이해가 됩니다.
    그런데,
    두 번째 부터 realloc(array ~) 와 같은 식으로 할당을 받았을 때, 전혀 새로운 메모리 영역을 할당받게 되어 리턴받는 메모리 주소값이 처음과 다를 때는 결국 다른 메모리 공간을 가리키게 되므로 realloc 함수를 사용할 때마다 해제를 해주어야 하는 것 아닌가요?

    주위에 물어볼 사람도 없고 검색을 해도 속시원하게 나온 곳이 없어서 답답한 마음에 질문 올려봅니다.^^;

    • Favicon of https://www.pmguda.com BlogIcon Ishaya seon 2010.05.24 09:18 신고

      자료를 다시 한번 정독해 보심이 좋을듯 합니다.
      본문中
      "free()는 한번만 사용했습니다. realloc()를 세 번 호출했지만, 두 번째 호출부터는 내부적으로 free()를 호출하기 때문에 realloc() 호출 횟수만큼 free()를 호출해서는 안됩니다" 라는 부분이 명시되어 있습니다.

  2. Favicon of http://psycholian.com BlogIcon psycholian 2010.06.23 01:19

    와~답변 달아주셔서 감사합니다.^^
    너무 오래된 글에 댓글을 등록한 것 같아 답변을 달아주시리라고는 기대하지 않았는데
    오늘 와보니!! 감사합니다.^^

    음...네. 본문에는 내부적으로 free 함수를 호출한다는 내용이 있었군요!
    저의 급한 성질머리 때문에 중요한 부분을 놓치고 넘어갔네요..ㅜㅜ

    • Favicon of https://www.pmguda.com BlogIcon Ishaya seon 2010.06.23 09:08 신고

      ㅎㅎ 댓글 감사합니다.
      자료가 도움이 되었으면 좋겠네요 ㅎㅎ

+ Recent posts