Skip to content

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
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -3,6 +3,8 @@

package user

import "fmt"

const (
// SettingsKeyHiddenCommentTypes is the setting key for 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 = "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
Original file line number Diff line number Diff line change
@@ -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

32 changes: 32 additions & 0 deletions routers/web/repo/issue_list.go
Original file line number Diff line number Diff line change
@@ -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"
)

@@ -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
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
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
Original file line number Diff line number Diff line change
@@ -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() {
2 changes: 2 additions & 0 deletions services/mailer/incoming/incoming.go
Original file line number Diff line number Diff line change
@@ -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)
@@ -350,6 +351,7 @@ func searchTokenInAddresses(addresses []*net_mail.Address) string {
type MailContent struct {
Content string
Attachments []*Attachment
Subject string
}

type Attachment struct {
84 changes: 82 additions & 2 deletions services/mailer/incoming/incoming_handler.go
Original file line number Diff line number Diff line change
@@ -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"
@@ -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
@@ -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
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
}
61 changes: 61 additions & 0 deletions services/mailer/incoming/payload/payload.go
Original file line number Diff line number Diff line change
@@ -7,6 +7,8 @@ import (
"context"

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

@@ -17,8 +19,22 @@ type payloadReferenceType byte
const (
payloadReferenceIssue payloadReferenceType = iota
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.
func CreateReferencePayload(reference any) ([]byte, error) {
var refType payloadReferenceType
@@ -31,6 +47,17 @@ func CreateReferencePayload(reference any) ([]byte, error) {
case *issues_model.Comment:
refType = payloadReferenceComment
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:
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)
case payloadReferenceComment:
return issues_model.GetCommentByID(ctx, id)
case payloadReferenceNewIssue:
return repo_model.GetRepositoryByID(ctx, id)
case payloadReferenceNewPullRequest:
return repo_model.GetRepositoryByID(ctx, id)
default:
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{}
}
}
4 changes: 2 additions & 2 deletions services/mailer/mail.go
Original file line number Diff line number Diff line change
@@ -325,7 +325,7 @@ func composeIssueCommentMessages(ctx *mailCommentContext, lang string, recipient

if setting.IncomingEmail.Enabled {
if replyPayload != nil {
token, err := token.CreateToken(token.ReplyHandlerType, recipient, replyPayload)
token, err := token.CreateToken(ctx, token.ReplyHandlerType, recipient, replyPayload)
if err != nil {
log.Error("CreateToken failed: %v", err)
} 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 {
log.Error("CreateToken failed: %v", err)
} else {
15 changes: 9 additions & 6 deletions services/mailer/token/token.go
Original file line number Diff line number Diff line change
@@ -13,6 +13,7 @@ import (

user_model "code.gitea.io/gitea/models/user"
"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.
@@ -34,6 +35,8 @@ const (
UnknownHandlerType HandlerType = iota
ReplyHandlerType
UnsubscribeHandlerType
NewIssueHandlerType
NewPullRequestHandlerType
)

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
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(
time.Now().AddDate(tokenLifetimeInYears, 0, 0).Unix(),
ht,
@@ -63,7 +66,7 @@ func CreateToken(ht HandlerType, user *user_model.User, data []byte) (string, er

packagedData, err := util.PackData(
user.ID,
generateHmac([]byte(user.Rands), payload),
generateHmac(incoming_payload.GetRandsFromPayload(ctx, user, data), payload),
payload,
)
if err != nil {
@@ -100,17 +103,17 @@ func ExtractToken(ctx context.Context, token string) (HandlerType, *user_model.U
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 handlerType HandlerType
var innerPayload []byte
if err := util.UnpackData(payload, &expiresUnix, &handlerType, &innerPayload); err != nil {
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()) {
return UnknownHandlerType, nil, nil, &ErrToken{"token expired"}
}
3 changes: 3 additions & 0 deletions templates/repo/issue/list.tmpl
Original file line number Diff line number Diff line change
@@ -50,6 +50,9 @@
</div>
</div>
{{template "shared/issuelist" dict "." . "listType" "repo"}}
{{if and .PageIsIssueList .MailToIssueEnabled}}
{{template "repo/issue/mailto_module" dict "." .}}
{{end}}
</div>
</div>
{{template "base/footer" .}}
24 changes: 24 additions & 0 deletions templates/repo/issue/mailto_module.tmpl
Original file line number Diff line number Diff line change
@@ -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>
4 changes: 2 additions & 2 deletions tests/integration/incoming_email_test.go
Original file line number Diff line number Diff line change
@@ -63,7 +63,7 @@ func TestIncomingEmail(t *testing.T) {

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.NotEmpty(t, token)

@@ -186,7 +186,7 @@ func TestIncomingEmail(t *testing.T) {

payload, err := incoming_payload.CreateReferencePayload(issue)
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)

msg := sender_service.NewMessageFrom(
26 changes: 26 additions & 0 deletions web_src/js/features/repo-issue-list.ts
Original file line number Diff line number Diff line change
@@ -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() {
if (document.querySelector('.page-content.repository.issue-list, .page-content.repository.milestone-issue-list')) {
initRepoIssueListCheckboxes();
@@ -232,4 +257,5 @@ export function initRepoIssueList() {
// user or org home: issue list, pull request list
queryElems(document, '.ui.dropdown.user-remote-search', (el) => initDropdownUserRemoteSearch(el));
}
initGetMailToAddrModal();
}