Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

add creating new issue by email support #33571

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
a1012112796 wants to merge 3 commits into go-gitea:main
base: main
Choose a base branch
Loading
from a1012112796:zzc/dev/incoming_mail_new_issue
Open
Show file tree
Hide file tree
Changes from all 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
32 changes: 32 additions & 0 deletions models/user/setting.go
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -210,3 +210,35 @@ func upsertUserSettingValue(ctx context.Context, userID int64, key, value string
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
}
10 changes: 10 additions & 0 deletions models/user/setting_keys.go
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

package user

import "fmt"

const (
// SettingsKeyHiddenCommentTypes is the setting key for hidden comment types
SettingsKeyHiddenCommentTypes = "issue.hidden_comment_types"
Expand All @@ -19,3 +21,11 @@ const (
// SignupUserAgent is the user agent that the user signed up with
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))
}
7 changes: 7 additions & 0 deletions options/locale/locale_en-US.ini
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -1808,6 +1808,13 @@ issues.content_history.delete_from_history_confirm = Delete from history?
issues.content_history.options = Options
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_head = compare

Expand Down
32 changes: 32 additions & 0 deletions routers/web/repo/issue_list.go
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
issue_service "code.gitea.io/gitea/services/issue"
"code.gitea.io/gitea/services/mailer/incoming"
pull_service "code.gitea.io/gitea/services/pull"
)

Expand Down Expand Up @@ -780,5 +781,36 @@ func Issues(ctx *context.Context) {

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)
}

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 changes: 45 additions & 0 deletions routers/web/repo/issue_list_test.go
View file Open in desktop
Original file line number Diff line number Diff line change
@@ -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 changes: 37 additions & 0 deletions routers/web/user/setting/repo.go
View file Open in desktop
Original file line number Diff line number Diff line change
@@ -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})
}
2 changes: 2 additions & 0 deletions routers/web/web.go
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -683,6 +683,8 @@ func registerRoutes(m *web.Router) {
m.Get("", user_setting.BlockedUsers)
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))

m.Group("/user", func() {
Expand Down
2 changes: 2 additions & 0 deletions services/mailer/incoming/incoming.go
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@ loop:
}

content := getContentFromMailReader(env)
content.Subject = env.GetHeader("Subject")

if err := handler.Handle(ctx, content, user, payload); err != nil {
return fmt.Errorf("could not handle message: %w", err)
Expand Down Expand Up @@ -350,6 +351,7 @@ func searchTokenInAddresses(addresses []*net_mail.Address) string {
type MailContent struct {
Content string
Attachments []*Attachment
Subject string
}

type Attachment struct {
Expand Down
84 changes: 82 additions & 2 deletions services/mailer/incoming/incoming_handler.go
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ package incoming
import (
"bytes"
"context"
"errors"
"fmt"

issues_model "code.gitea.io/gitea/models/issues"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
Expand All @@ -28,8 +30,10 @@ type MailHandler interface {
}

var handlers = map[token.HandlerType]MailHandler{
token.ReplyHandlerType: &ReplyHandler{},
token.UnsubscribeHandlerType: &UnsubscribeHandler{},
token.ReplyHandlerType: &ReplyHandler{},
token.UnsubscribeHandlerType: &UnsubscribeHandler{},
token.NewIssueHandlerType: &NewIssueHandler{},
token.NewPullRequestHandlerType: &NewPullRequest{},
}

// ReplyHandler handles incoming emails to create a reply from them
Expand Down Expand Up @@ -178,3 +182,79 @@ func (h *UnsubscribeHandler) Handle(ctx context.Context, _ *MailContent, doer *u

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 changes: 38 additions & 0 deletions services/mailer/incoming/mailto_new_issue.go
View file Open in desktop
Original file line number Diff line number Diff line change
@@ -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
}
Loading
Loading

AltStyle によって変換されたページ (->オリジナル) /