一步一步從原理跟我學郵件收取及發送 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("\n", buf);
        index = pos(NewString("\r\n", 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] = '\0'; //直接修改為字符串結束符號,如果是只讀的字符串這樣做其實是不對的,不過效率很高 
            
            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 = '\0'; //判斷第4個字符 
    
    lstring * mline = NewString("", pool);

    
    for (i=0; i<50; i++)
    {
        rs = RecvLine(so, pool, _buf); //只收取一行 
        
        mline->Append(mline, rs);
        LString_AppendConst(mline, "\r\n");
        
        //printf("\r\nRecvMCmd:%s\r\n", 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("連接成功!\r\n");

    
    //--------------------------------------------------
    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);    

    
    ////rs = RecvLine(gSo, m, &buf); //只收取一行 
    rs = RecvMCmd(gSo, m, &buf); //只收取一行 

    printf("\r\nRecvLine:");
    printf(rs->str); printf("\r\n");
    
    //--------------------------------------------------
    //用 base64 登錄 
    s = NewString("AUTH LOGIN\r\n", m);
    SendBuf(gSo, s->str, s->len);    
    
    rs = RecvLine(gSo, m, &buf); //只收取一行 
    printf("\r\nRecvLine:%s\r\n", rs->str); 
    
    s = NewString("test1@newbt.net", m); //要換成你的用戶名,注意 163 郵箱的話不要帶后面的 @域名 部分 
    s = base64_encode(s);
    LString_AppendConst(s,"\r\n");
    SendBuf(gSo, s->str, s->len);    
    
    rs = RecvLine(gSo, m, &buf); //只收取一行 
    printf("\r\nRecvLine:%s\r\n", rs->str); 
    

    s = NewString("123456", m); //要換成您的密碼 
    s = base64_encode(s);
    LString_AppendConst(s,"\r\n");
    SendBuf(gSo, s->str, s->len);    
    
    rs = RecvLine(gSo, m, &buf); //只收取一行 
    printf("\r\nRecvLine:%s\r\n", 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, ">\r\n"); //注意"<" 符號和前面的空格。空格在協議中有和沒有都可能,最好還是有 
    SendBuf(gSo, s->str, s->len);
    rs = RecvLine(gSo, m, &buf); //只收取一行 
    printf("\r\nRecvLine:%s\r\n", rs->str);         
    
    s = NewString("RCPT TO: <", m); s->Append(s, to); s->AppendConst(s, ">\r\n");
    SendBuf(gSo, s->str, s->len);    
    rs = RecvLine(gSo, m, &buf); //只收取一行 
    printf("\r\nRecvLine:%s\r\n", rs->str);     
    
    s = NewString("DATA\r\n", m);
    SendBuf(gSo, s->str, s->len);
    rs = RecvLine(gSo, m, &buf); //只收取一行 
    printf("\r\nRecvLine:%s\r\n", rs->str);         
    
    s = NewString("From: \"test1@newbt.net\" <test1@newbt.net>\r\nTo: \"clq@newbt.net\" <clq@newbt.net>\r\nSubject: test\r\nDate: Sun, 21 Jan 2018 11:48:15 GMT\r\n\r\nHello World.\r\n", m);//郵件內容,正式的應該用一個函數生成 
    SendBuf(gSo, s->str, s->len);    
    
    
    s = NewString("\r\n.\r\n", m); //郵件結束符 
    SendBuf(gSo, s->str, s->len);    
    
    rs = RecvLine(gSo, m, &buf); //只收取一行 
    printf("\r\nRecvLine:%s\r\n", rs->str); 
    

    //--------------------------------------------------
    
    Pool_Free(&mem); //釋放內存池 
    
    printf("gMallocCount:%d \r\n", 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\r\n");
    //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('\n'); //如何設定超時?
    line, err := gRead.ReadString('\n'); //如何設定超時?
    
    if err != nil { return ""; }
    
    line = strings.Split(line, "\r")[0]; //還要再去掉 "\r",其實不去掉也可以
    
    return line;
}//

func SendLine(line string){
    gWrite.WriteString(line + "\r\n");
    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='\r';
    //var c4 rune = '\0'; //判斷第4個字符//golang 似乎不支持這種表示
    
    mline := "";

    for i=0; i<50; i++ {
        rs = _RecvLine(); //只收取一行
        
        mline = mline + rs + "\r\n";
        
        //printf("\r\nRecvMCmd:%s\r\n", 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>\r\nTo: \"clq@newbt.net\" <clq@newbt.net>\r\nSubject: test golang\r\nDate: Sun, 21 Jan 2018 11:48:15 GMT\r\n\r\nHello World.\r\n";//郵件內容,正式的應該用一個函數生成 
    s = MakeMail(from,to,"test golang","Hello World.");
    SendLine(s);    
    
    
    s = "\r\n.\r\n"; //郵件結束符 
    SendLine(s);
    
    //收取一行
    line = _RecvLine();
    fmt.Println("recv:" + line)        
    
}//

//這只是個簡單的內容,真實的郵件內容復雜得多
func MakeMail(from,to,subject,text string)(string) {
    //s := "From: \"test1@newbt.net\" <test1@newbt.net>\r\nTo: \"clq@newbt.net\" <clq@newbt.net>\r\nSubject: test golang\r\nDate: Sun, 21 Jan 2018 11:48:15 GMT\r\n\r\nHello World.\r\n";//郵件內容,正式的應該用一個函數生成 
    s := "From: \"" + from + "\"\r\nTo: \"" + to + "\" " + to + "\r\nSubject: " + subject + 
        "\r\nDate: Sun, 21 Jan 2018 11:48:15 GMT\r\n\r\n" + //內容前是兩個回車換行
        text + "\r\n";
    
    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\r\n", []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, "\n")
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的程序員學前班" .

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM