Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions custom/conf/app.ini.sample
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,8 @@ MAX_FILE_SIZE = 1048576
[admin]
; Disallow regular (non-admin) users from creating organizations.
DISABLE_REGULAR_ORG_CREATION = false
; Default configuration for email notifications for users (user configurable). Options: enabled, onmention, disabled
DEFAULT_EMAIL_NOTIFICATIONS = enabled

[security]
; Whether the installer is disabled
Expand Down
3 changes: 3 additions & 0 deletions docs/content/doc/advanced/config-cheat-sheet.en-us.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,9 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.
- `UPDATE_BUFFER_LEN`: **20**: Buffer length of index request.
- `MAX_FILE_SIZE`: **1048576**: Maximum size in bytes of files to be indexed.

## Admin (`admin`)
- `DEFAULT_EMAIL_NOTIFICATIONS`: **enabled**: Default configuration for email notifications for users (user configurable). Options: enabled, onmention, disabled

## Security (`security`)

- `INSTALL_LOCK`: **false**: Disallow access to the install page.
Expand Down
9 changes: 9 additions & 0 deletions models/fixtures/user.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
name: user1
full_name: User One
email: user1@example.com
email_notifications_preference: enabled
passwd: 7d93daa0d1e6f2305cc8fa496847d61dc7320bb16262f9c55dd753480207234cdd96a93194e408341971742f4701772a025a # password
type: 0 # individual
salt: ZogKvWdyEx
Expand All @@ -22,6 +23,7 @@
full_name: " < U<se>r Tw<o > >< "
email: user2@example.com
keep_email_private: true
email_notifications_preference: enabled
passwd: 7d93daa0d1e6f2305cc8fa496847d61dc7320bb16262f9c55dd753480207234cdd96a93194e408341971742f4701772a025a # password
type: 0 # individual
salt: ZogKvWdyEx
Expand All @@ -40,6 +42,7 @@
name: user3
full_name: " <<<< >> >> > >> > >>> >> "
email: user3@example.com
email_notifications_preference: onmention
passwd: 7d93daa0d1e6f2305cc8fa496847d61dc7320bb16262f9c55dd753480207234cdd96a93194e408341971742f4701772a025a # password
type: 1 # organization
salt: ZogKvWdyEx
Expand All @@ -56,6 +59,7 @@
name: user4
full_name: " "
email: user4@example.com
email_notifications_preference: onmention
passwd: 7d93daa0d1e6f2305cc8fa496847d61dc7320bb16262f9c55dd753480207234cdd96a93194e408341971742f4701772a025a # password
type: 0 # individual
salt: ZogKvWdyEx
Expand All @@ -72,6 +76,7 @@
name: user5
full_name: User Five
email: user5@example.com
email_notifications_preference: enabled
passwd: 7d93daa0d1e6f2305cc8fa496847d61dc7320bb16262f9c55dd753480207234cdd96a93194e408341971742f4701772a025a # password
type: 0 # individual
salt: ZogKvWdyEx
Expand All @@ -89,6 +94,7 @@
name: user6
full_name: User Six
email: user6@example.com
email_notifications_preference: enabled
passwd: 7d93daa0d1e6f2305cc8fa496847d61dc7320bb16262f9c55dd753480207234cdd96a93194e408341971742f4701772a025a # password
type: 1 # organization
salt: ZogKvWdyEx
Expand All @@ -105,6 +111,7 @@
name: user7
full_name: User Seven
email: user7@example.com
email_notifications_preference: disabled
passwd: 7d93daa0d1e6f2305cc8fa496847d61dc7320bb16262f9c55dd753480207234cdd96a93194e408341971742f4701772a025a # password
type: 1 # organization
salt: ZogKvWdyEx
Expand All @@ -121,6 +128,7 @@
name: user8
full_name: User Eight
email: user8@example.com
email_notifications_preference: enabled
passwd: 7d93daa0d1e6f2305cc8fa496847d61dc7320bb16262f9c55dd753480207234cdd96a93194e408341971742f4701772a025a # password
type: 0 # individual
salt: ZogKvWdyEx
Expand All @@ -138,6 +146,7 @@
name: user9
full_name: User Nine
email: user9@example.com
email_notifications_preference: onmention
passwd: 7d93daa0d1e6f2305cc8fa496847d61dc7320bb16262f9c55dd753480207234cdd96a93194e408341971742f4701772a025a # password
type: 0 # individual
salt: ZogKvWdyEx
Expand Down
8 changes: 4 additions & 4 deletions models/issue_mail.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,17 +70,17 @@ func mailIssueCommentToParticipants(e Engine, issue *Issue, doer *User, content
if err != nil {
return fmt.Errorf("GetUserByID [%d]: %v", watchers[i].UserID, err)
}
if to.IsOrganization() {
if to.IsOrganization() || to.EmailNotifications() == EmailNotificationsEnabled {
continue
}

tos = append(tos, to.Email)
names = append(names, to.Name)
}
for i := range participants {
if participants[i].ID == doer.ID {
continue
} else if com.IsSliceContainsStr(names, participants[i].Name) {
if participants[i].ID == doer.ID ||
com.IsSliceContainsStr(names, participants[i].Name) ||
participants[i].EmailNotifications() == EmailNotificationsEnabled {
continue
}

Expand Down
2 changes: 2 additions & 0 deletions models/migrations/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,8 @@ var migrations = []Migration{
NewMigration("add index on owner_id of repository and type, review_id of comment", addIndexOnRepositoryAndComment),
// v92 -> v93
NewMigration("remove orphaned repository index statuses", removeLingeringIndexStatus),
// v93 -> v94
NewMigration("add email notification enabled preference to user", addEmailNotificationEnabledToUser),
}

// Migrate database to current version
Expand Down
16 changes: 16 additions & 0 deletions models/migrations/v93.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

package migrations

import "github.com/go-xorm/xorm"

func addEmailNotificationEnabledToUser(x *xorm.Engine) error {
// Issue see models/user.go
type User struct {
EmailNotificationsPreference string `xorm:"NOT NULL DEFAULT 'enabled'"`
}

return x.Sync2(new(User))
}
33 changes: 27 additions & 6 deletions models/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,13 @@ const (
algoScrypt = "scrypt"
algoArgon2 = "argon2"
algoPbkdf2 = "pbkdf2"

// EmailNotificationsEnabled indicates that the user would like to receive all email notifications
EmailNotificationsEnabled = "enabled"
// EmailNotificationsOnMention indicates that the user would like to be notified via email when mentioned.
EmailNotificationsOnMention = "onmention"
// EmailNotificationsDisabled indicates that the user would not like to be notified via email.
EmailNotificationsDisabled = "disabled"
)

var (
Expand Down Expand Up @@ -87,10 +94,11 @@ type User struct {
Name string `xorm:"UNIQUE NOT NULL"`
FullName string
// Email is the primary email address (to be used for communication)
Email string `xorm:"NOT NULL"`
KeepEmailPrivate bool
Passwd string `xorm:"NOT NULL"`
PasswdHashAlgo string `xorm:"NOT NULL DEFAULT 'pbkdf2'"`
Email string `xorm:"NOT NULL"`
KeepEmailPrivate bool
EmailNotificationsPreference string `xorm:"NOT NULL DEFAULT 'enabled'"`
Passwd string `xorm:"NOT NULL"`
PasswdHashAlgo string `xorm:"NOT NULL DEFAULT 'pbkdf2'"`

// MustChangePassword is an attribute that determines if a user
// is to change his/her password after registration.
Expand Down Expand Up @@ -719,6 +727,17 @@ func (u *User) IsMailable() bool {
return u.IsActive
}

// EmailNotifications returns the User's email notification preference
func (u *User) EmailNotifications() string {
return u.EmailNotificationsPreference
}

// SetEmailNotifications sets the user's email notification preference
func (u *User) SetEmailNotifications(set string) {
u.EmailNotificationsPreference = set
_ = UpdateUser(u)
}

func isUserExist(e Engine, uid int64, name string) (bool, error) {
if len(name) == 0 {
return false, nil
Expand Down Expand Up @@ -868,6 +887,7 @@ func CreateUser(u *User) (err error) {
}
u.HashPassword(u.Passwd)
u.AllowCreateOrganization = setting.Service.DefaultAllowCreateOrganization && !setting.Admin.DisableRegularOrgCreation
u.EmailNotificationsPreference = setting.Admin.DefaultEmailNotification
u.MaxRepoCreation = -1
u.Theme = setting.UI.DefaultTheme

Expand Down Expand Up @@ -1253,7 +1273,8 @@ func getUserByName(e Engine, name string) (*User, error) {
return u, nil
}

// GetUserEmailsByNames returns a list of e-mails corresponds to names.
// GetUserEmailsByNames returns a list of e-mails corresponds to names of users
// that have their email notifications set to enabled or onmention.
func GetUserEmailsByNames(names []string) []string {
return getUserEmailsByNames(x, names)
}
Expand All @@ -1265,7 +1286,7 @@ func getUserEmailsByNames(e Engine, names []string) []string {
if err != nil {
continue
}
if u.IsMailable() {
if u.IsMailable() && u.EmailNotifications() != EmailNotificationsDisabled {
mails = append(mails, u.Email)
}
}
Expand Down
33 changes: 33 additions & 0 deletions models/user_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ func TestGetUserEmailsByNames(t *testing.T) {
// ignore none active user email
assert.Equal(t, []string{"user8@example.com"}, GetUserEmailsByNames([]string{"user8", "user9"}))
assert.Equal(t, []string{"user8@example.com", "user5@example.com"}, GetUserEmailsByNames([]string{"user8", "user5"}))

assert.Equal(t, []string{"user8@example.com"}, GetUserEmailsByNames([]string{"user8", "user7"}))
}

func TestUser_APIFormat(t *testing.T) {
Expand Down Expand Up @@ -196,6 +198,37 @@ func TestDeleteUser(t *testing.T) {
test(11)
}

func TestEmailNotificationPreferences(t *testing.T) {
assert.NoError(t, PrepareTestDatabase())
for _, test := range []struct {
expected string
userID int64
}{
{EmailNotificationsEnabled, 1},
{EmailNotificationsEnabled, 2},
{EmailNotificationsOnMention, 3},
{EmailNotificationsOnMention, 4},
{EmailNotificationsEnabled, 5},
{EmailNotificationsEnabled, 6},
{EmailNotificationsDisabled, 7},
{EmailNotificationsEnabled, 8},
{EmailNotificationsOnMention, 9},
} {
user := AssertExistsAndLoadBean(t, &User{ID: test.userID}).(*User)
assert.Equal(t, test.expected, user.EmailNotifications())

// Try all possible settings
user.SetEmailNotifications(EmailNotificationsDisabled)
assert.Equal(t, EmailNotificationsDisabled, user.EmailNotifications())

user.SetEmailNotifications(EmailNotificationsOnMention)
assert.Equal(t, EmailNotificationsOnMention, user.EmailNotifications())

user.SetEmailNotifications(EmailNotificationsDisabled)
assert.Equal(t, EmailNotificationsDisabled, user.EmailNotifications())
}
}

func TestHashPasswordDeterministic(t *testing.T) {
b := make([]byte, 16)
rand.Read(b)
Expand Down
4 changes: 4 additions & 0 deletions modules/setting/setting.go
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@ var (
// Admin settings
Admin struct {
DisableRegularOrgCreation bool
DefaultEmailNotification string
}

// Picture settings
Expand Down Expand Up @@ -762,6 +763,9 @@ func NewContext() {
}
}

sec = Cfg.Section("admin")
Admin.DefaultEmailNotification = sec.Key("DEFAULT_EMAIL_NOTIFICATIONS").MustString("enabled")

sec = Cfg.Section("security")
InstallLock = sec.Key("INSTALL_LOCK").MustBool(false)
SecretKey = sec.Key("SECRET_KEY").MustString("!#@FDEWREWR&*(")
Expand Down
9 changes: 9 additions & 0 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,11 @@ confirm_delete_account = Confirm Deletion
delete_account_title = Delete User Account
delete_account_desc = Are you sure you want to permanently delete this user account?

email_notifications.enable = Enable Email Notifications
email_notifications.onmention = Only Email on Mention
email_notifications.disable = Disable Email Notifications
email_notifications.submit = Set Email Preference

[repo]
owner = Owner
repo_name = Repository Name
Expand Down Expand Up @@ -1126,6 +1131,10 @@ settings.basic_settings = Basic Settings
settings.mirror_settings = Mirror Settings
settings.sync_mirror = Synchronize Now
settings.mirror_sync_in_progress = Mirror synchronization is in progress. Check back in a minute.
settings.email_notifications.enable = Enable Email Notifications
settings.email_notifications.onmention = Only Email on Mention
settings.email_notifications.disable = Disable Email Notifications
settings.email_notifications.submit = Set Email Preference
settings.site = Website
settings.update_settings = Update Settings
settings.advanced_settings = Advanced Settings
Expand Down
2 changes: 1 addition & 1 deletion public/css/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -786,7 +786,7 @@ footer .ui.left,footer .ui.right{line-height:40px}
.ui.form .dropzone .dz-error-message{top:140px}
.settings .content{margin-top:2px}
.settings .content .segment,.settings .content>.header{box-shadow:0 1px 2px 0 rgba(34,36,38,.15)}
.settings .list>.item .green{color:#21ba45}
.settings .list>.item .green:not(.ui.button){color:#21ba45}
.settings .list>.item:not(:first-child){border-top:1px solid #eaeaea;padding:1rem;margin:15px -1rem -1rem -1rem}
.settings .list>.item>.mega-octicon{display:table-cell}
.settings .list>.item>.mega-octicon+.content{display:table-cell;padding:0 0 0 .5em;vertical-align:top}
Expand Down
2 changes: 1 addition & 1 deletion public/less/_repository.less
Original file line number Diff line number Diff line change
Expand Up @@ -2137,7 +2137,7 @@

.list {
> .item {
.green {
.green:not(.ui.button) {
color: #21ba45;
}

Expand Down
14 changes: 14 additions & 0 deletions routers/user/setting/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ func Account(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsAccount"] = true
ctx.Data["Email"] = ctx.User.Email
ctx.Data["EmailNotificationsPreference"] = ctx.User.EmailNotifications()

loadAccountData(ctx)

Expand Down Expand Up @@ -81,6 +82,19 @@ func EmailPost(ctx *context.Context, form auth.AddEmailForm) {
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
return
}
// Set Email Notification Preference
if ctx.Query("_method") == "NOTIFICATION" {
preference := ctx.Query("preference")
if !(preference == models.EmailNotificationsEnabled ||
preference == models.EmailNotificationsOnMention ||
preference == models.EmailNotificationsDisabled) {
log.Error("Email notifications preference change returned unrecognized option %s: %s", preference, ctx.User.Name)
}
ctx.User.SetEmailNotifications(preference)
log.Trace("Email notifications preference made %s: %s", preference, ctx.User.Name)
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
return
}

if ctx.HasError() {
loadAccountData(ctx)
Expand Down
27 changes: 25 additions & 2 deletions templates/user/settings/account.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,30 @@
<div class="ui attached segment">
<div class="ui email list">
<div class="item">
{{.i18n.Tr "settings.email_desc"}}
<form action="{{AppSubUrl}}/user/settings/account/email" class="ui form" method="post">
{{.i18n.Tr "settings.email_desc"}}
<div class="right floated content">
<div class="field">
<button class="ui green button">{{$.i18n.Tr "settings.email_notifications.submit"}}</button>
</div>
</div>
<div class="right floated content">
{{$.CsrfTokenHtml}}
<input name="_method" type="hidden" value="NOTIFICATION">
<div class="field">
<div class="ui selection dropdown" tabindex="0">
<input name="preference" type="hidden" value="{{.EmailNotificationsPreference}}">
<i class="dropdown icon"></i>
<div class="text">{{$.i18n.Tr "settings.email_notifications"}}</div>
<div class="menu">
<div data-value="enabled" class="{{if eq .EmailNotificationsPreference "enabled"}}active selected {{end}}item">{{$.i18n.Tr "settings.email_notifications.enable"}}</div>
<div data-value="onmention" class="{{if eq .EmailNotificationsPreference "onmention"}}active selected {{end}}item">{{$.i18n.Tr "settings.email_notifications.onmention"}}</div>
<div data-value="disabled" class="{{if eq .EmailNotificationsPreference "disabled"}}active selected {{end}}item">{{$.i18n.Tr "settings.email_notifications.disable"}}</div>
</div>
</div>
</div>
</div>
</form>
</div>
{{range .Emails}}
<div class="item">
Expand Down Expand Up @@ -103,7 +126,7 @@
<i class="dropdown icon"></i>
<div class="text">
{{range $i,$a := .AllThemes}}
{{if eq $.SignedUser.Theme $a}}{{$a}}{{end}}
{{if eq $.SignedUser.Theme $a}}{{$a}}{{end}}
{{end}}
</div>

Expand Down