Skip to content

Commit

Permalink
Merge pull request #168 from bobbyrullo/invite_emails
Browse files Browse the repository at this point in the history
Invite emails
  • Loading branch information
bobbyrullo committed Oct 30, 2015
2 parents 9172f54 + d1e292e commit 095aff6
Show file tree
Hide file tree
Showing 12 changed files with 99 additions and 8 deletions.
1 change: 0 additions & 1 deletion email/mailgun_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ func TestNewEmailConfigFromReader(t *testing.T) {
{
json: `{"type":"mailgun","id":"mg","privateAPIKey":"private","publicAPIKey":"public","domain":"example.com"}`,
want: MailgunEmailerConfig{
ID: "mg",
PrivateAPIKey: "private",
PublicAPIKey: "public",
Domain: "example.com",
Expand Down
9 changes: 9 additions & 0 deletions email/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ type TemplatizedEmailer struct {
textTemplates *template.Template
htmlTemplates *htmltemplate.Template
emailer Emailer
globalCtx map[string]interface{}
}

func (t *TemplatizedEmailer) SetGlobalContext(ctx map[string]interface{}) {
t.globalCtx = ctx
}

// SendMail queues an email to be sent to a recipient.
Expand All @@ -59,6 +64,10 @@ func (t *TemplatizedEmailer) SendMail(from, subject, tplName string, data map[st
data["from"] = from
data["subject"] = subject

for k, v := range t.globalCtx {
data[k] = v
}

var textBuffer bytes.Buffer
if textTpl != nil {
err := textTpl.Execute(&textBuffer, data)
Expand Down
21 changes: 21 additions & 0 deletions email/template_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ import (
const (
textTemplateString = `{{define "T1.txt"}}{{.gift}} from {{.from}} to {{.to}}.{{end}}
{{define "T3.txt"}}Hello there, {{.name}}!{{end}}
{{define "T4.txt"}}Hello there, {{.name}}! Welcome to {{.planet}}!{{end}}
`
htmlTemplateString = `{{define "T1.html"}}<html><body>{{.gift}} from {{.from}} to {{.to}}.</body></html>{{end}}
{{define "T2.html"}}<html><body>Hello, {{.name}}!</body></html>{{end}}
{{define "T4.html"}}<html><body>Hello there, {{.name}}! Welcome to {{.planet}}!</body></html>{{end}}
`
)

Expand Down Expand Up @@ -51,6 +53,7 @@ func TestTemplatizedEmailSendMail(t *testing.T) {
wantText string
wantHtml string
wantErr bool
ctx map[string]interface{}
}{
{
tplName: "T1",
Expand Down Expand Up @@ -97,11 +100,29 @@ func TestTemplatizedEmailSendMail(t *testing.T) {
wantText: "",
wantHtml: htmlStart + "Hello, Alice&lt;script&gt;alert(&#39;hacked!&#39;)&lt;/script&gt;!" + htmlEnd,
},
{
tplName: "T4",
from: "[email protected]",
to: "[email protected]",
subject: "hello there",
data: map[string]interface{}{
"name": "Alice",
},
wantText: "Hello there, Alice! Welcome to Mars!",
ctx: map[string]interface{}{
"planet": "Mars",
},
wantHtml: "<html><body>Hello there, Alice! Welcome to Mars!</body></html>",
},
}

for i, tt := range tests {
emailer := &testEmailer{}
templatizer := NewTemplatizedEmailerFromTemplates(textTemplates, htmlTemplates, emailer)
if tt.ctx != nil {
templatizer.SetGlobalContext(tt.ctx)
}

err := templatizer.SendMail(tt.from, tt.subject, tt.tplName, tt.data, tt.to)
if tt.wantErr {
if err == nil {
Expand Down
16 changes: 16 additions & 0 deletions integration/user_api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,7 @@ func TestCreateUser(t *testing.T) {
cantEmail: tt.cantEmail,
lastEmail: tt.req.User.Email,
lastClientID: "XXX",
lastWasInvite: true,
lastRedirectURL: *urlParsed,
}
if diff := pretty.Compare(wantEmalier, f.emailer); diff != "" {
Expand Down Expand Up @@ -578,13 +579,28 @@ type testEmailer struct {
lastEmail string
lastClientID string
lastRedirectURL url.URL
lastWasInvite bool
}

// SendResetPasswordEmail returns resetPasswordURL when it can't email, mimicking the behavior of the real UserEmailer.
func (t *testEmailer) SendResetPasswordEmail(email string, redirectURL url.URL, clientID string) (*url.URL, error) {
t.lastEmail = email
t.lastRedirectURL = redirectURL
t.lastClientID = clientID
t.lastWasInvite = false

var retURL *url.URL
if t.cantEmail {
retURL = &testResetPasswordURL
}
return retURL, nil
}

func (t *testEmailer) SendInviteEmail(email string, redirectURL url.URL, clientID string) (*url.URL, error) {
t.lastEmail = email
t.lastRedirectURL = redirectURL
t.lastClientID = clientID
t.lastWasInvite = true

var retURL *url.URL
if t.cantEmail {
Expand Down
7 changes: 5 additions & 2 deletions server/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ func (cfg *ServerConfig) Server() (*Server, error) {
return nil, err
}

err = setEmailer(&srv, cfg.EmailFromAddress, cfg.EmailerConfigFile, cfg.EmailTemplateDirs)
err = setEmailer(&srv, cfg.IssuerName, cfg.EmailFromAddress, cfg.EmailerConfigFile, cfg.EmailTemplateDirs)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -238,7 +238,7 @@ func setTemplates(srv *Server, tpls *template.Template) error {
return nil
}

func setEmailer(srv *Server, fromAddress, emailerConfigFile string, emailTemplateDirs []string) error {
func setEmailer(srv *Server, issuerName, fromAddress, emailerConfigFile string, emailTemplateDirs []string) error {

cfg, err := email.NewEmailerConfigFromFile(emailerConfigFile)
if err != nil {
Expand Down Expand Up @@ -290,6 +290,9 @@ func setEmailer(srv *Server, fromAddress, emailerConfigFile string, emailTemplat
}
}
tMailer := email.NewTemplatizedEmailerFromTemplates(textTemplates, htmlTemplates, emailer)
tMailer.SetGlobalContext(map[string]interface{}{
"issuer_name": issuerName,
})

ue := useremail.NewUserEmailer(srv.UserRepo,
srv.PasswordInfoRepo,
Expand Down
4 changes: 2 additions & 2 deletions server/password_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ func TestSendResetPasswordEmailHandler(t *testing.T) {
wantEmailer: &testEmailer{
to: str("[email protected]"),
from: "[email protected]",
subject: "Reset your password.",
subject: "Reset Your Password",
},
wantPRUserID: "ID-1",
wantPRRedirect: &testRedirectURL,
Expand Down Expand Up @@ -138,7 +138,7 @@ func TestSendResetPasswordEmailHandler(t *testing.T) {
wantEmailer: &testEmailer{
to: str("[email protected]"),
from: "[email protected]",
subject: "Reset your password.",
subject: "Reset Your Password",
},
wantPRPassword: "password",
wantPRUserID: "ID-1",
Expand Down
7 changes: 7 additions & 0 deletions static/email/invite.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<html>
<body>
Welcome to Dex! Click below to set your password:

<a href="{{ .link }}">Set Password</a>
</body>
</html>
4 changes: 4 additions & 0 deletions static/email/invite.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Welcome to Dex! Click below to set your password:

Link:
{{ .link }}
2 changes: 1 addition & 1 deletion test
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ COVER=${COVER:-"-cover"}

source ./build

TESTABLE="connector db integration pkg/crypto pkg/flag pkg/http pkg/net pkg/time pkg/html functional/repo server session user user/api"
TESTABLE="connector db integration pkg/crypto pkg/flag pkg/http pkg/net pkg/time pkg/html functional/repo server session user user/api email"
FORMATTABLE="$TESTABLE cmd/dexctl cmd/dex-worker cmd/dex-overlord examples/app functional pkg/log"

# user has not provided PKG override
Expand Down
3 changes: 2 additions & 1 deletion user/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ type UsersAPI struct {

type Emailer interface {
SendResetPasswordEmail(string, url.URL, string) (*url.URL, error)
SendInviteEmail(string, url.URL, string) (*url.URL, error)
}

type Creds struct {
Expand Down Expand Up @@ -169,7 +170,7 @@ func (u *UsersAPI) CreateUser(creds Creds, usr schema.User, redirURL url.URL) (s

usr = userToSchemaUser(userUser)

url, err := u.emailer.SendResetPasswordEmail(usr.Email, validRedirURL, creds.ClientID)
url, err := u.emailer.SendInviteEmail(usr.Email, validRedirURL, creds.ClientID)

// An email is sent only if we don't get a link and there's no error.
emailSent := err == nil && url == nil
Expand Down
11 changes: 11 additions & 0 deletions user/api/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,23 @@ type testEmailer struct {
lastEmail string
lastClientID string
lastRedirectURL url.URL
lastWasInvite bool
}

// SendResetPasswordEmail returns resetPasswordURL when it can't email, mimicking the behavior of the real UserEmailer.
func (t *testEmailer) SendResetPasswordEmail(email string, redirectURL url.URL, clientID string) (*url.URL, error) {
return t.sendEmail(email, redirectURL, clientID, false)
}

func (t *testEmailer) SendInviteEmail(email string, redirectURL url.URL, clientID string) (*url.URL, error) {
return t.sendEmail(email, redirectURL, clientID, true)
}

func (t *testEmailer) sendEmail(email string, redirectURL url.URL, clientID string, invite bool) (*url.URL, error) {
t.lastEmail = email
t.lastRedirectURL = redirectURL
t.lastClientID = clientID
t.lastWasInvite = invite

var retURL *url.URL
if t.cantEmail {
Expand Down Expand Up @@ -369,6 +379,7 @@ func TestCreateUser(t *testing.T) {
lastEmail: tt.usr.Email,
lastClientID: tt.creds.ClientID,
lastRedirectURL: tt.redirURL,
lastWasInvite: true,
}
if diff := pretty.Compare(wantEmalier, emailer); diff != "" {
t.Errorf("case %d: Compare(want, got) = %v", i,
Expand Down
22 changes: 21 additions & 1 deletion user/email/email.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,16 @@ func NewUserEmailer(ur user.UserRepo,
// This method DOES NOT check for client ID, redirect URL validity - it is expected that upstream users have already done so.
// If there is no emailer is configured, the URL of the aforementioned link is returned, otherwise nil is returned.
func (u *UserEmailer) SendResetPasswordEmail(email string, redirectURL url.URL, clientID string) (*url.URL, error) {
return u.sendResetPasswordOrInviteEmail(email, redirectURL, clientID, false)
}

// SendInviteEmail is exactly the same as SendResetPasswordEmail, except that it uses the invite template and subject name.
// In the near future, invite emails might diverge further.
func (u *UserEmailer) SendInviteEmail(email string, redirectURL url.URL, clientID string) (*url.URL, error) {
return u.sendResetPasswordOrInviteEmail(email, redirectURL, clientID, true)
}

func (u *UserEmailer) sendResetPasswordOrInviteEmail(email string, redirectURL url.URL, clientID string, invite bool) (*url.URL, error) {
usr, err := u.ur.GetByEmail(nil, email)
if err == user.ErrorNotFound {
log.Errorf("No Such user for email: %q", email)
Expand Down Expand Up @@ -95,8 +105,17 @@ func (u *UserEmailer) SendResetPasswordEmail(email string, redirectURL url.URL,
q.Set("token", token)
resetURL.RawQuery = q.Encode()

var tmplName, subj string
if invite {
tmplName = "invite"
subj = "Activate Your Account"
} else {
tmplName = "password-reset"
subj = "Reset Your Password"
}

if u.emailer != nil {
err = u.emailer.SendMail(u.fromAddress, "Reset your password.", "password-reset",
err = u.emailer.SendMail(u.fromAddress, subj, tmplName,
map[string]interface{}{
"email": usr.Email,
"link": resetURL.String(),
Expand All @@ -107,6 +126,7 @@ func (u *UserEmailer) SendResetPasswordEmail(email string, redirectURL url.URL,
return nil, err
}
return &resetURL, nil

}

// SendEmailVerification sends an email to the user with the given userID containing a link which when visited marks the user as having had their email verified.
Expand Down

0 comments on commit 095aff6

Please sign in to comment.