From 257a758b21ade44133abc1063e0ef1ef86157a56 Mon Sep 17 00:00:00 2001 From: Minho Date: Fri, 2 Feb 2018 18:41:27 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E5=8F=91=E9=80=81=E9=82=AE?= =?UTF-8?q?=E4=BB=B6=E6=94=AF=E6=8C=81SSL=E5=92=8CTLS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- conf/app.conf.example | 3 +- conf/mail.go | 6 + controllers/account.go | 69 ++++--- mail/smtp.go | 452 +++++++++++++++++++++++++++++++++++++++++ mail/smtp_test.go | 29 +++ mail/util.go | 51 +++++ 6 files changed, 586 insertions(+), 24 deletions(-) create mode 100644 mail/smtp.go create mode 100644 mail/smtp_test.go create mode 100644 mail/util.go diff --git a/conf/app.conf.example b/conf/app.conf.example index 5a771c93..0cc0294c 100644 --- a/conf/app.conf.example +++ b/conf/app.conf.example @@ -69,7 +69,8 @@ smtp_port=25 form_user_name=admin@iminho.me #邮件有效期30分钟 mail_expired=30 - +#加密类型NONE 无认证、SSL 加密、LOGIN 普通用户登录 +secure=LOGIN ###############配置PDF生成工具地址################### wkhtmltopdf=D:/Program Files/wkhtmltopdf/bin/wkhtmltopdf.exe diff --git a/conf/mail.go b/conf/mail.go index 162ceb1d..2935269e 100644 --- a/conf/mail.go +++ b/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 } diff --git a/controllers/account.go b/controllers/account.go index 9033eeec..88d54284 100644 --- a/controllers/account.go +++ b/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, - ) + go func(mailConf *conf.SmtpConf, email string, body string) { - 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) + 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")) diff --git a/mail/smtp.go b/mail/smtp.go new file mode 100644 index 00000000..ce27cbfb --- /dev/null +++ b/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 " + 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 " + 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 +} diff --git a/mail/smtp_test.go b/mail/smtp_test.go new file mode 100644 index 00000000..e3d193bd --- /dev/null +++ b/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("发送成功") + } +} diff --git a/mail/util.go b/mail/util.go new file mode 100644 index 00000000..52844975 --- /dev/null +++ b/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 " + 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) +}