다음 이전 차례

9. 가상 메일/POP 서버

9.1 문제

가상 메일의 지원에 대한 요청은 날로 증가하고 있다. 센드메일은 가상 메일 시스템을 지원한다고 말한다. 하지만 그것이 지원하는 것은 여러 도메인에서 메일들을 검사하는 기능이다. 그 후 당신은 특정 메일을 다른 곳으로 포워딩 할 수 있다. 하지만, 로컬 머신으로 포워딩된 메일이나 bob@domain1.com과 bob@domain2.com에 온 메일은 같은 메일 폴더에 들어가 있게 된다. 이들이 서로 다른 메일이고 두명의 bob이 서로 다른 사람일 때에는 문제가 된다.

9.2 해결책

당신은 각각의 사용자 이름에 숫자를 붙여서, 혹은 정해진 문자를 앞에 붙여서 중복되는 사용자가 아님을 구분할 수 있는 방법이 있다.(예: bob1, bob2 혹은 dom1bob, dom2bob) 당신은 또한 mail이나 pop를 고쳐서 이런 전환이 보이지 않게 이루어질 수 있게 만들수도 있다. 외부로 나가는 메일 역시 이런 식으로 각각의 서브도메인에 대해서 그 이름을 사용하게 만들 수 있다.

내가 가진 해결책은 두가지이다. 하나는 sendmail을 이용하는 것이고, 다른 하나는 Qmail을 이용하는 것이다. Sendmail을 이용한 해결책은 이 기능을 추가하여 sendmail 을 설치하는 것이다. 하지만, 이 방법은 모든 제약이 sendmail에 똑같이 적용된다. 이 방법은 또한 각각의 도메인에 대해서 하나씩의 sendmail이 queue mode로 실행되어야 한다는 단점을 가지고 있다. 50개 혹은 그 이상의 senmail queue 프로세스는 매시간 시스템을 바쁘게 만들 것이다.

Qmail을 이용하는 해결책은 여러개의 Qmail을 필요로하지도 않고, 하나의 queue 디렉토리 이외에서도 실행 가능하다. 이 방법은 Qmail이 virtuald와 맞지 않기 때문에 추가적인 프로그램을 필요로 한다. 난 sendmail을 이용한 방법 역시 비슷한 과정을 필요로 한다고 믿는다. 하지만, Qmail은 이 경우 보다 해결책을 위한 준비가 잘 되어있는 것 같다.

내가 한 프로그램이 다른 프로그램보다 낫다는 것을 보증하는 것은 아니다. Sendmail 설치는 보다 직접적인 해결책이지만, Qmail을 이용하는 방법이 아마 더 강력한 해결책이 될 수 있다.

9.3 Sendmail을 이용한 해결책

소개

각각의 가상 파일 시스템은 자신의 /etc/passwd 안에 도메인을 설정한다. 이것은 bob@domain1.com과 bob@domain2.com이 서로 다른 사용자로 /etc/passwd 안에 등록되어 있다는 것을 의미하며, 메일 프로그램에서 두 사용자를 구분하는 데에는 아무 문제가 없다. 또한 자신만의 스풀 디렉토리 역시 가지고 있으므로 다른 가상 파일 시스템에 대해서 서로 다른 파일로서 메일 폴더가 존재하게 된다.

Sendmail 설정 파일 만들기

일반적인 /etc/sendmail.cf 를 m4를 통해 만든다. 내가 사용하는 것은 다음과 같다:

divert(0)
VERSIONID(`tcpproto.mc')
OSTYPE(linux)
FEATURE(redirect)
FEATURE(always_add_domain)
FEATURE(use_cw_file)
FEATURE(local_procmail)
MAILER(local)
MAILER(smtp)

Sendmail 설정파일 편집하기

/virtual/domain1.com/etc/sendmail.cf 를 가상 도메인에 반응할 수 있도록 편집한다:

vi /virtual/domain1.com/etc/sendmail.cf # Approximately Line 86 
It should say:

#Dj$w.Foo.COM

Replace it with:

Djdomain1.com

Sendmail 지역 배달

/virtual/domain1.com/etc/sendmail.cw를 지역 호스트이름으로 편집한다.

vi /virtual/domain1.com/etc/sendmail.cw
mail.domain1.com
domain1.com
domain1
localhost

가상 도메인 사이의 Sendmail : The Hack (PRE8.8.6)

하지만, sendmail은 작은 소스 코드 변환을 필요로 한다. Sendmail은 /etc/sendmail.cw 라는 파일을 가지고 있는데, 여기에는 sendmail이 로컬 내에서(외부의 다른 머신이 아닌) 배달할 모든 머신들의 이름이 기록되어 있다. Sendmail은 내부에서 머신의 모든 장치들에 대해 검사하여 이 리스트를 로컬 IP를 가지고 초기화한다. 이점 때문에 만약 같은 머신 내의 가상 도메인 사이에서 메일을 주고받고자 할 때 문제가 될 수 있다. Sendmail은 다른 가상 도메인을 로컬 어드레스로 생각하고 로컬 지역으로 메일을 스풀링하게 된다. 예를 들면, bob@domain1.com이 fred@domain2.com에게 메일을 보냈다고 하자. 그러면 domain1.com의 sendmail은 domain2.com을 로컬로 인식하고 메일을 domain1.com에 스풀링할 것이다. (당연히 domain2.com으로는 메일이 가지 않을 것이다.) 따라서 당신은 sendmail을 변형시켜야 한다. (이 변형은 v8.8.5에서 테스트해본 결과 아무 문제가 없었다.)

vi v8.8.5/src/main.c # Approximately Line 494
It should say:

load_if_names();

Replace it with:

/* load_if_names(); Commented out since hurts virtual */

만약 가상 도메인 사이에서 메일을 주고받을 필요가 있을 경우에만 이 설정을 이용하라. (아마 대부분의 경우 그러하겠지만)

이것은 문제점을 해결할 것이다. 하지만, 주된 이더넷 장치인 eth0는 없어지지 않는다. 따라서, 만약 당신이 가상 IP에서 eth0로 메일을 보내게 되면 이것은 로컬로 배달이 될 것이다. 따라서 나는 이것을 더미(dummy) IP인 virtual1.maindomain.com(10.10.10.157)로 이용한다. 난 절대 이 호스트로 메일을 보내지 않으며, 물론 그 가상 도메인으로도 메일은 가지 않는다. 이 방법은 또한 내가 ssh를 사용하는 IP를 가진 시스템이 정상적인지를 확인하는 방법이기도 하다.

가상 도메인 사이의 Sendmail : Sendmail의 새로운 기능 (POST8.8.6)

Sendmail V8.8.6부터는 추가적인 네트워크 인터페이스의 비사용 탑재(disable loading)에 대한 새로운 옵션이 생겼다. 따라서 코드를 바꿀 필요는 없게 되었는데, 이것을 DontProbeInterfaces라 한다.

/virtual/domain1.com/etc/sendmail.cf를 편집하라.

vi /virtual/domain1.com/etc/sendmail.cf # Add the line
O DontProbeInterfaces=True

Sendmail.init

Sendmail은 독립적으로 실행이 불가능하고 항상 inetd를 통해서 실행되게 된다. 이 방법은 비효율적이고 시작하는 데 시간이 걸리겠지만, 만약 당신이 운영하는 사이트가 이런 점이 문제가 될 정도로 네트워크가 빈번하다면 하나의 시스템에서 가상의 여러 도메인을 같이 사용하는 것은 좋은 방법이 아니다. -bd 플랙(flag)과 같이 사용하지 않도록 주의하라. 또한 각각의 도메인에 대해서 sendmail -q 을 실행하여 배달되지 않은 메일들에 대한 큐 작업을 가능하게 하는 것도 잊지 말라. 새로운 sendmail.init 파일은 다음과 같다:

#!/bin/sh

. /etc/rc.d/init.d/functions

case "$1" in
  start)
        echo -n "Starting sendmail: "
        daemon sendmail -q1h
        echo
        echo -n "Starting virtual sendmail: "
        for i in /virtual/*
        do
                if [ ! -d "$i" ]
                then
                        continue
                fi
                if [ "$i" = "/virtual/lost+found" ]
                then
                        continue
                fi
                chroot $i sendmail -q1h
                echo -n "."
        done
        echo " done"
        touch /var/lock/subsys/sendmail
        ;;
  stop)
        echo -n "Stopping sendmail: "
        killproc sendmail
        echo
        rm -f /var/lock/subsys/sendmail
        ;;
  *)
        echo "Usage: sendmail {start|stop}"
        exit 1
esac

exit 0

Inetd 설정

Pop는 다른 영향없이 정상적으로 설치될 것이다. 단지 inetd의 엔트리에서 이 항을 가상의 포트와 함께 고려할 필요가 있다. inetd.conf 엔트리에서 sendmail과 pop에 대한 것은 다음과 같다:

pop-3 stream tcp nowait root /usr/local/bin/virtuald \
        virtuald /virtual/conf.pop in.qpop -s 
smtp stream tcp nowait root /usr/local/bin/virtuald \
        virtuald /virtual/conf.mail sendmail -bs

9.4 Qmail을 이용한 방법

소개

이 방법은 qmail-local의 배달 시스템을 차용하기 때문에, 가상의 홈 디렉토리 안의 .qmail 파일은 작동하지 않게 된다. 하지만, 각각의 도메인은 도메인 전체의 앨리어싱 (aliasing)을 통제하는 도메인 주인 사용자(domain master user)를 갖는다. 두 개의 외부 프로그램들이 도메인 주인의 .qmail-default 파일을 사용할 수 있게 해줄 것이다. 각각의 도메인에 메일이 배달되기 위해서는 이들 두 프로그램을 통해야 할 것이다.

두 개의 프로그램이 필요한데, 그 가운데 하나는 setuid root 상태로 실행된다. 이 작은 프로그램은 일단 프로세스의 소유권을 root가 아닌 사용자로 바꾸고, 다시 두번째 프로그램을 실행시킨다. 가까운 보안 관련 사이트에서 왜 이런 방식이 필요한지를 참고할 수 있을 것이다.

이 방법은 virtuald를 사용할 필요성이 별로 없다. Qmail은 매우 유동적인 프로그램이라 일반적인 virtuald 설정을 필요로하지 않는다. Qmail은 메일의 배달을 위해 프로그램들의 연결을 이용하도록 설계되었다. 이 디자인은 가상 서비스 부분을 Qmail 배달 프로세스 중간에 쉽게 삽입할 수 있게 한다.

당신이 Qmail을 사용한다면 메인 서버의 도메인에서 무제한의 도메인 이름을 만들어 낼 수 있다. 이것은 각각의 도메인에 대해 분리된 Qmail을 갖는 것이 아니기 때문에 가능하다. 메일 클라이언트 프로그램(유도라나 elm, mutt 등)에서 당신이 임의로 만들어낸 도메인 이름을 인식하는 것을 확인해 보라.

가상 도메인 설정

Qmail은 당신이 제공하는 각각의 가상 도메인을 받아들일 수 있도록 설정되어야 한다. 아래의 명령어들을 수행하라.

echo "domain1.com:domain1" >> /var/qmail/control/virtualdomains

도메인의 주인(Domain Master User) 설정

메인 /etc/passwd 파일에 domain1의 사용자들을 추가한다. 나는 /bin/false 셸을 만들어 도메인 주인(the domain master)이 로그인하지 못하게 만들었다. 도메인 주인은 domain1의 .qmail 파일들을 추가할 수 있고, 도메인의 모든 메일들은 이 계정을 통하여 발송된다. 사용자 이름은 여덟 자리까지 가능하며 도메인 이름은 더 길어지 수 있다는 것을 주의하기 바란다. 나머지 문자들은 무시된다. 이것은 domain12라는 사용자와 domain123이라는 사용자가 같은 사용자로 인식되기 때문에 Qmail이 혼동할 수 있다는 것을 의미한다. 따라서 도메인 주인 이름 결정에 주의를 기울이기 바란다.

다음과 같은 절차를 통하여 도메인 주인의 .qmail 파일을 만들자. 다른 시스템 앨리어스 - 예를 들면 웹마스터나 호스트마스터- 가 이 지점에 추가된다.

echo "user@domain1.com" > /home/d/domain1/.qmail-mailer-daemon
echo "user@domain1.com" > /home/d/domain1/.qmail-postmaster
echo "user@domain1.com" > /home/d/domain1/.qmail-root

도메인 주인의 .qmail-default 파일을 만들자. 이것은 모든 메일을 가상의 도메인으로 걸러주게 될 것이다.

echo "| /usr/local/bin/virtmailfilter" > /home/d/domain1/.qmail-default

Tcpserver

Qmail은 Maildir 형식을 지원하는 특별한 pop을 필요로 한다. 이 pop 프로그램 또한 가상 시스템에 맞게 되어야 한다. Qmail의 저자는 tcpserver(inetd 대용)를 Qmail과 함께 사용할 것을 권하는데, 나의 예제에서도 inetd 대신에 tcpserver를 사용하였다.

Tcpserver는 설정 파일을 필요로 하지 않는다. 모든 정보는 명령행에서 주어지게 된다. 여기 메일 데몬과 popper를 사용하기 위한 tcpserver.init이 있다.

#!/bin/sh

. /etc/rc.d/init.d/functions

QMAILDUSER=`grep qmaild /etc/passwd | cut -d: -f3`
QMAILDGROUP=`grep qmaild /etc/passwd | cut -d: -f4`

# See how we were called.
case "$1" in
  start)
        echo -n "Starting tcpserver: "
        tcpserver -u 0 -g 0 0 pop-3 /usr/local/bin/virtuald \
                /virtual/conf.pop qmail-popup virt.domain1.com \
                /bin/checkpassword /bin/qmail-pop3d Maildir &
        echo -n "pop "  
        tcpserver -u $QMAILDUSER -g $QMAILDGROUP 0 smtp \
                /var/qmail/bin/qmail-smtpd &
        echo -n "qmail "
        echo
        touch /var/lock/subsys/tcpserver
        ;;
  stop)
        echo -n "Stopping tcpserver: "
        killall -TERM tcpserver 
        echo -n "killing "
        echo 
        rm -f /var/lock/subsys/tcpserver
        ;;
  *)
        echo "Usage: tcpserver {start|stop}"
        exit 1
esac

exit 0

Qmail.init

당신은 제공되는 표준 Qmail 초기 스크립트를 바로 사용할 수 있다. Qmail은 이것을 어떻게 설정해야 하는지에 대해 상당히 좋은 문서와 함께 배포된다.

소스(Source)

Qmail로 가상 메일 서비스를 구축하기 위해서는 두개의 서로 다른 프로그램이 필요하다. 하나는 virtmailfilter이고, 다른 하나는 virtmaildelivery이다. 여기 virtmailfilter에 대한 C 소스 코드가 있다. 이 프로그램은 /usr/local/bin에 4750의 소유권을 가지고, root 소유, nofiles 그룹으로 설치되어야 한다.

#include <sys/wait.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <ctype.h>
#include <pwd.h>

#define VIRTPRE                 "/virtual"

#define VIRTPWFILE              "etc/passwd"
#define VIRTDELIVERY            "/usr/local/bin/virtmaildelivery"
#define VIRTDELIVERY0           "virtmaildelivery"

#define PERM                    100
#define TEMP                    111
#define BUFSIZE                 8192

int main(int argc,char **argv)
{
        char *username,*usernameptr,*domain,*domainptr,*homedir;
        char virtpath[BUFSIZE];
        struct passwd *p;
        FILE *fppw;
        int status;
        gid_t gid;
        pid_t pid;

        if (!(username=getenv("EXT")))
        {
                fprintf(stdout,"environment variable EXT not set\n");
                exit(TEMP);
        }

        for(usernameptr=username;*usernameptr;usernameptr++)
        {
                *usernameptr=tolower(*usernameptr);
        }

        if (!(domain=getenv("HOST")))
        {
                fprintf(stdout,"environment variable HOST not set\n");
                exit(TEMP);
        }

        for(domainptr=domain;*domainptr;domainptr++)
        {
                if (*domainptr=='.' && *(domainptr+1)=='.')
                {
                        fprintf(stdout,"environment variable HOST has ..\n");
                        exit(TEMP);
                }
                if (*domainptr=='/')
                {
                        fprintf(stdout,"environment variable HOST has /\n");
                        exit(TEMP);
                }

                *domainptr=tolower(*domainptr);
        }

        for(domainptr=domain;;)
        {
                snprintf(virtpath,BUFSIZE,"%s/%s",VIRTPRE,domainptr);
                if (chdir(virtpath)>=0)
                        break;

                if (!(domainptr=strchr(domainptr,'.')))
                {
                        fprintf(stdout,"domain failed: %s\n",domain);
                        exit(TEMP);
                }

                domainptr++;
        }

        if (!(fppw=fopen(VIRTPWFILE,"r+")))
        {
                fprintf(stdout,"fopen failed: %s\n",VIRTPWFILE);
                exit(TEMP);
        }

        while((p=fgetpwent(fppw))!=NULL)
        {
                if (!strcmp(p->pw_name,username))
                        break;
        }

        if (!p)
        {
                fprintf(stdout,"user %s: not exist\n",username);
                exit(PERM);
        }

        if (fclose(fppw)==EOF)
        {
                fprintf(stdout,"fclose failed\n");
                exit(TEMP);
        }

        gid=p->pw_gid;
        homedir=p->pw_dir;

        if (setgid(gid)<0 || setuid(p->pw_uid)<0)
        {
                fprintf(stdout,"setuid/setgid failed\n");
                exit(TEMP);
        }

        switch(pid=fork())
        {
                case -1:
                        fprintf(stdout,"fork failed\n");
                        exit(TEMP);
                case 0:
                        if (execl(VIRTDELIVERY,VIRTDELIVERY0,username,homedir,NULL)<0)
                        {
                                fprintf(stdout,"execl failed\n");
                                exit(TEMP);
                        }
                default:
                        if (wait(&status)<0)
                        {
                                fprintf(stdout,"wait failed\n");
                                exit(TEMP);
                        }
                        if (!WIFEXITED(status))
                        {
                                fprintf(stdout,"child did not exit normally\n");
                                exit(TEMP);
                        }
                        break;
        }

        exit(WEXITSTATUS(status));
}

소스(Source)

여기에는 virtmaildelivery에 대한 C 소스 코드가 있다. 이것은 /usr/local/bin에 0755의 소유권으로, 소유자와 그룹 모두 root로 설치되어야 한다.

#include <sys/stat.h>
#include <sys/file.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <stdio.h>
#include <errno.h>
#include <time.h>

#define TEMP                    111
#define BUFSIZE                 8192
#define ATTEMPTS                10

int main(int argc,char **argv)
{
        char *user,*homedir,*dtline,*rpline,buffer[BUFSIZE],*p,mail[BUFSIZE];
        char maildir[BUFSIZE],newmaildir[BUFSIZE],host[BUFSIZE];
        int fd,n,nl,i,retval;
        struct stat statp;
        time_t thetime;
        pid_t pid;
        FILE *fp;

        retval=0;

        if (!argv[1])
        {
                fprintf(stdout,"invalid arguments: need username\n");
                exit(TEMP);
        }

        user=argv[1];

        if (!argv[2])
        {
                fprintf(stdout,"invalid arguments: need home directory\n");
                exit(TEMP);
        }

        homedir=argv[2];

        if (!(dtline=getenv("DTLINE")))
        {
                fprintf(stdout,"environment variable DTLINE not set\n");
                exit(TEMP);
        }

        if (!(rpline=getenv("RPLINE")))
        {
                fprintf(stdout,"environment variable RPLINE not set\n");
                exit(TEMP);
        }

        while (*homedir=='/')
                homedir++;
        snprintf(maildir,BUFSIZE,"%s/Maildir",homedir);
        if (chdir(maildir)<0)
        {
                fprintf(stdout,"chdir failed: %s\n",maildir);
                exit(TEMP);
        }

        time(&thetime);
        pid=getpid();
        if (gethostname(host,BUFSIZE)<0)
        {
                fprintf(stdout,"gethostname failed\n");
                exit(TEMP);
        }

        for(i=0;i<ATTEMPTS;i++)
        {
                snprintf(mail,BUFSIZE,"tmp/%u.%d.%s",thetime,pid,host);
                errno=0;
                stat(mail,&statp);
                if (errno==ENOENT)
                        break;

                sleep(2);
                time(&thetime);
        }
        if (i>=ATTEMPTS)
        {
                fprintf(stdout,"could not create %s\n",mail);
                exit(TEMP);
        }

        if (!(fp=fopen(mail,"w+")))
        {
                fprintf(stdout,"fopen failed: %s\n",mail);
                retval=TEMP; goto unlinkit;
        }

        fd=fileno(fp);

        if (fprintf(fp,"%s",rpline)<0)
        {
                fprintf(stdout,"fprintf failed\n");
                retval=TEMP; goto unlinkit;
        }

        if (fprintf(fp,"%s",dtline)<0)
        {
                fprintf(stdout,"fprintf failed\n");
                retval=TEMP; goto unlinkit;
        }

        while(fgets(buffer,BUFSIZE,stdin))
        {
                for(p=buffer;*p=='>';p++)
                        ;

                if (!strncmp(p,"From ",5))
                {
                        if (fputc('>',fp)<0)
                        {
                                fprintf(stdout,"fputc failed\n");
                                retval=TEMP; goto unlinkit;
                        }
                }

                if (fprintf(fp,"%s",buffer)<0)
                {
                        fprintf(stdout,"fprintf failed\n");
                        retval=TEMP; goto unlinkit;
                }
        }

        p=buffer+strlen(buffer);
        nl=2;
        if (*p=='\n')
                nl=1;

        for(n=0;n<nl;n++)
        {
                if (fputc('\n',fp)<0)
                {
                        fprintf(stdout,"fputc failed\n");
                        retval=TEMP; goto unlinkit;
                }
        }

        if (fsync(fd)<0)
        {
                fprintf(stdout,"fsync failed\n");
                retval=TEMP; goto unlinkit;
        }

        if (fclose(fp)==EOF)
        {
                fprintf(stdout,"fclose failed\n");
                retval=TEMP; goto unlinkit;
        }

        snprintf(newmaildir,BUFSIZE,"new/%u.%d.%s",thetime,pid,host);
        if (link(mail,newmaildir)<0)
        {
                fprintf(stdout,"link failed: %s %s\n",mail,newmaildir);
                retval=TEMP; goto unlinkit;
        }

unlinkit:
        if (unlink(mail)<0)
        {
                fprintf(stdout,"unlink failed: %s\n",mail);
                retval=TEMP;
        }

        exit(retval);
}

9.5 감사 (Acknowledgement)

Qmail에 의한 해결책을 가능하게 도움을 준 Vicente Gonzalez (vince@nycrc.net) 에게 감사한다. 아마 Vince에게 감사의 메일 정도는 보낼 수 있겠지만, Qmail에 대한 것을 포함하여 이 HOWTO에 포함된 내용의 질문과 의견은 모두 나에게 보내도록 하라.


다음 이전 차례