之前我写过 python邮件发送模块 ,里面有提到发送带附件的邮件,当时使用python自带的email 模块里已经封装好的包,直接引用即可。在golang下,原生的也有三个对应模块”mime”、”net/mail”、”net/smtp” ,不过这三个模块使用时,不能直接像python里那样简单的套用即可。golang下需要按照smtp协议的规范—RFC531 和MIME规范来生成相应的格式并发出去。

一、有关MIME规范

1、MIME规范

多用途互联网邮件扩展(MIME,Multipurpose Internet Mail Extensions)是一个互联网标准,它扩展了电子邮件标准,使其能够支持:

1非ASCII字符文本;
2非文本格式附件(二进制、声音、图像等);
3由多部分(multiple parts)组成的消息体;
4包含非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、带附件邮件发送格式

其发送格式如下:

 1From:sender_user@361way.com
 2To:to_user@361way.com
 3Subject:这是主题
 4Mime-Version:1.0 //通常是1.0
 5Content-Type:Multipart/mixed;Boundary="THIS_IS_BOUNDARY_JUST_MAKE_YOURS" //boundary为分界字符,跟http传文件时类似
 6Date:当前时间
 7--THIS_IS_BOUNDARY_JUST_MAKE_YOURS         //boundary前边需要加上连接符 -- , 首部和第一个boundary之间有两个空行
 8Content-Type:text/plain;chart-set=utf-8
 9                                            //单个部分的首部和正文间有个空行
10这是正文1
11这是正文2
12--THIS_IS_BOUNDARY_JUST_MAKE_YOURS                  //每个部分的与上一部分之间一个空行
13Content-Type:image/jpg;name="test.jpg"
14Content-Transfer-Encoding:base64
15Content-Description:这个是描述
16                                            //单个部分的首部和正文间有个空行
17base64编码的文件                              //文件内容使用base64 编码,单行不超过80字节,需要插入\r\n进行换行
18--THIS_IS_BOUNDARY_JUST_MAKE_YOURS--        //最后结束的标识--boundary--

具体也可以参看你自己foxmail 邮件中查看源代码的信息,如下图:

mime-email
mime-email

上图是我从一封邮件里提取了部分信息,有兴趣的可以自上而下看下全部信息。

格式书写时,需要注意以下几个问题:

11、头部写的boundary 在下边用的时候要拼上--前缀
22、含有附件的,第一个boundary 和 header 中间有两个空行
33、结束标记为 --boundary--
44、循环文件内容进行插入换行时,如果出现逻辑错误,则传输的附件显示异常,无法正常查看和下载。

二、golang实现

根据网上找到的代码,修改后的代码示例如下:

 1package main
 2import (
 3    "bytes"
 4    "encoding/base64"
 5    "fmt"
 6    "io/ioutil"
 7    "net"
 8    "net/smtp"
 9    "strings"
10)
11const (
12    emlUser = "xcl@xxx.com"
13    emlPwd  = "-------"
14    emlSMTP = "smtp.xxx.com:25"
15)
16func main() {
17    err := eml()
18    if err != nil {
19        fmt.Println(" err:", err)
20    }
21}
22// 发普通文本邮件
23func eml() error {
24    to := "xcl@xxx.com"
25    cc := "cc@xxx.com"
26    sendTo := strings.Split(to, ";")
27    subject := "test mail for 361way.com"
28    boundary := "ds13difsknfsifuere134" //boundary 用于分割邮件内容,可自定义. 注意它的开始和结束格式
29    mime := bytes.NewBuffer(nil)
30    //设置邮件
31    mime.WriteString(fmt.Sprintf("From: %s\r\nTo: %s\r\nCC: %s\r\nSubject: %s\r\nMIME-Version: 1.0\r\n", emlUser, emlUser, to, cc, subject))
32    mime.WriteString(fmt.Sprintf("Content-Type: multipart/mixed; boundary=%s\r\n", boundary))
33    mime.WriteString("Content-Description: 这是一封带附档的邮件\r\n")
34    //邮件普通Text正文
35    mime.WriteString(fmt.Sprintf("--%s\r\n", boundary))
36    mime.WriteString("Content-Type: text/plain; charset=utf-8\r\n")
37    mime.WriteString("This is a multipart message in MIME format.")
38    // 第一个附件
39    attaFile := "/Users/xcl/xclcode/tconv.go"
40    attaFileName := "tconv.go"
41    mime.WriteString(fmt.Sprintf("\n--%s\r\n", boundary))
42    mime.WriteString("Content-Type: application/octet-stream\r\n")
43    mime.WriteString("Content-Description: 附一个Go文件\r\n")
44    mime.WriteString("Content-Transfer-Encoding: base64\r\n")
45    mime.WriteString("Content-Disposition: attachment; filename=\"" + attaFileName + "\"\r\n\r\n")
46    //读取并编码文件内容
47    attaData, err := ioutil.ReadFile(attaFile)
48    if err != nil {
49        return err
50    }
51    b := make([]byte, base64.StdEncoding.EncodedLen(len(attaData)))
52    base64.StdEncoding.Encode(b, attaData)
53    mime.Write(b)
54    //第二个附件
55    mime.WriteString(fmt.Sprintf("\r\n--%s\r\n", boundary))
56    mime.WriteString("Content-Type: text/plain\r\n")
57    mime.WriteString("Content-Description: 附一个Text文件\r\n")
58    mime.WriteString("Content-Disposition: attachment; filename=\"test.txt\"\r\n\r\n")
59    mime.WriteString("this is the attachment text") //这里写入的是附件test.txt的内容
60    //邮件结束
61    mime.WriteString("\r\n--" + boundary + "--\r\n\r\n")
62    fmt.Println(mime.String())
63    //发送相关
64    smtpHost, _, err := net.SplitHostPort(emlSMTP)
65    if err != nil {
66        return err
67    }
68    auth := smtp.PlainAuth("", emlUser, emlPwd, smtpHost)
69    return smtp.SendMail(emlSMTP, auth, emlUser, sendTo, mime.Bytes())
70}

三、第三方模块

虽然在golang的原生模块里是没有简洁方便的附件调用发送方法,但在很多第三方模块里都已经很好的进行了封装,这里列这个第三方模块,具体也可以参看下他们实现的方式。

https://github.com/scorredoira/email

https://github.com/jordan-wright/email

https://github.com/go-gomail/gomail/