通過了這個系列的前幾篇文章的學習,如今要寫出一個完整的 smtp 郵件發送過程簡直易如反掌。
例如咱們能夠輕鬆地寫出如下的純 C 語言代碼(引用的其餘C語言文件請看文末的 github 地址):java
#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> }
運行結果如圖:git
好了,咱們用其餘語言也來一個吧。可是這裏有個問題: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 發送的郵件的真實截圖:github
不過用到真實環境中,你們要再測試一下超時的狀況,能夠考慮本身加點超時,而後生成郵件的時間那裏注意一下就差很少了。我後面還會給出一個直接修改自 go 源碼的示例。golang
既然說到了 gmail 是要 ssl 支持的,那麼怎樣開發一個支持 ssl 的發送過程呢。其實一點也不難,ssl 不過是 socket 過程的加密版本而已,有興趣的同窗你們能夠看個人以下幾篇文章:
OpenSSL解惑1:原理 https://baijiahao.baidu.com/s?id=1591824116725476286&wfr=spider&for=pcwindows
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 ,內容以下(我其實仍是推薦你們本身改前面的代碼,那樣更好把握,因此這份代碼我默認摺疊了):dom
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 }
能夠看到 golang 的代碼是寫得真簡潔,因此也比較好改。
必定有網友以爲我發送部分說得太多過久了,好吧,那咱們下一篇插播一下 pop3 接收,到時候你們就會明白,其實還有好多內容沒講。
完整代碼你們能夠到如下 github 地址下載或查看:
https://github.com/clqsrc/c_lib_lstring/tree/master/email_book/book_11
--------------------------------------------------
版權聲明:
本系列文章已受權百家號 "clq的程序員學前班" .