跟go語言的net/smtp鬥爭了一天,記錄下歷程。服務器
先用最標準的例子
host := net.JoinHostPort(hostname, port)
auth := smtp.PlainAuth("", username, password, hostname)
to := []string{address}
msg := []byte("To: " +
address +
"\r\n" +
"Subject:" +
title +
"\r\n" +
"\r\n" +
content +
"\r\n")
err := smtp.SendMail(host, auth, from, to, msg)
程序持續報一個 unencrypted connection 的錯誤。原來新版本的smtp爲了防止密碼以明文傳輸,強制以SSL鏈接發送郵件。但我手上的服務器沒有SSL鏈接,只好去庫裏看在哪兒作的判斷,找到auth.go裏面func Start()中的這樣一段話
if !server.TLS {
advertised := false
for _, mechanism := range server.Auth {
if mechanism == "PLAIN" {
advertised = true
break
}
}
if !advertised {
return "", nil, errors.New("unencrypted connection")
}
}
看樣子判斷是在這裏進行的了。在網上找到一個假裝TLS連接的方法。
首先,在代碼里加上
/*use unSSL to link mail server*/
type unencryptedAuth struct {
smtp.Auth
}
func (a unencryptedAuth) Start(server *smtp.ServerInfo) (string, []byte, error) {
s := *server
s.TLS = true
login, resp, th := a.Auth.Start(&s)
return "LOGIN", resp, th
}
將TLS的值設爲true, 發郵件部分這樣寫
auth := unencryptedAuth {
smtp.PlainAuth(
"",
username,
password,
hostname,
)
}
err := smtp.SendMail(host, auth, from, to, msg)
這樣連接成立了,報的錯誤變成 unrecognized authentication type. 查到func Start() 的返回值爲
return "PLAIN", resp, nil
原來這裏強制以plain登錄。參考前人的方法修改思路,重寫Start方法
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", nil, nil
}
func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
command := string(fromServer)
command = strings.TrimSpace(command)
command = strings.TrimSuffix(command, ":")
command = strings.ToLower(command)
if more {
if (command == "username") {
return []byte(fmt.Sprintf("%s", a.username)), nil
} else if (command == "password") {
return []byte(fmt.Sprintf("%s", a.password)), nil
} else {
// We've already sent everything.
return nil, fmt.Errorf("unexpected server challenge: %s", command)
}
}
return nil, nil
}
Login的認證方式協議和Plain不一樣,因此Next方法也重寫了,否則報那個unexpected server challenge的錯誤,這樣就能順利地使用用戶名和密碼認證,發郵件的認證部分這樣寫:
auth := LoginAuth("username, password)
如此一來就能夠成功發送郵件了。
可是當我換用另外一臺郵件服務器時,又出現了certificate signed by unknown authority
部署到服務器上時,錯誤顯示爲cannot validate certificate for 10.11.64.80 because it doesn't contain any IP SANS
總之都是相似於認證的問題。這兩臺服務器的區別是第一臺使用465端口,即smtps,而第二臺使用25端口。
查看smtp.go發現func SendMail()中有這樣一段
if ok, _ := c.Extension("STARTTLS"); ok {
config := &tls.Config{ServerName: c.serverName}
if testHookStartTLS != nil {
testHookStartTLS(config)
}
if err = c.StartTLS(config); err != nil {
return err
}
}
我乾脆把SendMail方法拷出來,去掉這一段判斷,同時smtp裏面涉及到的func和struct都拷出來,寫了一個新的.go,在發郵件的時候直接使用這個新的SendMail。部分原有的公共方法和結構不用拷出,直接以smtp.調用,如此一來就能直接用端口25的那臺服務器發郵件了。
雖然郵件發送成功,可是查看日誌裏總輸出一個錯誤250 Mail OK queued as XXXX,看着很不爽,但這輸出又不像錯誤。按照telnet hostname port後的操做對照SendMail的執行過程。發如今發送DATA指令以後,會收到一個回覆碼354,接收輸入郵件內容,以句號回車結尾後,會再收到一個250的回覆。在代碼中,發送了DATA,收到354,接着發送郵件內容,代碼並未接收這個250。最後發送QUIT,這裏收到的是上一個回覆碼250,和QUIT的正常回復碼221做比較,程序就會返回error。我也不知道哪一個函數能夠只接收回復,簡單起見,乾脆在Quit函數裏發了兩遍QUIT,判斷第一個返回250,第二個返回221,終於再也不報錯。
研究完這個函數,對smtp就從一無所知到至關了解了。另外,要從根本上解決問題,仍是升級爲SSL吧!