之前我写过 python邮件发送模块 ,里面有提到发送带附件的邮件,当时使用python自带的email 模块里已经封装好的包,直接引用即可。在golang下,原生的也有三个对应模块”mime”、”net/mail”、”net/smtp” ,不过这三个模块使用时,不能直接像python里那样简单的套用即可。golang下需要按照smtp协议的规范—RFC531 和MIME规范来生成相应的格式并发出去。
一、有关MIME规范
1、MIME规范
多用途互联网邮件扩展(MIME,Multipurpose Internet Mail Extensions)是一个互联网标准,它扩展了电子邮件标准,使其能够支持:
非ASCII字符文本; 非文本格式附件(二进制、声音、图像等); 由多部分(multiple parts)组成的消息体; 包含非ASCII字符的头信息(Header information)。
这个标准被定义在RFC 2045、RFC 2046、RFC 2047、RFC 2048、RFC 2049等RFC中。MIME改善了由RFC 822转变而来的RFC 2822,这些旧标准规定电子邮件标准并不允许在邮件消息中使用7位ASCII字符集以外的字符。正因如此,一些非英语字符消息和二进制文件,图像,声音等非文字消息原本都不能在电子邮件中传输(MIME可以)。MIME规定了用于表示各种各样的数据类型的符号化方法。此外,在万维网中使用的HTTP协议中也使用了MIME的框架,标准被扩展为互联网媒体类型。
2、带附件邮件发送格式
其发送格式如下:
From:sender_user@361way.com To:to_user@361way.com Subject:这是主题 Mime-Version:1.0 //通常是1.0 Content-Type:Multipart/mixed;Boundary="THIS_IS_BOUNDARY_JUST_MAKE_YOURS" //boundary为分界字符,跟http传文件时类似 Date:当前时间 --THIS_IS_BOUNDARY_JUST_MAKE_YOURS //boundary前边需要加上连接符 -- , 首部和第一个boundary之间有两个空行 Content-Type:text/plain;chart-set=utf-8 //单个部分的首部和正文间有个空行 这是正文1 这是正文2 --THIS_IS_BOUNDARY_JUST_MAKE_YOURS //每个部分的与上一部分之间一个空行 Content-Type:image/jpg;name="test.jpg" Content-Transfer-Encoding:base64 Content-Description:这个是描述 //单个部分的首部和正文间有个空行 base64编码的文件 //文件内容使用base64 编码,单行不超过80字节,需要插入\r\n进行换行 --THIS_IS_BOUNDARY_JUST_MAKE_YOURS-- //最后结束的标识--boundary--
具体也可以参看你自己foxmail 邮件中查看源代码的信息,如下图:
上图是我从一封邮件里提取了部分信息,有兴趣的可以自上而下看下全部信息。
格式书写时,需要注意以下几个问题:
1、头部写的boundary 在下边用的时候要拼上--前缀 2、含有附件的,第一个boundary 和 header 中间有两个空行 3、结束标记为 --boundary-- 4、循环文件内容进行插入换行时,如果出现逻辑错误,则传输的附件显示异常,无法正常查看和下载。
二、golang实现
根据网上找到的代码,修改后的代码示例如下:
package main import ( "bytes" "encoding/base64" "fmt" "io/ioutil" "net" "net/smtp" "strings" ) const ( emlUser = "xcl@xxx.com" emlPwd = "-------" emlSMTP = "smtp.xxx.com:25" ) func main() { err := eml() if err != nil { fmt.Println(" err:", err) } } // 发普通文本邮件 func eml() error { to := "xcl@xxx.com" cc := "cc@xxx.com" sendTo := strings.Split(to, ";") subject := "test mail for 361way.com" boundary := "ds13difsknfsifuere134" //boundary 用于分割邮件内容,可自定义. 注意它的开始和结束格式 mime := bytes.NewBuffer(nil) //设置邮件 mime.WriteString(fmt.Sprintf("From: %s<%s>\r\nTo: %s\r\nCC: %s\r\nSubject: %s\r\nMIME-Version: 1.0\r\n", emlUser, emlUser, to, cc, subject)) mime.WriteString(fmt.Sprintf("Content-Type: multipart/mixed; boundary=%s\r\n", boundary)) mime.WriteString("Content-Description: 这是一封带附档的邮件\r\n") //邮件普通Text正文 mime.WriteString(fmt.Sprintf("--%s\r\n", boundary)) mime.WriteString("Content-Type: text/plain; charset=utf-8\r\n") mime.WriteString("This is a multipart message in MIME format.") // 第一个附件 attaFile := "/Users/xcl/xclcode/tconv.go" attaFileName := "tconv.go" mime.WriteString(fmt.Sprintf("\n--%s\r\n", boundary)) mime.WriteString("Content-Type: application/octet-stream\r\n") mime.WriteString("Content-Description: 附一个Go文件\r\n") mime.WriteString("Content-Transfer-Encoding: base64\r\n") mime.WriteString("Content-Disposition: attachment; filename=\"" + attaFileName + "\"\r\n\r\n") //读取并编码文件内容 attaData, err := ioutil.ReadFile(attaFile) if err != nil { return err } b := make([]byte, base64.StdEncoding.EncodedLen(len(attaData))) base64.StdEncoding.Encode(b, attaData) mime.Write(b) //第二个附件 mime.WriteString(fmt.Sprintf("\r\n--%s\r\n", boundary)) mime.WriteString("Content-Type: text/plain\r\n") mime.WriteString("Content-Description: 附一个Text文件\r\n") mime.WriteString("Content-Disposition: attachment; filename=\"test.txt\"\r\n\r\n") mime.WriteString("this is the attachment text") //这里写入的是附件test.txt的内容 //邮件结束 mime.WriteString("\r\n--" + boundary + "--\r\n\r\n") fmt.Println(mime.String()) //发送相关 smtpHost, _, err := net.SplitHostPort(emlSMTP) if err != nil { return err } auth := smtp.PlainAuth("", emlUser, emlPwd, smtpHost) return smtp.SendMail(emlSMTP, auth, emlUser, sendTo, mime.Bytes()) }
三、第三方模块
虽然在golang的原生模块里是没有简洁方便的附件调用发送方法,但在很多第三方模块里都已经很好的进行了封装,这里列这个第三方模块,具体也可以参看下他们实现的方式。
https://github.com/scorredoira/email