添加邮件发送工具

This commit is contained in:
zhucheer 2020-01-25 01:19:43 +08:00
parent da3c72bf96
commit f51192eebe
5 changed files with 435 additions and 0 deletions

BIN
mailer/attachment.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

30
mailer/init.go Normal file
View File

@ -0,0 +1,30 @@
package mailer
import (
"gitee.com/zhucheer/orange/cfg"
"errors"
)
func NewMailer(optionName string) (mailer *sendMail, err error){
options:=cfg.Config.Exists("mailer."+optionName)
if options == false{
return nil, errors.New("not found mailer config by "+ optionName+"")
}
host:=cfg.Config.GetString("mailer."+optionName+".host")
port:=cfg.Config.GetInt("mailer."+optionName+".port")
username:=cfg.Config.GetString("mailer."+optionName+".username")
password:=cfg.Config.GetString("mailer."+optionName+".password")
openTsl:=cfg.Config.GetBool("mailer."+optionName+".tsl")
mailerOption:=Mailer{
Host:host,
Port:port,
UserName:username,
Password:password,
OpenTsl:openTsl,
}
mailer,err= GetSendMailer(mailerOption)
return
}

335
mailer/smtp.go Normal file
View File

@ -0,0 +1,335 @@
package mailer
import (
"encoding/base64"
"bytes"
"mime/quotedprintable"
"path"
"io/ioutil"
"strconv"
"net/smtp"
"errors"
"time"
"fmt"
"strings"
"crypto/tls"
"net"
)
type Mailer struct {
Host string
Port int
UserName string
Password string
OpenTsl bool
}
// MiniType类型枚举
var miniTypeMap = map[string]string{
".jpg":"image/jpeg",
".jpeg":"image/jpeg",
".png":"image/png",
".gif":"image/gif",
".bmp":"application/x-bmp",
}
type attachment struct {
Name string
MiniType string
ContentId string
Content string
}
type sendMail struct{
option Mailer // 邮箱配置信息
from string // 发送人名称 默认是发送邮箱
to []string // 收件人
cc []string // 抄送人
bcc []string // 密送人
subject string // 邮件标题
body string // 邮件内容
bodyType string // 邮件正文类下 html/plain
attachments []attachment
}
// SetType 设置邮件类型 html/plain
func (sm *sendMail) SetType(bodyType string) (*sendMail){
sm.bodyType = bodyType
return sm
}
// SetHtml 设置html格式邮件类型
func (sm *sendMail) SetHtml() (*sendMail){
return sm.SetType("html")
}
// SetText 设置文本邮件类型
func (sm *sendMail) SetText() (*sendMail){
return sm.SetType("plain")
}
// From 添加发送人名称
func (sm *sendMail) From(fromuser string) (*sendMail){
sm.from = fromuser
return sm
}
// To 添加收件人
func (sm *sendMail) To(touser []string) (*sendMail){
sm.to = touser
return sm
}
// Cc 添加抄送人
func (sm *sendMail) Cc(ccuser []string) (*sendMail){
sm.cc = ccuser
return sm
}
// Bcc 添加密送人
func (sm *sendMail) Bcc(bccuser []string) (*sendMail){
sm.bcc = bccuser
return sm
}
// Subject添加邮件主题
func (sm *sendMail) Subject(subject string) (*sendMail){
sm.subject = subject
return sm
}
// byte2Base64将byte数据转base64
// rfc2045中要求base64一行不要超过76个字符超过需要添加换行符 https://tools.ietf.org/html/rfc2045
func (sm *sendMail)byte2Base64(strByte []byte) string{
strByteLen := len(strByte)
payload := make([]byte, base64.StdEncoding.EncodedLen(strByteLen))
base64.StdEncoding.Encode(payload, strByte)
stream := ""
for index, size := 0, len(payload); index < size; index++ {
stream += string(payload[index])
if (index+1)%76 == 0 {
stream += "\r\n"
}
}
return stream
}
// quotedEncode 将字符串进行quoted-printable编码
func (sm *sendMail)quotedEncode(str string) string{
buffer := bytes.NewBuffer(nil)
w := quotedprintable.NewWriter(buffer)
w.Write([]byte(str))
w.Close()
return buffer.String()
}
// 添加附件
func (sm *sendMail) AddAttachment(filePath string) (contentId string, err error){
fileName := path.Base(filePath)
fileExt := path.Ext(filePath)
fileMiniType := "application/octet-stream"
if _,ok := miniTypeMap[fileExt]; ok {
fileMiniType = miniTypeMap[fileExt]
}
fileBytes, err := ioutil.ReadFile(filePath)
if err != nil {
return contentId, err
}
contentId = "qqivy"+strconv.Itoa(len(sm.attachments) + 1)+"@orange.generate"
contentString:= sm.byte2Base64(fileBytes)
attachmentInfo := attachment{
Name:fileName,
MiniType:fileMiniType,
ContentId:contentId ,
Content:contentString,
}
sm.attachments = append(sm.attachments, attachmentInfo)
return contentId, nil
}
// 发送邮件
func (sm *sendMail) Send(message string) (err error) {
err = sm.validata()
if err != nil{
return err
}
// Set up authentication information.
auth := smtp.PlainAuth("", sm.option.UserName, sm.option.Password, sm.option.Host)
buffer := bytes.NewBuffer(nil)
boudary := "THIS_IS_BOUNDARY_ORANGE"
sm.body = message
sm.writeHeader(buffer, boudary)
sm.writeBody(buffer, boudary)
sm.writeAttachment(buffer, boudary)
buffer.WriteString("\r\n--" + boudary + "--\r\n")
toUsers := make([]string, len(sm.to) + len(sm.cc) + len(sm.bcc))
toUsers = append(sm.to, sm.cc...)
toUsers = append(toUsers, sm.bcc...)
addr:=fmt.Sprintf("%s:%d",sm.option.Host, sm.option.Port)
if sm.option.OpenTsl == true {
err = sm.option.sendMailByTslDo(addr, auth, sm.option.UserName, toUsers, buffer.Bytes())
}else{
err = smtp.SendMail(addr, auth, sm.option.UserName, toUsers, buffer.Bytes())
}
return err
}
// sendMailByTslDo
func (mail *Mailer)sendMailByTslDo(addr string, auth smtp.Auth, from string,
to []string, msg []byte) (err error) {
//create smtp client
c, err := mail.TlsDial(addr)
if err != nil {
return err
}
defer c.Close()
if auth != nil {
if ok, _ := c.Extension("AUTH"); ok {
if err = c.Auth(auth); 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()
}
//return a smtp client by TLS connect
func (mail *Mailer)TlsDial(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)
}
// writeAttachment 构建smtp邮件附件
func (sm *sendMail) writeAttachment(buffer *bytes.Buffer, boudary string) (attachment string) {
for _, item:=range sm.attachments{
attachment = "\r\n--" + boudary + "\r\n"
attachment += "Content-Type: " + item.MiniType + "; name=" + item.Name + "\r\n"
attachment += "Content-Transfer-Encoding: base64\r\n"
attachment += "Content-ID: <" + item.ContentId +">\r\n"
if item.MiniType != "application/octet-stream"{
attachment += "Content-Disposition: inline; filename="+ item.Name+ "\r\n"
}
attachment += "\r\n"
attachment += item.Content
attachment += "\r\n"
buffer.WriteString(attachment)
}
return attachment
}
// writeBody 构建smtp邮件正文
func (sm *sendMail) writeBody(buffer *bytes.Buffer, boudary string) string {
body := "\r\n--" + boudary + "\r\n"
body += "Content-Type: text/"+sm.bodyType+"; charset=utf-8\r\n"
body += "Content-Transfer-Encoding: quoted-printable\r\n"
body += "\r\n" + sm.quotedEncode(sm.body) +"\r\n\r\n"
buffer.WriteString(body)
return body
}
// writeHeader 构建smtp邮件头部
func (sm *sendMail) writeHeader(buffer *bytes.Buffer, boudary string) string {
header := "Date: "+ time.Now().Format(time.RFC1123Z) +"\r\n"
header += "Subject: "+sm.subject+"\r\n"
header += fmt.Sprintf("From: %s <%s>\r\n", "=?utf-8?Q?"+sm.quotedEncode(sm.from)+"?=", sm.option.UserName)
header += "To: "+ strings.Join(sm.to, ", ") +"\r\n"
if len(sm.cc) > 0{
header += "Cc: "+ strings.Join(sm.cc, ", ") +"\r\n"
}
if len(sm.bcc) > 0{
header += "Bcc: "+ strings.Join(sm.bcc, ", ") +"\r\n"
}
header += "MIME-Version: 1.0\r\n"
header += "Content-Type: multipart/related;\r\n boundary=\""+ boudary +"\"\r\n"
headerString := header
headerString += "\r\n"
buffer.WriteString(headerString)
return headerString
}
// validata 检测发送信息有效性
func (sm *sendMail)validata() (err error){
if sm.subject == ""{
return errors.New("mail subject is empty")
}
if len(sm.to) == 0{
return errors.New("to user addr is empty")
}
return
}
// 获取发送邮件对象
func GetSendMailer(mailOption Mailer) (ret *sendMail, err error){
if mailOption.Host == "" || mailOption.Port == 0 ||
mailOption.UserName == "" || mailOption.Password == ""{
return ret, errors.New("mail option have an error")
}
ret = &sendMail{
option: mailOption,
from: mailOption.UserName, // 配置默认发送人
bodyType: "html",
}
// 465端口自动开启tsl
if mailOption.Port == 465{
ret.option.OpenTsl = true
}
return ret, nil
}

63
mailer/smtp_test.go Normal file
View File

@ -0,0 +1,63 @@
package mailer
import (
"testing"
"gitee.com/zhucheer/orange/cfg"
)
func TestNewMailer(t *testing.T){
// go test smtp_test.go smtp.go init.go -v -args --config=../project/config/config.toml
cfg.ParseParams()
cfg.StartConfig()
defaultMailer, err := NewMailer("default")
if err != nil{
t.Errorf("NewMailer have error #1 %v", err)
}
if defaultMailer.option.Host == ""{
t.Error("defaultMailer no Host")
}
_,err=NewMailer("mailer01")
if err.Error() != "not found mailer config by mailer01"{
t.Error("mailer read have an error")
}
}
func TestGetSendMailerDefaultPort(t *testing.T) {
mailer,err:= GetSendMailer(Mailer{
Host:"smtp.qq.com",
Port:25,
UserName:"000@qq.com",
Password:"000",
OpenTsl:false,
})
err = mailer.To([]string{"zhu2943@qq.com"}).Cc([]string{"zhu.cheer@qq.com"}).SetText().Subject("test abc").Send("hello")
if err != nil{
t.Error(err)
}
}
func TestGetSendMailer(t *testing.T) {
mailer,err:= GetSendMailer(Mailer{
Host:"smtp.qq.com",
Port:465,
UserName:"000@qq.com",
Password:"000",
OpenTsl:true,
})
cid, err:=mailer.AddAttachment("./attachment.jpg")
mailer.To([]string{"zhu.cheer@qq.com"}).From("Orange Test").Cc([]string{"zhu.cheer@qq.com"}).Bcc([]string{"zhu.cheer@qq.com"}).
SetHtml().Subject("test cc").Send("<b>bcc mailer</b> with an attachment")
if cid == "" || err != nil{
t.Error("add attachment is error")
}
}

View File

@ -17,6 +17,13 @@
storage = "./storage/allimg"
maxSize = 2096157
ext = ["jpg"]
[mailer]
[mailer.default]
host="smtp.qq.com"
port=22
username="xxx@qq.com"
password="xxxx"
tsl=true
[database]
initCap = 2
maxCap = 5