diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index cc02cbda3b..6b603f9d42 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -1455,6 +1455,8 @@ LEVEL = Info ;; ;; Default configuration for email notifications for users (user configurable). Options: enabled, onmention, disabled ;DEFAULT_EMAIL_NOTIFICATIONS = enabled +;; Send email notifications to all instance admins on new user sign-ups. Options: enabled, true, false +;NOTIFY_NEW_SIGN_UPS = false ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/modules/setting/admin.go b/modules/setting/admin.go index 2d2dd26de9..4e2f343715 100644 --- a/modules/setting/admin.go +++ b/modules/setting/admin.go @@ -7,6 +7,7 @@ package setting var Admin struct { DisableRegularOrgCreation bool DefaultEmailNotification string + NotifyNewSignUps bool } func loadAdminFrom(rootCfg ConfigProvider) { diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 9efe07a3d7..71b2a45107 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -438,6 +438,10 @@ activate_email = Verify your email address activate_email.title = %s, please verify your email address activate_email.text = Please click the following link to verify your email address within %s: +admin.new_user.subject = New user %s +admin.new_user.user_info = User Information +admin.new_user.text = Please click here to manage the user from the admin panel. + register_notify = Welcome to Gitea register_notify.title = %[1]s, welcome to %[2]s register_notify.text_1 = this is your registration confirmation email for %s! diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go index c20a45ebc9..3f1d7881fe 100644 --- a/routers/web/auth/auth.go +++ b/routers/web/auth/auth.go @@ -30,6 +30,7 @@ import ( "code.gitea.io/gitea/services/externalaccount" "code.gitea.io/gitea/services/forms" "code.gitea.io/gitea/services/mailer" + notify_service "code.gitea.io/gitea/services/notify" "github.com/markbates/goth" ) @@ -568,6 +569,7 @@ func handleUserCreated(ctx *context.Context, u *user_model.User, gothUser *goth. } } + notify_service.NewUserSignUp(ctx, u) // update external user information if gothUser != nil { if err := externalaccount.UpdateExternalUser(u, *gothUser); err != nil { @@ -591,7 +593,6 @@ func handleUserCreated(ctx *context.Context, u *user_model.User, gothUser *goth. ctx.Data["Email"] = u.Email ctx.Data["ActiveCodeLives"] = timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale) ctx.HTML(http.StatusOK, TplActivate) - if setting.CacheService.Enabled { if err := ctx.Cache.Put("MailResendLimit_"+u.LowerName, u.LowerName, 180); err != nil { log.Error("Set cache(MailResendLimit) fail: %v", err) diff --git a/services/mailer/mail_admin_new_user.go b/services/mailer/mail_admin_new_user.go new file mode 100644 index 0000000000..332afa5e49 --- /dev/null +++ b/services/mailer/mail_admin_new_user.go @@ -0,0 +1,82 @@ +// Copyright 2023 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT +package mailer + +import ( + "bytes" + "context" + "strconv" + + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/templates" + "code.gitea.io/gitea/modules/translation" +) + +const ( + tplNewUserMail base.TplName = "admin_new_user" +) + +var sa = SendAsyncs + +// MailNewUser sends notification emails on new user registrations to all admins +func MailNewUser(ctx context.Context, u *user_model.User) { + if !setting.Admin.NotifyNewSignUps { + return + } + + if setting.MailService == nil { + // No mail service configured + return + } + + recipients, err := user_model.GetAllUsers() + if err != nil { + log.Error("user_model.GetAllUsers: %v", err) + return + } + + langMap := make(map[string][]string) + for _, r := range recipients { + if r.IsAdmin { + langMap[r.Language] = append(langMap[r.Language], r.Email) + } + } + + for lang, tos := range langMap { + mailNewUser(ctx, u, lang, tos) + } +} + +func mailNewUser(ctx context.Context, u *user_model.User, lang string, tos []string) { + locale := translation.NewLocale(lang) + + subject := locale.Tr("mail.admin.new_user.subject", u.Name) + manageUserURL := setting.AppSubURL + "/admin/users/" + strconv.FormatInt(u.ID, 10) + body := locale.Tr("mail.admin.new_user.text", manageUserURL) + mailMeta := map[string]any{ + "NewUser": u, + "Subject": subject, + "Body": body, + "Language": locale.Language(), + "locale": locale, + "Str2html": templates.Str2html, + } + + var mailBody bytes.Buffer + + if err := bodyTemplates.ExecuteTemplate(&mailBody, string(tplNewUserMail), mailMeta); err != nil { + log.Error("ExecuteTemplate [%s]: %v", string(tplNewUserMail)+"/body", err) + return + } + + msgs := make([]*Message, 0, len(tos)) + for _, to := range tos { + msg := NewMessage(to, subject, mailBody.String()) + msg.Info = subject + msgs = append(msgs, msg) + } + sa(msgs) +} diff --git a/services/mailer/mail_admin_new_user_test.go b/services/mailer/mail_admin_new_user_test.go new file mode 100644 index 0000000000..0772bb0da5 --- /dev/null +++ b/services/mailer/mail_admin_new_user_test.go @@ -0,0 +1,88 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package mailer + +import ( + "context" + "strconv" + "strings" + "testing" + + "code.gitea.io/gitea/models/db" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/setting" + + "github.com/stretchr/testify/assert" +) + +func getTestUsers() []*user_model.User { + admin := new(user_model.User) + admin.Name = "admin" + admin.IsAdmin = true + admin.Language = "en_US" + admin.Email = "admin@forgejo.org" + + newUser := new(user_model.User) + newUser.Name = "new_user" + newUser.Language = "en_US" + newUser.IsAdmin = false + newUser.Email = "new_user@forgejo.org" + newUser.LastLoginUnix = 1693648327 + newUser.CreatedUnix = 1693648027 + + user_model.CreateUser(admin) + user_model.CreateUser(newUser) + + users := make([]*user_model.User, 0) + users = append(users, admin) + users = append(users, newUser) + + return users +} + +func cleanUpUsers(ctx context.Context, users []*user_model.User) { + for _, u := range users { + db.DeleteByID(ctx, u.ID, new(user_model.User)) + } +} + +func TestAdminNotificationMail_test(t *testing.T) { + mailService := setting.Mailer{ + From: "test@forgejo.org", + Protocol: "dummy", + } + + setting.MailService = &mailService + setting.Domain = "localhost" + setting.AppSubURL = "http://localhost" + + // test with NOTIFY_NEW_SIGNUPS enabled + setting.Admin.NotifyNewSignUps = true + + ctx := context.Background() + NewContext(ctx) + + users := getTestUsers() + oldSendAsyncs := sa + defer func() { + sa = oldSendAsyncs + cleanUpUsers(ctx, users) + }() + + sa = func(msgs []*Message) { + assert.Equal(t, len(msgs), 1, "Test provides only one admin user, so only one email must be sent") + assert.Equal(t, msgs[0].To, users[0].Email, "checks if the recipient is the admin of the instance") + manageUserURL := "/admin/users/" + strconv.FormatInt(users[1].ID, 10) + assert.True(t, strings.ContainsAny(msgs[0].Body, manageUserURL), "checks if the message contains the link to manage the newly created user from the admin panel") + } + MailNewUser(ctx, users[1]) + + // test with NOTIFY_NEW_SIGNUPS disabled; emails shouldn't be sent + setting.Admin.NotifyNewSignUps = false + sa = func(msgs []*Message) { + assert.Equal(t, 1, 0, "this shouldn't execute. MailNewUser must exit early since NOTIFY_NEW_SIGNUPS is disabled") + } + + MailNewUser(ctx, users[1]) +} diff --git a/services/mailer/notify.go b/services/mailer/notify.go index f0419e2cbb..0b7e8178e6 100644 --- a/services/mailer/notify.go +++ b/services/mailer/notify.go @@ -199,3 +199,7 @@ func (m *mailNotifier) RepoPendingTransfer(ctx context.Context, doer, newOwner * log.Error("SendRepoTransferNotifyMail: %v", err) } } + +func (m *mailNotifier) NewUserSignUp(ctx context.Context, newUser *user_model.User) { + MailNewUser(ctx, newUser) +} diff --git a/services/notify/notifier.go b/services/notify/notifier.go index d1dbe44c11..a65dc22dd6 100644 --- a/services/notify/notifier.go +++ b/services/notify/notifier.go @@ -72,4 +72,5 @@ type Notifier interface { PackageCreate(ctx context.Context, doer *user_model.User, pd *packages_model.PackageDescriptor) PackageDelete(ctx context.Context, doer *user_model.User, pd *packages_model.PackageDescriptor) + NewUserSignUp(ctx context.Context, newUser *user_model.User) } diff --git a/services/notify/notify.go b/services/notify/notify.go index 71bc1c7d58..6410e15a5b 100644 --- a/services/notify/notify.go +++ b/services/notify/notify.go @@ -360,3 +360,10 @@ func PackageDelete(ctx context.Context, doer *user_model.User, pd *packages_mode notifier.PackageDelete(ctx, doer, pd) } } + +// NewUserSignUp notifies deletion of a package to notifiers +func NewUserSignUp(ctx context.Context, newUser *user_model.User) { + for _, notifier := range notifiers { + notifier.NewUserSignUp(ctx, newUser) + } +} diff --git a/services/notify/null.go b/services/notify/null.go index c5b31f83d6..3535e014c8 100644 --- a/services/notify/null.go +++ b/services/notify/null.go @@ -204,3 +204,7 @@ func (*NullNotifier) PackageCreate(ctx context.Context, doer *user_model.User, p // PackageDelete places a place holder function func (*NullNotifier) PackageDelete(ctx context.Context, doer *user_model.User, pd *packages_model.PackageDescriptor) { } + +// NotifyNewUserSignUp notifies deletion of a package to notifiers +func (*NullNotifier) NewUserSignUp(ctx context.Context, newUser *user_model.User) { +} diff --git a/templates/mail/admin_new_user.tmpl b/templates/mail/admin_new_user.tmpl new file mode 100644 index 0000000000..58b5c264e7 --- /dev/null +++ b/templates/mail/admin_new_user.tmpl @@ -0,0 +1,22 @@ + + +
+ +{{.Body | Str2html}}
+ +