2006-01-10

基于postfix实现邮件用户的分布式处理

基于一些特殊的考虑,现在邮件系统经常面临把用户分散在几个不同服务器上的需求。这里谈一下我在使用postfix完成该功能时的一点认识和实践,需求所限,没有涉及到用户通过SMTP或者POP3登录收发邮件的部分。请各位朋友谅解并指正。

一般分布情况下,MX和用户所在的服务器是分离的;本文所描述的即是这种设计。下面把用户所在的服务器简称为单元(UNIT)。

MX

MX是接受邮件的关键,可以由一台或一组服务器组成。它对外开放SMTP端口,负责接受邮件并转投到收件人所在单元服务器。

简单的说,一封信投递到我们的系统中,首先会进入MX,MX检查收件人是否存在,然后根据用户的信息把信通过smtp协议转发到他所在的UNIT上。

由于用户分散在不同主机中,所以MX对用户进行查证的工作必须进行特殊的处理;这里大家可以集思广益,设计一两百种方案我看是没什么问题。不过,我们的任务是说明postfix如何处理用户分布的情况,所以我们假定已经有一个写好了的函数,调用它就可以查证用户了。我们利用它,就可以实现dict_unit这个map。利用选项 local_recipient_maps,使外部发信到我们的用户时,调用我们指定的接口对用户查证;输入为收件人的邮件地址。
local_recipient_maps = unit:smtpcheck

这里dict_unit的实现在后面详细解释。

用户查证是在访问者发出“RCPT ”指令时,如果postfix确认了该用户存在,就开始准备接收“DATA”指令发过来的邮件正文了。成功收到邮件后,通过配置好的 transport_maps,postfix可以询问我们的dict_unit,获得下一步处理的指令。比如,输入收件人邮件地址“user@my.com”,输出“smtp:unit12.my.com”,这就会让postfix通过smtp把信转投到unit12.my.com域名的这台机器上。
transport_maps = unit:transportmx


UNIT

邮件系统中的用户通过注册、转移等方式,被存放在不同的单元服务器中。他们负责接收从MX转投过来的信件,并最终放入存储系统中。

在这里,一样会使用local_recipient_maps检查收件人是否真实存在。但与MX不同的是,这是信件的最终目的地,transport_maps查询的结果就不能再是“smtp:unit12.my.com”,要设置成合适的本地处理程序,比如local或者指定mda的名称(在etc/master.cf中设置)等。

local_recipient_maps = unit:smtpcheck
transport_maps = unit:transportunit


自定义的map

这里是dict_unit的实现示意。通过这个map,我们可以把前面提到的用户查证、转发邮件、处理邮件的所有指引工作在一起完成;实际上,它相当于一个多向的阀门,可以让邮件流向不同的方向。

注意,这仅仅是示意的代码。有需要做开发的朋友请自行参考dict_pam[1]的实现,那是真正可执行的代码。请不要写信询问我如何编写并调试可用的dict,google[2]postfix官方网站[3]上都可以查到。

这个函数负责处理用户查证的工作。
static int dict_unit_smtpcheck(const char *name, char *result)
{
char userhost[128] = {0};

if (msg_verbose)
msg_info("dict_unit: lookup smtp user: %s", name);
ret = getuser(name, &userhost);
if (ret != 0){
if (msg_verbose)
msg_warn("dict_unit: lookup smtp failed, name: %s", name);
return 1;
}
if (msg_verbose)
msg_info("dict_unit: lookup transport successed, name: %s, host: %s, path: %s",
name,
userhost);

strncpy(result, userhost, sizeof(userhost));
return 0;
}


这个函数负责控制MX服务中邮件的流向。

static int dict_unit_transportmx(const char *name, char *result)
{
char userhost[128] = {0};

if (msg_verbose)
msg_info("dict_unit: lookup transport user: %s", name);

ret = getuser(name, &userhost);
if (ret != 0){
if (msg_verbose)
msg_warn("dict_unit: lookup transport failed, name: %s", name);
return 1;
}
if (msg_verbose)
msg_info("dict_unit: lookup transport successed, name: %s, host: %s,",
name,
userhost);

/* result formal like: "smtp:unit12.my.com" */
strncpy(result, "smtp:", sizeof(userhost));
strncat(result, userhost, sizeof(userhost)-strlen("smtp:"));
return 0;
}


这个函数负责控制UNIT中的邮件的流向。值得一提的是,这里我们使用了var_myhostname来鉴别该用户是不是本机的(很简陋,演示嘛),要正确编译的话,在src/global/dict_unit.c中,必须加入这一行:
#include "mail_params.h"

要正确运行的话,则必须再在etc/main.cf中配置好正确的本机域名:
myhostname = unit12.my.com

最后,我们这里的是调用了一个定制的MDA程序“mymda”来完成最终信件的投递。要让它能正确运行,得配置etc/master.cf,请自行查阅手册。

static int dict_unit_transportunit(const char *name, char *result)
{
char userhost[128] = {0};

if (msg_verbose)
msg_info("dict_unit: lookup transport user: %s", name);

ret = getuser(name, &userhost);
if (ret != 0){
if (msg_verbose)
msg_warn("dict_unit: lookup transport failed, name: %s", name);
return 1;
}
if (msg_verbose)
msg_info("dict_unit: lookup transport successed, name: %s, host: %s",
name,
userhost);

/* for local user, result is: "mymda:"
* for remote user, result is: "smtp:mx.my.com"
*/
if (0==strnstr(userhost, var_myhostname, sizeof(userhost)))
/* MX can delivery it */
strncpy(result, "smtp:mx.my.com", sizeof(userhost));
else
/* local MDA is ok */
strncpy(result, "mymda:", sizeof(userhost));

return 0;
}


最后,为了让上面的三个lookup能真正工作起来,要在dict对外提供的接口处调用他们。这个分流是通过lookup时传入的dict->service来辨认的,也就是在main.cf中的配置“unit:smtpcheck”冒号后面的部分。

static const char *dict_unit_lookup(DICT *dict, const char *name)
{
DICT_unit *dp = (DICT_unit *) dict;
char result[128] = {0};
int ret;

if (0 == strcmp(vstring_str(dp->service), "smtpcheck"))
ret = dict_unit_smtpcheck(name, result);
else if (0 == strcmp(vstring_str(dp->service), "transportmx"))
ret = dict_unit_transportmx(name, result);
else if (0 == strcmp(vstring_str(dp->service), "transportunit"))
ret = dict_unit_transportunit(name, result);
else
{
if (msg_verbose)
msg_error("dict_unit: lookup unknown service %s, name %s.",
vstring_str(dp->service), name);
return (0);
}
if ((0 != ret) || (0 == result[0]))
return (0);

succ:
if (msg_verbose)
msg_info("dict_unit: service %s, name: %s, result: %s.",
vstring_str(dp->service), name, result);
vstring_strncpy(dp->result, result, sizeof(result));
return vstring_str(dp->result);
}


组装在一起,这个控制邮件流向的大阀门就算是完工了。

以上是利用postfix实现用户分布的一次实践。当然,方案并非只有一种,大家各有不同。请大家多多讨论,共同进步。

本文基于创作共用协议[4]之“署名-保持一致”[5]许可证发表,使用前请阅读该协议。-- xyb

参考
[1] http://d.scn.ru/proj/postfix/dict_pam/
[2] http://www.google.com/
[3] http://www.postfix.org/
[4] http://www.creativecommons.cn/
[5] http://creativecommons.cn/licenses/by-sa/1.0/

没有评论: