Sfoglia il codice sorgente

实现发送邮件支持SSL和TLS

Minho 7 anni fa
parent
commit
257a758b21
6 ha cambiato i file con 587 aggiunte e 25 eliminazioni
  1. 2 1
      conf/app.conf.example
  2. 6 0
      conf/mail.go
  3. 47 24
      controllers/account.go
  4. 452 0
      mail/smtp.go
  5. 29 0
      mail/smtp_test.go
  6. 51 0
      mail/util.go

+ 2 - 1
conf/app.conf.example

@@ -69,7 +69,8 @@ smtp_port=25
 [email protected]
 #邮件有效期30分钟
 mail_expired=30
-
+#加密类型NONE 无认证、SSL 加密、LOGIN 普通用户登录
+secure=LOGIN
 
 ###############配置PDF生成工具地址###################
 wkhtmltopdf=D:/Program Files/wkhtmltopdf/bin/wkhtmltopdf.exe

+ 6 - 0
conf/mail.go

@@ -14,6 +14,7 @@ type SmtpConf struct {
 	SmtpPort     int
 	FormUserName string
 	MailExpired  int
+	Secure	string
 }
 
 func GetMailConfig() *SmtpConf {
@@ -24,7 +25,11 @@ func GetMailConfig() *SmtpConf {
 	form_user_name := beego.AppConfig.String("form_user_name")
 	enable_mail := beego.AppConfig.String("enable_mail")
 	mail_number := beego.AppConfig.DefaultInt("mail_number", 5)
+	secure := beego.AppConfig.DefaultString("secure","NONE")
 
+	if secure != "NONE" && secure != "LOGIN" && secure != "SSL" {
+		secure = "NONE"
+	}
 	c := &SmtpConf{
 		EnableMail:   strings.EqualFold(enable_mail, "true"),
 		MailNumber:   mail_number,
@@ -33,6 +38,7 @@ func GetMailConfig() *SmtpConf {
 		SmtpPassword: password,
 		FormUserName: form_user_name,
 		SmtpPort:     smtp_port,
+		Secure:secure,
 	}
 	return c
 }

+ 47 - 24
controllers/account.go

@@ -2,19 +2,17 @@ package controllers
 
 import (
 	"regexp"
-	"strconv"
 	"strings"
 	"time"
+	"net/url"
 
-	"net/smtp"
-
+	"github.com/lifei6671/mindoc/mail"
 	"github.com/astaxie/beego"
 	"github.com/astaxie/beego/logs"
 	"github.com/lifei6671/gocaptcha"
 	"github.com/lifei6671/mindoc/conf"
 	"github.com/lifei6671/mindoc/models"
 	"github.com/lifei6671/mindoc/utils"
-	"net/url"
 )
 
 // AccountController 用户登录与注册
@@ -256,27 +254,52 @@ func (c *AccountController) FindPassword() {
 			c.JsonResult(6003, "邮件发送失败")
 		}
 
-		go func(mail_conf *conf.SmtpConf, email string, body string) {
-			auth := smtp.PlainAuth(
-				"",
-				mail_conf.SmtpUserName,
-				mail_conf.SmtpPassword,
-				mail_conf.SmtpHost,
-			)
-
-			mime := "MIME-version: 1.0;\nContent-Type: text/html; charset=\"UTF-8\";\n\n"
-			subject := "Subject: 找回密码!\n"
-
-			err = smtp.SendMail(
-				mail_conf.SmtpHost+":"+strconv.Itoa(mail_conf.SmtpPort),
-				auth,
-				mail_conf.FormUserName,
-				[]string{email},
-				[]byte(subject+mime+"\n"+body),
-			)
-			if err != nil {
-				beego.Error("邮件发送失败 => ", email, err)
+		go func(mailConf *conf.SmtpConf, email string, body string) {
+
+			mailConfig := &mail.SMTPConfig{
+				Username: mailConf.SmtpUserName,
+				Password: mailConf.SmtpPassword,
+				Host:     mailConf.SmtpHost,
+				Port:     mailConf.SmtpPort,
+				Secure:   mailConf.Secure,
+				Identity:"",
+			}
+			beego.Info(mailConfig)
+
+			c := mail.NewSMTPClient(mailConfig)
+			m := mail.NewMail()
+
+			m.AddFrom(mailConf.FormUserName)
+			m.AddFromName(mailConf.FormUserName)
+			m.AddSubject("找回密码")
+			m.AddHTML(body)
+			m.AddTo(email)
+
+			if e := c.Send(m); e != nil {
+				beego.Error("发送邮件失败:" + e.Error())
+			} else {
+				beego.Info("邮件发送成功:" + email)
 			}
+			//auth := smtp.PlainAuth(
+			//	"",
+			//	mail_conf.SmtpUserName,
+			//	mail_conf.SmtpPassword,
+			//	mail_conf.SmtpHost,
+			//)
+			//
+			//mime := "MIME-version: 1.0;\nContent-Type: text/html; charset=\"UTF-8\";\n\n"
+			//subject := "Subject: 找回密码!\n"
+			//
+			//err = smtp.SendMail(
+			//	mail_conf.SmtpHost+":"+strconv.Itoa(mail_conf.SmtpPort),
+			//	auth,
+			//	mail_conf.FormUserName,
+			//	[]string{email},
+			//	[]byte(subject+mime+"\n"+body),
+			//)
+			//if err != nil {
+			//	beego.Error("邮件发送失败 => ", email, err)
+			//}
 		}(mail_conf, email, body)
 
 		c.JsonResult(0, "ok", c.BaseUrl()+beego.URLFor("AccountController.Login"))

+ 452 - 0
mail/smtp.go

@@ -0,0 +1,452 @@
+package mail
+
+import (
+	"bytes"
+	"crypto/md5"
+	"crypto/tls"
+	"encoding/base64"
+	"encoding/hex"
+	"errors"
+	"fmt"
+	"io/ioutil"
+	"log"
+	"net/mail"
+	"net/smtp"
+	"path"
+	"path/filepath"
+	"regexp"
+	"strconv"
+	"strings"
+)
+
+var (
+	imageRegex  = regexp.MustCompile(`(src|background)=["'](.*?)["']`)
+	schemeRegxp = regexp.MustCompile(`^[A-z]+://`)
+)
+
+// Mail will represent a formatted email
+type Mail struct {
+	To         []string
+	ToName     []string
+	Subject    string
+	HTML       string
+	Text       string
+	From       string
+	Bcc        []string
+	FromName   string
+	ReplyTo    string
+	Date       string
+	Files      map[string]string
+	Headers    string
+	BaseDir    string //内容中图片路径
+	Charset    string //编码
+	RetReceipt string //回执地址,空白则禁用回执
+}
+
+// NewMail returns a new Mail
+func NewMail() Mail {
+	return Mail{}
+}
+
+// SMTPClient struct
+type SMTPClient struct {
+	smtpAuth smtp.Auth
+	host     string
+	port     string
+	user     string
+	secure   string
+}
+
+// SMTPConfig 配置结构体
+type SMTPConfig struct {
+	Username string
+	Password string
+	Host     string
+	Port     int
+	Secure   string
+	Identity string
+}
+
+func (s *SMTPConfig) Address() string {
+	if s.Port == 0 {
+		s.Port = 25
+	}
+	return s.Host + `:` + strconv.Itoa(s.Port)
+}
+
+func (s *SMTPConfig) Auth() smtp.Auth {
+	var auth smtp.Auth
+	s.Secure = strings.ToUpper(s.Secure)
+	switch s.Secure {
+	case "NONE":
+		auth = unencryptedAuth{smtp.PlainAuth(s.Identity, s.Username, s.Password, s.Host)}
+	case "LOGIN":
+		auth = LoginAuth(s.Username, s.Password)
+	case "SSL":
+		fallthrough
+	default:
+		//auth = smtp.PlainAuth(s.Identity, s.Username, s.Password, s.Host)
+		auth = unencryptedAuth{smtp.PlainAuth(s.Identity, s.Username, s.Password, s.Host)}
+	}
+	return auth
+}
+
+func NewSMTPClient(conf *SMTPConfig) SMTPClient {
+	return SMTPClient{
+		smtpAuth: conf.Auth(),
+		host:     conf.Host,
+		port:     strconv.Itoa(conf.Port),
+		user:     conf.Username,
+		secure:   conf.Secure,
+	}
+}
+
+// NewMail returns a new Mail
+func (c *SMTPClient) NewMail() Mail {
+	return NewMail()
+}
+
+// Send - It can be used for generic SMTP stuff
+func (c *SMTPClient) Send(m Mail) error {
+	length := 0
+	if len(m.Charset) == 0 {
+		m.Charset = "utf-8"
+	}
+	boundary := "COSCMSBOUNDARYFORSMTPGOLIB"
+	var message bytes.Buffer
+	message.WriteString(fmt.Sprintf("X-SMTPAPI: %s\r\n", m.Headers))
+	//回执
+	if len(m.RetReceipt) > 0 {
+		message.WriteString(fmt.Sprintf("Return-Receipt-To: %s\r\n", m.RetReceipt))
+		message.WriteString(fmt.Sprintf("Disposition-Notification-To: %s\r\n", m.RetReceipt))
+	}
+	message.WriteString(fmt.Sprintf("From: %s <%s>\r\n", m.FromName, m.From))
+	if len(m.ReplyTo) > 0 {
+		message.WriteString(fmt.Sprintf("Return-Path: %s\r\n", m.ReplyTo))
+	}
+	length = len(m.To)
+	if length > 0 {
+		nameLength := len(m.ToName)
+		if nameLength > 0 {
+			message.WriteString(fmt.Sprintf("To: %s <%s>", m.ToName[0], m.To[0]))
+		} else {
+			message.WriteString(fmt.Sprintf("To: <%s>", m.To[0]))
+		}
+		for i := 1; i < length; i++ {
+			if nameLength > i {
+				message.WriteString(fmt.Sprintf(", %s <%s>", m.ToName[i], m.To[i]))
+			} else {
+				message.WriteString(fmt.Sprintf(", <%s>", m.To[i]))
+			}
+		}
+	}
+	length = len(m.Bcc)
+	if length > 0 {
+		message.WriteString(fmt.Sprintf("Bcc: <%s>", m.Bcc[0]))
+		for i := 1; i < length; i++ {
+			message.WriteString(fmt.Sprintf(", <%s>", m.Bcc[i]))
+		}
+	}
+	message.WriteString("\r\n")
+	message.WriteString(fmt.Sprintf("Subject: %s\r\n", m.Subject))
+	message.WriteString("MIME-Version: 1.0\r\n")
+	if m.Files != nil {
+		message.WriteString(fmt.Sprintf("Content-Type: multipart/mixed; boundary=\"%s\"\r\n\n--%s\r\n", boundary, boundary))
+	}
+	if len(m.HTML) > 0 {
+		//解析内容中的图片
+		rs := imageRegex.FindAllStringSubmatch(m.HTML, -1)
+		var embedImages string
+		for _, v := range rs {
+			surl := v[2]
+			if v2 := schemeRegxp.FindStringIndex(surl); v2 == nil {
+				filename := path.Base(surl)
+				directory := path.Dir(surl)
+				if directory == "." {
+					directory = ""
+				}
+				h := md5.New()
+				h.Write([]byte(surl + "@coscms.0"))
+				cid := hex.EncodeToString(h.Sum(nil))
+				if len(m.BaseDir) > 0 && !strings.HasSuffix(m.BaseDir, "/") {
+					m.BaseDir += "/"
+				}
+				if len(directory) > 0 && !strings.HasSuffix(directory, "/") {
+					directory += "/"
+				}
+				if str, err := m.ReadAttachment(m.BaseDir + directory + filename); err == nil {
+					re3 := regexp.MustCompile(v[1] + `=["']` + regexp.QuoteMeta(surl) + `["']`)
+					m.HTML = re3.ReplaceAllString(m.HTML, v[1]+`="cid:`+cid+`"`)
+
+					embedImages += fmt.Sprintf("--%s\r\n", boundary)
+					embedImages += fmt.Sprintf("Content-Type: application/octet-stream; name=\"%s\"; charset=\"%s\"\r\n", filename, m.Charset)
+					embedImages += fmt.Sprintf("Content-Description: %s\r\n", filename)
+					embedImages += fmt.Sprintf("Content-Disposition: inline; filename=\"%s\"; charset=\"%s\"\r\n", filename, m.Charset)
+					embedImages += fmt.Sprintf("Content-Transfer-Encoding: base64\r\nContent-ID: <%s>\r\n\r\n%s\r\n\n", cid, str)
+				}
+			}
+		}
+		part := fmt.Sprintf("Content-Type: text/html\r\n\n%s\r\n\n", m.HTML)
+		message.WriteString(part)
+		message.WriteString(embedImages)
+	} else {
+		part := fmt.Sprintf("Content-Type: text/plain\r\n\n%s\r\n\n", m.Text)
+		message.WriteString(part)
+	}
+	if m.Files != nil {
+		for key, value := range m.Files {
+			message.WriteString(fmt.Sprintf("--%s\r\n", boundary))
+			message.WriteString("Content-Type: application/octect-stream\r\n")
+			message.WriteString("Content-Transfer-Encoding:base64\r\n")
+			message.WriteString(fmt.Sprintf("Content-Disposition: attachment; filename=\"%s\"; charset=\"%s\"\r\n\r\n%s\r\n\n", key, m.Charset, value))
+		}
+		message.WriteString(fmt.Sprintf("--%s--", boundary))
+	}
+	if c.secure == "SSL" || c.secure == "TLS" {
+		return c.SendTLS(m, message)
+	}
+	return smtp.SendMail(c.host+":"+c.port, c.smtpAuth, m.From, m.To, message.Bytes())
+}
+
+//SendTLS 通过TLS发送
+func (c *SMTPClient) SendTLS(m Mail, message bytes.Buffer) error {
+
+	var ct *smtp.Client
+	var err error
+	// TLS config
+	//tlsconfig := &tls.Config{
+	//	InsecureSkipVerify: true,
+	//	ServerName:         c.host,
+	//}
+
+	// Here is the key, you need to call tls.Dial instead of smtp.Dial
+	// for smtp servers running on 465 that require an ssl connection
+	// from the very beginning (no starttls)
+	conn, err := tls.Dial("tcp", c.host+":"+c.port, nil)
+	if err != nil {
+		log.Println(err, c.host)
+		return err
+	}
+
+	ct, err = smtp.NewClient(conn, c.host)
+	if err != nil {
+		log.Println(err)
+		return err
+	}
+	fmt.Println(c.smtpAuth)
+	// Auth
+	if err = ct.Auth(c.smtpAuth); err != nil {
+		log.Println("Auth Error:",
+			err,
+			c.user,
+		)
+		return err
+	}
+
+	// To && From
+	if err = ct.Mail(m.From); err != nil {
+		log.Println("Mail Error:", err, m.From)
+		return err
+	}
+
+	for _, v := range m.To {
+		if err := ct.Rcpt(v); err != nil {
+			log.Println("Rcpt Error:", err, v)
+			return err
+		}
+	}
+
+	// Data
+	w, err := ct.Data()
+	if err != nil {
+		log.Println("Data Object Error:", err)
+		return err
+	}
+
+	_, err = w.Write(message.Bytes())
+	if err != nil {
+		log.Println("Write Data Object Error:", err)
+		return err
+	}
+
+	err = w.Close()
+	if err != nil {
+		log.Println("Data Object Close Error:", err)
+		return err
+	}
+
+	ct.Quit()
+	return nil
+}
+
+// AddTo will take a valid email address and store it in the mail.
+// It will return an error if the email is invalid.
+func (m *Mail) AddTo(email string) error {
+	//Parses a single RFC 5322 address, e.g. "Barry Gibbs <[email protected]>"
+	parsedAddess, e := mail.ParseAddress(email)
+	if e != nil {
+		return e
+	}
+	m.AddRecipient(parsedAddess)
+	return nil
+}
+
+// SetTos 设置收信人Email地址
+func (m *Mail) SetTos(emails []string) {
+	m.To = emails
+}
+
+// AddToName will add a new receipient name to mail
+func (m *Mail) AddToName(name string) {
+	m.ToName = append(m.ToName, name)
+}
+
+// AddRecipient will take an already parsed mail.Address
+func (m *Mail) AddRecipient(receipient *mail.Address) {
+	m.To = append(m.To, receipient.Address)
+	if len(receipient.Name) > 0 {
+		m.ToName = append(m.ToName, receipient.Name)
+	}
+}
+
+// AddSubject will set the subject of the mail
+func (m *Mail) AddSubject(s string) {
+	m.Subject = s
+}
+
+// AddHTML will set the body of the mail
+func (m *Mail) AddHTML(html string) {
+	m.HTML = html
+}
+
+// AddText will set the body of the email
+func (m *Mail) AddText(text string) {
+	m.Text = text
+}
+
+// AddFrom will set the senders email
+func (m *Mail) AddFrom(from string) error {
+	//Parses a single RFC 5322 address, e.g. "Barry Gibbs <[email protected]>"
+	parsedAddess, e := mail.ParseAddress(from)
+	if e != nil {
+		return e
+	}
+	m.From = parsedAddess.Address
+	m.FromName = parsedAddess.Name
+	return nil
+}
+
+// AddBCC works like AddTo but for BCC
+func (m *Mail) AddBCC(email string) error {
+	parsedAddess, e := mail.ParseAddress(email)
+	if e != nil {
+		return e
+	}
+	m.Bcc = append(m.Bcc, parsedAddess.Address)
+	return nil
+}
+
+// AddRecipientBCC works like AddRecipient but for BCC
+func (m *Mail) AddRecipientBCC(email *mail.Address) {
+	m.Bcc = append(m.Bcc, email.Address)
+}
+
+// AddFromName will set the senders name
+func (m *Mail) AddFromName(name string) {
+	m.FromName = name
+}
+
+// AddReplyTo will set the return address
+func (m *Mail) AddReplyTo(reply string) {
+	m.ReplyTo = reply
+}
+
+// AddDate specifies the date
+func (m *Mail) AddDate(date string) {
+	m.Date = date
+}
+
+// AddAttachment will include file/s in mail
+func (m *Mail) AddAttachment(filePath string) error {
+	if m.Files == nil {
+		m.Files = make(map[string]string)
+	}
+	str, err := m.ReadAttachment(filePath)
+	if err != nil {
+		return err
+	}
+	_, filename := filepath.Split(filePath)
+	m.Files[filename] = str
+	return nil
+}
+
+// ReadAttachment reading attachment
+func (m *Mail) ReadAttachment(filePath string) (string, error) {
+	file, e := ioutil.ReadFile(filePath)
+	if e != nil {
+		return "", e
+	}
+	encoded := base64.StdEncoding.EncodeToString(file)
+	totalChars := len(encoded)
+	maxLength := 500 //每行最大长度
+	totalLines := totalChars / maxLength
+	var buf bytes.Buffer
+	for i := 0; i < totalLines; i++ {
+		buf.WriteString(encoded[i*maxLength:(i+1)*maxLength] + "\n")
+	}
+	buf.WriteString(encoded[totalLines*maxLength:])
+	return buf.String(), nil
+}
+
+// AddHeaders addding header string
+func (m *Mail) AddHeaders(headers string) {
+	m.Headers = headers
+}
+
+// =======================================================
+// unencryptedAuth
+// =======================================================
+
+type unencryptedAuth struct {
+	smtp.Auth
+}
+
+func (a unencryptedAuth) Start(server *smtp.ServerInfo) (string, []byte, error) {
+	s := *server
+	s.TLS = true
+	return a.Auth.Start(&s)
+}
+
+// ======================================================
+// loginAuth
+// ======================================================
+
+type loginAuth struct {
+	username, password string
+}
+
+// LoginAuth loginAuth方式认证
+func LoginAuth(username, password string) smtp.Auth {
+	return &loginAuth{username, password}
+}
+
+func (a *loginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) {
+	if !server.TLS {
+		return "", nil, errors.New("unencrypted connection")
+	}
+	return "LOGIN", []byte(a.username), 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
+		default:
+			return nil, errors.New("Unkown fromServer")
+		}
+	}
+	return nil, nil
+}

+ 29 - 0
mail/smtp_test.go

@@ -0,0 +1,29 @@
+package mail
+
+import (
+	"os"
+	"testing"
+)
+
+func TestSend(t *testing.T) {
+	conf := &SMTPConfig{
+		Username: "swh@adm***.com",
+		Password: "",
+		Host:     "smtp.exmail.qq.com",
+		Port:     465,
+		Secure:   "SSL",
+	}
+	c := NewSMTPClient(conf)
+	m := NewMail()
+	m.AddTo("brother <1556****@qq.com>")
+	m.AddFrom("hank <" + conf.Username + ">")
+	m.AddSubject("Testing")
+	m.AddText("Some text :)")
+	filepath, _ := os.Getwd()
+	m.AddAttachment(filepath + "/README.md")
+	if e := c.Send(m); e != nil {
+		t.Error(e)
+	} else {
+		t.Log("发送成功")
+	}
+}

+ 51 - 0
mail/util.go

@@ -0,0 +1,51 @@
+package mail
+
+import (
+	"net/mail"
+)
+
+func MailAddr(name string, address string) *mail.Address {
+	return &mail.Address{
+		Name:    name,
+		Address: address,
+	}
+}
+
+type Attachments struct {
+	Files   []string
+	BaseDir string
+}
+
+//SendMail 发送电邮
+func SendMail(subject string, content string, receiver, sender string,
+	bcc []string, smtpConfig *SMTPConfig, attachments *Attachments) error {
+	c := NewSMTPClient(smtpConfig)
+	m := NewMail()
+	err := m.AddTo(receiver) //receiver e.g. "Barry Gibbs <[email protected]>"
+	if err != nil {
+		return err
+	}
+	err = m.AddFrom(sender)
+	if err != nil {
+		return err
+	}
+	m.AddSubject(subject)
+	//m.AddText("Some text :)")
+	m.AddHTML(content)
+	if attachments != nil {
+		m.BaseDir = attachments.BaseDir
+		for _, v := range attachments.Files {
+			err = m.AddAttachment(v)
+			if err != nil {
+				return err
+			}
+		}
+	}
+	for _, addr := range bcc {
+		err = m.AddBCC(addr)
+		if err != nil {
+			return err
+		}
+	}
+	return c.Send(m)
+}