diff --git a/app/boot_levels.go b/app/boot_levels.go index 1157380ac..ee28725bd 100644 --- a/app/boot_levels.go +++ b/app/boot_levels.go @@ -114,29 +114,6 @@ func (app *CortezaApp) Setup() (err error) { auth.SetupDefault(app.Opt.Auth.Secret, app.Opt.Auth.Expiry) - mail.SetupDialer( - app.Opt.SMTP.Host, - app.Opt.SMTP.Port, - app.Opt.SMTP.User, - app.Opt.SMTP.Pass, - app.Opt.SMTP.From, - - // Apply TLS configuration - func(d *gomail.Dialer) { - if d.TLSConfig == nil { - d.TLSConfig = &tls.Config{ServerName: d.Host} - } - - if app.Opt.SMTP.TlsInsecure { - d.TLSConfig.InsecureSkipVerify = true - } - - if app.Opt.SMTP.TlsServerName != "" { - d.TLSConfig.ServerName = app.Opt.SMTP.TlsServerName - } - }, - ) - http.SetupDefaults( app.Opt.HTTPClient.HttpClientTimeout, app.Opt.HTTPClient.ClientTSLInsecure, @@ -490,6 +467,12 @@ func (app *CortezaApp) Activate(ctx context.Context) (err error) { return err } + if err = applySmtpOptionsToSettings(ctx, app.Log, app.Opt.SMTP, sysService.CurrentSettings); err != nil { + return err + } + + updateSmtpSettings(app.Log, sysService.CurrentSettings) + if app.AuthService, err = authService.New(ctx, app.Log, app.Store, app.Opt.Auth); err != nil { return fmt.Errorf("failed to init auth service: %w", err) } @@ -639,3 +622,126 @@ func updateLocaleSettings(opt options.LocaleOpt) { updateResourceLanguages(appSettings) }) } + +// takes current options (SMTP_* env variables) and copies their values to settings +func applySmtpOptionsToSettings(ctx context.Context, log *zap.Logger, opt options.SMTPOpt, current *types.AppSettings) (err error) { + if len(opt.Host) == 0 { + // nothing to do here, SMTP_HOST not set + return + } + + // Create SMTP server settings struct + // from the environmental variables (SMTP_*) + // we'll use it for provisioning empty SMTP settings + // and for comparison to issue a warning + optServer := &types.SmtpServers{ + Host: opt.Host, + Port: opt.Port, + User: opt.User, + Pass: opt.Pass, + From: opt.From, + TlsInsecure: opt.TlsInsecure, + TlsServerName: opt.TlsServerName, + } + + if len(current.SMTP.Servers) > 0 { + if current.SMTP.Servers[0] != *optServer { + // ENV variables changed OR settings changed. + // One way or the other, this can lead to unexpected situations + // + // Let's log a warning + log.Warn( + "Environmental variables (SMTP_*) and SMTP settings " + + "(most likely changed via admin console) are not the same. " + + "When server was restarted, values from environmental" + + "variables were copied to settings for easier management. " + + "To avoid confusion and potential issues, we suggest you to " + + "remove all SMTP_* variables") + } + + return + } + + // SMTP server settings do not exist but + // there is something in the options (SMTP_HOST) + ctx = auth.SetIdentityToContext(ctx, auth.ServiceUser()) + + // When settings for the SMTP servers are missing, + // we'll try to use one from the options (environmental vars) + s := &types.SettingValue{Name: "smtp.servers"} + err = s.SetValue([]*types.SmtpServers{optServer}) + + if err != nil { + return + } + + if err = sysService.DefaultSettings.Set(ctx, s); err != nil { + return + } + + if err = sysService.DefaultSettings.UpdateCurrent(ctx); err != nil { + return + } + + return +} + +func updateSmtpSettings(log *zap.Logger, current *types.AppSettings) { + sysService.DefaultSettings.Register("smtp", func(ctx context.Context, current interface{}, _ types.SettingValueSet) { + appSettings, is := current.(*types.AppSettings) + if !is { + return + } + + setupSmtpDialer(log, appSettings.SMTP.Servers...) + }) + setupSmtpDialer(log, current.SMTP.Servers...) +} + +func setupSmtpDialer(log *zap.Logger, servers ...types.SmtpServers) { + if len(servers) == 0 { + log.Warn("no SMTP servers found, email sending will be disabled") + return + } + + // Supporting only one server for now + s := servers[0] + + if s.Host == "" { + log.Warn("SMTP server configured without host/server, email sending will be disabled") + return + } + + log.Info("reloading SMTP configuration", + zap.String("host", s.Host), + zap.Int("port", s.Port), + zap.String("user", s.User), + logger.Mask("pass", s.Pass), + zap.Bool("tsl-insecure", s.TlsInsecure), + zap.String("tls-server-name", s.TlsServerName), + ) + + mail.SetupDialer( + s.Host, + s.Port, + s.User, + s.Pass, + s.From, + + // Apply TLS configuration + func(d *gomail.Dialer) { + if d.TLSConfig == nil { + d.TLSConfig = &tls.Config{ServerName: d.Host} + } + + if s.TlsInsecure { + d.TLSConfig.InsecureSkipVerify = true + } + + if s.TlsServerName != "" { + d.TLSConfig.ServerName = s.TlsServerName + } + }, + ) + +} diff --git a/pkg/mail/mail.go b/pkg/mail/mail.go index ed66f6b0c..0ed4c2e6d 100644 --- a/pkg/mail/mail.go +++ b/pkg/mail/mail.go @@ -2,10 +2,11 @@ package mail import ( "fmt" - gomail "gopkg.in/mail.v2" "regexp" "strconv" "strings" + + gomail "gopkg.in/mail.v2" ) type ( @@ -36,7 +37,7 @@ func init() { // Host variable can contain ":" that will override port value func SetupDialer(host string, port int, user, pass, from string, ff ...applyCfg) { if host == "" { - defaultDialerError = fmt.Errorf("No hostname provided for SMTP") + defaultDialerError = fmt.Errorf("no hostname provided for SMTP") return } @@ -47,7 +48,7 @@ func SetupDialer(host string, port int, user, pass, from string, ff ...applyCfg) } if port == 0 { - defaultDialerError = fmt.Errorf("No port provided for SMTP") + defaultDialerError = fmt.Errorf("no port provided for SMTP") return } diff --git a/pkg/options/SMTP.yaml b/pkg/options/SMTP.yaml index 037fa746b..38441e26c 100644 --- a/pkg/options/SMTP.yaml +++ b/pkg/options/SMTP.yaml @@ -1,7 +1,11 @@ docs: title: Email sending intro: |- - Configure your local SMTP server or use one of the available providers + Configure your local SMTP server or use one of the available providers. + + These values are copied to settings when the server starts and can be managed from the administration console. + We recommend you remove these values after they are copied to settings. + If server detects difference between these options and settings, it shows a warning in the log on server start. props: - name: host diff --git a/system/service/auth_notification.go b/system/service/auth_notification.go index b5ae4a802..037a3c584 100644 --- a/system/service/auth_notification.go +++ b/system/service/auth_notification.go @@ -137,11 +137,24 @@ func (svc authNotification) send(ctx context.Context, name, sendTo string, paylo } ntf.SetBody("text/html", string(tmp)) + err = mail.Send(ntf) + + if err != nil { + svc.log(ctx).Error( + "auth notification send failed", + zap.String("name", name), + zap.String("email", sendTo), + zap.Error(err), + ) + + return fmt.Errorf("could not send email, contact your administrator") + } + svc.log(ctx).Debug( - "sending auth notification", + "auth notification sent", zap.String("name", name), zap.String("email", sendTo), ) - return mail.Send(ntf) + return nil } diff --git a/system/service/service.go b/system/service/service.go index 964373139..a6eeef5b2 100644 --- a/system/service/service.go +++ b/system/service/service.go @@ -221,7 +221,7 @@ func Initialize(ctx context.Context, log *zap.Logger, s store.Storer, ws websock } func Activate(ctx context.Context) (err error) { - // Run initial update of current settings with super-user credentials + // Run initial update of current settings err = DefaultSettings.UpdateCurrent(ctx) if err != nil { return diff --git a/system/types/app_settings.go b/system/types/app_settings.go index 9c3dc6944..9ca8b8dad 100644 --- a/system/types/app_settings.go +++ b/system/types/app_settings.go @@ -34,6 +34,10 @@ type ( } } `json:"-"` + SMTP struct { + Servers []SmtpServers `json:"-" kv:"servers,final"` + } `json:"-" kv:"smtp"` + Auth struct { Internal struct { // Is internal authentication (username + password) enabled @@ -206,6 +210,16 @@ type ( IssuerUrl string `json:"-" kv:"issuer"` Weight int `json:"-"` } + + SmtpServers struct { + Host string `json:"host"` + Port int `json:"port,string"` + User string `json:"user"` + Pass string `json:"pass"` + From string `json:"from"` + TlsInsecure bool `json:"tlsInsecure"` + TlsServerName string `json:"tlsServerName"` + } ) func (set *ExternalAuthProvider) ValidConfiguration() bool {