下一页 上一页 目录

9. 虚拟邮件/Pop

9.1 问题

对虚拟邮件支持的需求日益增长。Sendmail 声称它支持虚拟邮件。它所支持的是监听来自不同域的传入邮件。然后你可以指定将邮件转发到某个地方。然而,如果你将它转发到本地机器,并且有发送给 bob@domain1.com 和 bob@domain2.com 的传入邮件,它们将会进入同一个邮件文件夹。这是一个问题,因为两个 bob 是不同的人,拥有不同的邮件。

9.2 解决方案

你可以通过使用编号方案来确保每个用户名是唯一的:bob1,bob2 等,或者在每个用户名前面加上几个字符 dom1bob,dom2bob 等。你也可以修改邮件和 pop 来在后台进行这些转换,但这可能会变得混乱。传出邮件也有横幅 maindomain.com,你希望每个子域的传出邮件横幅都不同。

我有两个解决方案。一个适用于 sendmail,另一个适用于 Qmail。使用 sendmail 的解决方案应该适用于 sendmail 的标准安装。然而,它也继承了 sendmail 内置的所有限制。它还需要为每个域运行一个队列模式的 sendmail。拥有 50 个或更多的 sendmail 队列进程每小时唤醒一次,可能会给机器带来一些压力。

Qmail 提供的解决方案不需要 Qmail 的多个实例,并且可以从一个队列目录中运行。它确实需要一个额外的程序,因为 Qmail 不依赖于 virtuald。我相信类似的程序可以用 sendmail 完成。然而,Qmail 更容易实现这种解决方案。

我并不偏袒任何一个程序。Sendmail 的安装稍微简单一些,但 Qmail 可能是两个邮件服务器软件包中更强大的一个。

9.3 Sendmail 解决方案

简介

每个虚拟文件系统都为域提供了自己的 /etc/passwd。这意味着 bob@domain1.com 和 bob@domain2.com 是不同 /etc/passwd 中的不同用户,因此邮件不会有问题。它们也有自己的假脱机目录,因此邮件文件夹将是不同虚拟文件系统上的不同文件。

创建 Sendmail 配置文件

像往常一样通过 m4 创建 /etc/sendmail.cf。我使用了

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 虚拟域之间通信:技巧 (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,它将本地投递。因此,我只是将 virtual1.maindomain.com (10.10.10.157) 用作虚拟 IP。我从不向这个主机发送邮件,虚拟域也不会。这也是我将用于 ssh 进入该机器以检查系统是否正常的 IP。

Sendmail 虚拟域之间通信:新的 Sendmail 功能 (POST8.8.6)

从 Sendmail V8.8.6 开始,有一个新的选项可以禁用加载额外的网络接口。这意味着你无需以任何方式更改代码。它被称为 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 标志运行。另请注意,你需要为每个域运行一个 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 应该可以正常安装,无需额外工作。它只需要为其添加带有 virtuald 部分的 inetd 条目。sendmail 和 pop 的 inetd.conf 条目

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 文件将不起作用。但是,每个域仍然会获得一个域管理员用户,该用户将控制整个域的别名。两个外部程序将用于该域管理员的 .qmail-default 文件。邮件将通过这两个程序传递,以便为每个域投递邮件。

需要两个程序,因为其中一个程序以 setuid root 运行。它是一个小程序,它切换到非 root 用户,然后运行第二个程序。请查阅你最近的安全相关站点,以了解为什么这是必要的讨论。

此解决方案绕过了使用 virtuald 的需要。Qmail 足够灵活,不需要通用的 virtuald 设置。Qmail 的设计利用程序的链式连接来投递邮件。这种设计使得将虚拟部分插入到 Qmail 投递过程中非常容易,而无需更改 Qmail 的标准安装。

请注意,由于你正在使用一个 Qmail,任何未限定的域名都将使用主服务器的域名进行扩展。这是因为你没有为每个域单独设置 Qmail 服务器。因此,请确保你的客户端(Eudora, elm, mutt 等)知道扩展所有未限定的域名。

设置虚拟域

必须配置 Qmail 以接受你将要服务的每个虚拟域的邮件。输入以下命令。

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

设置域管理员用户

将用户 domain1 添加到你的主 /etc/passwd 文件中。我会将 shell 设置为 /bin/false,以便域管理员无法登录。该用户将能够添加 .qmail 文件,并且所有发往 domain1 的邮件都将通过该帐户路由。请注意,用户名只能是八个字符长,而域名可以更长。剩余的字符将被截断。这意味着用户 domain12 和 domain123 将是同一个用户,Qmail 可能会感到困惑。因此,在你的主域用户命名约定中要小心。

使用以下命令创建域管理员的 .qmail 文件。此时添加任何其他系统别名。例如,webmaster 或 hostmaster。

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 需要一个特殊的 pop,它可以支持 Maildir 格式。pop 程序必须虚拟化。Qmail 的作者建议将 tcpserver(inetd 的替代品)与 Qmail 一起使用,因此我的示例使用 tcpserver 而不是 inetd。

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 init 脚本。Qmail 附带了非常好的文档,描述了如何进行设置。

源代码

你需要另外两个程序才能使虚拟邮件与 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));
}

源代码

你需要另外两个程序才能使虚拟邮件与 Qmail 一起工作。它们是 virtmailfilter 和 virtmaildelivery。这是 virtmaildelivery 的 C 源代码。它应该安装在 /usr/local/bin 中,权限为 0755,用户为 root,组为 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 致谢

感谢 Vicente Gonzalez (vince@nycrc.net) 帮助使 Qmail 解决方案成为可能。你当然可以向 Vince 发送邮件表示感谢,但是所有问题和评论,包括关于 Qmail 的问题,关于本 HOWTO 的问题,都应继续直接发送给我。


下一页 上一页 目录