mirror of
https://github.com/go-gitea/gitea.git
synced 2025-02-20 11:43:57 +08:00
Compare commits
4 Commits
f16587a0d2
...
d8492a1e9f
Author | SHA1 | Date | |
---|---|---|---|
|
d8492a1e9f | ||
|
e4cd1ebbdd | ||
|
b366b328f5 | ||
|
da19f04a6f |
@ -210,3 +210,35 @@ func upsertUserSettingValue(ctx context.Context, userID int64, key, value string
|
|||||||
return err
|
return err
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RepositoryRandsType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
RepositoryRandsTypeNewIssue RepositoryRandsType = "new_issue"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CreatRandsForRepository(ctx context.Context, userID, repoID int64, event RepositoryRandsType) (string, error) {
|
||||||
|
rand, err := GetUserSalt()
|
||||||
|
if err != nil {
|
||||||
|
return rand, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return rand, SetUserSetting(ctx, userID, SettingsKeyUserRandsForRepo(repoID, string(event)), rand)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetRandsForRepository(ctx context.Context, userID, repoID int64, event RepositoryRandsType) (string, error) {
|
||||||
|
return GetSetting(ctx, userID, SettingsKeyUserRandsForRepo(repoID, string(event)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *User) GetOrCreateRandsForRepository(ctx context.Context, repoID int64, event RepositoryRandsType) (string, error) {
|
||||||
|
rand, err := GetRandsForRepository(ctx, u.ID, repoID, event)
|
||||||
|
if err != nil && !IsErrUserSettingIsNotExist(err) {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(rand) == 0 || err != nil {
|
||||||
|
rand, err = CreatRandsForRepository(ctx, u.ID, repoID, event)
|
||||||
|
}
|
||||||
|
|
||||||
|
return rand, err
|
||||||
|
}
|
||||||
|
@ -3,6 +3,8 @@
|
|||||||
|
|
||||||
package user
|
package user
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// SettingsKeyHiddenCommentTypes is the setting key for hidden comment types
|
// SettingsKeyHiddenCommentTypes is the setting key for hidden comment types
|
||||||
SettingsKeyHiddenCommentTypes = "issue.hidden_comment_types"
|
SettingsKeyHiddenCommentTypes = "issue.hidden_comment_types"
|
||||||
@ -19,3 +21,11 @@ const (
|
|||||||
// SignupUserAgent is the user agent that the user signed up with
|
// SignupUserAgent is the user agent that the user signed up with
|
||||||
SignupUserAgent = "signup.user_agent"
|
SignupUserAgent = "signup.user_agent"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func SettingsKeyUserRands(key string) string {
|
||||||
|
return "rands." + key
|
||||||
|
}
|
||||||
|
|
||||||
|
func SettingsKeyUserRandsForRepo(repoID int64, key string) string {
|
||||||
|
return SettingsKeyUserRands(fmt.Sprintf("repo.%d.%s", repoID, key))
|
||||||
|
}
|
||||||
|
@ -1810,6 +1810,13 @@ issues.content_history.delete_from_history_confirm = Delete from history?
|
|||||||
issues.content_history.options = Options
|
issues.content_history.options = Options
|
||||||
issues.reference_link = Reference: %s
|
issues.reference_link = Reference: %s
|
||||||
|
|
||||||
|
issues.mailto_modal.title = Create new issue by email
|
||||||
|
issues.mailto_modal.desc_1 = You can create a new issue inside this project by sending an email to the following email address:
|
||||||
|
issues.mailto_modal.desc_2 = The subject will be used as the title of the new issue, and the message will be the description.
|
||||||
|
issues.mailto_modal.desc_3 = `This is a private email address generated just for you. Anyone who has it can create issues as if they were you. If that happens, <a href="#" class="%s">reset this token</a>.`
|
||||||
|
issues.mailto_modal.mailto_link = Email a new issue to this repository
|
||||||
|
issues.mailto_modal.send_mail = send mail
|
||||||
|
|
||||||
compare.compare_base = base
|
compare.compare_base = base
|
||||||
compare.compare_head = compare
|
compare.compare_head = compare
|
||||||
|
|
||||||
|
@ -27,6 +27,7 @@ import (
|
|||||||
"code.gitea.io/gitea/services/context"
|
"code.gitea.io/gitea/services/context"
|
||||||
"code.gitea.io/gitea/services/convert"
|
"code.gitea.io/gitea/services/convert"
|
||||||
issue_service "code.gitea.io/gitea/services/issue"
|
issue_service "code.gitea.io/gitea/services/issue"
|
||||||
|
"code.gitea.io/gitea/services/mailer/incoming"
|
||||||
pull_service "code.gitea.io/gitea/services/pull"
|
pull_service "code.gitea.io/gitea/services/pull"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -780,5 +781,36 @@ func Issues(ctx *context.Context) {
|
|||||||
|
|
||||||
ctx.Data["CanWriteIssuesOrPulls"] = ctx.Repo.CanWriteIssuesOrPulls(isPullList)
|
ctx.Data["CanWriteIssuesOrPulls"] = ctx.Repo.CanWriteIssuesOrPulls(isPullList)
|
||||||
|
|
||||||
|
if !isPullList {
|
||||||
|
err := renderMailToIssue(ctx)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("renderMailToIssue", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ctx.HTML(http.StatusOK, tplIssues)
|
ctx.HTML(http.StatusOK, tplIssues)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func renderMailToIssue(ctx *context.Context) error {
|
||||||
|
if !setting.IncomingEmail.Enabled {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ctx.IsSigned {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
token, mailToAddress, err := incoming.GenerateMailToRepoURL(ctx, ctx.Doer, ctx.Repo.Repository, user_model.RepositoryRandsTypeNewIssue)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Data["MailToIssueEnabled"] = true
|
||||||
|
ctx.Data["MailToIssueAddress"] = mailToAddress
|
||||||
|
ctx.Data["MailToIssueLink"] = fmt.Sprintf("mailto:%s", mailToAddress)
|
||||||
|
ctx.Data["MailToIssueToken"] = token
|
||||||
|
ctx.Data["MailToIssueTokenResetUrl"] = fmt.Sprintf("%s/user/settings/repo_mailto_rands_reset/%d", setting.AppSubURL, ctx.Repo.Repository.ID)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
45
routers/web/repo/issue_list_test.go
Normal file
45
routers/web/repo/issue_list_test.go
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
|
"code.gitea.io/gitea/models/unittest"
|
||||||
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
"code.gitea.io/gitea/services/context"
|
||||||
|
"code.gitea.io/gitea/services/contexttest"
|
||||||
|
"code.gitea.io/gitea/services/mailer/token"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRenderMailToIssue(t *testing.T) {
|
||||||
|
unittest.PrepareTestEnv(t)
|
||||||
|
|
||||||
|
ctx, _ := contexttest.MockContext(t, "user2/repo1")
|
||||||
|
|
||||||
|
ctx.IsSigned = true
|
||||||
|
ctx.Doer = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
|
||||||
|
ctx.Repo = &context.Repository{
|
||||||
|
Repository: unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}),
|
||||||
|
}
|
||||||
|
|
||||||
|
setting.IncomingEmail.Enabled = true
|
||||||
|
setting.IncomingEmail.ReplyToAddress = "test%{token}@gitea.io"
|
||||||
|
setting.IncomingEmail.TokenPlaceholder = "%{token}"
|
||||||
|
|
||||||
|
err := renderMailToIssue(ctx)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
key, ok := ctx.Data["MailToIssueToken"].(string)
|
||||||
|
assert.True(t, ok)
|
||||||
|
|
||||||
|
handlerType, user, _, err := token.ExtractToken(ctx, key)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.EqualValues(t, token.NewIssueHandlerType, handlerType)
|
||||||
|
assert.EqualValues(t, ctx.Doer.ID, user.ID)
|
||||||
|
}
|
37
routers/web/user/setting/repo.go
Normal file
37
routers/web/user/setting/repo.go
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package setting
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
|
"code.gitea.io/gitea/services/context"
|
||||||
|
"code.gitea.io/gitea/services/mailer/incoming"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ResetRepoMailToRands(ctx *context.Context) {
|
||||||
|
repoID, _ := strconv.ParseInt(ctx.PathParam("repo_id"), 10, 64)
|
||||||
|
repo, err := repo_model.GetRepositoryByID(ctx, repoID)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetRepositoryByID", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = user_model.CreatRandsForRepository(ctx, ctx.Doer.ID, repo.ID, user_model.RepositoryRandsTypeNewIssue)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("CreatRandsForRepository", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, url, err := incoming.GenerateMailToRepoURL(ctx, ctx.Doer, repo, user_model.RepositoryRandsTypeNewIssue)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GenerateMailToRepoURL", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.JSON(http.StatusOK, map[string]string{"url": url})
|
||||||
|
}
|
@ -683,6 +683,8 @@ func registerRoutes(m *web.Router) {
|
|||||||
m.Get("", user_setting.BlockedUsers)
|
m.Get("", user_setting.BlockedUsers)
|
||||||
m.Post("", web.Bind(forms.BlockUserForm{}), user_setting.BlockedUsersPost)
|
m.Post("", web.Bind(forms.BlockUserForm{}), user_setting.BlockedUsersPost)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
m.Post("/repo_mailto_rands_reset/{repo_id}", user_setting.ResetRepoMailToRands)
|
||||||
}, reqSignIn, ctxDataSet("PageIsUserSettings", true, "EnablePackages", setting.Packages.Enabled))
|
}, reqSignIn, ctxDataSet("PageIsUserSettings", true, "EnablePackages", setting.Packages.Enabled))
|
||||||
|
|
||||||
m.Group("/user", func() {
|
m.Group("/user", func() {
|
||||||
|
@ -255,6 +255,7 @@ loop:
|
|||||||
}
|
}
|
||||||
|
|
||||||
content := getContentFromMailReader(env)
|
content := getContentFromMailReader(env)
|
||||||
|
content.Subject = env.GetHeader("Subject")
|
||||||
|
|
||||||
if err := handler.Handle(ctx, content, user, payload); err != nil {
|
if err := handler.Handle(ctx, content, user, payload); err != nil {
|
||||||
return fmt.Errorf("could not handle message: %w", err)
|
return fmt.Errorf("could not handle message: %w", err)
|
||||||
@ -350,6 +351,7 @@ func searchTokenInAddresses(addresses []*net_mail.Address) string {
|
|||||||
type MailContent struct {
|
type MailContent struct {
|
||||||
Content string
|
Content string
|
||||||
Attachments []*Attachment
|
Attachments []*Attachment
|
||||||
|
Subject string
|
||||||
}
|
}
|
||||||
|
|
||||||
type Attachment struct {
|
type Attachment struct {
|
||||||
|
@ -6,11 +6,13 @@ package incoming
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
issues_model "code.gitea.io/gitea/models/issues"
|
issues_model "code.gitea.io/gitea/models/issues"
|
||||||
access_model "code.gitea.io/gitea/models/perm/access"
|
access_model "code.gitea.io/gitea/models/perm/access"
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
|
"code.gitea.io/gitea/models/unit"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
@ -28,8 +30,10 @@ type MailHandler interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var handlers = map[token.HandlerType]MailHandler{
|
var handlers = map[token.HandlerType]MailHandler{
|
||||||
token.ReplyHandlerType: &ReplyHandler{},
|
token.ReplyHandlerType: &ReplyHandler{},
|
||||||
token.UnsubscribeHandlerType: &UnsubscribeHandler{},
|
token.UnsubscribeHandlerType: &UnsubscribeHandler{},
|
||||||
|
token.NewIssueHandlerType: &NewIssueHandler{},
|
||||||
|
token.NewPullRequestHandlerType: &NewPullRequest{},
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReplyHandler handles incoming emails to create a reply from them
|
// ReplyHandler handles incoming emails to create a reply from them
|
||||||
@ -178,3 +182,79 @@ func (h *UnsubscribeHandler) Handle(ctx context.Context, _ *MailContent, doer *u
|
|||||||
|
|
||||||
return fmt.Errorf("unsupported unsubscribe reference: %v", ref)
|
return fmt.Errorf("unsupported unsubscribe reference: %v", ref)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewIssueHandler handles new issues
|
||||||
|
type NewIssueHandler struct{}
|
||||||
|
|
||||||
|
func (h *NewIssueHandler) Handle(ctx context.Context, content *MailContent, doer *user_model.User, payload []byte) error {
|
||||||
|
if doer == nil {
|
||||||
|
return util.NewInvalidArgumentErrorf("doer can't be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
ref, err := incoming_payload.GetReferenceFromPayload(ctx, payload)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var repo *repo_model.Repository
|
||||||
|
|
||||||
|
switch r := ref.(type) {
|
||||||
|
case *repo_model.Repository:
|
||||||
|
repo = r
|
||||||
|
default:
|
||||||
|
return util.NewInvalidArgumentErrorf("unsupported reply reference: %v", ref)
|
||||||
|
}
|
||||||
|
|
||||||
|
if util.IsEmptyString(content.Subject) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
perm, err := access_model.GetUserRepoPermission(ctx, repo, doer)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !perm.CanRead(unit.TypeIssues) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
attachmentIDs := make([]string, 0, len(content.Attachments))
|
||||||
|
if setting.Attachment.Enabled {
|
||||||
|
for _, attachment := range content.Attachments {
|
||||||
|
a, err := attachment_service.UploadAttachment(ctx, bytes.NewReader(attachment.Content), setting.Attachment.AllowedTypes, int64(len(attachment.Content)), &repo_model.Attachment{
|
||||||
|
Name: attachment.Name,
|
||||||
|
UploaderID: doer.ID,
|
||||||
|
RepoID: repo.ID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if upload.IsErrFileTypeForbidden(err) {
|
||||||
|
log.Info("NewIssueHandler: Skipping disallowed attachment type: %s", attachment.Name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
attachmentIDs = append(attachmentIDs, a.UUID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
issue := &issues_model.Issue{
|
||||||
|
RepoID: repo.ID,
|
||||||
|
Repo: repo,
|
||||||
|
Title: content.Subject,
|
||||||
|
PosterID: doer.ID,
|
||||||
|
Poster: doer,
|
||||||
|
Content: content.Content,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := issue_service.NewIssue(ctx, repo, issue, []int64{}, attachmentIDs, []int64{}, 0); err != nil {
|
||||||
|
log.Warn("NewIssueHandler: Failed to create issue: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPullRequest handles new pull requests
|
||||||
|
type NewPullRequest struct{}
|
||||||
|
|
||||||
|
func (h *NewPullRequest) Handle(ctx context.Context, _ *MailContent, doer *user_model.User, payload []byte) error {
|
||||||
|
return errors.New("not implemented")
|
||||||
|
}
|
||||||
|
38
services/mailer/incoming/mailto_new_issue.go
Normal file
38
services/mailer/incoming/mailto_new_issue.go
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package incoming
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
incoming_payload "code.gitea.io/gitea/services/mailer/incoming/payload"
|
||||||
|
"code.gitea.io/gitea/services/mailer/token"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GenerateMailToRepoURL(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, event user_model.RepositoryRandsType) (string, string, error) {
|
||||||
|
_, err := doer.GetOrCreateRandsForRepository(ctx, repo.ID, event)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, err := incoming_payload.CreateReferencePayload(&incoming_payload.ReferenceRepository{
|
||||||
|
RepositoryID: repo.ID,
|
||||||
|
ActionType: incoming_payload.ReferenceRepositoryActionTypeNewIssue,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := token.CreateToken(ctx, token.NewIssueHandlerType, doer, payload)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
mailToAddress := strings.Replace(setting.IncomingEmail.ReplyToAddress, setting.IncomingEmail.TokenPlaceholder, token, 1)
|
||||||
|
return token, mailToAddress, nil
|
||||||
|
}
|
@ -7,6 +7,8 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
|
|
||||||
issues_model "code.gitea.io/gitea/models/issues"
|
issues_model "code.gitea.io/gitea/models/issues"
|
||||||
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -17,8 +19,22 @@ type payloadReferenceType byte
|
|||||||
const (
|
const (
|
||||||
payloadReferenceIssue payloadReferenceType = iota
|
payloadReferenceIssue payloadReferenceType = iota
|
||||||
payloadReferenceComment
|
payloadReferenceComment
|
||||||
|
payloadReferenceNewIssue
|
||||||
|
payloadReferenceNewPullRequest
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type ReferenceRepositoryActionType int64
|
||||||
|
|
||||||
|
const (
|
||||||
|
ReferenceRepositoryActionTypeNewIssue ReferenceRepositoryActionType = iota
|
||||||
|
ReferenceRepositoryActionTypeNewPullRequest
|
||||||
|
)
|
||||||
|
|
||||||
|
type ReferenceRepository struct {
|
||||||
|
RepositoryID int64
|
||||||
|
ActionType ReferenceRepositoryActionType
|
||||||
|
}
|
||||||
|
|
||||||
// CreateReferencePayload creates data which GetReferenceFromPayload resolves to the reference again.
|
// CreateReferencePayload creates data which GetReferenceFromPayload resolves to the reference again.
|
||||||
func CreateReferencePayload(reference any) ([]byte, error) {
|
func CreateReferencePayload(reference any) ([]byte, error) {
|
||||||
var refType payloadReferenceType
|
var refType payloadReferenceType
|
||||||
@ -31,6 +47,17 @@ func CreateReferencePayload(reference any) ([]byte, error) {
|
|||||||
case *issues_model.Comment:
|
case *issues_model.Comment:
|
||||||
refType = payloadReferenceComment
|
refType = payloadReferenceComment
|
||||||
refID = r.ID
|
refID = r.ID
|
||||||
|
case *ReferenceRepository:
|
||||||
|
switch r.ActionType {
|
||||||
|
case ReferenceRepositoryActionTypeNewIssue:
|
||||||
|
refType = payloadReferenceNewIssue
|
||||||
|
refID = r.RepositoryID
|
||||||
|
case ReferenceRepositoryActionTypeNewPullRequest:
|
||||||
|
refType = payloadReferenceNewPullRequest
|
||||||
|
refID = r.RepositoryID
|
||||||
|
default:
|
||||||
|
return nil, util.NewInvalidArgumentErrorf("unsupported repository reference action type: %d", r.ActionType)
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return nil, util.NewInvalidArgumentErrorf("unsupported reference type: %T", r)
|
return nil, util.NewInvalidArgumentErrorf("unsupported reference type: %T", r)
|
||||||
}
|
}
|
||||||
@ -64,7 +91,41 @@ func GetReferenceFromPayload(ctx context.Context, payload []byte) (any, error) {
|
|||||||
return issues_model.GetIssueByID(ctx, id)
|
return issues_model.GetIssueByID(ctx, id)
|
||||||
case payloadReferenceComment:
|
case payloadReferenceComment:
|
||||||
return issues_model.GetCommentByID(ctx, id)
|
return issues_model.GetCommentByID(ctx, id)
|
||||||
|
case payloadReferenceNewIssue:
|
||||||
|
return repo_model.GetRepositoryByID(ctx, id)
|
||||||
|
case payloadReferenceNewPullRequest:
|
||||||
|
return repo_model.GetRepositoryByID(ctx, id)
|
||||||
default:
|
default:
|
||||||
return nil, util.NewInvalidArgumentErrorf("unsupported reference type: %T", ref)
|
return nil, util.NewInvalidArgumentErrorf("unsupported reference type: %T", ref)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetRandsFromPayload(ctx context.Context, doer *user_model.User, payload []byte) []byte {
|
||||||
|
if len(payload) < 1 {
|
||||||
|
return []byte{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if payload[0] != replyPayloadVersion1 {
|
||||||
|
return []byte{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var ref payloadReferenceType
|
||||||
|
var id int64
|
||||||
|
if err := util.UnpackData(payload[1:], &ref, &id); err != nil {
|
||||||
|
return []byte{}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ref {
|
||||||
|
case payloadReferenceIssue:
|
||||||
|
return []byte(doer.Rands)
|
||||||
|
case payloadReferenceComment:
|
||||||
|
return []byte(doer.Rands)
|
||||||
|
case payloadReferenceNewIssue:
|
||||||
|
rands, _ := user_model.GetRandsForRepository(ctx, doer.ID, id, user_model.RepositoryRandsTypeNewIssue)
|
||||||
|
return []byte(rands)
|
||||||
|
case payloadReferenceNewPullRequest:
|
||||||
|
return []byte{}
|
||||||
|
default:
|
||||||
|
return []byte{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -325,7 +325,7 @@ func composeIssueCommentMessages(ctx *mailCommentContext, lang string, recipient
|
|||||||
|
|
||||||
if setting.IncomingEmail.Enabled {
|
if setting.IncomingEmail.Enabled {
|
||||||
if replyPayload != nil {
|
if replyPayload != nil {
|
||||||
token, err := token.CreateToken(token.ReplyHandlerType, recipient, replyPayload)
|
token, err := token.CreateToken(ctx, token.ReplyHandlerType, recipient, replyPayload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("CreateToken failed: %v", err)
|
log.Error("CreateToken failed: %v", err)
|
||||||
} else {
|
} else {
|
||||||
@ -337,7 +337,7 @@ func composeIssueCommentMessages(ctx *mailCommentContext, lang string, recipient
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
token, err := token.CreateToken(token.UnsubscribeHandlerType, recipient, unsubscribePayload)
|
token, err := token.CreateToken(ctx, token.UnsubscribeHandlerType, recipient, unsubscribePayload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("CreateToken failed: %v", err)
|
log.Error("CreateToken failed: %v", err)
|
||||||
} else {
|
} else {
|
||||||
|
@ -13,6 +13,7 @@ import (
|
|||||||
|
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
incoming_payload "code.gitea.io/gitea/services/mailer/incoming/payload"
|
||||||
)
|
)
|
||||||
|
|
||||||
// A token is a verifiable container describing an action.
|
// A token is a verifiable container describing an action.
|
||||||
@ -34,6 +35,8 @@ const (
|
|||||||
UnknownHandlerType HandlerType = iota
|
UnknownHandlerType HandlerType = iota
|
||||||
ReplyHandlerType
|
ReplyHandlerType
|
||||||
UnsubscribeHandlerType
|
UnsubscribeHandlerType
|
||||||
|
NewIssueHandlerType
|
||||||
|
NewPullRequestHandlerType
|
||||||
)
|
)
|
||||||
|
|
||||||
var encodingWithoutPadding = base32.StdEncoding.WithPadding(base32.NoPadding)
|
var encodingWithoutPadding = base32.StdEncoding.WithPadding(base32.NoPadding)
|
||||||
@ -51,7 +54,7 @@ func (err *ErrToken) Unwrap() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CreateToken creates a token for the action/user tuple
|
// CreateToken creates a token for the action/user tuple
|
||||||
func CreateToken(ht HandlerType, user *user_model.User, data []byte) (string, error) {
|
func CreateToken(ctx context.Context, ht HandlerType, user *user_model.User, data []byte) (string, error) {
|
||||||
payload, err := util.PackData(
|
payload, err := util.PackData(
|
||||||
time.Now().AddDate(tokenLifetimeInYears, 0, 0).Unix(),
|
time.Now().AddDate(tokenLifetimeInYears, 0, 0).Unix(),
|
||||||
ht,
|
ht,
|
||||||
@ -63,7 +66,7 @@ func CreateToken(ht HandlerType, user *user_model.User, data []byte) (string, er
|
|||||||
|
|
||||||
packagedData, err := util.PackData(
|
packagedData, err := util.PackData(
|
||||||
user.ID,
|
user.ID,
|
||||||
generateHmac([]byte(user.Rands), payload),
|
generateHmac(incoming_payload.GetRandsFromPayload(ctx, user, data), payload),
|
||||||
payload,
|
payload,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -100,10 +103,6 @@ func ExtractToken(ctx context.Context, token string) (HandlerType, *user_model.U
|
|||||||
return UnknownHandlerType, nil, nil, err
|
return UnknownHandlerType, nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !crypto_hmac.Equal(hmac, generateHmac([]byte(user.Rands), payload)) {
|
|
||||||
return UnknownHandlerType, nil, nil, &ErrToken{"verification failed"}
|
|
||||||
}
|
|
||||||
|
|
||||||
var expiresUnix int64
|
var expiresUnix int64
|
||||||
var handlerType HandlerType
|
var handlerType HandlerType
|
||||||
var innerPayload []byte
|
var innerPayload []byte
|
||||||
@ -111,6 +110,10 @@ func ExtractToken(ctx context.Context, token string) (HandlerType, *user_model.U
|
|||||||
return UnknownHandlerType, nil, nil, err
|
return UnknownHandlerType, nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !crypto_hmac.Equal(hmac, generateHmac(incoming_payload.GetRandsFromPayload(ctx, user, innerPayload), payload)) {
|
||||||
|
return UnknownHandlerType, nil, nil, &ErrToken{"verification failed"}
|
||||||
|
}
|
||||||
|
|
||||||
if time.Unix(expiresUnix, 0).Before(time.Now()) {
|
if time.Unix(expiresUnix, 0).Before(time.Now()) {
|
||||||
return UnknownHandlerType, nil, nil, &ErrToken{"token expired"}
|
return UnknownHandlerType, nil, nil, &ErrToken{"token expired"}
|
||||||
}
|
}
|
||||||
|
@ -50,6 +50,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{template "shared/issuelist" dict "." . "listType" "repo"}}
|
{{template "shared/issuelist" dict "." . "listType" "repo"}}
|
||||||
|
{{if and .PageIsIssueList .MailToIssueEnabled}}
|
||||||
|
{{template "repo/issue/mailto_module" dict "." .}}
|
||||||
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{template "base/footer" .}}
|
{{template "base/footer" .}}
|
||||||
|
24
templates/repo/issue/mailto_module.tmpl
Normal file
24
templates/repo/issue/mailto_module.tmpl
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<div class="tw-flex tw-justify-center tw-mb-4">
|
||||||
|
<div class="ui small modal get-mailto-addr" id="get-mailto-addr" data-reset-url="{{.MailToIssueTokenResetUrl}}">
|
||||||
|
<div class="header">{{ctx.Locale.Tr "repo.issues.mailto_modal.title"}}</div>
|
||||||
|
<div class="content tw-flex tw-flex-col">
|
||||||
|
<div>{{ctx.Locale.Tr "repo.issues.mailto_modal.desc_1"}}</div>
|
||||||
|
<div class="ui action input mailto-buttons-combo tw-p-2">
|
||||||
|
<input size="60" class="repo-mailto-url" value="{{.MailToIssueAddress}}" readonly>
|
||||||
|
<button class="ui small icon button" data-clipboard-target=".repo-mailto-url" data-tooltip-content="{{ctx.Locale.Tr "copy_url"}}">
|
||||||
|
{{svg "octicon-copy" 14}}
|
||||||
|
</button>
|
||||||
|
<a data-tooltip-content="{{ctx.Locale.Tr "repo.issues.mailto_modal.send_mail"}}" class="ui small icon button send-mail-link" href="{{.MailToIssueLink}}">
|
||||||
|
{{svg "octicon-mail"}}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div>{{ctx.Locale.Tr "repo.issues.mailto_modal.desc_2"}}</div>
|
||||||
|
<div>{{ctx.Locale.Tr "repo.issues.mailto_modal.desc_3" "reset-get-mailto-addr"}}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tw-justify-center">
|
||||||
|
<button class="btn show-modal show-get-mailto-addr" data-modal="#get-mailto-addr">
|
||||||
|
{{ctx.Locale.Tr "repo.issues.mailto_modal.mailto_link"}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -63,7 +63,7 @@ func TestIncomingEmail(t *testing.T) {
|
|||||||
|
|
||||||
payload := []byte{1, 2, 3, 4, 5}
|
payload := []byte{1, 2, 3, 4, 5}
|
||||||
|
|
||||||
token, err := token_service.CreateToken(token_service.ReplyHandlerType, user, payload)
|
token, err := token_service.CreateToken(db.DefaultContext, token_service.ReplyHandlerType, user, payload)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.NotEmpty(t, token)
|
assert.NotEmpty(t, token)
|
||||||
|
|
||||||
@ -186,7 +186,7 @@ func TestIncomingEmail(t *testing.T) {
|
|||||||
|
|
||||||
payload, err := incoming_payload.CreateReferencePayload(issue)
|
payload, err := incoming_payload.CreateReferencePayload(issue)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
token, err := token_service.CreateToken(token_service.ReplyHandlerType, user, payload)
|
token, err := token_service.CreateToken(db.DefaultContext, token_service.ReplyHandlerType, user, payload)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
msg := sender_service.NewMessageFrom(
|
msg := sender_service.NewMessageFrom(
|
||||||
|
@ -223,6 +223,31 @@ async function initIssuePinSort() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function initGetMailToAddrModal() {
|
||||||
|
const modal = document.querySelector('.modal.get-mailto-addr');
|
||||||
|
if (modal === null) return;
|
||||||
|
|
||||||
|
const url = modal.getAttribute('data-reset-url');
|
||||||
|
|
||||||
|
const input = modal.querySelector<HTMLInputElement>('.repo-mailto-url');
|
||||||
|
const buttonReset = modal.querySelector<HTMLAnchorElement>('.reset-get-mailto-addr');
|
||||||
|
const sendMailLink = modal.querySelector<HTMLAnchorElement>('.send-mail-link');
|
||||||
|
|
||||||
|
buttonReset.addEventListener('click', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const rsp = await POST(url);
|
||||||
|
if (rsp.status !== 200) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await rsp.json();
|
||||||
|
|
||||||
|
input.value = data.url;
|
||||||
|
sendMailLink.href = `mailto:${data.url}`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function initRepoIssueList() {
|
export function initRepoIssueList() {
|
||||||
if (document.querySelector('.page-content.repository.issue-list, .page-content.repository.milestone-issue-list')) {
|
if (document.querySelector('.page-content.repository.issue-list, .page-content.repository.milestone-issue-list')) {
|
||||||
initRepoIssueListCheckboxes();
|
initRepoIssueListCheckboxes();
|
||||||
@ -232,4 +257,5 @@ export function initRepoIssueList() {
|
|||||||
// user or org home: issue list, pull request list
|
// user or org home: issue list, pull request list
|
||||||
queryElems(document, '.ui.dropdown.user-remote-search', (el) => initDropdownUserRemoteSearch(el));
|
queryElems(document, '.ui.dropdown.user-remote-search', (el) => initDropdownUserRemoteSearch(el));
|
||||||
}
|
}
|
||||||
|
initGetMailToAddrModal();
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user