程序员眼中的qmail(qmail源代码分析)

JavaGG 发布于 2009/05/05 16:53
阅读 524
收藏 1

很多人对qmail smtp的认证机制,环境变量,执行顺序不太了解。
仔细看完这一大篇代码后相信你会明白很多你过去不太明白的问题。
当然你要有一点点c语言基础。也只要一点点。


Programmer:夜未眠
Date:Apr 28,2003
Comefrom: ChongQing Gearbox co.,ltd

这份文件还不完善,如果您完善了它请发一份给我: beggar110@163.com
这份文件是给想深入了解qmail和想hacker qmail的人读的,如果你只是想建立一个能够运作的mail服务器,没有必要读下去了。它将浪费你很多的时间。

如果你对qmail控制文件还不是很了解,阅读这份文件之前,请先阅读rainbow的《qmail控制文件详解》
在这里你可以找到http://www.chinaunix.net/forum/viewtopic.php?t=1126


好的。开始我们qmail内部的漫游吧!!!Let's go!




qmail 总览

tcpserver                                     MUA
     |                                                |
    V                                               V
qmail-smtpd                            qmail-inject
    |                                                  |
    +----------->;qmail-queue<-----------+
                         |
                         |
                 qmail-send
                         |
        +------------+------------+
        |                                 |
        V                               V
    qmail-rspawn              qmail-lspawn
        |                                 |
        V                               V
    qmail-remote              qmail-local
        |                                 |
        |                                 |
       V                                V
    INTERNET                   ;  <----qmail-pop3d
                                                              |
                                                              |
                                                        vchkpw
                                                              |
                                                              |
                                                     qmail-popup
                                                              |
                                                              |
                                              tcpserver--+





qmail-smtpd.c源代码分析(去掉了所有include)

qmail-smtpd是由tcpserver或由tcp-env启动。tcpserver负责监听端口,如果指定了-x rule.cbd,tcpserver会先决断是断开连接还是启动qmail子进程。如果没有指定-x参数启动tcpserver,那么直接启动qmail-smtpd.启动qmail-smtpd之前将来自网络的数据连接重定向到qmail-smtpd的fd0,fd1.还会初始化一些qmail-smtpd需要的环境变量,如TCPREMOTEIP.
tcp-env只会初始化qmail-smtpd的环境变量,不负责监听端口及重定向网络连接。所以tcp-env要和inetd配合使用。当然,由于初始化环境变量的工作tcpserver也会作,所以没有必要tcpserver和tcp-env配合使用.

qmail-smtpd完成邮件smtp命令的接收,并调用相应的处理程序。
检查mail 中的地址是否在control/badmailfrom中定义(MAIL命令)
检查是否设置了RELAYCLIENT环境变量或 rcpt 中的地址是否是control/rcpthosts中定义(RCPT命令)
需要明确的是qmail-smtpd只是简单的接收邮件内容传送给qmail-queue,并不对邮件进行转发(DATA命令)。
当然还要向qmail-queue传送mailfrom,mailto





#define MAXHOPS 100
unsigned int databytes = 0; //邮件最大长度:0=无限
int timeout = 1200; //默认超时20分钟


//向网络写,超时值为control/timeoutsmtpd指定的值。没有这个文件则取默认值20分钟
int safewrite(fd,buf,len) int fd; char *buf; int len;
{
  int r;
  r = timeoutwrite(timeout,fd,buf,len);
  if (r <= 0) _exit(1);
  return r;
}

char ssoutbuf[512];
substdio ssout = SUBSTDIO_FDBUF(safewrite,1,ssoutbuf,sizeof ssoutbuf);

void flush() { substdio_flush(&ssout); }
void out(s) char *s; { substdio_puts(&ssout,s); }

//错误处理函数
void die_read() { _exit(1); }
void die_alarm() { out("451 timeout (#4.4.2)\r\n"); flush(); _exit(1); }
void die_nomem() { out("421 out of memory (#4.3.0)\r\n"); flush(); _exit(1); }
void die_control() { out("421 unable to read controls (#4.3.0)\r\n"); flush(); _exit(1); }
void die_ipme() { out("421 unable to figure out my IP addresses (#4.3.0)\r\n"); flush(); _exit(1); }
void straynewline() { out("451 See http://pobox.com/~djb/docs/smtplf.html.\r\n"); flush(); _exit(1); }

void err_bmf() { out("553 sorry, your envelope sender is in my badmailfrom list (#5.7.1)\r\n"); }
void err_nogateway() { out("553 sorry, that domain isn't in my list of allowed rcpthosts (#5.7.1)\r\n"); }
void err_unimpl() { out("502 unimplemented (#5.5.1)\r\n"); }
void err_syntax() { out("555 syntax error (#5.5.4)\r\n"); }
void err_wantmail() { out("503 MAIL first (#5.5.1)\r\n"); }
void err_wantrcpt() { out("503 RCPT first (#5.5.1)\r\n"); }
void err_noop() { out("250 ok\r\n"); }
void err_vrfy() { out("252 send some mail, i'll try my best\r\n"); }
void err_qqt() { out("451 qqt failure (#4.3.0)\r\n"); }


stralloc greeting = {0};

//输出提示信息*code
void smtp_greet(code) char *code;
{
  substdio_puts(&ssout,code);
  substdio_put(&ssout,greeting.s,greeting.len);
}


void smtp_help()
{
  out("214 qmail home page: http://pobox.com/~djb/qmail.html\r\n");
}

void smtp_quit()
{
  smtp_greet("221 "); out("\r\n"); flush(); _exit(0);
}

char *remoteip;   //远端ip地址
char *remotehost;  //远端主机名
char *remoteinfo; //远端信息
char *local; //本地主机
char *relayclient; //是否检查rcpthosts文件

stralloc helohost = {0}; 
char *fakehelo; /* pointer into helohost, or 0 */

void dohelo(arg) char *arg; {
  if (!stralloc_copys(&helohost,arg)) die_nomem(); 
  if (!stralloc_0(&helohost)) die_nomem(); 
  //fakehelo变量,如果helo 参数指定的主机名与TCPREMOTEHOST环境变量中的主机名不同则
  //fakehelo的值为helo命令的参数指定的主机名.如果两者相同则fekehelo为NULL;
  //data命令处理程式用到这个变量
  fakehelo = case_diffs(remotehost,helohost.s) ? helohost.s : 0;
}

int liphostok = 0;
stralloc liphost = {0};
int bmfok = 0;
stralloc bmf = {0};
struct constmap mapbmf;

void setup()
{
  char *x;
  unsigned long u;
 
  if (control_init() == -1) die_control(); //control/me
  //读入欢迎信息greeting,如果不存在则从me文件复制
  if (control_rldef(&greeting,"control/smtpgreeting",1,(char *) 0) != 1)
    die_control();
  //读入localiphost,如果文件不存在则从me文件复制
  liphostok = control_rldef(&liphost,"control/localiphost",1,(char *) 0);
  if (liphostok == -1) die_control();

  //读control/timeoutsmtpd存入timeout,用于控制超时的情况.
  if (control_readint(&timeout,"control/timeoutsmtpd") == -1) die_control();
  if (timeout <= 0) timeout = 1;

  if (rcpthosts_init() == -1) die_control();

  //读入badmailfrom文件存入 bmf 
  bmfok = control_readfile(&bmf,"control/badmailfrom",0);
  if (bmfok == -1) die_control();
  if (bmfok)
    if (!constmap_init(&mapbmf,bmf.s,bmf.len,0)) die_nomem();
 
  //读入databytes文件存入 databytes,如果该文件不存在,则将
  //databytes的值设为0.

  if (control_readint(&databytes,"control/databytes") == -1) die_control();
  x = env_get("DATABYTES");
  if (x) { scan_ulong(x,&u); databytes = u; }
  if (!(databytes + 1)) --databytes;
 
  //取tcp-environ环境变量,如果环境变量没有设置,将它的值设置为unknow.
  //这些信息来自tcpserver,或tcp-env之类的程式
  remoteip = env_get("TCPREMOTEIP");
  if (!remoteip) remoteip = "unknown";
  local = env_get("TCPLOCALHOST");
  if (!local) local = env_get("TCPLOCALIP");
  if (!local) local = "unknown";
  remotehost = env_get("TCPREMOTEHOST");
  if (!remotehost) remotehost = "unknown";
  remoteinfo = env_get("TCPREMOTEINFO");

  //从环境变量RELAYCLIENT读入.
  //如果RELAYCLIENT变量没有设置那么relayclient将会是NULL.
  relayclient = env_get("RELAYCLIENT");
  dohelo(remotehost);
}


stralloc addr = {0}; /* will be 0-terminated, if addrparse returns 1 */


//对命令参数arg进行邮件地址分析
//并将分离出的email地址存入全局缓存addr
//成功返回值为1,失败返回0
int addrparse(arg)
char *arg;
{
  int i;
  char ch;
  char terminator;
  struct ip_address ip;
  int flagesc;
  int flagquoted;
 
  //分离出邮件地址
  //例如: arg=" ;",或 arg=": email@eg.org  "
  //执行下面这段程式后arg="email@eg.org"
  terminator = '>;';
  i = str_chr(arg,'<');
  if (arg
    arg += i + 1; 
  else { /* partner should go read rfc 821  */
    terminator = ' ';
    arg += str_chr(arg,':');
    if (*arg == ':') ++arg;
    while (*arg == ' ') ++arg;
  }

  /* strip source route */
  if (*arg == '@') while (*arg) if (*arg++ == ':') break;

  if (!stralloc_copys(&addr,"")) die_nomem();
  flagesc = 0;
  flagquoted = 0;
  for (i = 0;ch = arg ;++i) { /* copy arg to addr, stripping quotes */
    if (flagesc) {
      if (!stralloc_append(&addr,&ch)) die_nomem();
      flagesc = 0;
    }
    else {
      if (!flagquoted && (ch == terminator)) break;
      switch(ch) {
        case '\\': flagesc = 1; break;
        case '"': flagquoted = !flagquoted; break;
        default: if (!stralloc_append(&addr,&ch)) die_nomem();
      }
    }
  }

  /* could check for termination failure here, but why bother? */
  if (!stralloc_append(&addr,"")) die_nomem();

  //将ip地址转换为主机名:
  //如 test@[10.0.6.21]  转换为 test@host.mydomain.org
  //依据是control/localiphost文件中有host.mydomain.org
  if (liphostok) {
    i = byte_rchr(addr.s,addr.len,'@');
    if (i < addr.len) /* if not, partner should go read rfc 821 */
      if (addr.s[i + 1] == '[')//比较是否是用[]括起来的IP地址
        if (!addr.s[i + 1 + ip_scanbracket(addr.s + i + 1,&ip)])
          if (ipme_is(&ip)) {
            addr.len = i + 1;
            if (!stralloc_cat(&addr,&liphost)) die_nomem();
            if (!stralloc_0(&addr)) die_nomem();
          }
  }
  if (addr.len >; 900) return 0; //地址太长,出错返回
  return 1;//成功返回
}



//简单的垃圾邮件检查
//检查全局缓冲区addr中的地址是否有在badmailfrom中定义,
//如果有则返回 1,否则返回 0.
int bmfcheck()
{
  int j;
  if (!bmfok) return 0;
  if (constmap(&mapbmf,addr.s,addr.len - 1)) return 1;
  j = byte_rchr(addr.s,addr.len,'@');
  if (j < addr.len)
    if (constmap(&mapbmf,addr.s + j,addr.len - j - 1)) return 1;
  return 0;
}


//检查全局缓存addr中的邮件地址是否要进行转发(依据control/rcpthosts文件)
//可以进行转发返回1
//拒绝转发返回0
int addrallowed()
{
  int r;
  r = rcpthosts(addr.s,str_len(addr.s));
  if (r == -1) die_control();
  return r;
}


int seenmail = 0;
int flagbarf; /* defined if seenmail */
stralloc mailfrom = {0};
stralloc rcptto = {0};

void smtp_helo(arg) char *arg;
{
  smtp_greet("250 "); out("\r\n");
  seenmail = 0; dohelo(arg);
}
void smtp_ehlo(arg) char *arg;
{
  smtp_greet("250-"); out("\r\n250-PIPELINING\r\n250 8BITMIME\r\n");
  seenmail = 0; dohelo(arg);
}



//重新初始化
//调用helo或ehlo命令都会完成相同的功能
void smtp_rset()
{
  seenmail = 0;
  out("250 flushed\r\n");
}



//mail命令解释程式.                      重要变量: [mailfrom /全局]
//该函数完成检查mailfrom是否在badmailfrom中定义
//设置标志指明mail命令已经执行
void smtp_mail(arg) char *arg;
{
  if (!addrparse(arg)) { err_syntax(); return; }
  flagbarf = bmfcheck(); //检查是否badmailfrom,如果是设置相应标志,这个标志在rcpt命令的处理程式中才起作用
  seenmail = 1;//指示已经执行过mail命令.


  if (!stralloc_copys(&rcptto,"")) die_nomem();//分配rcptto缓冲区
  if (!stralloc_copys(&mailfrom,addr.s)) die_nomem();//复制mail命令中指定的地址到mailfrom
  if (!stralloc_0(&mailfrom)) die_nomem();
  out("250 ok\r\n");
}

//rcpt命令解释程式. 重要变量: [ rcptto /全局]
void smtp_rcpt(arg) char *arg; {
  if (!seenmail) { err_wantmail(); return; }//mail命令是否已执行?
  if (!addrparse(arg)) { err_syntax(); return; }//分离邮件地址参数存入全局缓存addr
  if (flagbarf) { err_bmf(); return; }//如果mail命令中的地址在control/badmailfrom中有定义,返回


  //至此addr缓存中包含了rcpt命令指定的email地址.
  //如果rcpt  ;命令,则有addr="email@eg.org".这个变量是在addrparse函数中符值的
  //如果 RELAYCLIENT 环境变量设置将不进行rcpthosts,morercpthosts.cdb的比较
  //注意,打过smtp认证补丁,如果通过认证后会设置relayclient=""
  if (relayclient) { 
    --addr.len;
    if (!stralloc_cats(&addr,relayclient)) die_nomem();
    if (!stralloc_0(&addr)) die_nomem();
  }
  else//如果没有指定RELAYCLIENT变量,则由control/rcpthosts决定是否进行转发
    if (!addrallowed()) { err_nogateway(); return; }
  //生成头连接到全局缓存rcptto:
  //例如地址'rcpt test@eg.org' 命令将产生 rcptto="Temail@eg.org ;"
  //多次执行rcpt命令效果会是rcptto="Ttest@eg.org ;Ttwo@eg.org ;"
  if (!stralloc_cats(&rcptto,"T")) die_nomem();
  if (!stralloc_cats(&rcptto,addr.s)) die_nomem();
  if (!stralloc_0(&rcptto)) die_nomem();
  out("250 ok\r\n");
}


//saferead,从网络读len个字节到buf缓冲区
//返回实际读到的字节数.
//超时值为control/timeoutsmtpd文件中指定的值。见setup()函数.(默认值1200秒)
int saferead(fd,buf,len) int fd; char *buf; int len;
{
  int r;
  flush();
  r = timeoutread(timeout,fd,buf,len);
  if (r == -1) if (errno == error_timeout) die_alarm();
  if (r <= 0) die_read();
  return r;
}

char ssinbuf[1024];
substdio ssin = SUBSTDIO_FDBUF(saferead,0,ssinbuf,sizeof ssinbuf);

struct qmail qqt;
unsigned int bytestooverflow = 0;

void put(ch)
char *ch;
{
  if (bytestooverflow)
    if (!--bytestooverflow)
      qmail_fail(&qqt);
  qmail_put(&qqt,ch,1);
}

void blast(hops)
int *hops;
{
  char ch;
  int state;
  int flaginheader;
  int pos; /* number of bytes since most recent \n, if fih */
  int flagmaybex; /* 1 if this line might match RECEIVED, if fih */
  int flagmaybey; /* 1 if this line might match \r\n, if fih */
  int flagmaybez; /* 1 if this line might match DELIVERED, if fih */
 
  state = 1;
  *hops = 0;
  flaginheader = 1;
  pos = 0; flagmaybex = flagmaybey = flagmaybez = 1;
  for (;;) {
    substdio_get(&ssin,&ch,1);//从标准输入(也就是网络)读邮件内容直到读到仅有一个点的行.
    if (flaginheader) {
      if (pos < 9) {
        if (ch != "delivered"[pos]) if (ch != "DELIVERED"[pos]) flagmaybez = 0;
        if (flagmaybez) if (pos == 8) ++*hops;
        if (pos < 8)
          if (ch != "received"[pos]) if (ch != "RECEIVED"[pos]) flagmaybex = 0;
        if (flagmaybex) if (pos == 7) ++*hops;
        if (pos < 2) if (ch != "\r\n"[pos]) flagmaybey = 0;
        if (flagmaybey) if (pos == 1) flaginheader = 0;
      }
      ++pos;
      if (ch == '\n') { pos = 0; flagmaybex = flagmaybey = flagmaybez = 1; }
    }
    switch(state) {
      case 0:
        if (ch == '\n') straynewline();
        if (ch == '\r') { state = 4; continue; }
        break;
      case 1: /* \r\n */
        if (ch == '\n') straynewline();
        if (ch == '.') { state = 2; continue; }
        if (ch == '\r') { state = 4; continue; }
        state = 0;
        break;
      case 2: /* \r\n + . */
        if (ch == '\n') straynewline();
        if (ch == '\r') { state = 3; continue; }
        state = 0;
        break;
      case 3: /* \r\n + .\r */
        if (ch == '\n') return;
        put(".");
        put("\r");
        if (ch == '\r') { state = 4; continue; }
        state = 0;
        break;
      case 4: /* + \r */
        if (ch == '\n') { state = 1; break; }
        if (ch != '\r') { put("\r"); state = 0; }
    }
    put(&ch);
  }
}

char accept_buf[FMT_ULONG];
void acceptmessage(qp) unsigned long qp;
{
  datetime_sec when;
  when = now();
  out("250 ok ");
  accept_buf[fmt_ulong(accept_buf,(unsigned long) when)] = 0;
  out(accept_buf);
  out(" qp ");
  accept_buf[fmt_ulong(accept_buf,qp)] = 0;
  out(accept_buf);
  out("\r\n");
}


//data 命令解释程式
//完成向qmail-queue投递邮件
void smtp_data() {
  int hops;
  unsigned long qp;
  char *qqx;
 
  if (!seenmail) { err_wantmail(); return; } //如果没有执行过mail命令,出错返回
  if (!rcptto.len) { err_wantrcpt(); return; } //如果没有执行rcpt命令,出错返回
  seenmail = 0; //将mail命令标志失效,
  //databytes 邮件最大长度,如果没有指定那么它的值将是0
  if (databytes) bytestooverflow = databytes + 1;
  if (qmail_open(&qqt) == -1) { err_qqt(); return; }//建立子进程执行qmail-queue
  qp = qmail_qp(&qqt); //qp 为qmail-queue process缩写,it's a process id.
  out("354 go ahead\r\n");
 
  //向新建立的进程传送邮件头
  received(&qqt,"SMTP",local,remoteip,remotehost,remoteinfo,fakehelo);
  blast(&hops);
  hops = (hops >;= MAXHOPS);
  if (hops) qmail_fail(&qqt);
  //向qmail-queue传送邮件头信息.
  //如果hong@hg.org 向 lyx@hg.org发送邮件,那么向qmail-queue传送的字符串将是
  // Fhong@hg.org ;Tlyx@hg.org ;
  qmail_from(&qqt,mailfrom.s);
  qmail_put(&qqt,rcptto.s,rcptto.len);
 
  qqx = qmail_close(&qqt);
  if (!*qqx) { acceptmessage(qp); return; }//如果接收成功
  if (hops) { out("554 too many hops, this message is looping (#5.4.6)\r\n"); return; }
  if (databytes) if (!bytestooverflow) { out("552 sorry, that message size exceeds my databytes limit (#5.3.4)\r\n"); return; }
  if (*qqx == 'D') out("554 "); else out("451 ");

  out(qqx + 1);
  out("\r\n");
}


//smtp命令处理函数表
struct commands smtpcommands[] = {
  { "rcpt", smtp_rcpt, 0 }
, { "mail", smtp_mail, 0 }
, { "data", smtp_data, flush }  //建立子进程执行qamil-queue,并向其传送邮件.
, { "quit", smtp_quit, flush }
, { "helo", smtp_helo, flush }
, { "ehlo", smtp_ehlo, flush }
, { "rset", smtp_rset, 0 }
, { "help", smtp_help, flush }
, { "noop", err_noop, flush } //实际上未实现的命令
, { "vrfy", err_vrfy, flush } //实际上未实现的命令
, { 0, err_unimpl, flush } //命令错误
} ;



/*
   qmail-smtpd 是由tcpserver,或tcp-env之类的程式启动
   tcpserver,tcp-env将来自网络的连接重定向到qmail-smtpd的标准输入及标准输出.这些程式建立一些环境变量(如TCPREMOTEHOST,TCPREMOTEIP)将由setup()函数使用
*/
void main()
{
  sig_pipeignore();//忽略信号.
  if (chdir(auto_qmail) == -1) die_control();//改变当前目录到 /var/qmail.
  setup();//读控制文件及相应的环境变量.
  if (ipme_init() != 1) die_ipme(); //取本地接口的IP地址:
  smtp_greet("220 "); //显示欢迎信息.
  out(" ESMTP\r\n"); 
  //从标准输入(网络连接)读入smtp命令.
  if (commands(&ssin,&smtpcommands) == 0) die_read();
  die_nomem();
}


==完==



加载中
OSCHINA
登录后可查看更多优质内容
返回顶部
顶部