From f8cbc57119d84849ca43ba7e1da077879c6687c2 Mon Sep 17 00:00:00 2001 From: Joel Lee Date: Tue, 26 Mar 2024 11:48:08 +0700 Subject: [PATCH] fix: refactor email sending functions (#1495) ## What kind of change does this PR introduce? The overall goal of this to expose a unified interface for emails. So that we can potentially implement the Hook as a Custom Mailer. This is to ensure that the impact on the existing code flow is minimal and that we can turn off the Hook easily if needed. After this change, we can do something similar to: ``` mailer. := a.Mailer() if a.config.Hook.Enabled { mailer = a.CustomMailer } ``` and have all Hook logic live in the custom Mailer Specific changes are: - Removes context from mailer as it is currently unused - pushes down mailer into respective email sending methods - Adds remaining send methods as API methods - Fetch OTP Length and MaxFrequency from config - Add convenience function for checking if an email was sent within frequency limit - push down `externalURL` and `referrer` into send function --------- Co-authored-by: Kang Ming --- internal/api/api.go | 2 +- internal/api/external.go | 5 +- internal/api/identity.go | 6 +-- internal/api/invite.go | 7 +-- internal/api/magic_link.go | 6 +-- internal/api/mail.go | 96 +++++++++++++++++++++++++--------- internal/api/reauthenticate.go | 3 +- internal/api/recover.go | 7 +-- internal/api/resend.go | 8 +-- internal/api/signup.go | 6 +-- internal/api/user.go | 6 +-- internal/mailer/mailer.go | 13 ++--- internal/mailer/template.go | 13 ++--- 13 files changed, 95 insertions(+), 83 deletions(-) diff --git a/internal/api/api.go b/internal/api/api.go index edac716a6..02c3f5332 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -320,7 +320,7 @@ func (a *API) HealthCheck(w http.ResponseWriter, r *http.Request) error { } // Mailer returns NewMailer with the current tenant config -func (a *API) Mailer(ctx context.Context) mailer.Mailer { +func (a *API) Mailer() mailer.Mailer { config := a.config return mailer.NewMailer(config) } diff --git a/internal/api/external.go b/internal/api/external.go index 8fa27f4ae..8a00e3d25 100644 --- a/internal/api/external.go +++ b/internal/api/external.go @@ -382,10 +382,7 @@ func (a *API) createAccountFromExternalIdentity(tx *storage.Connection, r *http. } else { emailConfirmationSent := false if decision.CandidateEmail.Email != "" { - mailer := a.Mailer(ctx) - referrer := utilities.GetReferrer(r, config) - externalURL := getExternalHost(ctx) - if terr = sendConfirmation(tx, user, mailer, config.SMTP.MaxFrequency, referrer, externalURL, config.Mailer.OtpLength, models.ImplicitFlow); terr != nil { + if terr = a.sendConfirmation(r, tx, user, models.ImplicitFlow); terr != nil { if errors.Is(terr, MaxFrequencyLimitError) { return nil, tooManyRequestsError(ErrorCodeOverEmailSendRateLimit, "For security purposes, you can only request this once every minute") } diff --git a/internal/api/identity.go b/internal/api/identity.go index 858810f70..e3bcf3cad 100644 --- a/internal/api/identity.go +++ b/internal/api/identity.go @@ -11,7 +11,6 @@ import ( "github.com/supabase/auth/internal/api/provider" "github.com/supabase/auth/internal/models" "github.com/supabase/auth/internal/storage" - "github.com/supabase/auth/internal/utilities" ) func (a *API) DeleteIdentity(w http.ResponseWriter, r *http.Request) error { @@ -132,10 +131,7 @@ func (a *API) linkIdentityToUser(r *http.Request, ctx context.Context, tx *stora return nil, terr } if !userData.Metadata.EmailVerified { - mailer := a.Mailer(ctx) - referrer := utilities.GetReferrer(r, a.config) - externalURL := getExternalHost(ctx) - if terr := sendConfirmation(tx, targetUser, mailer, a.config.SMTP.MaxFrequency, referrer, externalURL, a.config.Mailer.OtpLength, models.ImplicitFlow); terr != nil { + if terr := a.sendConfirmation(r, tx, targetUser, models.ImplicitFlow); terr != nil { if errors.Is(terr, MaxFrequencyLimitError) { return nil, tooManyRequestsError(ErrorCodeOverSMSSendRateLimit, "For security purposes, you can only request this once every minute") } diff --git a/internal/api/invite.go b/internal/api/invite.go index 2e912b79c..2e07e7135 100644 --- a/internal/api/invite.go +++ b/internal/api/invite.go @@ -7,7 +7,6 @@ import ( "github.com/supabase/auth/internal/api/provider" "github.com/supabase/auth/internal/models" "github.com/supabase/auth/internal/storage" - "github.com/supabase/auth/internal/utilities" ) // InviteParams are the parameters the Signup endpoint accepts @@ -20,7 +19,6 @@ type InviteParams struct { func (a *API) Invite(w http.ResponseWriter, r *http.Request) error { ctx := r.Context() db := a.db.WithContext(ctx) - config := a.config adminUser := getAdminUser(ctx) params := &InviteParams{} if err := retrieveRequestParams(r, params); err != nil { @@ -81,10 +79,7 @@ func (a *API) Invite(w http.ResponseWriter, r *http.Request) error { return terr } - mailer := a.Mailer(ctx) - referrer := utilities.GetReferrer(r, config) - externalURL := getExternalHost(ctx) - if err := sendInvite(tx, user, mailer, referrer, externalURL, config.Mailer.OtpLength); err != nil { + if err := a.sendInvite(r, tx, user); err != nil { return internalServerError("Error inviting user").WithInternalError(err) } return nil diff --git a/internal/api/magic_link.go b/internal/api/magic_link.go index c0aaded7a..e197d72f6 100644 --- a/internal/api/magic_link.go +++ b/internal/api/magic_link.go @@ -12,7 +12,6 @@ import ( "github.com/sethvargo/go-password/password" "github.com/supabase/auth/internal/models" "github.com/supabase/auth/internal/storage" - "github.com/supabase/auth/internal/utilities" ) // MagicLinkParams holds the parameters for a magic link request @@ -139,10 +138,7 @@ func (a *API) MagicLink(w http.ResponseWriter, r *http.Request) error { if terr := models.NewAuditLogEntry(r, tx, user, models.UserRecoveryRequestedAction, "", nil); terr != nil { return terr } - mailer := a.Mailer(ctx) - referrer := utilities.GetReferrer(r, config) - externalURL := getExternalHost(ctx) - return a.sendMagicLink(tx, user, mailer, config.SMTP.MaxFrequency, referrer, externalURL, config.Mailer.OtpLength, flowType) + return a.sendMagicLink(r, tx, user, flowType) }) if err != nil { if errors.Is(err, MaxFrequencyLimitError) { diff --git a/internal/api/mail.go b/internal/api/mail.go index 448f5a038..db280a9c4 100644 --- a/internal/api/mail.go +++ b/internal/api/mail.go @@ -2,7 +2,6 @@ package api import ( "net/http" - "net/url" "strings" "time" @@ -11,9 +10,7 @@ import ( "github.com/pkg/errors" "github.com/sethvargo/go-password/password" "github.com/supabase/auth/internal/api/provider" - "github.com/supabase/auth/internal/conf" "github.com/supabase/auth/internal/crypto" - "github.com/supabase/auth/internal/mailer" "github.com/supabase/auth/internal/models" "github.com/supabase/auth/internal/storage" "github.com/supabase/auth/internal/utilities" @@ -45,7 +42,7 @@ func (a *API) adminGenerateLink(w http.ResponseWriter, r *http.Request) error { ctx := r.Context() db := a.db.WithContext(ctx) config := a.config - mailer := a.Mailer(ctx) + mailer := a.Mailer() adminUser := getAdminUser(ctx) params := &GenerateLinkParams{} if err := retrieveRequestParams(r, params); err != nil { @@ -263,10 +260,17 @@ func (a *API) adminGenerateLink(w http.ResponseWriter, r *http.Request) error { return sendJSON(w, http.StatusOK, resp) } -func sendConfirmation(tx *storage.Connection, u *models.User, mailer mailer.Mailer, maxFrequency time.Duration, referrerURL string, externalURL *url.URL, otpLength int, flowType models.FlowType) error { +func (a *API) sendConfirmation(r *http.Request, tx *storage.Connection, u *models.User, flowType models.FlowType) error { + ctx := r.Context() + mailer := a.Mailer() + config := a.config + otpLength := config.Mailer.OtpLength + maxFrequency := config.SMTP.MaxFrequency + referrerURL := utilities.GetReferrer(r, config) + externalURL := getExternalHost(ctx) var err error - if u.ConfirmationSentAt != nil && !u.ConfirmationSentAt.Add(maxFrequency).Before(time.Now()) { - return MaxFrequencyLimitError + if err := validateSentWithinFrequencyLimit(u.ConfirmationSentAt, maxFrequency); err != nil { + return err } oldToken := u.ConfirmationToken otp, err := crypto.GenerateOtp(otpLength) @@ -277,7 +281,7 @@ func sendConfirmation(tx *storage.Connection, u *models.User, mailer mailer.Mail token := crypto.GenerateTokenHash(u.GetEmail(), otp) u.ConfirmationToken = addFlowPrefixToToken(token, flowType) now := time.Now() - if err := mailer.ConfirmationMail(u, otp, referrerURL, externalURL); err != nil { + if err := mailer.ConfirmationMail(r, u, otp, referrerURL, externalURL); err != nil { u.ConfirmationToken = oldToken return errors.Wrap(err, "Error sending confirmation email") } @@ -290,7 +294,13 @@ func sendConfirmation(tx *storage.Connection, u *models.User, mailer mailer.Mail return nil } -func sendInvite(tx *storage.Connection, u *models.User, mailer mailer.Mailer, referrerURL string, externalURL *url.URL, otpLength int) error { +func (a *API) sendInvite(r *http.Request, tx *storage.Connection, u *models.User) error { + ctx := r.Context() + mailer := a.Mailer() + config := a.config + otpLength := config.Mailer.OtpLength + referrerURL := utilities.GetReferrer(r, config) + externalURL := getExternalHost(ctx) var err error oldToken := u.ConfirmationToken otp, err := crypto.GenerateOtp(otpLength) @@ -300,7 +310,7 @@ func sendInvite(tx *storage.Connection, u *models.User, mailer mailer.Mailer, re } u.ConfirmationToken = crypto.GenerateTokenHash(u.GetEmail(), otp) now := time.Now() - if err := mailer.InviteMail(u, otp, referrerURL, externalURL); err != nil { + if err := mailer.InviteMail(r, u, otp, referrerURL, externalURL); err != nil { u.ConfirmationToken = oldToken return errors.Wrap(err, "Error sending invite email") } @@ -314,10 +324,17 @@ func sendInvite(tx *storage.Connection, u *models.User, mailer mailer.Mailer, re return nil } -func (a *API) sendPasswordRecovery(tx *storage.Connection, u *models.User, mailer mailer.Mailer, maxFrequency time.Duration, referrerURL string, externalURL *url.URL, otpLength int, flowType models.FlowType) error { +func (a *API) sendPasswordRecovery(r *http.Request, tx *storage.Connection, u *models.User, flowType models.FlowType) error { + ctx := r.Context() + config := a.config + maxFrequency := config.SMTP.MaxFrequency + otpLength := config.Mailer.OtpLength + referrerURL := utilities.GetReferrer(r, config) + externalURL := getExternalHost(ctx) + mailer := a.Mailer() var err error - if u.RecoverySentAt != nil && !u.RecoverySentAt.Add(maxFrequency).Before(time.Now()) { - return MaxFrequencyLimitError + if err := validateSentWithinFrequencyLimit(u.RecoverySentAt, maxFrequency); err != nil { + return err } oldToken := u.RecoveryToken @@ -329,7 +346,7 @@ func (a *API) sendPasswordRecovery(tx *storage.Connection, u *models.User, maile token := crypto.GenerateTokenHash(u.GetEmail(), otp) u.RecoveryToken = addFlowPrefixToToken(token, flowType) now := time.Now() - if err := mailer.RecoveryMail(u, otp, referrerURL, externalURL); err != nil { + if err := mailer.RecoveryMail(r, u, otp, referrerURL, externalURL); err != nil { u.RecoveryToken = oldToken return errors.Wrap(err, "Error sending recovery email") } @@ -342,10 +359,15 @@ func (a *API) sendPasswordRecovery(tx *storage.Connection, u *models.User, maile return nil } -func (a *API) sendReauthenticationOtp(tx *storage.Connection, u *models.User, mailer mailer.Mailer, maxFrequency time.Duration, otpLength int) error { +func (a *API) sendReauthenticationOtp(r *http.Request, tx *storage.Connection, u *models.User) error { + config := a.config + maxFrequency := config.SMTP.MaxFrequency + otpLength := config.Mailer.OtpLength + mailer := a.Mailer() var err error - if u.ReauthenticationSentAt != nil && !u.ReauthenticationSentAt.Add(maxFrequency).Before(time.Now()) { - return MaxFrequencyLimitError + + if err := validateSentWithinFrequencyLimit(u.ReauthenticationSentAt, maxFrequency); err != nil { + return err } oldToken := u.ReauthenticationToken @@ -356,7 +378,7 @@ func (a *API) sendReauthenticationOtp(tx *storage.Connection, u *models.User, ma } u.ReauthenticationToken = crypto.GenerateTokenHash(u.GetEmail(), otp) now := time.Now() - if err := mailer.ReauthenticateMail(u, otp); err != nil { + if err := mailer.ReauthenticateMail(r, u, otp); err != nil { u.ReauthenticationToken = oldToken return errors.Wrap(err, "Error sending reauthentication email") } @@ -369,13 +391,21 @@ func (a *API) sendReauthenticationOtp(tx *storage.Connection, u *models.User, ma return nil } -func (a *API) sendMagicLink(tx *storage.Connection, u *models.User, mailer mailer.Mailer, maxFrequency time.Duration, referrerURL string, externalURL *url.URL, otpLength int, flowType models.FlowType) error { +func (a *API) sendMagicLink(r *http.Request, tx *storage.Connection, u *models.User, flowType models.FlowType) error { + ctx := r.Context() + mailer := a.Mailer() + config := a.config + otpLength := config.Mailer.OtpLength + maxFrequency := config.SMTP.MaxFrequency + referrerURL := utilities.GetReferrer(r, config) + externalURL := getExternalHost(ctx) var err error // since Magic Link is just a recovery with a different template and behaviour // around new users we will reuse the recovery db timer to prevent potential abuse - if u.RecoverySentAt != nil && !u.RecoverySentAt.Add(maxFrequency).Before(time.Now()) { - return MaxFrequencyLimitError + if err := validateSentWithinFrequencyLimit(u.RecoverySentAt, maxFrequency); err != nil { + return err } + oldToken := u.RecoveryToken otp, err := crypto.GenerateOtp(otpLength) if err != nil { @@ -386,7 +416,7 @@ func (a *API) sendMagicLink(tx *storage.Connection, u *models.User, mailer maile u.RecoveryToken = addFlowPrefixToToken(token, flowType) now := time.Now() - if err := mailer.MagicLinkMail(u, otp, referrerURL, externalURL); err != nil { + if err := mailer.MagicLinkMail(r, u, otp, referrerURL, externalURL); err != nil { u.RecoveryToken = oldToken return errors.Wrap(err, "Error sending magic link email") } @@ -400,11 +430,18 @@ func (a *API) sendMagicLink(tx *storage.Connection, u *models.User, mailer maile } // sendEmailChange sends out an email change token to the new email. -func (a *API) sendEmailChange(tx *storage.Connection, config *conf.GlobalConfiguration, u *models.User, mailer mailer.Mailer, email, referrerURL string, externalURL *url.URL, otpLength int, flowType models.FlowType) error { +func (a *API) sendEmailChange(r *http.Request, tx *storage.Connection, u *models.User, email string, flowType models.FlowType) error { + ctx := r.Context() + config := a.config + otpLength := config.Mailer.OtpLength var err error - if u.EmailChangeSentAt != nil && !u.EmailChangeSentAt.Add(config.SMTP.MaxFrequency).Before(time.Now()) { - return MaxFrequencyLimitError + mailer := a.Mailer() + if err := validateSentWithinFrequencyLimit(u.EmailChangeSentAt, config.SMTP.MaxFrequency); err != nil { + return err } + referrerURL := utilities.GetReferrer(r, config) + externalURL := getExternalHost(ctx) + otpNew, err := crypto.GenerateOtp(otpLength) if err != nil { // OTP generation must succeed @@ -427,7 +464,7 @@ func (a *API) sendEmailChange(tx *storage.Connection, config *conf.GlobalConfigu u.EmailChangeConfirmStatus = zeroConfirmation now := time.Now() - if err := mailer.EmailChangeMail(u, otpNew, otpCurrent, referrerURL, externalURL); err != nil { + if err := mailer.EmailChangeMail(r, u, otpNew, otpCurrent, referrerURL, externalURL); err != nil { return err } @@ -457,3 +494,10 @@ func validateEmail(email string) (string, error) { } return strings.ToLower(email), nil } + +func validateSentWithinFrequencyLimit(sentAt *time.Time, frequency time.Duration) error { + if sentAt != nil && sentAt.Add(frequency).After(time.Now()) { + return MaxFrequencyLimitError + } + return nil +} diff --git a/internal/api/reauthenticate.go b/internal/api/reauthenticate.go index 84b080070..54ee8f775 100644 --- a/internal/api/reauthenticate.go +++ b/internal/api/reauthenticate.go @@ -42,8 +42,7 @@ func (a *API) Reauthenticate(w http.ResponseWriter, r *http.Request) error { return terr } if email != "" { - mailer := a.Mailer(ctx) - return a.sendReauthenticationOtp(tx, user, mailer, config.SMTP.MaxFrequency, config.Mailer.OtpLength) + return a.sendReauthenticationOtp(r, tx, user) } else if phone != "" { smsProvider, terr := sms_provider.GetSmsProvider(*config) if terr != nil { diff --git a/internal/api/recover.go b/internal/api/recover.go index a3201852d..0fa9760ae 100644 --- a/internal/api/recover.go +++ b/internal/api/recover.go @@ -6,7 +6,6 @@ import ( "github.com/supabase/auth/internal/models" "github.com/supabase/auth/internal/storage" - "github.com/supabase/auth/internal/utilities" ) // RecoverParams holds the parameters for a password recovery request @@ -34,7 +33,6 @@ func (p *RecoverParams) Validate() error { func (a *API) Recover(w http.ResponseWriter, r *http.Request) error { ctx := r.Context() db := a.db.WithContext(ctx) - config := a.config params := &RecoverParams{} if err := retrieveRequestParams(r, params); err != nil { return err @@ -66,10 +64,7 @@ func (a *API) Recover(w http.ResponseWriter, r *http.Request) error { if terr := models.NewAuditLogEntry(r, tx, user, models.UserRecoveryRequestedAction, "", nil); terr != nil { return terr } - mailer := a.Mailer(ctx) - referrer := utilities.GetReferrer(r, config) - externalURL := getExternalHost(ctx) - return a.sendPasswordRecovery(tx, user, mailer, config.SMTP.MaxFrequency, referrer, externalURL, config.Mailer.OtpLength, flowType) + return a.sendPasswordRecovery(r, tx, user, flowType) }) if err != nil { if errors.Is(err, MaxFrequencyLimitError) { diff --git a/internal/api/resend.go b/internal/api/resend.go index fdad38c43..7b9297a69 100644 --- a/internal/api/resend.go +++ b/internal/api/resend.go @@ -9,7 +9,6 @@ import ( "github.com/supabase/auth/internal/conf" "github.com/supabase/auth/internal/models" "github.com/supabase/auth/internal/storage" - "github.com/supabase/auth/internal/utilities" ) // ResendConfirmationParams holds the parameters for a resend request @@ -115,9 +114,6 @@ func (a *API) Resend(w http.ResponseWriter, r *http.Request) error { } messageID := "" - mailer := a.Mailer(ctx) - referrer := utilities.GetReferrer(r, config) - externalURL := getExternalHost(ctx) err = db.Transaction(func(tx *storage.Connection) error { switch params.Type { case signupVerification: @@ -125,7 +121,7 @@ func (a *API) Resend(w http.ResponseWriter, r *http.Request) error { return terr } // PKCE not implemented yet - return sendConfirmation(tx, user, mailer, config.SMTP.MaxFrequency, referrer, externalURL, config.Mailer.OtpLength, models.ImplicitFlow) + return a.sendConfirmation(r, tx, user, models.ImplicitFlow) case smsVerification: if terr := models.NewAuditLogEntry(r, tx, user, models.UserRecoveryRequestedAction, "", nil); terr != nil { return terr @@ -140,7 +136,7 @@ func (a *API) Resend(w http.ResponseWriter, r *http.Request) error { } messageID = mID case emailChangeVerification: - return a.sendEmailChange(tx, config, user, mailer, user.EmailChange, referrer, externalURL, config.Mailer.OtpLength, models.ImplicitFlow) + return a.sendEmailChange(r, tx, user, user.EmailChange, models.ImplicitFlow) case phoneChangeVerification: smsProvider, terr := sms_provider.GetSmsProvider(*config) if terr != nil { diff --git a/internal/api/signup.go b/internal/api/signup.go index a17323687..91c14ef7c 100644 --- a/internal/api/signup.go +++ b/internal/api/signup.go @@ -14,7 +14,6 @@ import ( "github.com/supabase/auth/internal/metering" "github.com/supabase/auth/internal/models" "github.com/supabase/auth/internal/storage" - "github.com/supabase/auth/internal/utilities" ) // SignupParams are the parameters the Signup endpoint accepts @@ -234,8 +233,6 @@ func (a *API) Signup(w http.ResponseWriter, r *http.Request) error { return internalServerError("Database error updating user").WithInternalError(terr) } } else { - mailer := a.Mailer(ctx) - referrer := utilities.GetReferrer(r, config) if terr = models.NewAuditLogEntry(r, tx, user, models.UserConfirmationRequestedAction, "", map[string]interface{}{ "provider": params.Provider, }); terr != nil { @@ -247,8 +244,7 @@ func (a *API) Signup(w http.ResponseWriter, r *http.Request) error { return terr } } - externalURL := getExternalHost(ctx) - if terr = sendConfirmation(tx, user, mailer, config.SMTP.MaxFrequency, referrer, externalURL, config.Mailer.OtpLength, flowType); terr != nil { + if terr = a.sendConfirmation(r, tx, user, flowType); terr != nil { if errors.Is(terr, MaxFrequencyLimitError) { now := time.Now() left := user.ConfirmationSentAt.Add(config.SMTP.MaxFrequency).Sub(now) / time.Second diff --git a/internal/api/user.go b/internal/api/user.go index 9fe0dcef8..43b2e9de8 100644 --- a/internal/api/user.go +++ b/internal/api/user.go @@ -10,7 +10,6 @@ import ( "github.com/supabase/auth/internal/api/sms_provider" "github.com/supabase/auth/internal/models" "github.com/supabase/auth/internal/storage" - "github.com/supabase/auth/internal/utilities" ) // UserUpdateParams parameters for updating a user @@ -194,8 +193,6 @@ func (a *API) UserUpdate(w http.ResponseWriter, r *http.Request) error { } if params.Email != "" && params.Email != user.GetEmail() { - mailer := a.Mailer(ctx) - referrer := utilities.GetReferrer(r, config) flowType := getFlowFromChallenge(params.CodeChallenge) if isPKCEFlow(flowType) { _, terr := generateFlowState(tx, models.EmailChange.String(), models.EmailChange, params.CodeChallengeMethod, params.CodeChallenge, &user.ID) @@ -204,8 +201,7 @@ func (a *API) UserUpdate(w http.ResponseWriter, r *http.Request) error { } } - externalURL := getExternalHost(ctx) - if terr = a.sendEmailChange(tx, config, user, mailer, params.Email, referrer, externalURL, config.Mailer.OtpLength, flowType); terr != nil { + if terr = a.sendEmailChange(r, tx, user, params.Email, flowType); terr != nil { if errors.Is(terr, MaxFrequencyLimitError) { return tooManyRequestsError(ErrorCodeOverEmailSendRateLimit, "For security purposes, you can only request this once every 60 seconds") } diff --git a/internal/mailer/mailer.go b/internal/mailer/mailer.go index 702bbd7e3..34460c744 100644 --- a/internal/mailer/mailer.go +++ b/internal/mailer/mailer.go @@ -2,6 +2,7 @@ package mailer import ( "fmt" + "net/http" "net/url" "github.com/gofrs/uuid" @@ -15,12 +16,12 @@ import ( // Mailer defines the interface a mailer must implement. type Mailer interface { Send(user *models.User, subject, body string, data map[string]interface{}) error - InviteMail(user *models.User, otp, referrerURL string, externalURL *url.URL) error - ConfirmationMail(user *models.User, otp, referrerURL string, externalURL *url.URL) error - RecoveryMail(user *models.User, otp, referrerURL string, externalURL *url.URL) error - MagicLinkMail(user *models.User, otp, referrerURL string, externalURL *url.URL) error - EmailChangeMail(user *models.User, otpNew, otpCurrent, referrerURL string, externalURL *url.URL) error - ReauthenticateMail(user *models.User, otp string) error + InviteMail(r *http.Request, user *models.User, otp, referrerURL string, externalURL *url.URL) error + ConfirmationMail(r *http.Request, user *models.User, otp, referrerURL string, externalURL *url.URL) error + RecoveryMail(r *http.Request, user *models.User, otp, referrerURL string, externalURL *url.URL) error + 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) } diff --git a/internal/mailer/template.go b/internal/mailer/template.go index ba6c22335..9ecc749f1 100644 --- a/internal/mailer/template.go +++ b/internal/mailer/template.go @@ -2,6 +2,7 @@ package mailer import ( "fmt" + "net/http" "net/url" "strings" @@ -75,7 +76,7 @@ func (m TemplateMailer) ValidateEmail(email string) error { } // InviteMail sends a invite mail to a new user -func (m *TemplateMailer) InviteMail(user *models.User, otp, referrerURL string, externalURL *url.URL) error { +func (m *TemplateMailer) InviteMail(r *http.Request, user *models.User, otp, referrerURL string, externalURL *url.URL) error { path, err := getPath(m.Config.Mailer.URLPaths.Invite, &EmailParams{ Token: user.ConfirmationToken, Type: "invite", @@ -106,7 +107,7 @@ func (m *TemplateMailer) InviteMail(user *models.User, otp, referrerURL string, } // ConfirmationMail sends a signup confirmation mail to a new user -func (m *TemplateMailer) ConfirmationMail(user *models.User, otp, referrerURL string, externalURL *url.URL) error { +func (m *TemplateMailer) ConfirmationMail(r *http.Request, user *models.User, otp, referrerURL string, externalURL *url.URL) error { path, err := getPath(m.Config.Mailer.URLPaths.Confirmation, &EmailParams{ Token: user.ConfirmationToken, Type: "signup", @@ -136,7 +137,7 @@ func (m *TemplateMailer) ConfirmationMail(user *models.User, otp, referrerURL st } // ReauthenticateMail sends a reauthentication mail to an authenticated user -func (m *TemplateMailer) ReauthenticateMail(user *models.User, otp string) error { +func (m *TemplateMailer) ReauthenticateMail(r *http.Request, user *models.User, otp string) error { data := map[string]interface{}{ "SiteURL": m.Config.SiteURL, "Email": user.Email, @@ -154,7 +155,7 @@ func (m *TemplateMailer) ReauthenticateMail(user *models.User, otp string) error } // EmailChangeMail sends an email change confirmation mail to a user -func (m *TemplateMailer) EmailChangeMail(user *models.User, otpNew, otpCurrent, referrerURL string, externalURL *url.URL) error { +func (m *TemplateMailer) EmailChangeMail(r *http.Request, user *models.User, otpNew, otpCurrent, referrerURL string, externalURL *url.URL) error { type Email struct { Address string Otp string @@ -229,7 +230,7 @@ func (m *TemplateMailer) EmailChangeMail(user *models.User, otpNew, otpCurrent, } // RecoveryMail sends a password recovery mail -func (m *TemplateMailer) RecoveryMail(user *models.User, otp, referrerURL string, externalURL *url.URL) error { +func (m *TemplateMailer) RecoveryMail(r *http.Request, user *models.User, otp, referrerURL string, externalURL *url.URL) error { path, err := getPath(m.Config.Mailer.URLPaths.Recovery, &EmailParams{ Token: user.RecoveryToken, Type: "recovery", @@ -258,7 +259,7 @@ func (m *TemplateMailer) RecoveryMail(user *models.User, otp, referrerURL string } // MagicLinkMail sends a login link mail -func (m *TemplateMailer) MagicLinkMail(user *models.User, otp, referrerURL string, externalURL *url.URL) error { +func (m *TemplateMailer) MagicLinkMail(r *http.Request, user *models.User, otp, referrerURL string, externalURL *url.URL) error { path, err := getPath(m.Config.Mailer.URLPaths.Recovery, &EmailParams{ Token: user.RecoveryToken, Type: "magiclink",