山坡網需要能夠每周給注冊用戶發送一封名為“本周最熱書籍”的郵件,而之前一直使用的騰訊企業郵箱罷工了,提示說發送請求太多太密集。
一番尋找之后發現了大家口碑不錯的搜狐SendCloud服務,看了看文檔,價格實惠用起來也方便,于是準備使用它做郵件發送服務器。按照文檔的配置一步步走下來發現在發送郵件的時候竟然出錯了,錯誤提示是“unencrypted connection”,奇怪了。
由于用的是smtp包的PLAIN認證方式,所以打開源代碼看了看(SublimeText3+GoSublime里ctrl+. ctrl+a輸入包名和結構名直接查看源代碼,誰用誰喜歡),發現這里要求使用加密連接,否則就會出上述錯誤。恩,也能理解,畢竟這里明文發送密碼了。關鍵代碼如下。
auth := smtp.PlainAuth("", Config.Username, Config.Password, Config.Host)
smtp.SendMail(addr, auth, from, to, []byte(self.String()))
問題明白之后思路也出來了,自己寫一個不需要加密鏈接的PLAIN認證就好了。這里提一下smtp包的設計,看下面這段代碼。
// Auth is implemented by an SMTP authentication mechanism.
type Auth interface {
// Start begins an authentication with a server.
// It returns the name of the authentication protocol
// and optionally data to include in the initial AUTH message
// sent to the server. It can return proto == "" to indicate
// that the authentication should be skipped.
// If it returns a non-nil error, the SMTP client aborts
// the authentication attempt and closes the connection.
Start(server *ServerInfo) (proto string, toServer []byte, err error)// Next continues the authentication. The server has just sent
// the fromServer data. If more is true, the server expects a
// response, which Next should return as toServer; otherwise
// Next should return toServer == nil.
// If Next returns a non-nil error, the SMTP client aborts
// the authentication attempt and closes the connection.
Next(fromServer []byte, more bool) (toServer []byte, err error)
}
// SendMail connects to the server at addr, switches to TLS if
// possible, authenticates with the optional mechanism a if possible,
// and then sends an email from address from, to addresses to, with
// message msg.
func SendMail(addr string, a Auth, from string, to []string, msg []byte) error
smtp.SendMail的第二個參數是一個Auth接口,用來實現多種認證方式。標準庫中實現了兩種認證方式,PLAIN和CRAMMD5Auth,關于這部分知識大家可以自行參考smtp協議中認證部分的定義。這里就不贅述了。
搞清楚了原理就動手吧。直接把標準庫中PLAIN的實現拿過來,刪除其中需要加密函數的部分,如下紅字部分。
type plainAuth struct {
identity, username, password string
host string
}
func UnEncryptedPlainAuth(identity, username, password, host string) Auth {
return &plainAuth{identity, username, password, host}
}func (a *plainAuth) Start(server *ServerInfo) (string, []byte, error) {
if !server.TLS {if server.Name != a.host {
advertised := false
for _, mechanism := range server.Auth {
if mechanism == "PLAIN" {
advertised = true
break
}
}
if !advertised {
return "", nil, errors.New("unencrypted connection")
}
}
return "", nil, errors.New("wrong host name")
}
resp := []byte(a.identity + "\x00" + a.username + "\x00" + a.password)
return "PLAIN", resp, nil
}func (a *plainAuth) Next(fromServer []byte, more bool) ([]byte, error) {
return nil, nil
}
把發送郵件的代碼改成下面這樣,再試試看。
auth := UnEncryptedPlainAuth("", Config.Username, Config.Password, Config.Host)
smtp.SendMail(addr, auth, from, to, []byte(self.String()))
恩,還是出錯,這次的錯誤變成“unrecognized command”,看來是SendCloud的服務器并不支持這種驗證方式。于是我打開它的文檔,發現smtp使用介紹的頁面有幾種語言的范例代碼,看了看Python的代碼后發現SendCloud應該用的是Login認證。好吧,之前是犯了經驗主義錯誤了。
再次打開smtp協議的定義,翻到WikiPedia上smtp的(這里標紅是因為wiki上的文檔也是會過期的)LOGIN認證的文檔,上面說,采用LOGIN認證服務器和客戶端應該會產生如下對話,下面S代表服務器,C代表客戶端。
C:auth login ------------------------------------------------- 進行用戶身份認證
S:334 VXNlcm5hbWU6 ----------------------------------- BASE64編碼“Username:”
C:Y29zdGFAYW1heGl0Lm5ldA== ----------------------------------- 用戶名,使用BASE64編碼
S:334 UGFzc3dvcmQ6 -------------------------------------BASE64編碼"Password:"
C:MTk4MjIxNA== ----------------------------------------------- 密碼,使用BASE64編碼
S:235 auth successfully -------------------------------------- 身份認證成功
看起來挺簡單,照著寫了一個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
}func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
if more {
switch string(fromServer) {
case "Username:":
return []byte(a.username), nil
case "Password:":
return []byte(a.password), nil
}
}
return nil, nil
}
把發送郵件的代碼改成下面這樣。
auth := LoginAuth(Config.Username, Config.Password)
smtp.SendMail(addr, auth, from, to, []byte(self.String()))
運行,還報錯,這次錯誤信息是 Authentication Failed,認證失敗。這說明Login認證的方式是對的,但登錄失敗了。再三確定賬號和密碼的正確之后我決定用WireShark抓包看看過程。
注意看,AUTH LOGIN之后來了兩條334 Password,咦?這里不應該是先來Username接著來Password的嗎?為什么是來了兩次Password。難道是LOGIN協議改了?
為了確認登陸過程,我用SendCloud文檔中Python的代碼跑了一遍,終于發現了不同。原來,在發送AUTH LOGIN之后需要帶上Username。修改LoginAuth的Start函數。
func (a *loginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) {
return "LOGIN", []byte{}, nil
return "LOGIN", []byte(a.username), nil
}
好了!郵件發送成功!大約花了30分鐘,我從一個完全不懂SMTP協議的人完成了LOGIN協議的補充。感嘆一下Go的簡單,標準庫沒有黑盒子一樣的厚重感,薄的一捅就透,一看就懂。
文章列表