Skip to content
This repository has been archived by the owner on Jul 12, 2023. It is now read-only.

Commit

Permalink
Move email send out to controller (#833)
Browse files Browse the repository at this point in the history
* Move email send out to controller

* review stuff

* wrap errs
  • Loading branch information
whaught authored Oct 15, 2020
1 parent cdb779b commit c29b6a2
Show file tree
Hide file tree
Showing 9 changed files with 140 additions and 116 deletions.
12 changes: 6 additions & 6 deletions cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,13 +160,13 @@ func realMain(ctx context.Context) error {
}

// Setup server emailer
cfg.Email.ProviderType = email.ProviderTypeFirebase
var emailer email.Provider
if cfg.Email.HasSMTPCreds() {
cfg.Email.ProviderType = email.ProviderTypeSMTP
}
emailer, err := email.ProviderFor(ctx, &cfg.Email, h, auth)
if err != nil {
return fmt.Errorf("failed to configure internal firebase client: %w", err)
emailer, err = email.ProviderFor(ctx, &cfg.Email)
if err != nil {
return fmt.Errorf("failed to configure internal firebase client: %w", err)
}
}

// Rate limiting
Expand Down Expand Up @@ -377,7 +377,7 @@ func realMain(ctx context.Context) error {
userSub.Use(requireMFA)
userSub.Use(rateLimit)

userController := user.New(ctx, auth, emailer, cacher, cfg, db, h)
userController := user.New(ctx, firebaseInternal, auth, emailer, cacher, cfg, db, h)
userSub.Handle("", userController.HandleIndex()).Methods("GET")
userSub.Handle("", userController.HandleIndex()).
Queries("offset", "{[0-9]*}", "email", "").Methods("GET")
Expand Down
53 changes: 53 additions & 0 deletions pkg/controller/email_compose.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright 2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package controller

import (
"context"
"fmt"

"firebase.google.com/go/auth"
"github.com/google/exposure-notifications-verification-server/pkg/render"
)

// ComposeInviteEmail uses the renderer and auth client to generate a password reset link
// and emit an invite email
func ComposeInviteEmail(
ctx context.Context,
h *render.Renderer,
auth *auth.Client, toEmail, fromEmail, realmName string) ([]byte, error) {
inviteLink, err := auth.PasswordResetLink(ctx, toEmail)
if err != nil {
return nil, fmt.Errorf("failed generating reset link: %w", err)
}

// Compose message
message, err := h.RenderEmail("email/invite",
struct {
ToEmail string
FromEmail string
InviteLink string
RealmName string
}{
ToEmail: toEmail,
FromEmail: fromEmail,
InviteLink: inviteLink,
RealmName: realmName,
})
if err != nil {
return nil, fmt.Errorf("failed rendering invite template: %w", err)
}
return message, nil
}
32 changes: 18 additions & 14 deletions pkg/controller/user/user.go → pkg/controller/user/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"context"

"firebase.google.com/go/auth"
"github.com/google/exposure-notifications-verification-server/internal/firebase"
"github.com/google/exposure-notifications-verification-server/pkg/cache"
"github.com/google/exposure-notifications-verification-server/pkg/config"
"github.com/google/exposure-notifications-verification-server/pkg/database"
Expand All @@ -32,18 +33,20 @@ import (

// Controller manages users
type Controller struct {
cacher cache.Cacher
client *auth.Client
emailer email.Provider
config *config.ServerConfig
db *database.Database
h *render.Renderer
logger *zap.SugaredLogger
cacher cache.Cacher
firebaseInternal *firebase.Client
client *auth.Client
emailer email.Provider
config *config.ServerConfig
db *database.Database
h *render.Renderer
logger *zap.SugaredLogger
}

// New creates a new controller for managing users.
func New(
ctx context.Context,
firebaseInternal *firebase.Client,
client *auth.Client,
emailer email.Provider,
cacher cache.Cacher,
Expand All @@ -53,12 +56,13 @@ func New(
logger := logging.FromContext(ctx)

return &Controller{
cacher: cacher,
client: client,
emailer: emailer,
config: config,
db: db,
h: h,
logger: logger,
cacher: cacher,
firebaseInternal: firebaseInternal,
client: client,
emailer: emailer,
config: config,
db: db,
h: h,
logger: logger,
}
}
37 changes: 35 additions & 2 deletions pkg/controller/user/importbatch.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
package user

import (
"context"
"errors"
"fmt"
"net/http"

"github.com/google/exposure-notifications-verification-server/pkg/api"
Expand Down Expand Up @@ -74,8 +76,8 @@ func (c *Controller) HandleImportBatch() http.Handler {
continue
} else if created {
newUsers = append(newUsers, &batchUser)
if err := c.emailer.SendNewUserInvitation(ctx, user.Email); err != nil {
c.logger.Warnw("failed sending invitation", "error", err)

if err := c.sendInvitation(ctx, user.Email); err != nil {
batchErr = multierror.Append(batchErr, errors.New("send invitation failed"))
continue
}
Expand Down Expand Up @@ -105,3 +107,34 @@ func (c *Controller) HandleImportBatch() http.Handler {
c.h.RenderJSON(w, http.StatusOK, response)
})
}

func (c *Controller) sendInvitation(ctx context.Context, toEmail string) error {
// Send email with emailer
if c.emailer != nil {
realmName := ""
if realm := controller.RealmFromContext(ctx); realm != nil {
realmName = realm.Name
}

from := c.emailer.From()
message, err := controller.ComposeInviteEmail(ctx, c.h, c.client, toEmail, from, realmName)
if err != nil {
c.logger.Warnw("failed composing invitation", "error", err)
return fmt.Errorf("failed composing invitation: %w", err)
}
if err := c.emailer.SendEmail(ctx, toEmail, message); err != nil {
c.logger.Warnw("failed sending invitation", "error", err)
return fmt.Errorf("failed sending invitation: %w", err)
}

return nil
}

// Fallback to Firebase

if err := c.firebaseInternal.SendNewUserInvitation(ctx, toEmail); err != nil {
c.logger.Warnw("failed sending invitation", "error", err)
return fmt.Errorf("failed sending invitation: %w", err)
}
return nil
}
4 changes: 1 addition & 3 deletions pkg/controller/user/reset_password.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,7 @@ func (c *Controller) ensureFirebaseUserExists(ctx context.Context, user *databas
}

if created {
err := c.emailer.SendNewUserInvitation(ctx, user.Email)
if err != nil {
c.logger.Warnw("failed sending invitation", "error", err)
if err := c.sendInvitation(ctx, user.Email); err != nil {
flash.Error("Could not send new user invitation.")
return true, err
}
Expand Down
19 changes: 7 additions & 12 deletions pkg/email/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,7 @@ import (
"context"
"fmt"

"firebase.google.com/go/auth"
"github.com/google/exposure-notifications-server/pkg/secrets"
"github.com/google/exposure-notifications-verification-server/pkg/render"
)

// ProviderType represents a type of email provider.
Expand All @@ -30,10 +28,6 @@ const (
// ProviderTypeNoop is a no-op provider
ProviderTypeNoop ProviderType = "NOOP"

// ProviderTypeFirebase falls back to firebase's default email template.
// it uses password-reset rather than a true invitation.
ProviderTypeFirebase ProviderType = "FIREBASE"

// ProviderTypeSMTP composes emails and sends them via an external SMTP server.
ProviderTypeSMTP ProviderType = "SIMPLE_SMTP"
)
Expand Down Expand Up @@ -65,8 +59,11 @@ type Config struct {

// Provider is an interface for email-sending mechanisms.
type Provider interface {
// SendNewUserInvitation sends an invite to join the server.
SendNewUserInvitation(ctx context.Context, email string) error
// SendEmail sends an email with the given message.
SendEmail(ctx context.Context, toEmail string, message []byte) error

// From returns who shown as the sender of the email.
From() string
}

// HasSMTPCreds returns true if required fields for connecting to SMTP are set.
Expand All @@ -75,14 +72,12 @@ func (c *Config) HasSMTPCreds() bool {
}

// ProviderFor creates an email provider given a Config.
func ProviderFor(ctx context.Context, c *Config, h *render.Renderer, auth *auth.Client) (Provider, error) {
func ProviderFor(ctx context.Context, c *Config) (Provider, error) {
switch typ := c.ProviderType; typ {
case ProviderTypeNoop:
return NewNoop(), nil
case ProviderTypeFirebase:
return NewFirebase(ctx)
case ProviderTypeSMTP:
return NewSMTP(ctx, c.User, c.Password, c.SMTPHost, c.SMTPPort, h, auth), nil
return NewSMTP(ctx, c.User, c.Password, c.SMTPHost, c.SMTPPort), nil
default:
return nil, fmt.Errorf("unknown email provider type: %v", typ)
}
Expand Down
34 changes: 0 additions & 34 deletions pkg/email/firebase.go

This file was deleted.

11 changes: 8 additions & 3 deletions pkg/email/noop.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,14 @@ func NewNoop() Provider {
return &NoopProvider{}
}

// SendNewUserInvitation sends a password reset email to the user.
func (s *NoopProvider) SendNewUserInvitation(ctx context.Context, toEmail string) error {
// SendEmail sends a password reset email to the user.
func (s *NoopProvider) SendEmail(ctx context.Context, toEmail string, message []byte) error {
logger := logging.FromContext(ctx)
logger.Infow("Noop send invitation", "email", toEmail)
logger.Infow("Noop send email", "email", toEmail)
return nil
}

// From returns who the invitation should be send from.
func (s *NoopProvider) From() string {
return ""
}
54 changes: 12 additions & 42 deletions pkg/email/smtp.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,66 +19,31 @@ import (
"context"
"net/smtp"

"firebase.google.com/go/auth"
"github.com/google/exposure-notifications-server/pkg/logging"
"github.com/google/exposure-notifications-verification-server/pkg/controller"
"github.com/google/exposure-notifications-verification-server/pkg/render"
)

var _ Provider = (*SMTPProvider)(nil)

// SMTPProvider sends messages via an external SMTP server.
type SMTPProvider struct {
FirebaseAuth *auth.Client

Renderer *render.Renderer

User string
Password string
SMTPHost string
SMTPPort string
}

// NewSMTP creates a new Smtp email sender with the given auth.
func NewSMTP(ctx context.Context, user, password, host, port string, h *render.Renderer, auth *auth.Client) Provider {
func NewSMTP(ctx context.Context, user, password, host, port string) Provider {
return &SMTPProvider{
FirebaseAuth: auth,
Renderer: h,
User: user,
Password: password,
SMTPHost: host,
SMTPPort: port,
User: user,
Password: password,
SMTPHost: host,
SMTPPort: port,
}
}

// SendNewUserInvitation sends a password reset email to the user.
func (s *SMTPProvider) SendNewUserInvitation(ctx context.Context, toEmail string) error {
inviteLink, err := s.FirebaseAuth.PasswordResetLink(ctx, toEmail)
if err != nil {
return err
}

realmName := ""
if realm := controller.RealmFromContext(ctx); realm != nil {
realmName = realm.Name
}

// Compose message
message, err := s.Renderer.RenderEmail("email/invite",
struct {
ToEmail string
FromEmail string
InviteLink string
RealmName string
}{
ToEmail: toEmail,
FromEmail: s.User,
InviteLink: inviteLink,
RealmName: realmName,
})
if err != nil {
return err
}
// SendEmail sends an email to the user.
func (s *SMTPProvider) SendEmail(ctx context.Context, toEmail string, message []byte) error {

// Authentication.
auth := smtp.PlainAuth("", s.User, s.Password, s.SMTPHost)
Expand All @@ -96,3 +61,8 @@ func (s *SMTPProvider) sendMail(ctx context.Context, auth smtp.Auth, toEmail str
logger.Warnw("failed to send invitation email", "error", err)
}
}

// From returns who shown as the sender of the email.
func (s *SMTPProvider) From() string {
return s.User
}

0 comments on commit c29b6a2

Please sign in to comment.