From 9354b83a48a3edcb49197c997a1e96efc80c5383 Mon Sep 17 00:00:00 2001 From: Chris Stockton <180184+cstockton@users.noreply.github.com> Date: Tue, 15 Oct 2024 09:16:15 -0700 Subject: [PATCH] feat: mailer logging (#1805) This is a quick patch to add basic logging information to the mailer package. I wasn't sure how to only log this information when using the Supabase default mail provider without adding another custom config flag to enable it. --------- Co-authored-by: Chris Stockton --- internal/conf/configuration.go | 17 ++++---- internal/conf/configuration_test.go | 4 ++ internal/mailer/mailer.go | 17 ++++---- internal/mailer/mailme.go | 40 ++++++++++++------ internal/mailer/noop.go | 2 +- internal/mailer/template.go | 9 ++++- internal/mailer/template_test.go | 63 ++++++++++++++++++++++------- 7 files changed, 108 insertions(+), 44 deletions(-) diff --git a/internal/conf/configuration.go b/internal/conf/configuration.go index 5ea9c71fe..ad4e48635 100644 --- a/internal/conf/configuration.go +++ b/internal/conf/configuration.go @@ -342,14 +342,15 @@ type ProviderConfiguration struct { } type SMTPConfiguration struct { - MaxFrequency time.Duration `json:"max_frequency" split_words:"true"` - Host string `json:"host"` - Port int `json:"port,omitempty" default:"587"` - User string `json:"user"` - Pass string `json:"pass,omitempty"` - AdminEmail string `json:"admin_email" split_words:"true"` - SenderName string `json:"sender_name" split_words:"true"` - Headers string `json:"headers"` + MaxFrequency time.Duration `json:"max_frequency" split_words:"true"` + Host string `json:"host"` + Port int `json:"port,omitempty" default:"587"` + User string `json:"user"` + Pass string `json:"pass,omitempty"` + AdminEmail string `json:"admin_email" split_words:"true"` + SenderName string `json:"sender_name" split_words:"true"` + Headers string `json:"headers"` + LoggingEnabled bool `json:"logging_enabled" split_words:"true" default:"false"` fromAddress string `json:"-"` normalizedHeaders map[string][]string `json:"-"` diff --git a/internal/conf/configuration_test.go b/internal/conf/configuration_test.go index eaef0335d..5c1b65f6e 100644 --- a/internal/conf/configuration_test.go +++ b/internal/conf/configuration_test.go @@ -23,8 +23,12 @@ func TestGlobal(t *testing.T) { os.Setenv("API_EXTERNAL_URL", "http://localhost:9999") os.Setenv("GOTRUE_HOOK_MFA_VERIFICATION_ATTEMPT_URI", "pg-functions://postgres/auth/count_failed_attempts") os.Setenv("GOTRUE_HOOK_SEND_SMS_SECRETS", "v1,whsec_aWxpa2VzdXBhYmFzZXZlcnltdWNoYW5kaWhvcGV5b3Vkb3Rvbw==") + os.Setenv("GOTRUE_SMTP_HEADERS", `{"X-PM-Metadata-project-ref":["project_ref"],"X-SES-Message-Tags":["ses:feedback-id-a=project_ref,ses:feedback-id-b=$messageType"]}`) + os.Setenv("GOTRUE_SMTP_LOGGING_ENABLED", "true") gc, err := LoadGlobal("") require.NoError(t, err) + assert.Equal(t, true, gc.SMTP.LoggingEnabled) + assert.Equal(t, "project_ref", gc.SMTP.NormalizedHeaders()["X-PM-Metadata-project-ref"][0]) require.NotNil(t, gc) assert.Equal(t, "X-Request-ID", gc.API.RequestIDHeader) assert.Equal(t, "pg-functions://postgres/auth/count_failed_attempts", gc.Hook.MFAVerificationAttempt.URI) diff --git a/internal/mailer/mailer.go b/internal/mailer/mailer.go index 5c8a90a1f..8dabe4a56 100644 --- a/internal/mailer/mailer.go +++ b/internal/mailer/mailer.go @@ -49,14 +49,15 @@ func NewMailer(globalConfig *conf.GlobalConfiguration) Mailer { mailClient = &noopMailClient{} } 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(), + 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, } } diff --git a/internal/mailer/mailme.go b/internal/mailer/mailme.go index bd659be7d..4a27f2173 100644 --- a/internal/mailer/mailme.go +++ b/internal/mailer/mailme.go @@ -24,21 +24,22 @@ 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 + From string + Host string + Port int + User string + Pass string + BaseURL string + LocalName string + FuncMap template.FuncMap + cache *TemplateCache + Logger logrus.FieldLogger + MailLogging bool } // 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) error { +func (m *MailmeMailer) Mail(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{}{} } @@ -60,6 +61,7 @@ func (m *MailmeMailer) Mail(to, subjectTemplate, templateURL, defaultTemplate st if err != nil { return err } + body, err := m.MailBody(templateURL, defaultTemplate, templateData) if err != nil { return err @@ -82,8 +84,22 @@ func (m *MailmeMailer) Mail(to, subjectTemplate, templateURL, defaultTemplate st if m.LocalName != "" { dial.LocalName = m.LocalName } - return dial.DialAndSend(mail) + if m.MailLogging { + defer func() { + fields := logrus.Fields{ + "event": "mail.send", + "mail_type": typ, + "mail_from": m.From, + "mail_to": to, + } + m.Logger.WithFields(fields).Info("mail.send") + }() + } + if err := dial.DialAndSend(mail); err != nil { + return err + } + return nil } type MailTemplate struct { diff --git a/internal/mailer/noop.go b/internal/mailer/noop.go index ed44d7b32..17151a877 100644 --- a/internal/mailer/noop.go +++ b/internal/mailer/noop.go @@ -6,7 +6,7 @@ import ( type noopMailClient struct{} -func (m *noopMailClient) Mail(to, subjectTemplate, templateURL, defaultTemplate string, templateData map[string]interface{}, headers map[string][]string) error { +func (m *noopMailClient) Mail(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") } diff --git a/internal/mailer/template.go b/internal/mailer/template.go index e66858f8a..1075d4aa0 100644 --- a/internal/mailer/template.go +++ b/internal/mailer/template.go @@ -12,7 +12,7 @@ import ( ) type MailClient interface { - Mail(string, string, string, string, map[string]interface{}, map[string][]string) error + Mail(string, string, string, string, map[string]interface{}, map[string][]string, string) error } // TemplateMailer will send mail and use templates from the site for easy mail styling @@ -151,6 +151,7 @@ func (m *TemplateMailer) InviteMail(r *http.Request, user *models.User, otp, ref defaultInviteMail, data, m.Headers("invite"), + "invite", ) } @@ -182,6 +183,7 @@ func (m *TemplateMailer) ConfirmationMail(r *http.Request, user *models.User, ot defaultConfirmationMail, data, m.Headers("confirm"), + "confirm", ) } @@ -201,6 +203,7 @@ func (m *TemplateMailer) ReauthenticateMail(r *http.Request, user *models.User, defaultReauthenticateMail, data, m.Headers("reauthenticate"), + "reauthenticate", ) } @@ -266,6 +269,7 @@ func (m *TemplateMailer) EmailChangeMail(r *http.Request, user *models.User, otp defaultEmailChangeMail, data, m.Headers("email_change"), + "email_change", ) }(email.Address, email.Otp, email.TokenHash, email.Template) } @@ -307,6 +311,7 @@ func (m *TemplateMailer) RecoveryMail(r *http.Request, user *models.User, otp, r defaultRecoveryMail, data, m.Headers("recovery"), + "recovery", ) } @@ -338,6 +343,7 @@ func (m *TemplateMailer) MagicLinkMail(r *http.Request, user *models.User, otp, defaultMagicLinkMail, data, m.Headers("magiclink"), + "magiclink", ) } @@ -350,6 +356,7 @@ func (m TemplateMailer) Send(user *models.User, subject, body string, data map[s body, data, m.Headers("other"), + "other", ) } diff --git a/internal/mailer/template_test.go b/internal/mailer/template_test.go index 782feac2c..f8fcd7417 100644 --- a/internal/mailer/template_test.go +++ b/internal/mailer/template_test.go @@ -8,23 +8,58 @@ import ( ) func TestTemplateHeaders(t *testing.T) { - mailer := TemplateMailer{ - Config: &conf.GlobalConfiguration{ - SMTP: conf.SMTPConfiguration{ - Headers: `{"X-Test-A": ["test-a", "test-b"], "X-Test-B": ["test-c", "abc $messageType"]}`, + cases := []struct { + from string + typ string + exp map[string][]string + }{ + { + from: `{"x-supabase-project-ref": ["abcjrhohrqmvcpjpsyzc"]}`, + typ: "OTHER-TYPE", + exp: map[string][]string{ + "x-supabase-project-ref": {"abcjrhohrqmvcpjpsyzc"}, }, }, - } - require.NoError(t, mailer.Config.SMTP.Validate()) + { + from: `{"X-Test-A": ["test-a", "test-b"], "X-Test-B": ["test-c", "abc $messageType"]}`, + typ: "TEST-MESSAGE-TYPE", + exp: map[string][]string{ + "X-Test-A": {"test-a", "test-b"}, + "X-Test-B": {"test-c", "abc TEST-MESSAGE-TYPE"}, + }, + }, - require.Equal(t, mailer.Headers("TEST-MESSAGE-TYPE"), map[string][]string{ - "X-Test-A": {"test-a", "test-b"}, - "X-Test-B": {"test-c", "abc TEST-MESSAGE-TYPE"}, - }) + { + from: `{"X-Test-A": ["test-a", "test-b"], "X-Test-B": ["test-c", "abc $messageType"]}`, + typ: "OTHER-TYPE", + exp: map[string][]string{ + "X-Test-A": {"test-a", "test-b"}, + "X-Test-B": {"test-c", "abc OTHER-TYPE"}, + }, + }, - require.Equal(t, mailer.Headers("OTHER-TYPE"), map[string][]string{ - "X-Test-A": {"test-a", "test-b"}, - "X-Test-B": {"test-c", "abc OTHER-TYPE"}, - }) + { + from: `{"X-Test-A": ["test-a", "test-b"], "X-Test-B": ["test-c", "abc $messageType"], "x-supabase-project-ref": ["abcjrhohrqmvcpjpsyzc"]}`, + typ: "OTHER-TYPE", + exp: map[string][]string{ + "X-Test-A": {"test-a", "test-b"}, + "X-Test-B": {"test-c", "abc OTHER-TYPE"}, + "x-supabase-project-ref": {"abcjrhohrqmvcpjpsyzc"}, + }, + }, + } + for _, tc := range cases { + mailer := TemplateMailer{ + Config: &conf.GlobalConfiguration{ + SMTP: conf.SMTPConfiguration{ + Headers: tc.from, + }, + }, + } + require.NoError(t, mailer.Config.SMTP.Validate()) + + hdrs := mailer.Headers(tc.typ) + require.Equal(t, hdrs, tc.exp) + } }