Go语言实战 - 使用SendCloud群发邮件

zhenxing1114 · · 6315 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

转自:http://www.kuqin.com/shuoit/20140303/338368.html

山坡网需要能够每周给注册用户发送一封名为“本周最热书籍”的邮件,而之前一直使用的腾讯企业邮箱罢工了,提示说发送请求太多太密集。

一番寻找之后发现了大家口碑不错的搜狐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的简单,标准库没有黑盒子一样的厚重感,薄的一捅就透,一看就懂。


有疑问加站长微信联系(非本文作者)

本文来自:博客园

感谢作者:zhenxing1114

查看原文:Go语言实战 - 使用SendCloud群发邮件

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

6315 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传