Skip to content

Commit

Permalink
fix: add email validation service support
Browse files Browse the repository at this point in the history
  • Loading branch information
Chris Stockton committed Dec 5, 2024
1 parent 4a16841 commit 2d1fcc1
Show file tree
Hide file tree
Showing 9 changed files with 429 additions and 102 deletions.
1 change: 1 addition & 0 deletions internal/api/errorcodes.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,4 +91,5 @@ const (
//#nosec G101 -- Not a secret value.
ErrorCodeInvalidCredentials ErrorCode = "invalid_credentials"
ErrorCodeEmailAddressNotAuthorized ErrorCode = "email_address_not_authorized"
ErrorCodeEmailAddressInvalid ErrorCode = "email_address_invalid"
)
30 changes: 22 additions & 8 deletions internal/api/mail.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"time"

"github.com/supabase/auth/internal/hooks"
"github.com/supabase/auth/internal/mailer"

Check failure on line 10 in internal/api/mail.go

View workflow job for this annotation

GitHub Actions / test (1.22.x)

package "github.com/supabase/auth/internal/mailer" is being imported more than once (ST1019)
mail "github.com/supabase/auth/internal/mailer"

Check failure on line 11 in internal/api/mail.go

View workflow job for this annotation

GitHub Actions / test (1.22.x)

other import of "github.com/supabase/auth/internal/mailer"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/metric"
Expand Down Expand Up @@ -601,7 +602,6 @@ func (a *API) checkEmailAddressAuthorization(email string) bool {
}

func (a *API) sendEmail(r *http.Request, tx *storage.Connection, u *models.User, emailActionType, otp, otpNew, tokenHashWithPrefix string) error {
mailer := a.Mailer()
ctx := r.Context()
config := a.config
referrerURL := utilities.GetReferrer(r, config)
Expand Down Expand Up @@ -675,20 +675,34 @@ func (a *API) sendEmail(r *http.Request, tx *storage.Connection, u *models.User,
return a.invokeHook(tx, r, &input, &output)
}

mr := a.Mailer()
var err error
switch emailActionType {
case mail.SignupVerification:
return mailer.ConfirmationMail(r, u, otp, referrerURL, externalURL)
err = mr.ConfirmationMail(r, u, otp, referrerURL, externalURL)
case mail.MagicLinkVerification:
return mailer.MagicLinkMail(r, u, otp, referrerURL, externalURL)
err = mr.MagicLinkMail(r, u, otp, referrerURL, externalURL)
case mail.ReauthenticationVerification:
return mailer.ReauthenticateMail(r, u, otp)
err = mr.ReauthenticateMail(r, u, otp)
case mail.RecoveryVerification:
return mailer.RecoveryMail(r, u, otp, referrerURL, externalURL)
err = mr.RecoveryMail(r, u, otp, referrerURL, externalURL)
case mail.InviteVerification:
return mailer.InviteMail(r, u, otp, referrerURL, externalURL)
err = mr.InviteMail(r, u, otp, referrerURL, externalURL)
case mail.EmailChangeVerification:
return mailer.EmailChangeMail(r, u, otpNew, otp, referrerURL, externalURL)
err = mr.EmailChangeMail(r, u, otpNew, otp, referrerURL, externalURL)
default:
err = errors.New("invalid email action type")
}

switch {
case errors.Is(err, mailer.ErrInvalidEmailAddress),
errors.Is(err, mailer.ErrInvalidEmailFormat),
errors.Is(err, mailer.ErrInvalidEmailDNS):
return badRequestError(
ErrorCodeEmailAddressInvalid,
"Email address %q is invalid",
u.GetEmail())
default:
return errors.New("invalid email action type")
return err
}
}
5 changes: 5 additions & 0 deletions internal/conf/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,11 @@ type MailerConfiguration struct {
OtpLength int `json:"otp_length" split_words:"true"`

ExternalHosts []string `json:"external_hosts" split_words:"true"`

// EXPERIMENTAL: May be removed in a future release.
EmailValidationExtended bool `json:"email_validation_extended" split_words:"true" default:"false"`
EmailValidationServiceURL string `json:"email_validation_service_url" split_words:"true"`
EmailValidationServiceKey string `json:"email_validation_service_key" split_words:"true"`
}

type PhoneProviderConfiguration struct {
Expand Down
24 changes: 13 additions & 11 deletions internal/mailer/mailer.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ type Mailer interface {
MagicLinkMail(r *http.Request, user *models.User, otp, referrerURL string, externalURL *url.URL) error
EmailChangeMail(r *http.Request, user *models.User, otpNew, otpCurrent, referrerURL string, externalURL *url.URL) error
ReauthenticateMail(r *http.Request, user *models.User, otp string) error
ValidateEmail(email string) error
GetEmailActionLink(user *models.User, actionType, referrerURL string, externalURL *url.URL) (string, error)
}

Expand Down Expand Up @@ -46,18 +45,21 @@ func NewMailer(globalConfig *conf.GlobalConfiguration) Mailer {
var mailClient MailClient
if globalConfig.SMTP.Host == "" {
logrus.Infof("Noop mail client being used for %v", globalConfig.SiteURL)
mailClient = &noopMailClient{}
mailClient = &noopMailClient{
EmailValidator: newEmailValidator(globalConfig.Mailer),
}
} else {
mailClient = &MailmeMailer{
Host: globalConfig.SMTP.Host,
Port: globalConfig.SMTP.Port,
User: globalConfig.SMTP.User,
Pass: globalConfig.SMTP.Pass,
LocalName: u.Hostname(),
From: from,
BaseURL: globalConfig.SiteURL,
Logger: logrus.StandardLogger(),
MailLogging: globalConfig.SMTP.LoggingEnabled,
Host: globalConfig.SMTP.Host,
Port: globalConfig.SMTP.Port,
User: globalConfig.SMTP.User,
Pass: globalConfig.SMTP.Pass,
LocalName: u.Hostname(),
From: from,
BaseURL: globalConfig.SiteURL,
Logger: logrus.StandardLogger(),
MailLogging: globalConfig.SMTP.LoggingEnabled,
EmailValidator: newEmailValidator(globalConfig.Mailer),
}
}

Expand Down
38 changes: 26 additions & 12 deletions internal/mailer/mailme.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package mailer

import (
"bytes"
"context"
"errors"
"html/template"
"io"
Expand All @@ -24,22 +25,29 @@ const TemplateExpiration = 10 * time.Second

// MailmeMailer lets MailMe send templated mails
type MailmeMailer struct {
From string
Host string
Port int
User string
Pass string
BaseURL string
LocalName string
FuncMap template.FuncMap
cache *TemplateCache
Logger logrus.FieldLogger
MailLogging bool
From string
Host string
Port int
User string
Pass string
BaseURL string
LocalName string
FuncMap template.FuncMap
cache *TemplateCache
Logger logrus.FieldLogger
MailLogging bool
EmailValidator *EmailValidator
}

// Mail sends a templated mail. It will try to load the template from a URL, and
// otherwise fall back to the default
func (m *MailmeMailer) Mail(to, subjectTemplate, templateURL, defaultTemplate string, templateData map[string]interface{}, headers map[string][]string, typ string) error {
func (m *MailmeMailer) Mail(
ctx context.Context,
to, subjectTemplate, templateURL, defaultTemplate string,
templateData map[string]interface{},
headers map[string][]string,
typ string,
) error {
if m.FuncMap == nil {
m.FuncMap = map[string]interface{}{}
}
Expand All @@ -51,6 +59,12 @@ func (m *MailmeMailer) Mail(to, subjectTemplate, templateURL, defaultTemplate st
}
}

if m.EmailValidator != nil {
if err := m.EmailValidator.Validate(ctx, to); err != nil {
return err
}
}

tmp, err := template.New("Subject").Funcs(template.FuncMap(m.FuncMap)).Parse(subjectTemplate)
if err != nil {
return err
Expand Down
18 changes: 16 additions & 2 deletions internal/mailer/noop.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,28 @@
package mailer

import (
"context"
"errors"
)

type noopMailClient struct{}
type noopMailClient struct {
EmailValidator *EmailValidator
}

func (m *noopMailClient) Mail(to, subjectTemplate, templateURL, defaultTemplate string, templateData map[string]interface{}, headers map[string][]string, typ string) error {
func (m *noopMailClient) Mail(
ctx context.Context,
to, subjectTemplate, templateURL, defaultTemplate string,
templateData map[string]interface{},
headers map[string][]string,
typ string,
) error {
if to == "" {
return errors.New("to field cannot be empty")
}
if m.EmailValidator != nil {
if err := m.EmailValidator.Validate(ctx, to); err != nil {
return err
}
}
return nil
}
49 changes: 27 additions & 22 deletions internal/mailer/template.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,37 @@
package mailer

import (
"context"
"fmt"
"net/http"
"net/url"
"strings"

"github.com/badoux/checkmail"
"github.com/supabase/auth/internal/conf"
"github.com/supabase/auth/internal/models"
)

type MailRequest struct {
To string
SubjectTemplate string
TemplateURL string
DefaultTemplate string
TemplateData map[string]interface{}
Headers map[string][]string
Type string
}

type MailClient interface {
Mail(string, string, string, string, map[string]interface{}, map[string][]string, string) error
Mail(
ctx context.Context,
to string,
subjectTemplate string,
templateURL string,
defaultTemplate string,
templateData map[string]interface{},
headers map[string][]string,
typ string,
) error
}

// TemplateMailer will send mail and use templates from the site for easy mail styling
Expand Down Expand Up @@ -81,12 +100,6 @@ const defaultReauthenticateMail = `<h2>Confirm reauthentication</h2>
<p>Enter the code: {{ .Token }}</p>`

// ValidateEmail returns nil if the email is valid,
// otherwise an error indicating the reason it is invalid
func (m TemplateMailer) ValidateEmail(email string) error {
return checkmail.ValidateFormat(email)
}

func (m *TemplateMailer) Headers(messageType string) map[string][]string {
originalHeaders := m.Config.SMTP.NormalizedHeaders()

Expand Down Expand Up @@ -145,6 +158,7 @@ func (m *TemplateMailer) InviteMail(r *http.Request, user *models.User, otp, ref
}

return m.Mailer.Mail(
r.Context(),
user.GetEmail(),
withDefault(m.Config.Mailer.Subjects.Invite, "You have been invited"),
m.Config.Mailer.Templates.Invite,
Expand Down Expand Up @@ -177,6 +191,7 @@ func (m *TemplateMailer) ConfirmationMail(r *http.Request, user *models.User, ot
}

return m.Mailer.Mail(
r.Context(),
user.GetEmail(),
withDefault(m.Config.Mailer.Subjects.Confirmation, "Confirm Your Email"),
m.Config.Mailer.Templates.Confirmation,
Expand All @@ -197,6 +212,7 @@ func (m *TemplateMailer) ReauthenticateMail(r *http.Request, user *models.User,
}

return m.Mailer.Mail(
r.Context(),
user.GetEmail(),
withDefault(m.Config.Mailer.Subjects.Reauthentication, "Confirm reauthentication"),
m.Config.Mailer.Templates.Reauthentication,
Expand Down Expand Up @@ -263,6 +279,7 @@ func (m *TemplateMailer) EmailChangeMail(r *http.Request, user *models.User, otp
"RedirectTo": referrerURL,
}
errors <- m.Mailer.Mail(
r.Context(),
address,
withDefault(m.Config.Mailer.Subjects.EmailChange, "Confirm Email Change"),
template,
Expand All @@ -280,7 +297,6 @@ func (m *TemplateMailer) EmailChangeMail(r *http.Request, user *models.User, otp
return e
}
}

return nil
}

Expand All @@ -305,6 +321,7 @@ func (m *TemplateMailer) RecoveryMail(r *http.Request, user *models.User, otp, r
}

return m.Mailer.Mail(
r.Context(),
user.GetEmail(),
withDefault(m.Config.Mailer.Subjects.Recovery, "Reset Your Password"),
m.Config.Mailer.Templates.Recovery,
Expand Down Expand Up @@ -337,6 +354,7 @@ func (m *TemplateMailer) MagicLinkMail(r *http.Request, user *models.User, otp,
}

return m.Mailer.Mail(
r.Context(),
user.GetEmail(),
withDefault(m.Config.Mailer.Subjects.MagicLink, "Your Magic Link"),
m.Config.Mailer.Templates.MagicLink,
Expand All @@ -347,19 +365,6 @@ func (m *TemplateMailer) MagicLinkMail(r *http.Request, user *models.User, otp,
)
}

// Send can be used to send one-off emails to users
func (m TemplateMailer) Send(user *models.User, subject, body string, data map[string]interface{}) error {
return m.Mailer.Mail(
user.GetEmail(),
subject,
"",
body,
data,
m.Headers("other"),
"other",
)
}

// GetEmailActionLink returns a magiclink, recovery or invite link based on the actionType passed.
func (m TemplateMailer) GetEmailActionLink(user *models.User, actionType, referrerURL string, externalURL *url.URL) (string, error) {
var err error
Expand Down
Loading

0 comments on commit 2d1fcc1

Please sign in to comment.