Skip to content

Commit 68c40a6

Browse files
cstocktonChris Stockton
andauthored
feat: refactor mailer client wiring and add validation wrapper (#2130)
This change simplifies how MailClient instances are constructed and used: - Introduce `NewMailClient` to encapsulate creation of the default `MailmeMailer` or `noopMailClient` based on configuration. - Add `NewMailerWithClient` to allow injecting a custom `MailClient`. - Extract email validation into `emailValidatorMailClient`, which wraps another `MailClient` and ensures `Validate` is always called before sending. Removed inline validation logic from `MailmeMailer`. - Update `TemplateMailer` to consistently use `MailClient` instead of the previous `Mailer` field. - Extend `noopMailClient` with an optional `Delay` field to simulate latency in tests. - Clean up struct definitions by removing redundant `EmailValidator` from `MailmeMailer`. Together, these changes make the mailer package easier to extend, improve separation of concerns, and provide hooks for testing latency and swapping in custom mail clients. Co-authored-by: Chris Stockton <[email protected]>
1 parent 767e371 commit 68c40a6

File tree

4 files changed

+100
-48
lines changed

4 files changed

+100
-48
lines changed

internal/mailer/mailer.go

Lines changed: 69 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package mailer
22

33
import (
4+
"context"
45
"fmt"
56
"net/http"
67
"net/url"
@@ -39,34 +40,81 @@ type EmailData struct {
3940

4041
// NewMailer returns a new gotrue mailer
4142
func NewMailer(globalConfig *conf.GlobalConfiguration) Mailer {
42-
from := globalConfig.SMTP.FromAddress()
43-
u, _ := url.ParseRequestURI(globalConfig.API.ExternalURL)
43+
mailClient := NewMailClient(globalConfig)
44+
return &TemplateMailer{
45+
SiteURL: globalConfig.SiteURL,
46+
Config: globalConfig,
47+
MailClient: mailClient,
48+
}
49+
}
50+
51+
type emailValidatorMailClient struct {
52+
ev *EmailValidator
53+
mc MailClient
54+
}
55+
56+
// Mail implements mailer.MailClient interface by calling validate before
57+
// passing the mail request to the next MailClient.
58+
func (o *emailValidatorMailClient) Mail(
59+
ctx context.Context,
60+
to string,
61+
subjectTemplate string,
62+
templateURL string,
63+
defaultTemplate string,
64+
templateData map[string]any,
65+
headers map[string][]string,
66+
typ string,
67+
) error {
68+
if err := o.ev.Validate(ctx, to); err != nil {
69+
return err
70+
}
71+
return o.mc.Mail(
72+
ctx,
73+
to,
74+
subjectTemplate,
75+
templateURL,
76+
defaultTemplate,
77+
templateData,
78+
headers,
79+
typ,
80+
)
81+
}
4482

45-
var mailClient MailClient
83+
// NewMailerWithClient returns a new Mailer that will use the given MailClient.
84+
func NewMailerWithClient(
85+
globalConfig *conf.GlobalConfiguration,
86+
mailClient MailClient,
87+
) Mailer {
88+
ev := newEmailValidator(globalConfig.Mailer)
89+
mr := &emailValidatorMailClient{ev: ev, mc: mailClient}
90+
return &TemplateMailer{
91+
SiteURL: globalConfig.SiteURL,
92+
Config: globalConfig,
93+
MailClient: mr,
94+
}
95+
}
96+
97+
// NewMailClient returns a new MailClient based on the given configuration.
98+
func NewMailClient(globalConfig *conf.GlobalConfiguration) MailClient {
4699
if globalConfig.SMTP.Host == "" {
47100
logrus.Infof("Noop mail client being used for %v", globalConfig.SiteURL)
48-
mailClient = &noopMailClient{
49-
EmailValidator: newEmailValidator(globalConfig.Mailer),
50-
}
51-
} else {
52-
mailClient = &MailmeMailer{
53-
Host: globalConfig.SMTP.Host,
54-
Port: globalConfig.SMTP.Port,
55-
User: globalConfig.SMTP.User,
56-
Pass: globalConfig.SMTP.Pass,
57-
LocalName: u.Hostname(),
58-
From: from,
59-
BaseURL: globalConfig.SiteURL,
60-
Logger: logrus.StandardLogger(),
61-
MailLogging: globalConfig.SMTP.LoggingEnabled,
101+
return &noopMailClient{
62102
EmailValidator: newEmailValidator(globalConfig.Mailer),
63103
}
64104
}
65105

66-
return &TemplateMailer{
67-
SiteURL: globalConfig.SiteURL,
68-
Config: globalConfig,
69-
Mailer: mailClient,
106+
from := globalConfig.SMTP.FromAddress()
107+
u, _ := url.ParseRequestURI(globalConfig.API.ExternalURL)
108+
return &MailmeMailer{
109+
Host: globalConfig.SMTP.Host,
110+
Port: globalConfig.SMTP.Port,
111+
User: globalConfig.SMTP.User,
112+
Pass: globalConfig.SMTP.Pass,
113+
LocalName: u.Hostname(),
114+
From: from,
115+
BaseURL: globalConfig.SiteURL,
116+
Logger: logrus.StandardLogger(),
117+
MailLogging: globalConfig.SMTP.LoggingEnabled,
70118
}
71119
}
72120

internal/mailer/mailme.go

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -25,18 +25,17 @@ const TemplateExpiration = 10 * time.Second
2525

2626
// MailmeMailer lets MailMe send templated mails
2727
type MailmeMailer struct {
28-
From string
29-
Host string
30-
Port int
31-
User string
32-
Pass string
33-
BaseURL string
34-
LocalName string
35-
FuncMap template.FuncMap
36-
cache *TemplateCache
37-
Logger logrus.FieldLogger
38-
MailLogging bool
39-
EmailValidator *EmailValidator
28+
From string
29+
Host string
30+
Port int
31+
User string
32+
Pass string
33+
BaseURL string
34+
LocalName string
35+
FuncMap template.FuncMap
36+
cache *TemplateCache
37+
Logger logrus.FieldLogger
38+
MailLogging bool
4039
}
4140

4241
// Mail sends a templated mail. It will try to load the template from a URL, and
@@ -59,12 +58,6 @@ func (m *MailmeMailer) Mail(
5958
}
6059
}
6160

62-
if m.EmailValidator != nil {
63-
if err := m.EmailValidator.Validate(ctx, to); err != nil {
64-
return err
65-
}
66-
}
67-
6861
tmp, err := template.New("Subject").Funcs(template.FuncMap(m.FuncMap)).Parse(subjectTemplate)
6962
if err != nil {
7063
return err

internal/mailer/noop.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ package mailer
33
import (
44
"context"
55
"errors"
6+
"time"
67
)
78

89
type noopMailClient struct {
910
EmailValidator *EmailValidator
11+
Delay time.Duration
1012
}
1113

1214
func (m *noopMailClient) Mail(
@@ -19,6 +21,15 @@ func (m *noopMailClient) Mail(
1921
if to == "" {
2022
return errors.New("to field cannot be empty")
2123
}
24+
25+
if m.Delay > 0 {
26+
select {
27+
case <-time.After(m.Delay):
28+
case <-ctx.Done():
29+
return ctx.Err()
30+
}
31+
}
32+
2233
if m.EmailValidator != nil {
2334
if err := m.EmailValidator.Validate(ctx, to); err != nil {
2435
return err

internal/mailer/template.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,9 @@ type MailClient interface {
3636

3737
// TemplateMailer will send mail and use templates from the site for easy mail styling
3838
type TemplateMailer struct {
39-
SiteURL string
40-
Config *conf.GlobalConfiguration
41-
Mailer MailClient
39+
SiteURL string
40+
Config *conf.GlobalConfiguration
41+
MailClient MailClient
4242
}
4343

4444
func encodeRedirectURL(referrerURL string) string {
@@ -157,7 +157,7 @@ func (m *TemplateMailer) InviteMail(r *http.Request, user *models.User, otp, ref
157157
"RedirectTo": referrerURL,
158158
}
159159

160-
return m.Mailer.Mail(
160+
return m.MailClient.Mail(
161161
r.Context(),
162162
user.GetEmail(),
163163
withDefault(m.Config.Mailer.Subjects.Invite, "You have been invited"),
@@ -190,7 +190,7 @@ func (m *TemplateMailer) ConfirmationMail(r *http.Request, user *models.User, ot
190190
"RedirectTo": referrerURL,
191191
}
192192

193-
return m.Mailer.Mail(
193+
return m.MailClient.Mail(
194194
r.Context(),
195195
user.GetEmail(),
196196
withDefault(m.Config.Mailer.Subjects.Confirmation, "Confirm Your Email"),
@@ -211,7 +211,7 @@ func (m *TemplateMailer) ReauthenticateMail(r *http.Request, user *models.User,
211211
"Data": user.UserMetaData,
212212
}
213213

214-
return m.Mailer.Mail(
214+
return m.MailClient.Mail(
215215
r.Context(),
216216
user.GetEmail(),
217217
withDefault(m.Config.Mailer.Subjects.Reauthentication, "Confirm reauthentication"),
@@ -281,7 +281,7 @@ func (m *TemplateMailer) EmailChangeMail(r *http.Request, user *models.User, otp
281281
"Data": user.UserMetaData,
282282
"RedirectTo": referrerURL,
283283
}
284-
errors <- m.Mailer.Mail(
284+
errors <- m.MailClient.Mail(
285285
ctx,
286286
address,
287287
withDefault(m.Config.Mailer.Subjects.EmailChange, "Confirm Email Change"),
@@ -323,7 +323,7 @@ func (m *TemplateMailer) RecoveryMail(r *http.Request, user *models.User, otp, r
323323
"RedirectTo": referrerURL,
324324
}
325325

326-
return m.Mailer.Mail(
326+
return m.MailClient.Mail(
327327
r.Context(),
328328
user.GetEmail(),
329329
withDefault(m.Config.Mailer.Subjects.Recovery, "Reset Your Password"),
@@ -356,7 +356,7 @@ func (m *TemplateMailer) MagicLinkMail(r *http.Request, user *models.User, otp,
356356
"RedirectTo": referrerURL,
357357
}
358358

359-
return m.Mailer.Mail(
359+
return m.MailClient.Mail(
360360
r.Context(),
361361
user.GetEmail(),
362362
withDefault(m.Config.Mailer.Subjects.MagicLink, "Your Magic Link"),

0 commit comments

Comments
 (0)