zoukankan      html  css  js  c++  java
  • 一步一步从原理跟我学邮件收取及发送 11.完整的发送示例与go语言

        经过了这个系列的前几篇文章的学习,现在要写出一个完整的 smtp 邮件发送过程简直易如反掌。
        例如我们可以轻松地写出以下的纯 C 语言代码(引用的其他C语言文件请看文末的 github 地址):

    #include <stdio.h>
    #include <windows.h>
    #include <time.h>
    #include <winsock.h>
    
    #include "lstring.c"
    #include "socketplus.c"
    #include "lstring_functions.c"
    #include "base64_functions.c"
    
    //vc 下要有可能要加 lib 
    //#pragma comment (lib,"*.lib")
    //#pragma comment (lib,"libwsock32.a")
    //#pragma comment (lib,"libwsock32.a")
    
    //SOCKET gSo = 0;
    SOCKET gSo = -1;
    
    
    //收取一行,可再优化 
    lstring * RecvLine(SOCKET so, struct MemPool * pool, lstring ** _buf)
    {
        int i = 0;
        int index = -1;
        int canread = 0;
        lstring * r = NULL;
        lstring * s = NULL;
        lstring * buf = *_buf;
    
        for (i=0;i<10;i++) //安全起见,不用 while ,用 for 一定次数就可以了 
        {
            //index = pos("
    ", buf);
            index = pos(NewString("
    ", pool), buf);
    
            if (index>-1) break;
            
            canread = SelectRead_Timeout(so, 3);//是否可读取,时间//超时返回,单位为秒 
            if (0 == canread) break;
            
            s = RecvBuf(so, pool);
            buf->Append(buf, s);
        }
        
        if (index <0 ) return NewString("", pool);
        
        r = substring(buf, 0, index);
        buf = substring(buf, index + 2, Length(buf));
        *_buf = buf;
        return r;
    }//
    
    //解码一行命令,这里比较简单就是按空格进行分隔就行了
    //这是用可怕的指针运算的版本 
    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] = ''; //直接修改为字符串结束符号,如果是只读的字符串这样做其实是不对的,不过效率很高 
                
                cmds[index] = line->str + i; //指针向后移动 
                
                if (i >= line->len - 1) break;//如果是最后一定字符了就要退出,如果不是指针还要再移动一位
                cmds[index] = line->str + i + 1;
                
                count++;
                if (count >= cmds_count) break; //不要大于缓冲区 
            }
            
        }//
    
    
    }//
    
    //读取多行结果 
    lstring * RecvMCmd(SOCKET so, struct MemPool * pool, lstring ** _buf)
    {
        int i = 0;
        int index = 0;
        int count = 0;
        lstring * rs;
        char c4 = ''; //判断第4个字符 
        
        lstring * mline = NewString("", pool);
    
        
        for (i=0; i<50; i++)
        {
            rs = RecvLine(so, pool, _buf); //只收取一行 
            
            mline->Append(mline, rs);
            LString_AppendConst(mline, "
    ");
            
            //printf("
    RecvMCmd:%s
    ", rs->str); 
                
            if (rs->len<4) break; //长度要足够 
            c4 = rs->str[4-1]; //第4个字符
            //if ('x20' == c4) break; //"xhh" 任意字符 二位十六进制//其实现在的转义符已经扩展得相当复杂,不建议用这个表示空格 
            if (' ' == c4) break; //第4个字符是空格就表示读取完了//也可以判断 "250[空格]" 
        
            
        }//
    
        return mline;
    }//
    
    
    void main()
    {
        int r;
        mempool mem, * m;
        lstring * s;
        lstring * rs;
        lstring * buf;
        lstring * domain;
        lstring * from;
        lstring * to;
        
        char * cmds[5] = {NULL}; 
        int cmds_count = 5;
        
        //--------------------------------------------------
    
        mem = makemem(); m = &mem; //内存池,重要 
        
        buf = NewString("", m);
    
        //--------------------------------------------------
        //直接装载各个 dll 函数
        LoadFunctions_Socket();
    
        InitWinSocket(); //初始化 socket, windows 下一定要有 
    
    
        gSo = CreateTcpClient();
        r = ConnectHost(gSo, "newbt.net", 25);
        //r = ConnectHost(gSo, "smtp.163.com", 25); //可以换成 163 的邮箱 
    
        if (r == 1) printf("连接成功!
    ");
    
        
        //--------------------------------------------------
        rs = RecvLine(gSo, m, &buf); //只收取一行 
    
        printf("
    RecvLine:");
        printf(rs->str); printf("
    ");
        
        DecodeCmd(rs, ' ', cmds, cmds_count); 
        printf("
    domain:%s
    ", cmds[1]); 
        
        domain = NewString(cmds[1], m);
        
        s = NewString("EHLO", m);
        LString_AppendConst(s," ");
        s->Append(s, domain); //去掉这一行试试,163 邮箱就会返回错误了 
        LString_AppendConst(s,"
    ");
    
        SendBuf(gSo, s->str, s->len);    
    
        
        ////rs = RecvLine(gSo, m, &buf); //只收取一行 
        rs = RecvMCmd(gSo, m, &buf); //只收取一行 
    
        printf("
    RecvLine:");
        printf(rs->str); printf("
    ");
        
        //--------------------------------------------------
        //用 base64 登录 
        s = NewString("AUTH LOGIN
    ", m);
        SendBuf(gSo, s->str, s->len);    
        
        rs = RecvLine(gSo, m, &buf); //只收取一行 
        printf("
    RecvLine:%s
    ", rs->str); 
        
        s = NewString("test1@newbt.net", m); //要换成你的用户名,注意 163 邮箱的话不要带后面的 @域名 部分 
        s = base64_encode(s);
        LString_AppendConst(s,"
    ");
        SendBuf(gSo, s->str, s->len);    
        
        rs = RecvLine(gSo, m, &buf); //只收取一行 
        printf("
    RecvLine:%s
    ", rs->str); 
        
    
        s = NewString("123456", m); //要换成您的密码 
        s = base64_encode(s);
        LString_AppendConst(s,"
    ");
        SendBuf(gSo, s->str, s->len);    
        
        rs = RecvLine(gSo, m, &buf); //只收取一行 
        printf("
    RecvLine:%s
    ", rs->str); 
        
        //--------------------------------------------------
        //邮件内容 
        from = NewString("test1@newbt.net", m);
        to = NewString("clq@newbt.net", m);
        
        s = NewString("MAIL FROM: <", m); s->Append(s, from); s->AppendConst(s, ">
    "); //注意"<" 符号和前面的空格。空格在协议中有和没有都可能,最好还是有 
        SendBuf(gSo, s->str, s->len);
        rs = RecvLine(gSo, m, &buf); //只收取一行 
        printf("
    RecvLine:%s
    ", rs->str);         
        
        s = NewString("RCPT TO: <", m); s->Append(s, to); s->AppendConst(s, ">
    ");
        SendBuf(gSo, s->str, s->len);    
        rs = RecvLine(gSo, m, &buf); //只收取一行 
        printf("
    RecvLine:%s
    ", rs->str);     
        
        s = NewString("DATA
    ", m);
        SendBuf(gSo, s->str, s->len);
        rs = RecvLine(gSo, m, &buf); //只收取一行 
        printf("
    RecvLine:%s
    ", rs->str);         
        
        s = NewString("From: "test1@newbt.net" <test1@newbt.net>
    To: "clq@newbt.net" <clq@newbt.net>
    Subject: test
    Date: Sun, 21 Jan 2018 11:48:15 GMT
    
    Hello World.
    ", m);//邮件内容,正式的应该用一个函数生成 
        SendBuf(gSo, s->str, s->len);    
        
        
        s = NewString("
    .
    ", m); //邮件结束符 
        SendBuf(gSo, s->str, s->len);    
        
        rs = RecvLine(gSo, m, &buf); //只收取一行 
        printf("
    RecvLine:%s
    ", rs->str); 
        
    
        //--------------------------------------------------
        
        Pool_Free(&mem); //释放内存池 
        
        printf("gMallocCount:%d 
    ", gMallocCount); //看看有没有内存泄漏//简单的检测而已  
        
        //-------------------------------------------------- 
    
        getch(); //getch().不过在VC中好象要用getch(),必须在头文件中加上<conio.h> 
    
    }

    运行结果如图:

        好了,我们用其他语言也来一个吧。但是这里有个问题:java 有很完善的电子邮件实现,实在是没必要再写一个。我换用一下 go 语言吧,之所以用 golang ,那是因为 golang 现在真的是开发服务器程序的极佳选择,而因为它又非常的新,所以有些功能并不完善,特别是支持库方面离 java 还有比较远的距离。刚好在电子邮件方面就是,而我刚好最近 golang 又用得比较多。

        用过 golang 发送邮件的同学一定都知道 go 语言中默认的 smtp 模块是无法在正常的 smtp 25 端口上去发送邮件的(有兴趣的网友可以自行用 163 的邮箱试试)。原因是 golang 本身起源就是为 google 公司的需求服务的,所以很多功能都先优先做了 google 需要的部分,而对电子邮件有一点了解的网友们应该都知道 google 的 gmail 是不支持常规的 smtp 25 端口的,它需要安全连接的 ssl 接口。所以大家如果搜索 golang 的 smtp 发送示例的话,基本上都是要进行一点改造的。其实这样的改造代码都不完善,最后都会注明有问题。这个问题其实源于 golang 的 smtp 源码(我看的是 1.7 版本)中对 "AUTH" 命令的实现与常规不太一样,它的实现后面跟了两个参数,而经过我们前几篇的文章,大家都知道实现的只有一个参数,那就是 "AUTH LOGIN"。知道了这一点,要改造 golang 的源码还是比较容易的。不过 golang 和 java 一样有点过度设计的意思,所以要看懂它的代码也不是太容易(不过 golang 中的各种协议代码设计得很精巧,远远不是 java 可比的)。所以我们既然已经知道了怎样自己写一个,那还不如自己明明白白的写一个出来。以下就是一个我写的示例,对协议有一定了解的同学很容易进行改写,为了方便大家理解我就没有设计成类了,大家可以自己动手:

    package main //clq
    
    //用于不加密环境的 smtp 发送电子邮件过程,因为不是所有的 smtp 环境都有加密支持的,不加密的适用范围更广一点,而且 smtp 服务器之间没有密码加密的过程
    //(因为对方 smtp 服务器不可能知道你的密码)所以原有的 golang 1.7.3 net/smtp 过程就不适合 smtp 之间发送邮件
    
    import (
        "fmt"
        "bufio"
    //    "crypto/tls"
        "encoding/base64"
    //    "errors"
    //    "io"
        "net"
    //    "net/smtp" //clq add
    //    "net/textproto"
        "strings"
        "strconv"
    )
    
    var gConn net.Conn;
    var gRead * bufio.Reader;
    var gWrite * bufio.Writer;
    
    //可以放到这样的类里
    type TcpClient struct {
        Conn net.Conn;
        Read * bufio.Reader;
        Write * bufio.Writer;
    }//
    
    
    func Connect(host string, port int) (net.Conn, * bufio.Reader, * bufio.Writer) {
        
        addr := host + ":" + strconv.Itoa(port);
        conn, err := net.Dial("tcp", addr);
        if err != nil {
            return nil, nil, nil
        }
        
        reader := bufio.NewReader(conn);
        writer := bufio.NewWriter(conn);
        
        //writer.WriteString("EHLO
    ");
        //writer.Flush();
        
        //host, _, _ := net.SplitHostPort(addr)
        //return NewClient(conn, host)    
        return conn, reader, writer;
    }//
    
    //收取一行,可再优化 
    //func RecvLine(conn *net.Conn) (string) {
    //func RecvLine(conn net.Conn, reader * bufio.Reader) (string) {
    func _RecvLine() (string) {
        
        //defer conn.Close();
        ////reader := bufio.NewReader(conn);
        //reader := bufio.NewReaderSize(conn,409600)
        
        //line, err := reader.ReadString('
    '); //如何设定超时?
        line, err := gRead.ReadString('
    '); //如何设定超时?
        
        if err != nil { return ""; }
        
        line = strings.Split(line, "
    ")[0]; //还要再去掉 "
    ",其实不去掉也可以
        
        return line;
    }//
    
    func SendLine(line string){
        gWrite.WriteString(line + "
    ");
        gWrite.Flush();
    }//
    
    //解码一行命令,这里比较简单就是按空格进行分隔就行了
    func DecodeCmd(line string, sp string) ([]string){
    
        //String[] tmp = line.split(sp); //用空格分开//“.”和“|”都是转义字符,必须得加"\";//不一定是空格也有可能是其他的
        //String[] cmds = {"", "", "", "", ""}; //先定义多几个,以面后面使用时产生异常
        
        tmp := strings.Split(line, sp);
        //var cmds = [5]string{"", "", "", "", ""}; //先定义多几个,以面后面使用时产生异常
        var cmds = []string{"", "", "", "", ""}; //先定义多几个,以面后面使用时产生异常
        //i:=0;
        for i:=0;i<len(tmp);i++ {
            if i >= len(cmds) { break;}
            cmds[i] = tmp[i];
        }
        return []string(cmds);
    }//
    
    //读取多行结果
    func RecvMCmd() (string) {
        i := 0;
        //index := 0;
        //count := 0;
        rs := "";
        //var c rune='
    ';
        //var c4 rune = ''; //判断第4个字符//golang 似乎不支持这种表示
        
        mline := "";
    
        for i=0; i<50; i++ {
            rs = _RecvLine(); //只收取一行
            
            mline = mline + rs + "
    ";
            
            //printf("
    RecvMCmd:%s
    ", rs->str);
                
            if len(rs)<4 {break;} //长度要足够
            c4 := rs[4-1]; //第4个字符
            //if ('x20' == c4) break; //"xhh" 任意字符 二位十六进制//其实现在的转义符已经扩展得相当复杂,不建议用这个表示空格
            if ' ' == c4 { break;} //第4个字符是空格就表示读取完了//也可以判断 "250[空格]"
        
            
        }//
    
        return mline;
        
    }//
    
    //简单的测试一下 smtp
    func test_smtp() {
        
        //连接
        //gConn, gRead, gWrite = Connect("newbt.net", 25);
        //gConn, gRead, gWrite = Connect("newbt.net", 25);
        gConn, gRead, gWrite = Connect("smtp.163.com", 25);
        
        //收取一行
        line := _RecvLine();
        fmt.Println("recv:" + line);
        
        //解码一下,这样后面的 EHLO 才能有正确的第二个参数
        cmds := DecodeCmd(line, " ");
        domain := cmds[1]; //要从对方的应答中取出域名//空格分开的各个命令参数中的第二个
        
        //发送一个命令
        //SendLine("EHLO"); //163 这样是不行的,一定要有 domain
        SendLine("EHLO" + " " + domain); //domain 要求其实来自 HELO 命令//HELO <SP> <domain> <CRLF>    
        
        //收取多行
        //line = _RecvLine();
        line = RecvMCmd();
        fmt.Println("recv:" + line);
        
        //--------------------------------------------------
        //用 base64 登录 
        SendLine("AUTH LOGIN");    
        
        //收取一行
        line = _RecvLine();
        fmt.Println("recv:" + line);
        
        //s :="test1@newbt.net"; //要换成你的用户名,注意 163 邮箱的话不要带后面的 @域名 部分 
        s :="clq_test"; //要换成你的用户名,注意 163 邮箱的话不要带后面的 @域名 部分 
        s = base64.StdEncoding.EncodeToString([]byte(s));
        //s = base64_encode(s);
        SendLine(s);    
        
        //收取一行
        line = _RecvLine();
        fmt.Println("recv:" + line);
        
    
        s = "123456"; //要换成您的密码 
        //s = base64_encode(s);
        s = base64.StdEncoding.EncodeToString([]byte(s));
        SendLine(s);    
        
        //收取一行
        line = _RecvLine();
        fmt.Println("recv:" + line);
        
        //--------------------------------------------------    
        //邮件内容 
        //from := "test1@newbt.net";
        from := "clq_test@163.com";
        to := "clq@newbt.net";
        
        SendLine("MAIL FROM: <" + from +">"); //注意"<" 符号和前面的空格。空格在协议中有和没有都可能,最好还是有 
        //收取一行
        line = _RecvLine();
        fmt.Println("recv:" + line);        
        
        SendLine("RCPT TO: <" + to+ ">");
        //收取一行
        line = _RecvLine();
        fmt.Println("recv:" + line);        
        
        SendLine("DATA");
        //收取一行
        line = _RecvLine();
        fmt.Println("recv:" + line)        
        
        // = "From: "test1@newbt.net" <test1@newbt.net>
    To: "clq@newbt.net" <clq@newbt.net>
    Subject: test golang
    Date: Sun, 21 Jan 2018 11:48:15 GMT
    
    Hello World.
    ";//邮件内容,正式的应该用一个函数生成 
        s = MakeMail(from,to,"test golang","Hello World.");
        SendLine(s);    
        
        
        s = "
    .
    "; //邮件结束符 
        SendLine(s);
        
        //收取一行
        line = _RecvLine();
        fmt.Println("recv:" + line)        
        
    }//
    
    //这只是个简单的内容,真实的邮件内容复杂得多
    func MakeMail(from,to,subject,text string)(string) {
        //s := "From: "test1@newbt.net" <test1@newbt.net>
    To: "clq@newbt.net" <clq@newbt.net>
    Subject: test golang
    Date: Sun, 21 Jan 2018 11:48:15 GMT
    
    Hello World.
    ";//邮件内容,正式的应该用一个函数生成 
        s := "From: "" + from + ""
    To: "" + to + "" " + to + "
    Subject: " + subject + 
            "
    Date: Sun, 21 Jan 2018 11:48:15 GMT
    
    " + //内容前是两个回车换行
            text + "
    ";
        
        return s;    
    
    }//

    这份代码可以直接使用在 163 的邮箱上,以下是 newbt 邮箱收到的 163 发送的邮件的真实截图:

        不过用到真实环境中,大家要再测试一下超时的情况,可以考虑自己加点超时,然后生成邮件的时间那里注意一下就差不多了。我后面还会给出一个直接修改自 go 源码的示例。


        既然说到了 gmail 是要 ssl 支持的,那么怎样开发一个支持 ssl 的发送过程呢。其实一点也不难,ssl 不过是 socket 过程的加密版本而已,有兴趣的同学大家可以看我的如下几篇文章:
    OpenSSL解惑1:原理 https://baijiahao.baidu.com/s?id=1591824116725476286&wfr=spider&for=pc

    OpenSSL解惑2:如何强制选择协议的版本 https://baijiahao.baidu.com/s?id=1591912273348927453&wfr=spider&for=pc

    OpenSSL解惑3:SSL_read的阻塞超时与它是否等同于recv函 https://baijiahao.baidu.com/s?id=1592012048270657934&wfr=spider&for=pc

    (我个人觉得第一二篇比较重要,百家号目前的问题比较多,链接代码什么的都不太好处理,大家将就看吧)

        总的来说,增加了 ssl 连接过程后再将 recv 函数变成 SSL_read,send 函数变成 SS_send 就可以了。对 gmail 的支持很多年前我就在 eEmail 中实现过了,不过现在 ssl 升级得比较多,估计也不能用了吧,以后等 gmail 能在国内访问了再修改了。关于协议升级的问题大家也可以看上面的提到的几篇 openssl 文章。如果使用 openssl 开发的话,要支持新的协议是非常简单的,用 golang 的话那就更不用说了。ssl 的示例,因为实在来不及了,所以以后再给出吧。

        另外,虽然我们没有给出 java 的示例,不过有了前面的几篇文章为基础相信大家可以轻松地写出来。

        再另外,我看过网友们对 golang smtp 的改造,说句不客气的,大部分都不太正确,具体原因前面已经说了过。我的做法是直接修改 golang 的源码,幸好 goalng 和以前的 delphi 一样可以方便地在不改动源码的情况下使用修改出来的另一份拷贝。我的方法是复制出来然后改掉 package 就可以了,以下是我修改过的 go1.7.3 的 smtp 源码,可以直接使用的,修改的地方其实很少,已经在 newbt 邮箱和 163 邮箱上使用过。调用的方法如下(其中 loginAuth  来自网络,不过实际上是有误的):

    type loginAuth struct {
        username, password string
    }
    
    func LoginAuth(username, password string) smtp.Auth {
        return &loginAuth{username, password}
    }
    
    func (a *loginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) {
        //return "LOGIN", []byte{}, nil
        //return "LOGIN", []byte(a.username), nil //clq 原作者说这个就 ok, 其实那只是对 163 邮箱, 大多数邮件服务器还要改 smtp.go 文件本身
        //return "LOGIN
    ", []byte(a.username), nil
        return "LOGIN", []byte{}, nil
    }
    
    //clq 这个步骤会一直被调用直到成功或者错误
    func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
        
        fmt.Println("smtp server:", string(fromServer));
        
        s_fromServer := strings.ToLower(string(fromServer)); 
        
        if more {
            //switch string(fromServer) {
            switch s_fromServer {
            //case "Username:":
            case "username:":
                return []byte(a.username), nil
            //case "Password:":
            case "password:":
                return []byte(a.password), nil
            }//switch
        }//if
        return nil, nil
    }//
    
    func SendMail_t1() {
        auth := LoginAuth("test1@newbt.net", "123456");
        to := []string{"clq@newbt.net"}
    
        mimes := //这里写上邮件内容
        err2 := smtp_SendMail_new("newbt.net:25", auth, "clq@newbt.net", to, []byte(mimes));
        
        fmt.Println(err);
    }//

    新的文件名为 smtp_new.go ,内容如下(我其实还是推荐大家自己改前面的代码,那样更好把握,所以这份代码我默认折叠了):

      1 // Copyright 2010 The Go Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style
      3 // license that can be found in the LICENSE file.
      4 
      5 // Package smtp implements the Simple Mail Transfer Protocol as defined in RFC 5321.
      6 // It also implements the following extensions:
      7 //    8BITMIME  RFC 1652
      8 //    AUTH      RFC 2554
      9 //    STARTTLS  RFC 3207
     10 // Additional extensions may be handled by clients.
     11 //
     12 // The smtp package is frozen and not accepting new features.
     13 // Some external packages provide more functionality. See:
     14 //
     15 //   https://godoc.org/?q=smtp
     16 //package smtp
     17 package main //clq
     18 
     19 //用于不加密环境的 smtp 发送电子邮件过程,因为不是所有的 smtp 环境都有加密支持的,不加密的适用范围更广一点,而且 smtp 服务器之间没有密码加密的过程
     20 //(因为对方 smtp 服务器不可能知道你的密码)所以原有的 golang 1.7.3 net/smtp 过程就不适合 smtp 之间发送邮件
     21 //修改自 smtp.go[1.7.3] , 原始文件为 go1.7.3 的 net/smtp
     22 //原文件在 code, msg64, err := c.cmd(0, "AUTH %s %s", mech, resp64) 处有误,因为第一个 AUTH 后面只有一个字符(指不加密的情况下) 
     23 
     24 import (
     25     "crypto/tls"
     26     "encoding/base64"
     27     "errors"
     28     "io"
     29     "net"
     30     "net/smtp" //clq add
     31     "net/textproto"
     32     "strings"
     33 )
     34 
     35 // A Client represents a client connection to an SMTP server.
     36 type Client struct {
     37     // Text is the textproto.Conn used by the Client. It is exported to allow for
     38     // clients to add extensions.
     39     Text *textproto.Conn
     40     // keep a reference to the connection so it can be used to create a TLS
     41     // connection later
     42     conn net.Conn
     43     // whether the Client is using TLS
     44     tls        bool
     45     serverName string
     46     // map of supported extensions
     47     ext map[string]string
     48     // supported auth mechanisms
     49     auth       []string
     50     localName  string // the name to use in HELO/EHLO
     51     didHello   bool   // whether we've said HELO/EHLO
     52     helloError error  // the error from the hello
     53 }
     54 
     55 // Dial returns a new Client connected to an SMTP server at addr.
     56 // The addr must include a port, as in "mail.example.com:smtp".
     57 func Dial(addr string) (*Client, error) {
     58     conn, err := net.Dial("tcp", addr)
     59     if err != nil {
     60         return nil, err
     61     }
     62     host, _, _ := net.SplitHostPort(addr)
     63     return NewClient(conn, host)
     64 }
     65 
     66 // NewClient returns a new Client using an existing connection and host as a
     67 // server name to be used when authenticating.
     68 func NewClient(conn net.Conn, host string) (*Client, error) {
     69     text := textproto.NewConn(conn)
     70     _, _, err := text.ReadResponse(220)
     71     if err != nil {
     72         text.Close()
     73         return nil, err
     74     }
     75     c := &Client{Text: text, conn: conn, serverName: host, localName: "localhost"}
     76     return c, nil
     77 }
     78 
     79 // Close closes the connection.
     80 func (c *Client) Close() error {
     81     return c.Text.Close()
     82 }
     83 
     84 // hello runs a hello exchange if needed.
     85 func (c *Client) hello() error {
     86     if !c.didHello {
     87         c.didHello = true
     88         err := c.ehlo()
     89         if err != nil {
     90             c.helloError = c.helo()
     91         }
     92     }
     93     return c.helloError
     94 }
     95 
     96 // Hello sends a HELO or EHLO to the server as the given host name.
     97 // Calling this method is only necessary if the client needs control
     98 // over the host name used. The client will introduce itself as "localhost"
     99 // automatically otherwise. If Hello is called, it must be called before
    100 // any of the other methods.
    101 func (c *Client) Hello(localName string) error {
    102     if c.didHello {
    103         return errors.New("smtp: Hello called after other methods")
    104     }
    105     c.localName = localName
    106     return c.hello()
    107 }
    108 
    109 // cmd is a convenience function that sends a command and returns the response
    110 func (c *Client) cmd(expectCode int, format string, args ...interface{}) (int, string, error) {
    111     id, err := c.Text.Cmd(format, args...)
    112     if err != nil {
    113         return 0, "", err
    114     }
    115     c.Text.StartResponse(id)
    116     defer c.Text.EndResponse(id)
    117     code, msg, err := c.Text.ReadResponse(expectCode)
    118     return code, msg, err
    119 }
    120 
    121 // helo sends the HELO greeting to the server. It should be used only when the
    122 // server does not support ehlo.
    123 func (c *Client) helo() error {
    124     c.ext = nil
    125     _, _, err := c.cmd(250, "HELO %s", c.localName)
    126     return err
    127 }
    128 
    129 // ehlo sends the EHLO (extended hello) greeting to the server. It
    130 // should be the preferred greeting for servers that support it.
    131 func (c *Client) ehlo() error {
    132     _, msg, err := c.cmd(250, "EHLO %s", c.localName)
    133     if err != nil {
    134         return err
    135     }
    136     ext := make(map[string]string)
    137     extList := strings.Split(msg, "
    ")
    138     if len(extList) > 1 {
    139         extList = extList[1:]
    140         for _, line := range extList {
    141             args := strings.SplitN(line, " ", 2)
    142             if len(args) > 1 {
    143                 ext[args[0]] = args[1]
    144             } else {
    145                 ext[args[0]] = ""
    146             }
    147         }
    148     }
    149     if mechs, ok := ext["AUTH"]; ok {
    150         c.auth = strings.Split(mechs, " ")
    151     }
    152     c.ext = ext
    153     return err
    154 }
    155 
    156 // StartTLS sends the STARTTLS command and encrypts all further communication.
    157 // Only servers that advertise the STARTTLS extension support this function.
    158 func (c *Client) StartTLS(config *tls.Config) error {
    159     if err := c.hello(); err != nil {
    160         return err
    161     }
    162     _, _, err := c.cmd(220, "STARTTLS")
    163     if err != nil {
    164         return err
    165     }
    166     c.conn = tls.Client(c.conn, config)
    167     c.Text = textproto.NewConn(c.conn)
    168     c.tls = true
    169     return c.ehlo()
    170 }
    171 
    172 // TLSConnectionState returns the client's TLS connection state.
    173 // The return values are their zero values if StartTLS did
    174 // not succeed.
    175 func (c *Client) TLSConnectionState() (state tls.ConnectionState, ok bool) {
    176     tc, ok := c.conn.(*tls.Conn)
    177     if !ok {
    178         return
    179     }
    180     return tc.ConnectionState(), true
    181 }
    182 
    183 // Verify checks the validity of an email address on the server.
    184 // If Verify returns nil, the address is valid. A non-nil return
    185 // does not necessarily indicate an invalid address. Many servers
    186 // will not verify addresses for security reasons.
    187 func (c *Client) Verify(addr string) error {
    188     if err := c.hello(); err != nil {
    189         return err
    190     }
    191     _, _, err := c.cmd(250, "VRFY %s", addr)
    192     return err
    193 }
    194 
    195 // Auth authenticates a client using the provided authentication mechanism.
    196 // A failed authentication closes the connection.
    197 // Only servers that advertise the AUTH extension support this function.
    198 func (c *Client) Auth(a smtp.Auth) error {
    199     if err := c.hello(); err != nil {
    200         return err
    201     }
    202     encoding := base64.StdEncoding
    203     mech, resp, err := a.Start(&smtp.ServerInfo{c.serverName, c.tls, c.auth})
    204     if err != nil {
    205         c.Quit()
    206         return err
    207     }
    208     resp64 := make([]byte, encoding.EncodedLen(len(resp)))
    209     encoding.Encode(resp64, resp)
    210     //code, msg64, err := c.cmd(0, "AUTH %s %s", mech, resp64); //clq 这里有误,标准就应该是先 'AUTH LOGIN' 后面并没有其他内容
    211     code, msg64, err := c.cmd(0, "AUTH %s", mech); //clq 这里有误,标准就应该是先 'AUTH LOGIN' 后面并没有其他内容//对于不加密的来说
    212     for err == nil {
    213         var msg []byte
    214         switch code {
    215         case 334:
    216             msg, err = encoding.DecodeString(msg64)
    217         case 235:
    218             // the last message isn't base64 because it isn't a challenge
    219             msg = []byte(msg64); //clq 对于不加密的来说这里一般就是返回登录成功了
    220         default:
    221             err = &textproto.Error{Code: code, Msg: msg64}
    222         }
    223         if err == nil {
    224             resp, err = a.Next(msg, code == 334)
    225         }
    226         if err != nil {
    227             // abort the AUTH
    228             c.cmd(501, "*")
    229             c.Quit()
    230             break
    231         }
    232         if resp == nil {
    233             break
    234         }
    235         resp64 = make([]byte, encoding.EncodedLen(len(resp)))
    236         encoding.Encode(resp64, resp)
    237         code, msg64, err = c.cmd(0, string(resp64))
    238     }
    239     return err
    240 }
    241 
    242 // Mail issues a MAIL command to the server using the provided email address.
    243 // If the server supports the 8BITMIME extension, Mail adds the BODY=8BITMIME
    244 // parameter.
    245 // This initiates a mail transaction and is followed by one or more Rcpt calls.
    246 func (c *Client) Mail(from string) error {
    247     if err := c.hello(); err != nil {
    248         return err
    249     }
    250     cmdStr := "MAIL FROM:<%s>"
    251     if c.ext != nil {
    252         if _, ok := c.ext["8BITMIME"]; ok {
    253             cmdStr += " BODY=8BITMIME"
    254         }
    255     }
    256     _, _, err := c.cmd(250, cmdStr, from)
    257     return err
    258 }
    259 
    260 // Rcpt issues a RCPT command to the server using the provided email address.
    261 // A call to Rcpt must be preceded by a call to Mail and may be followed by
    262 // a Data call or another Rcpt call.
    263 func (c *Client) Rcpt(to string) error {
    264     _, _, err := c.cmd(25, "RCPT TO:<%s>", to)
    265     return err
    266 }
    267 
    268 type dataCloser struct {
    269     c *Client
    270     io.WriteCloser
    271 }
    272 
    273 func (d *dataCloser) Close() error {
    274     d.WriteCloser.Close()
    275     _, _, err := d.c.Text.ReadResponse(250)
    276     return err
    277 }
    278 
    279 // Data issues a DATA command to the server and returns a writer that
    280 // can be used to write the mail headers and body. The caller should
    281 // close the writer before calling any more methods on c. A call to
    282 // Data must be preceded by one or more calls to Rcpt.
    283 func (c *Client) Data() (io.WriteCloser, error) {
    284     _, _, err := c.cmd(354, "DATA")
    285     if err != nil {
    286         return nil, err
    287     }
    288     return &dataCloser{c, c.Text.DotWriter()}, nil
    289 }
    290 
    291 var testHookStartTLS func(*tls.Config) // nil, except for tests
    292 
    293 // SendMail connects to the server at addr, switches to TLS if
    294 // possible, authenticates with the optional mechanism a if possible,
    295 // and then sends an email from address from, to addresses to, with
    296 // message msg.
    297 // The addr must include a port, as in "mail.example.com:smtp".
    298 //
    299 // The addresses in the to parameter are the SMTP RCPT addresses.
    300 //
    301 // The msg parameter should be an RFC 822-style email with headers
    302 // first, a blank line, and then the message body. The lines of msg
    303 // should be CRLF terminated. The msg headers should usually include
    304 // fields such as "From", "To", "Subject", and "Cc".  Sending "Bcc"
    305 // messages is accomplished by including an email address in the to
    306 // parameter but not including it in the msg headers.
    307 //
    308 // The SendMail function and the the net/smtp package are low-level
    309 // mechanisms and provide no support for DKIM signing, MIME
    310 // attachments (see the mime/multipart package), or other mail
    311 // functionality. Higher-level packages exist outside of the standard
    312 // library.
    313 //func SendMail(addr string, a smtp.Auth, from string, to []string, msg []byte) error {
    314 func smtp_SendMail_new(addr string, a smtp.Auth, from string, to []string, msg []byte) error { //clq  这段注释是说, msg 包含了 mime 的所有内容
    315     c, err := Dial(addr)
    316     if err != nil {
    317         return err
    318     }
    319     defer c.Close()
    320     if err = c.hello(); err != nil {
    321         return err
    322     }
    323     if ok, _ := c.Extension("STARTTLS"); ok {
    324         config := &tls.Config{ServerName: c.serverName}
    325         if testHookStartTLS != nil {
    326             testHookStartTLS(config)
    327         }
    328         if err = c.StartTLS(config); err != nil {
    329             return err
    330         }
    331     }
    332     if a != nil && c.ext != nil {
    333         if _, ok := c.ext["AUTH"]; ok {
    334             if err = c.Auth(a); err != nil { //clq c.Auth(a) 这个也有交互过程
    335                 return err
    336             }
    337         }
    338     }
    339     if err = c.Mail(from); err != nil {
    340         return err
    341     }
    342     for _, addr := range to {
    343         if err = c.Rcpt(addr); err != nil {
    344             return err
    345         }
    346     }
    347     w, err := c.Data()
    348     if err != nil {
    349         return err
    350     }
    351     _, err = w.Write(msg)
    352     if err != nil {
    353         return err
    354     }
    355     err = w.Close()
    356     if err != nil {
    357         return err
    358     }
    359     return c.Quit()
    360 }
    361 
    362 // Extension reports whether an extension is support by the server.
    363 // The extension name is case-insensitive. If the extension is supported,
    364 // Extension also returns a string that contains any parameters the
    365 // server specifies for the extension.
    366 func (c *Client) Extension(ext string) (bool, string) {
    367     if err := c.hello(); err != nil {
    368         return false, ""
    369     }
    370     if c.ext == nil {
    371         return false, ""
    372     }
    373     ext = strings.ToUpper(ext)
    374     param, ok := c.ext[ext]
    375     return ok, param
    376 }
    377 
    378 // Reset sends the RSET command to the server, aborting the current mail
    379 // transaction.
    380 func (c *Client) Reset() error {
    381     if err := c.hello(); err != nil {
    382         return err
    383     }
    384     _, _, err := c.cmd(250, "RSET")
    385     return err
    386 }
    387 
    388 // Quit sends the QUIT command and closes the connection to the server.
    389 func (c *Client) Quit() error {
    390     if err := c.hello(); err != nil {
    391         return err
    392     }
    393     _, _, err := c.cmd(221, "QUIT")
    394     if err != nil {
    395         return err
    396     }
    397     return c.Text.Close()
    398 }
    View Code

        可以看到 golang 的代码是写得真简洁,所以也比较好改。

        一定有网友觉得我发送部分说得太多太久了,好吧,那我们下一篇插播一下 pop3 接收,到时候大家就会明白,其实还有好多内容没讲。

    完整代码大家可以到以下 github 地址下载或查看:    
    https://github.com/clqsrc/c_lib_lstring/tree/master/email_book/book_11

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

    版权声明:

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

  • 相关阅读:
    经常使用的两个清爽的table样式
    wdcp新开站点或绑定域名打不开或无法访问的问题
    在线客服代码,可以用
    $smarty获取变量get,post等用法
    mysql 替换某个字段中的某个字符
    git 设置不需要输入密码
    linux批量查找文件内容
    使用PHP QR Code生成二维码
    RabbitMQ-2
    RabbitMQ-1
  • 原文地址:https://www.cnblogs.com/-clq/p/8446372.html
Copyright © 2011-2022 走看看