通过官方 net/smtp 包实现一个发送邮件的工具类(支持发送匿名邮件、附件邮件)
可用于发送简单的预警日志等场景
send_mail.go:
```
package mail
import (
"bytes"
"crypto/tls"
"encoding/base64"
"errors"
"fmt"
"math"
"net"
"net/smtp"
"strings"
"time"
)
type MailSmtp struct {
Anonymous bool // 是否匿名发送
SSL bool // 是否 TLS 连接
Addr string // SMTP 地址:端口
Username string
Password string
}
type MailContent struct {
From string
To string
Subject string // 主题
Content string // 正文
AttachName string // 附件名
AttachData []byte // 附件数据
}
// 发送邮件
func SendMail(mailSmtp MailSmtp, mailCont MailContent) {
// 匿名邮件
if mailSmtp.Anonymous {
err := smtp.SendMail(getMxHost(mailCont.To)+":25", nil, mailCont.From, []string{mailCont.To}, makeMimeData(mailCont))
if err != nil {
panic("邮件发送失败 => " + err.Error())
}
return
}
// 需要认证
host, _, err := net.SplitHostPort(mailSmtp.Addr)
if err != nil {
panic(err)
}
auth := smtp.PlainAuth("", mailSmtp.Username, mailSmtp.Password, host)
if mailSmtp.SSL {
// TLS 连接
err := sendMailTLS(mailSmtp.Addr, auth, mailCont.From, []string{mailCont.To}, makeMimeData(mailCont))
if err != nil {
panic("邮件发送失败 => " + err.Error())
}
} else {
// 普通连接
err := smtp.SendMail(mailSmtp.Addr, auth, mailCont.From, []string{mailCont.To}, makeMimeData(mailCont))
if err != nil {
panic("邮件发送失败 => " + err.Error())
}
}
}
// 构造邮件内容
func makeMimeData(mailCont MailContent) []byte {
mimeData := bytes.NewBuffer(nil)
boundary := "----=_Part_" + fmt.Sprintf("%014d", time.Now().Unix())
// 头部
mimeData.WriteString(fmt.Sprintf("From: \"%s\"<%s>\r\n", mailCont.From, mailCont.From))
mimeData.WriteString(fmt.Sprintf("To: \"%s\"<%s>\r\n", mailCont.To, mailCont.To))
mimeData.WriteString(fmt.Sprintf("Subject: =?UTF-8?B?%s?=\r\n", base64ToStr([]byte(mailCont.Subject))))
mimeData.WriteString("Mime-Version: 1.0\r\n")
mimeData.WriteString(fmt.Sprintf("Content-Type: multipart/mixed;charset=\"utf-8\";boundary=\"%s\"\r\n", boundary))
mimeData.WriteString("Content-Transfer-Encoding: 8Bit\r\n")
mimeData.WriteString(fmt.Sprintf("Date: %s\r\n", time.Now()))
mimeData.WriteString("X-Mailer: Go-Mail\r\n\r\n")
mimeData.WriteString("This is a multi-part message in MIME format.\r\n\r\n")
// 正文
mimeData.WriteString(fmt.Sprintf("--%s\r\n", boundary))
mimeData.WriteString("Content-Type: text/html;charset=\"utf-8\"\r\n")
mimeData.WriteString("Content-Transfer-Encoding: base64\r\n\r\n")
mimeData.WriteString(base64TolenCtrl([]byte(mailCont.Content)))
mimeData.WriteString("\r\n\r\n")
// 附件
if mailCont.AttachData != nil {
mimeData.WriteString(fmt.Sprintf("--%s\r\n", boundary))
mimeData.WriteString("Content-Type: application/octet-stream;charset=\"utf-8\"\r\n")
mimeData.WriteString(fmt.Sprintf("Content-Disposition: attachment; filename=\"=?UTF-8?B?%s?=\"\r\n", base64ToStr([]byte(mailCont.AttachName))))
mimeData.WriteString("Content-Transfer-Encoding: base64\r\n\r\n")
mimeData.WriteString(base64TolenCtrl(mailCont.AttachData))
mimeData.WriteString("\r\n\r\n")
}
// 结尾
mimeData.WriteString(fmt.Sprintf("--%s--", boundary))
return mimeData.Bytes()
}
// 控制字符串单行长度
func base64TolenCtrl(data []byte) string {
const maxLen = 70
var buff strings.Builder
encodeStr := base64ToStr(data)
for len(encodeStr) > maxLen {
buff.WriteString(encodeStr[:maxLen] + "\r\n")
encodeStr = encodeStr[maxLen:]
}
buff.WriteString(encodeStr)
return buff.String()
}
// 将数据进行 base64 编码
func base64ToStr(data []byte) string {
return base64.StdEncoding.EncodeToString(data)
}
// 根据邮箱取 MX 记录
func getMxHost(email string) string {
split := strings.Split(email, "@")
if len(split) != 2 {
panic("邮件格式错误 => " + email)
}
mxs, err := net.LookupMX(split[1])
if err != nil {
panic("解析邮箱MX失败 => " + err.Error())
}
if len(mxs) == 0 {
panic("对应邮箱无 MX 记录")
}
// 取优先级最高的记录
host := ""
pref := uint16(math.MaxUint16)
for _, mx := range mxs {
if mx.Pref < pref {
host = mx.Host
pref = mx.Pref
}
}
return host[:len(host)-1]
}
// 发送基于 TLS 的邮件
// 基于 Golang 函数 smtp.SendMail 做了补充,使其支持 TLS 连接
func sendMailTLS(addr string, a smtp.Auth, from string, to []string, msg []byte) error {
if err := validateLine(from); err != nil {
return err
}
for _, recp := range to {
if err := validateLine(recp); err != nil {
return err
}
}
c, err := dialTLS(addr)
if err != nil {
return err
}
defer c.Close()
if err = c.Hello("localhost"); err != nil {
return err
}
if ok, _ := c.Extension("STARTTLS"); ok {
host, _, _ := net.SplitHostPort(addr)
config := &tls.Config{ServerName: host}
if err = c.StartTLS(config); err != nil {
return err
}
}
if a != nil {
if ok, _ := c.Extension("AUTH"); ok {
if err = c.Auth(a); err != nil {
return err
}
}
}
if err = c.Mail(from); err != nil {
return err
}
for _, addr := range to {
if err = c.Rcpt(addr); err != nil {
return err
}
}
w, err := c.Data()
if err != nil {
return err
}
_, err = w.Write(msg)
if err != nil {
return err
}
err = w.Close()
if err != nil {
return err
}
return c.Quit()
}
func dialTLS(addr string) (*smtp.Client, error) {
conn, err := tls.Dial("tcp", addr, nil)
if err != nil {
return nil, err
}
host, _, _ := net.SplitHostPort(addr)
return smtp.NewClient(conn, host)
}
func validateLine(line string) error {
if strings.ContainsAny(line, "\n\r") {
return errors.New("smtp: A line must not contain CR or LF")
}
return nil
}
```
send_mail_test.go:
```
package mail
import (
"testing"
)
func TestSendMail(t *testing.T) {
mailStmp := MailSmtp{
Anonymous: true,
SSL: false,
Addr: "",
Username: "",
Password: "",
}
mailCont := MailContent{
From: "anonymous@anonymous.com",
To: "xxx@qq.com",
Subject: "匿名邮件测试",
Content: "匿名邮件测试
换行",
AttachName: "附件.txt",
AttachData: []byte("附件内容1234567890"),
}
SendMail(mailStmp, mailCont)
}
```
Golang 发送匿名邮件、普通邮件
2020-10-12 13:00:21
分类:编程 关键词:邮件,匿名
评论(0) 浏览(1477)