clq

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

    我们在上一篇中解决了接收一行命令的问题后,就可以来具体的分析邮件发送过程中涉及到的 SMTP 协议内容了。

    首先来看通讯过程中的第一个内容:服务器在客户端连接上来后会主动发送一个问好的信息,所以这第一行的内容是服务器发送的,这时候客户端要回答的内容其实并不确定。原因是根据不同的客户端意图,客户端要发送的内容是有些差异的。以我们示例中要发送一封信来说,要回复的第一句话就是 "EHLO" 命令,而看过我们前面文章都知道,要连接 163 邮箱这样的服务器光有这个命令还是不够的,还要带上 "163.com"。那么就有两个问题,一是谁规定要加这个和呢?二是如果是其实的邮箱应该加什么呢?

    这个答案就要到 RFC 文档中去找了,smtp 的 rfc 文档为 RFC821 可以在以下网址中看到(内容还是比较多的,其实大家可以先别急着看,我们文章会讲解其中的内容)
http://man.chinaunix.net/develop/rfc/RFC821.txt
或者备用网址
http://newbt.net/ms/vdisk/show_bbs.php?id=8001DA8441F9AE9DC7AFF0709A875279&pid=160

其实如果大家真的仔细看了 rfc 的文档一定会觉得奇怪,通篇文章都没有 "EHLO" 命令啊,这就涉及到 rfc 文档的一个特点了,那就是一种通讯协议通常是记载在多个文档当中的,造成这种情况的原因一是通讯协议在发展,加入废弃了一些内容,而 rfc 写上去后一般就是不修改了的,一般是另外再写一个文档补充;另外一种常见的就是某个通讯协议是将之前的好几种组合在一起形成的,所以就肯定要引用别的 rfc 文档了。回到 "EHLO" 命令上来,这个命令其实记述在另外一个文档  RFC1869 当中,可以在以下网址中查看:
http://man.chinaunix.net/develop/rfc/RFC1869.txt
或者备用网址
http://newbt.net/ms/vdisk/show_bbs.php?id=9D31E50F0BEE16B48703FDF4234A332E&pid=160

我又要说大家先别急着查看,因为文档中的说明太复杂了,单就 "EHLO" 命令后面带的内容来说,其实是来自第一个文档的 "HELO" 命令的表述部分(HELO 就是英文 hello 的意思,而 EHLO 是扩展的 hello 的意思)。文档记载为 "HELO <SP> <domain> <CRLF>"。文档中的表述其实仍然有很强的误导性,命令中的空格是千万不要放到命令中的,那只是文档为了分隔语句而已,你会说命令中确实有空格啊!没错,看到那个 "<SP>" 没有,那个才表示的是空格 ... 如果有同学一上来就看命令的一定会很郁闷<SP>到底是什么意思,当然文档中是有解释的,只不过是在文档的最后,如果你老老实实的从头读到尾的话 ... 基本上都要破口大骂吧。总的来说阅读 rfc 文档是件很辛苦的事,但有时候又很必要,而且以上的都是中文版本,实在上由于翻译和中英文差异的问题,有些细节操作时还得去查看英文原版的。

  在命令中的 "<domain>" 是关键,它就是前述命令中的 "163.com",而每个服务器的 domain 是不能乱写的,实际上要来自服务器风连接上的第一条响应命令行。例如 "220 newbt.net ESMTP eEmail-Server 2.0" 或者 "220 163.com Anti-spam GT for Coremail System (163com[20141201])",这个命令是由空格分隔的多个参数组成的,在实际的开发中实际上只需要按空格分隔字符串,然后取第二个参数就行了,这个就是 EHLO 命令后面要带上的东西。

代码实现:

知道了原理,其实用 java 语言来实现非常的简单:

    //解码一行命令,这里比较简单就是按空格进行分隔就行了
    public static String[] DecodeCmd(String line, String sp)
    {
        //String[] aa = "aaa|bbb|ccc".split("|");
        String[] tmp = line.split(sp); //用空格分开//“.”和“|”都是转义字符,必须得加"\\";//不一定是空格也有可能是其他的
        String[] cmds = {"", "", "", "", ""}; //先定义多几个,以面后面使用时产生异常
        
        for(int i=0;i<tmp.length;i++)
        {   
            if (i >= cmds.length) break;
            cmds[i] = tmp[i];
        }
        return cmds;
    }//

   
结合我们前面的例子就可以有:

        //解码一下,这样后面的 EHLO 才能有正确的第二个参数
        String cmds[] = DecodeCmd(line, " ");
        
        String domain = cmds[1]; //要从对方的应答中取出域名//空格分开的各个命令参数中的第二个
        
        //发送一个命令
        //SendLine("EHLO"); //163 这样是不行的,一定要有 domain
        SendLine("EHLO" + " " + domain); //domain 要求其实来自 HELO 命令//HELO <SP> <domain> <CRLF>

       
完整代码大家可以手工加入之前文章的代码中去,也可以到 github 地址去下载:
https://github.com/clqsrc/c_lib_lstring

另外这系列文章的 java 示例我只放了一个源码,就不象 C 语言系列那样给出每一篇演变的代码了,因为 java 的源码相对比较简单,大家应该都看得懂。
就不用象 C 语言的那样分得那么清楚了。

    C 语言要实现的话,有了前面的基础,要实现其实也不复杂。值得一提的是 C 语言的分隔字符串,要说的是 C 语言中分隔字符串时有种很常见的做法,就是利用 C 语言字符串的特点,直接在原字符串上打上字符串结束符号,这样的代码对于很多刚从学校 C 语言书本中走出来的初学者来说是个巨大的挑战,但是因为这种方法没有重新分配内存,运行效率是非常的高(以后有机会我再给大家详细讲解程序优化中不重新分配内存能让程序效率提高到什么程度,可以提前说下,服务端大量连接的情况下提高100倍都不止 -- 就是能有这么多)。

根据以上思想可以简单的写出一个版本的实现为:

//解码一行命令,这里比较简单就是按空格进行分隔就行了
//这是用可怕的指针运算的版本
void DecodeCmd(lstring * line, char sp, char ** cmds, int cmds_count)
{
    int i = 0;
    int index = 0;
    int count = 0;
    
    cmds[index] = line->str;
    
    for (i=0; i<line->len; i++)
    {
        if (sp == line->str[i])
        {
            index++;
            line->str[i] = '\0'; //直接修改为字符串结束符号,如果是只读的字符串这样做其实是不对的,不过效率很高
            
            cmds[index] = line->str + i; //指针向后移动
            
            if (i >= line->len - 1) break;//如果是最后一定字符了就要退出,如果不是指针还要再移动一位
            cmds[index] = line->str + i + 1;
            
            count++;
            if (count >= cmds_count) break; //不要大于缓冲区
        }
        
    }//


}//


调用前先要声明命令参数的缓冲区,如下:

    char * cmds[5] = {NULL};
    int cmds_count = 5;
    
    
    rs = RecvLine(gSo, m, &buf); //只收取一行

    printf("\r\nRecvLine:");
    printf(rs->str); printf("\r\n");
    
    DecodeCmd(rs, ' ', cmds, cmds_count);
    printf("\r\ndomain:%s\r\n", cmds[1]);
    
    domain = NewString(cmds[1], m);
    
    s = NewString("EHLO", m);
    LString_AppendConst(s," ");
    s->Append(s, domain); //去掉这一行试试,163 邮箱就会返回错误了
    LString_AppendConst(s,"\r\n");

    SendBuf(gSo, s->str, s->len);

   
完整代码就多了些,贴上来大家也难看清楚,可以到以下 github 地址下载或查看:    
https://github.com/clqsrc/c_lib_lstring/tree/master/email_book/book_8
       

--------------------------------------------------

版权声明:

本系列文章已授权百家号 "clq的程序员学前班" .

 

posted on 2018-02-02 16:52  clq  阅读(1654)  评论(2编辑  收藏  举报