Merge pull request #1849 from ywk253100/170329_send_email

Fix sending email bug
This commit is contained in:
Wenkai Yin 2017-03-30 14:15:14 +08:00 committed by GitHub
commit 89a01957f6
3 changed files with 179 additions and 147 deletions

View File

@ -16,87 +16,34 @@
package email package email
import ( import (
"bytes"
tlspkg "crypto/tls" tlspkg "crypto/tls"
"fmt"
"net" "net"
"strconv" "net/smtp"
"strings"
"time" "time"
"net/smtp"
"text/template"
"github.com/vmware/harbor/src/common/models"
"github.com/vmware/harbor/src/common/utils/log" "github.com/vmware/harbor/src/common/utils/log"
"github.com/vmware/harbor/src/ui/config"
) )
// Mail holds information about content of Email // Send ...
type Mail struct { func Send(addr, identity, username, password string,
From string timeout int, tls, insecure bool, from string,
To []string to []string, subject, message string) error {
Subject string
Message string
}
var mc *models.Email client, err := newClient(addr, identity, username,
password, timeout, tls, insecure)
// SendMail sends Email according to the configurations
func (m Mail) SendMail() error {
var err error
mc, err = config.Email()
if err != nil {
return err
}
mailTemplate, err := template.ParseFiles("views/mail.tpl")
if err != nil {
return err
}
mailContent := new(bytes.Buffer)
err = mailTemplate.Execute(mailContent, m)
if err != nil {
return err
}
content := mailContent.Bytes()
auth := smtp.PlainAuth(mc.Identity, mc.Username, mc.Password, mc.Host)
if mc.SSL {
err = sendMailWithTLS(m, auth, content)
} else {
err = sendMail(m, auth, content)
}
return err
}
func sendMail(m Mail, auth smtp.Auth, content []byte) error {
return smtp.SendMail(mc.Host+":"+strconv.Itoa(mc.Port), auth, m.From, m.To, content)
}
func sendMailWithTLS(m Mail, auth smtp.Auth, content []byte) error {
conn, err := tlspkg.Dial("tcp", mc.Host+":"+strconv.Itoa(mc.Port), nil)
if err != nil {
return err
}
client, err := smtp.NewClient(conn, mc.Host)
if err != nil { if err != nil {
return err return err
} }
defer client.Close() defer client.Close()
if ok, _ := client.Extension("AUTH"); ok { if err = client.Mail(from); err != nil {
if err = client.Auth(auth); err != nil {
return err
}
}
if err = client.Mail(m.From); err != nil {
return err return err
} }
for _, to := range m.To { for _, t := range to {
if err = client.Rcpt(to); err != nil { if err = client.Rcpt(t); err != nil {
return err return err
} }
} }
@ -106,7 +53,11 @@ func sendMailWithTLS(m Mail, auth smtp.Auth, content []byte) error {
return err return err
} }
_, err = w.Write(content) template := "From: %s\r\nTo: %s\r\nSubject: %s\r\nMIME-version: 1.0;\r\nContent-Type: text/html; charset=\"UTF-8\"\r\n\n%s\r\n"
data := fmt.Sprintf(template, from,
strings.Join(to, ","), subject, message)
_, err = w.Write([]byte(data))
if err != nil { if err != nil {
return err return err
} }
@ -126,18 +77,29 @@ func sendMailWithTLS(m Mail, auth smtp.Auth, content []byte) error {
// Ping doesn't verify the server's certificate and hostname when // Ping doesn't verify the server's certificate and hostname when
// needed if the parameter insecure is ture // needed if the parameter insecure is ture
func Ping(addr, identity, username, password string, func Ping(addr, identity, username, password string,
timeout int, tls, insecure bool) (err error) { timeout int, tls, insecure bool) error {
client, err := newClient(addr, identity, username, password,
timeout, tls, insecure)
if err != nil {
return err
}
defer client.Close()
return nil
}
// caller needs to close the client
func newClient(addr, identity, username, password string,
timeout int, tls, insecure bool) (*smtp.Client, error) {
log.Debugf("establishing TCP connection with %s ...", addr) log.Debugf("establishing TCP connection with %s ...", addr)
conn, err := net.DialTimeout("tcp", addr, conn, err := net.DialTimeout("tcp", addr,
time.Duration(timeout)*time.Second) time.Duration(timeout)*time.Second)
if err != nil { if err != nil {
return return nil, err
} }
defer conn.Close()
host, _, err := net.SplitHostPort(addr) host, _, err := net.SplitHostPort(addr)
if err != nil { if err != nil {
return return nil, err
} }
if tls { if tls {
@ -147,9 +109,8 @@ func Ping(addr, identity, username, password string,
InsecureSkipVerify: insecure, InsecureSkipVerify: insecure,
}) })
if err = tlsConn.Handshake(); err != nil { if err = tlsConn.Handshake(); err != nil {
return return nil, err
} }
defer tlsConn.Close()
conn = tlsConn conn = tlsConn
} }
@ -157,9 +118,8 @@ func Ping(addr, identity, username, password string,
log.Debugf("creating SMTP client for %s ...", host) log.Debugf("creating SMTP client for %s ...", host)
client, err := smtp.NewClient(conn, host) client, err := smtp.NewClient(conn, host)
if err != nil { if err != nil {
return return nil, err
} }
defer client.Close()
//try to swith to SSL/TLS //try to swith to SSL/TLS
if !tls { if !tls {
@ -169,7 +129,7 @@ func Ping(addr, identity, username, password string,
ServerName: host, ServerName: host,
InsecureSkipVerify: insecure, InsecureSkipVerify: insecure,
}); err != nil { }); err != nil {
return return nil, err
} }
} else { } else {
log.Debugf("the email server %s does not support STARTTLS", addr) log.Debugf("the email server %s does not support STARTTLS", addr)
@ -181,14 +141,14 @@ func Ping(addr, identity, username, password string,
// only support plain auth // only support plain auth
if err = client.Auth(smtp.PlainAuth(identity, if err = client.Auth(smtp.PlainAuth(identity,
username, password, host)); err != nil { username, password, host)); err != nil {
return return nil, err
} }
} else { } else {
log.Debugf("the email server %s does not support AUTH, skip", log.Debugf("the email server %s does not support AUTH, skip",
addr) addr)
} }
log.Debug("ping email server successfully") log.Debug("create smtp client successfully")
return return client, nil
} }

View File

@ -18,20 +18,81 @@ package email
import ( import (
"strings" "strings"
"testing" "testing"
"github.com/stretchr/testify/assert"
) )
func TestSend(t *testing.T) {
addr := "smtp.gmail.com:465"
identity := ""
username := "harbortestonly@gmail.com"
password := "harborharbor"
timeout := 60
tls := true
insecure := false
from := "from"
to := []string{username}
subject := "subject"
message := "message"
// tls connection
tls = true
err := Send(addr, identity, username, password,
timeout, tls, insecure, from, to,
subject, message)
assert.Nil(t, err)
/*not work on travis
// non-tls connection
addr = "smtp.gmail.com:25"
tls = false
err = Send(addr, identity, username, password,
timeout, tls, insecure, from, to,
subject, message)
assert.Nil(t, err)
*/
//invalid username/password
username = "invalid_username"
err = Send(addr, identity, username, password,
timeout, tls, insecure, from, to,
subject, message)
if err == nil {
t.Errorf("there should be an auth error")
} else {
if !strings.Contains(err.Error(), "535") {
t.Errorf("unexpected error: %v", err)
}
}
}
func TestPing(t *testing.T) { func TestPing(t *testing.T) {
addr := "smtp.gmail.com:465" addr := "smtp.gmail.com:465"
identity := "" identity := ""
username := "wrong_username" username := "harbortestonly@gmail.com"
password := "wrong_password" password := "harborharbor"
timeout := 60 timeout := 0
tls := true tls := true
insecure := false insecure := false
// test secure connection // tls connection
err := Ping(addr, identity, username, password, err := Ping(addr, identity, username, password,
timeout, tls, insecure) timeout, tls, insecure)
assert.Nil(t, err)
/*not work on travis
// non-tls connection
addr = "smtp.gmail.com:25"
tls = false
err = Ping(addr, identity, username, password,
timeout, tls, insecure)
assert.Nil(t, err)
*/
//invalid username/password
username = "invalid_username"
err = Ping(addr, identity, username, password,
timeout, tls, insecure)
if err == nil { if err == nil {
t.Errorf("there should be an auth error") t.Errorf("there should be an auth error")
} else { } else {

View File

@ -3,19 +3,21 @@ package controllers
import ( import (
"bytes" "bytes"
"html/template" "html/template"
"net"
"net/http" "net/http"
"os" "os"
"regexp" "regexp"
"strconv"
"github.com/astaxie/beego" "github.com/astaxie/beego"
"github.com/beego/i18n" "github.com/beego/i18n"
"github.com/vmware/harbor/src/common"
"github.com/vmware/harbor/src/common/dao" "github.com/vmware/harbor/src/common/dao"
"github.com/vmware/harbor/src/common/models" "github.com/vmware/harbor/src/common/models"
"github.com/vmware/harbor/src/common/utils" "github.com/vmware/harbor/src/common/utils"
"github.com/vmware/harbor/src/common/utils/email" email_util "github.com/vmware/harbor/src/common/utils/email"
"github.com/vmware/harbor/src/common/utils/log" "github.com/vmware/harbor/src/common/utils/log"
"github.com/vmware/harbor/src/ui/auth" "github.com/vmware/harbor/src/ui/auth"
"github.com/vmware/harbor/src/ui/config"
) )
// CommonController handles request from UI that doesn't expect a page, such as /SwitchLanguage /logout ... // CommonController handles request from UI that doesn't expect a page, such as /SwitchLanguage /logout ...
@ -87,72 +89,81 @@ func (cc *CommonController) UserExists() {
// SendEmail verifies the Email address and contact SMTP server to send reset password Email. // SendEmail verifies the Email address and contact SMTP server to send reset password Email.
func (cc *CommonController) SendEmail() { func (cc *CommonController) SendEmail() {
emailStr := cc.GetString("email") email := cc.GetString("email")
pass, _ := regexp.MatchString(`^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$`, emailStr)
if !pass {
cc.CustomAbort(http.StatusBadRequest, "email_content_illegal")
} else {
queryUser := models.User{Email: emailStr}
exist, err := dao.UserExists(queryUser, "email")
if err != nil {
log.Errorf("Error occurred in UserExists: %v", err)
cc.CustomAbort(http.StatusInternalServerError, "Internal error.")
}
if !exist {
cc.CustomAbort(http.StatusNotFound, "email_does_not_exist")
}
messageTemplate, err := template.ParseFiles("views/reset-password-mail.tpl")
if err != nil {
log.Errorf("Parse email template file failed: %v", err)
cc.CustomAbort(http.StatusInternalServerError, err.Error())
}
message := new(bytes.Buffer)
harborURL := common.ExtEndpoint
if harborURL == "" {
harborURL = "localhost"
}
uuid := utils.GenerateRandomString()
err = messageTemplate.Execute(message, messageDetail{
Hint: cc.Tr("reset_email_hint"),
URL: harborURL,
UUID: uuid,
})
if err != nil {
log.Errorf("Message template error: %v", err)
cc.CustomAbort(http.StatusInternalServerError, "internal_error")
}
config, err := beego.AppConfig.GetSection("mail")
if err != nil {
log.Errorf("Can not load app.conf: %v", err)
cc.CustomAbort(http.StatusInternalServerError, "internal_error")
}
mail := email.Mail{
From: config["from"],
To: []string{emailStr},
Subject: cc.Tr("reset_email_subject"),
Message: message.String()}
err = mail.SendMail()
if err != nil {
log.Errorf("Send email failed: %v", err)
cc.CustomAbort(http.StatusInternalServerError, "send_email_failed")
}
user := models.User{ResetUUID: uuid, Email: emailStr}
dao.UpdateUserResetUUID(user)
valid, err := regexp.MatchString(`^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$`, email)
if err != nil {
log.Errorf("failed to match regexp: %v", err)
cc.CustomAbort(http.StatusInternalServerError, "Internal error.")
} }
if !valid {
cc.CustomAbort(http.StatusBadRequest, "invalid email")
}
queryUser := models.User{Email: email}
exist, err := dao.UserExists(queryUser, "email")
if err != nil {
log.Errorf("Error occurred in UserExists: %v", err)
cc.CustomAbort(http.StatusInternalServerError, "Internal error.")
}
if !exist {
log.Debugf("email %s not found", email)
cc.CustomAbort(http.StatusNotFound, "email_does_not_exist")
}
uuid := utils.GenerateRandomString()
user := models.User{ResetUUID: uuid, Email: email}
if err = dao.UpdateUserResetUUID(user); err != nil {
log.Errorf("failed to update user reset UUID: %v", err)
cc.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
messageTemplate, err := template.ParseFiles("views/reset-password-mail.tpl")
if err != nil {
log.Errorf("Parse email template file failed: %v", err)
cc.CustomAbort(http.StatusInternalServerError, err.Error())
}
message := new(bytes.Buffer)
harborURL, err := config.ExtEndpoint()
if err != nil {
log.Errorf("failed to get domain name: %v", err)
cc.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
err = messageTemplate.Execute(message, messageDetail{
Hint: cc.Tr("reset_email_hint"),
URL: harborURL,
UUID: uuid,
})
if err != nil {
log.Errorf("Message template error: %v", err)
cc.CustomAbort(http.StatusInternalServerError, "internal_error")
}
settings, err := config.Email()
if err != nil {
log.Errorf("failed to get email configurations: %v", err)
cc.CustomAbort(http.StatusInternalServerError, "internal_error")
}
addr := net.JoinHostPort(settings.Host, strconv.Itoa(settings.Port))
err = email_util.Send(addr,
settings.Identity,
settings.Username,
settings.Password,
60, settings.SSL,
false, settings.From,
[]string{email},
cc.Tr("reset_email_subject"),
message.String())
if err != nil {
log.Errorf("Send email failed: %v", err)
cc.CustomAbort(http.StatusInternalServerError, "send_email_failed")
}
} }
// ResetPassword handles request from the reset page and reset password // ResetPassword handles request from the reset page and reset password