Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
78 changes: 59 additions & 19 deletions models/user/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"net/url"
"path/filepath"
"regexp"
"strconv"
"strings"
"sync"
"time"
Expand Down Expand Up @@ -212,7 +213,7 @@ func (u *User) SetLastLogin() {

// GetPlaceholderEmail returns an noreply email
func (u *User) GetPlaceholderEmail() string {
return fmt.Sprintf("%s@%s", u.LowerName, setting.Service.NoReplyAddress)
return fmt.Sprintf("%s+%d@%s", u.LowerName, u.ID, setting.Service.NoReplyAddress)
}

// GetEmail returns a noreply email, if the user has set to keep his
Expand Down Expand Up @@ -1197,14 +1198,19 @@ func GetUsersByEmails(ctx context.Context, emails []string) (*EmailUserMap, erro

needCheckEmails := make(container.Set[string])
needCheckUserNames := make(container.Set[string])
needCheckUserIDs := make(container.Set[int64])
noReplyAddressSuffix := "@" + strings.ToLower(setting.Service.NoReplyAddress)
for _, email := range emails {
emailLower := strings.ToLower(email)
needCheckEmails.Add(emailLower)
if noReplyUserNameLower, ok := strings.CutSuffix(emailLower, noReplyAddressSuffix); ok {
needCheckUserNames.Add(noReplyUserNameLower)
needCheckEmails.Add(emailLower)
} else {
needCheckEmails.Add(emailLower)
name, idstr, hasPlus := strings.Cut(noReplyUserNameLower, "+")
if hasPlus {
if id, err := strconv.ParseInt(idstr, 10, 64); err == nil {
needCheckUserIDs.Add(id)
}
}
needCheckUserNames.Add(name)
}
}

Expand Down Expand Up @@ -1234,13 +1240,46 @@ func GetUsersByEmails(ctx context.Context, emails []string) (*EmailUserMap, erro
}
}

users := make(map[int64]*User, len(needCheckUserNames))
if err := db.GetEngine(ctx).In("lower_name", needCheckUserNames.Values()).Find(&users); err != nil {
return nil, err
usersByIDs := make(map[string]*User)
if len(needCheckUserIDs) > 0 || len(needCheckUserNames) > 0 {
cond := builder.NewCond()
if len(needCheckUserIDs) > 0 {
cond = cond.Or(builder.In("id", needCheckUserIDs.Values()))
}
if len(needCheckUserNames) > 0 {
cond = cond.Or(builder.In("lower_name", needCheckUserNames.Values()))
}
if err := db.GetEngine(ctx).Where(cond).Find(&usersByIDs); err != nil {
return nil, err
Comment thread
TheFox0x7 marked this conversation as resolved.
Outdated
}
}

usersByName := make(map[string]*User)
for _, user := range usersByIDs {
usersByName[user.LowerName] = user
}
for _, user := range users {
results[strings.ToLower(user.GetPlaceholderEmail())] = user

for _, email := range emails {
emailLower := strings.ToLower(email)
if _, ok := results[emailLower]; ok {
continue
}

noReplyUserNameLower, ok := strings.CutSuffix(emailLower, noReplyAddressSuffix)
if !ok {
continue
}

name, id, hasPlus := strings.Cut(noReplyUserNameLower, "+")
if hasPlus {
if user, ok := usersByIDs[id]; ok {
results[emailLower] = user
}
} else if user, ok := usersByName[name]; ok {
results[emailLower] = user
}
}

return &EmailUserMap{results}, nil
}

Expand All @@ -1262,16 +1301,17 @@ func GetUserByEmail(ctx context.Context, email string) (*User, error) {
}

// Finally, if email address is the protected email address:
if before, ok := strings.CutSuffix(email, "@"+setting.Service.NoReplyAddress); ok {
username := before
user := &User{}
has, err := db.GetEngine(ctx).Where("lower_name=?", username).Get(user)
if err != nil {
return nil, err
}
if has {
return user, nil
if before, ok := strings.CutSuffix(email, strings.ToLower("@"+setting.Service.NoReplyAddress)); ok {
// check if the email is in format user+id@noreplyaddress
username, id, isAlias := strings.Cut(before, "+")
Comment thread
TheFox0x7 marked this conversation as resolved.
Outdated
if isAlias {
id, err := strconv.ParseInt(id, 10, 64)
if err != nil {
return nil, err
} // error or UserNotExist?
Comment thread
TheFox0x7 marked this conversation as resolved.
Outdated
return GetUserByID(ctx, id)
}
return GetUserByName(ctx, username)
}

return nil, ErrUserNotExist{Name: email}
Expand Down
47 changes: 37 additions & 10 deletions models/user/user_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,27 @@ func TestOAuth2Application_LoadUser(t *testing.T) {

func TestUserEmails(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
defer test.MockVariableValue(&setting.Service.NoReplyAddress, "NoReply.gitea.internal")()
t.Run("GetUserEmailsByNames", func(t *testing.T) {
// ignore none active user email
// ignore not active user email
assert.ElementsMatch(t, []string{"user8@example.com"}, user_model.GetUserEmailsByNames(t.Context(), []string{"user8", "user9"}))
assert.ElementsMatch(t, []string{"user8@example.com", "user5@example.com"}, user_model.GetUserEmailsByNames(t.Context(), []string{"user8", "user5"}))
assert.ElementsMatch(t, []string{"user8@example.com"}, user_model.GetUserEmailsByNames(t.Context(), []string{"user8", "org7"}))
})

cases := []struct {
Email string
UID int64
}{
{"UseR1@example.com", 1},
{"user1-2@example.COM", 1},
{"USER2@" + setting.Service.NoReplyAddress, 2},
{"user2+2@" + setting.Service.NoReplyAddress, 2},
{"oldUser2UsernameWhichDoesNotMatterForQuery+2@" + setting.Service.NoReplyAddress, 2},
{"badUser+99999@" + setting.Service.NoReplyAddress, 0},
{"user4@example.com", 4},
{"no-such", 0},
}
t.Run("GetUsersByEmails", func(t *testing.T) {
defer test.MockVariableValue(&setting.Service.NoReplyAddress, "NoReply.gitea.internal")()
testGetUserByEmail := func(t *testing.T, email string, uid int64) {
Expand All @@ -70,15 +85,27 @@ func TestUserEmails(t *testing.T) {
require.NotNil(t, user)
assert.Equal(t, uid, user.ID)
}
cases := []struct {
Email string
UID int64
}{
{"UseR1@example.com", 1},
{"user1-2@example.COM", 1},
{"USER2@" + setting.Service.NoReplyAddress, 2},
{"user4@example.com", 4},
{"no-such", 0},
for _, c := range cases {
t.Run(c.Email, func(t *testing.T) {
testGetUserByEmail(t, c.Email, c.UID)
})
}

t.Run("NoReplyConflict", func(t *testing.T) {
setting.Service.NoReplyAddress = "example.com"
testGetUserByEmail(t, "user1-2@example.COM", 1)
})
})
t.Run("GetUserByEmail", func(t *testing.T) {
testGetUserByEmail := func(t *testing.T, email string, uid int64) {
user, err := user_model.GetUserByEmail(t.Context(), email)
if uid == 0 {
require.Error(t, err)
assert.Nil(t, user)
} else {
require.NotNil(t, user)
assert.Equal(t, uid, user.ID)
}
}
for _, c := range cases {
t.Run(c.Email, func(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion tests/integration/editor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ func testEditorWebGitCommitEmail(t *testing.T) {
t.Run("DefaultEmailKeepPrivate", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
paramsForKeepPrivate["commit_email"] = ""
resp1 = makeReq(t, linkForKeepPrivate, paramsForKeepPrivate, "User Two", "user2@noreply.example.org")
resp1 = makeReq(t, linkForKeepPrivate, paramsForKeepPrivate, "User Two", "user2+2@noreply.example.org")
})
t.Run("ChooseEmail", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
Expand Down
12 changes: 6 additions & 6 deletions tests/integration/repofiles_change_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,14 +133,14 @@ func getExpectedFileResponseForRepoFilesCreate(commitID string, lastCommit *git.
Author: &api.CommitUser{
Identity: api.Identity{
Name: "User Two",
Email: "user2@noreply.example.org",
Email: "user2+2@noreply.example.org",
},
Date: time.Now().UTC().Format(time.RFC3339),
},
Committer: &api.CommitUser{
Identity: api.Identity{
Name: "User Two",
Email: "user2@noreply.example.org",
Email: "user2+2@noreply.example.org",
},
Date: time.Now().UTC().Format(time.RFC3339),
},
Expand Down Expand Up @@ -203,14 +203,14 @@ func getExpectedFileResponseForRepoFilesUpdate(commitID, filename, lastCommitSHA
Author: &api.CommitUser{
Identity: api.Identity{
Name: "User Two",
Email: "user2@noreply.example.org",
Email: "user2+2@noreply.example.org",
},
Date: time.Now().UTC().Format(time.RFC3339),
},
Committer: &api.CommitUser{
Identity: api.Identity{
Name: "User Two",
Email: "user2@noreply.example.org",
Email: "user2+2@noreply.example.org",
},
Date: time.Now().UTC().Format(time.RFC3339),
},
Expand Down Expand Up @@ -313,13 +313,13 @@ func getExpectedFileResponseForRepoFilesUpdateRename(commitID, lastCommitSHA str
Author: &api.CommitUser{
Identity: api.Identity{
Name: "User Two",
Email: "user2@noreply.example.org",
Email: "user2+2@noreply.example.org",
},
},
Committer: &api.CommitUser{
Identity: api.Identity{
Name: "User Two",
Email: "user2@noreply.example.org",
Email: "user2+2@noreply.example.org",
},
},
Parents: []*api.CommitMeta{
Expand Down
Loading