diff --git a/models/user/setting.go b/models/user/setting.go
index b4af0e5ccd684..7cd524f94f79f 100644
--- a/models/user/setting.go
+++ b/models/user/setting.go
@@ -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
+}
diff --git a/models/user/setting_keys.go b/models/user/setting_keys.go
index 3149aae18ba68..1b9c1593e94f4 100644
--- a/models/user/setting_keys.go
+++ b/models/user/setting_keys.go
@@ -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))
+}
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index bce64a81a3d9d..9c341cd4b6c5b 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -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
 
diff --git a/routers/web/repo/issue_list.go b/routers/web/repo/issue_list.go
index 2f615a100e3d0..96e2283c080ef 100644
--- a/routers/web/repo/issue_list.go
+++ b/routers/web/repo/issue_list.go
@@ -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
+}
diff --git a/routers/web/repo/issue_list_test.go b/routers/web/repo/issue_list_test.go
new file mode 100644
index 0000000000000..eb9410df9f783
--- /dev/null
+++ b/routers/web/repo/issue_list_test.go
@@ -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)
+}
diff --git a/routers/web/user/setting/repo.go b/routers/web/user/setting/repo.go
new file mode 100644
index 0000000000000..95d09ac32f7ed
--- /dev/null
+++ b/routers/web/user/setting/repo.go
@@ -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})
+}
diff --git a/routers/web/web.go b/routers/web/web.go
index f5bd6a92979db..25ddaf1356747 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -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() {
diff --git a/services/mailer/incoming/incoming.go b/services/mailer/incoming/incoming.go
index eade0cf271bf8..8e15618781460 100644
--- a/services/mailer/incoming/incoming.go
+++ b/services/mailer/incoming/incoming.go
@@ -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 {
diff --git a/services/mailer/incoming/incoming_handler.go b/services/mailer/incoming/incoming_handler.go
index 38a234eac1fd2..ce4024dacfc61 100644
--- a/services/mailer/incoming/incoming_handler.go
+++ b/services/mailer/incoming/incoming_handler.go
@@ -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")
+}
diff --git a/services/mailer/incoming/mailto_new_issue.go b/services/mailer/incoming/mailto_new_issue.go
new file mode 100644
index 0000000000000..e42d5159d2e58
--- /dev/null
+++ b/services/mailer/incoming/mailto_new_issue.go
@@ -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
+}
diff --git a/services/mailer/incoming/payload/payload.go b/services/mailer/incoming/payload/payload.go
index 00ada7826bdaf..3e4825e938b4b 100644
--- a/services/mailer/incoming/payload/payload.go
+++ b/services/mailer/incoming/payload/payload.go
@@ -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{}
+	}
+}
diff --git a/services/mailer/mail.go b/services/mailer/mail.go
index 52e19bde6f261..955f6f90c1600 100644
--- a/services/mailer/mail.go
+++ b/services/mailer/mail.go
@@ -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 {
diff --git a/services/mailer/token/token.go b/services/mailer/token/token.go
index 8a5a762d6b5fd..e97c3868286fb 100644
--- a/services/mailer/token/token.go
+++ b/services/mailer/token/token.go
@@ -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,10 +103,6 @@ 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
@@ -111,6 +110,10 @@ func ExtractToken(ctx context.Context, token string) (HandlerType, *user_model.U
 		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"}
 	}
diff --git a/templates/repo/issue/list.tmpl b/templates/repo/issue/list.tmpl
index 53d0eca171fee..e09e17d7cdb95 100644
--- a/templates/repo/issue/list.tmpl
+++ b/templates/repo/issue/list.tmpl
@@ -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" .}}
diff --git a/templates/repo/issue/mailto_module.tmpl b/templates/repo/issue/mailto_module.tmpl
new file mode 100644
index 0000000000000..b5016d30b0c2e
--- /dev/null
+++ b/templates/repo/issue/mailto_module.tmpl
@@ -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>
diff --git a/tests/integration/incoming_email_test.go b/tests/integration/incoming_email_test.go
index e968a2956ee05..4f8e262754bed 100644
--- a/tests/integration/incoming_email_test.go
+++ b/tests/integration/incoming_email_test.go
@@ -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(
diff --git a/web_src/js/features/repo-issue-list.ts b/web_src/js/features/repo-issue-list.ts
index 01d4bb6f78174..2fbe586a1c57f 100644
--- a/web_src/js/features/repo-issue-list.ts
+++ b/web_src/js/features/repo-issue-list.ts
@@ -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();
 }