文章出處

山坡網需要能夠每周給注冊用戶發送一封名為“本周最熱書籍”的郵件,而之前一直使用的騰訊企業郵箱罷工了,提示說發送請求太多太密集。

一番尋找之后發現了大家口碑不錯的搜狐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 {
        advertised := false
        for _, mechanism := range server.Auth {
            if mechanism == "PLAIN" {
                advertised = true
                break
            }
        }
        if !advertised {
            return "", nil, errors.New("unencrypted connection")
        }
    }
    if server.Name != a.host {
        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抓包看看過程。

V75FH7RLZ0RB$I`[$78{(_W

注意看,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的簡單,標準庫沒有黑盒子一樣的厚重感,薄的一捅就透,一看就懂。


文章列表


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()