1
1
mirror of https://github.com/go-gitea/gitea synced 2025-09-18 14:48:13 +00:00

Refactor and update mail templates (#35150)

- Moved mail templates to new directories.
- Added new devtest ymls.
- Embedded styles as much as possible.
- Added new translation keys for actions email.

---------

Signed-off-by: NorthRealm <155140859+NorthRealm@users.noreply.github.com>
Co-authored-by: delvh <dev.lh@web.de>
This commit is contained in:
NorthRealm
2025-09-05 11:20:42 +08:00
committed by GitHub
parent 5fe3296055
commit 07347634aa
29 changed files with 169 additions and 124 deletions

View File

@@ -260,18 +260,18 @@ func actionToTemplate(issue *issues_model.Issue, actionType activities_model.Act
}
}
template = typeName + "/" + name
template = "repo/" + typeName + "/" + name
ok := LoadedTemplates().BodyTemplates.Lookup(template) != nil
if !ok && typeName != "issue" {
template = "issue/" + name
template = "repo/issue/" + name
ok = LoadedTemplates().BodyTemplates.Lookup(template) != nil
}
if !ok {
template = typeName + "/default"
template = "repo/" + typeName + "/default"
ok = LoadedTemplates().BodyTemplates.Lookup(template) != nil
}
if !ok {
template = "issue/default"
template = "repo/issue/default"
}
return typeName, name, template
}

View File

@@ -19,7 +19,7 @@ import (
sender_service "code.gitea.io/gitea/services/mailer/sender"
)
const tplNewReleaseMail templates.TplName = "release"
const tplNewReleaseMail templates.TplName = "repo/release"
func generateMessageIDForRelease(release *repo_model.Release) string {
return fmt.Sprintf("<%s/releases/%d@%s>", release.Repo.FullName(), release.ID, setting.Domain)

View File

@@ -11,13 +11,17 @@ import (
"code.gitea.io/gitea/models/organization"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/translation"
sender_service "code.gitea.io/gitea/services/mailer/sender"
)
const mailRepoTransferNotify templates.TplName = "notify/repo_transfer"
const (
mailNotifyCollaborator templates.TplName = "repo/collaborator"
mailRepoTransferNotify templates.TplName = "repo/transfer"
)
// SendRepoTransferNotifyMail triggers a notification e-mail when a pending repository transfer was created
func SendRepoTransferNotifyMail(ctx context.Context, doer, newOwner *user_model.User, repo *repo_model.Repository) error {
@@ -91,3 +95,33 @@ func sendRepoTransferNotifyMailPerLang(lang string, newOwner, doer *user_model.U
return nil
}
// SendCollaboratorMail sends mail notification to new collaborator.
func SendCollaboratorMail(u, doer *user_model.User, repo *repo_model.Repository) {
if setting.MailService == nil || !u.IsActive {
return
}
locale := translation.NewLocale(u.Language)
repoName := repo.FullName()
subject := locale.TrString("mail.repo.collaborator.added.subject", doer.DisplayName(), repoName)
data := map[string]any{
"locale": locale,
"Subject": subject,
"RepoName": repoName,
"Link": repo.HTMLURL(),
"Language": locale.Language(),
}
var content bytes.Buffer
if err := LoadedTemplates().BodyTemplates.ExecuteTemplate(&content, string(mailNotifyCollaborator), data); err != nil {
log.Error("Template: %v", err)
return
}
msg := sender_service.NewMessage(u.EmailTo(), subject, content.String())
msg.Info = fmt.Sprintf("UID: %d, add collaborator", u.ID)
SendAsync(msg)
}

View File

@@ -19,7 +19,7 @@ import (
sender_service "code.gitea.io/gitea/services/mailer/sender"
)
const tplTeamInviteMail templates.TplName = "team_invite"
const tplTeamInviteMail templates.TplName = "org/team_invite"
// MailTeamInvite sends team invites
func MailTeamInvite(ctx context.Context, inviter *user_model.User, team *org_model.Team, invite *org_model.TeamInvite) error {

View File

@@ -115,7 +115,7 @@ func TestComposeIssueComment(t *testing.T) {
setting.IncomingEmail.Enabled = true
defer func() { setting.IncomingEmail.Enabled = false }()
prepareMailTemplates("issue/comment", subjectTpl, bodyTpl)
prepareMailTemplates("repo/issue/comment", subjectTpl, bodyTpl)
recipients := []*user_model.User{{Name: "Test", Email: "test@gitea.com"}, {Name: "Test2", Email: "test2@gitea.com"}}
msgs, err := composeIssueCommentMessages(t.Context(), &mailComment{
@@ -160,7 +160,7 @@ func TestComposeIssueComment(t *testing.T) {
func TestMailMentionsComment(t *testing.T) {
doer, _, issue, comment := prepareMailerTest(t)
comment.Poster = doer
prepareMailTemplates("issue/comment", subjectTpl, bodyTpl)
prepareMailTemplates("repo/issue/comment", subjectTpl, bodyTpl)
mails := 0
defer test.MockVariableValue(&SendAsync, func(msgs ...*sender_service.Message) {
@@ -175,7 +175,7 @@ func TestMailMentionsComment(t *testing.T) {
func TestComposeIssueMessage(t *testing.T) {
doer, _, issue, _ := prepareMailerTest(t)
prepareMailTemplates("issue/new", subjectTpl, bodyTpl)
prepareMailTemplates("repo/issue/new", subjectTpl, bodyTpl)
recipients := []*user_model.User{{Name: "Test", Email: "test@gitea.com"}, {Name: "Test2", Email: "test2@gitea.com"}}
msgs, err := composeIssueCommentMessages(t.Context(), &mailComment{
Issue: issue, Doer: doer, ActionType: activities_model.ActionCreateIssue,
@@ -204,14 +204,14 @@ func TestTemplateSelection(t *testing.T) {
doer, repo, issue, comment := prepareMailerTest(t)
recipients := []*user_model.User{{Name: "Test", Email: "test@gitea.com"}}
prepareMailTemplates("issue/default", "issue/default/subject", "issue/default/body")
prepareMailTemplates("repo/issue/default", "repo/issue/default/subject", "repo/issue/default/body")
texttmpl.Must(LoadedTemplates().SubjectTemplates.New("issue/new").Parse("issue/new/subject"))
texttmpl.Must(LoadedTemplates().SubjectTemplates.New("pull/comment").Parse("pull/comment/subject"))
texttmpl.Must(LoadedTemplates().SubjectTemplates.New("issue/close").Parse("")) // Must default to a fallback subject
template.Must(LoadedTemplates().BodyTemplates.New("issue/new").Parse("issue/new/body"))
template.Must(LoadedTemplates().BodyTemplates.New("pull/comment").Parse("pull/comment/body"))
template.Must(LoadedTemplates().BodyTemplates.New("issue/close").Parse("issue/close/body"))
texttmpl.Must(LoadedTemplates().SubjectTemplates.New("repo/issue/new").Parse("repo/issue/new/subject"))
texttmpl.Must(LoadedTemplates().SubjectTemplates.New("repo/pull/comment").Parse("repo/pull/comment/subject"))
texttmpl.Must(LoadedTemplates().SubjectTemplates.New("repo/issue/close").Parse("")) // Must default to a fallback subject
template.Must(LoadedTemplates().BodyTemplates.New("repo/issue/new").Parse("repo/issue/new/body"))
template.Must(LoadedTemplates().BodyTemplates.New("repo/pull/comment").Parse("repo/pull/comment/body"))
template.Must(LoadedTemplates().BodyTemplates.New("repo/issue/close").Parse("repo/issue/close/body"))
expect := func(t *testing.T, msg *sender_service.Message, expSubject, expBody string) {
subject := msg.ToMessage().GetGenHeader("Subject")
@@ -226,13 +226,13 @@ func TestTemplateSelection(t *testing.T) {
Issue: issue, Doer: doer, ActionType: activities_model.ActionCreateIssue,
Content: "test body",
}, recipients, false, "TestTemplateSelection")
expect(t, msg, "issue/new/subject", "issue/new/body")
expect(t, msg, "repo/issue/new/subject", "repo/issue/new/body")
msg = testComposeIssueCommentMessage(t, &mailComment{
Issue: issue, Doer: doer, ActionType: activities_model.ActionCommentIssue,
Content: "test body", Comment: comment,
}, recipients, false, "TestTemplateSelection")
expect(t, msg, "issue/default/subject", "issue/default/body")
expect(t, msg, "repo/issue/default/subject", "repo/issue/default/body")
pull := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2, Repo: repo, Poster: doer})
comment = unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 4, Issue: pull})
@@ -240,13 +240,13 @@ func TestTemplateSelection(t *testing.T) {
Issue: pull, Doer: doer, ActionType: activities_model.ActionCommentPull,
Content: "test body", Comment: comment,
}, recipients, false, "TestTemplateSelection")
expect(t, msg, "pull/comment/subject", "pull/comment/body")
expect(t, msg, "repo/pull/comment/subject", "repo/pull/comment/body")
msg = testComposeIssueCommentMessage(t, &mailComment{
Issue: issue, Doer: doer, ActionType: activities_model.ActionCloseIssue,
Content: "test body", Comment: comment,
}, recipients, false, "TestTemplateSelection")
expect(t, msg, "Re: [user2/repo1] issue1 (#1)", "issue/close/body")
expect(t, msg, "Re: [user2/repo1] issue1 (#1)", "repo/issue/close/body")
}
func TestTemplateServices(t *testing.T) {
@@ -256,7 +256,7 @@ func TestTemplateServices(t *testing.T) {
expect := func(t *testing.T, issue *issues_model.Issue, comment *issues_model.Comment, doer *user_model.User,
actionType activities_model.ActionType, fromMention bool, tplSubject, tplBody, expSubject, expBody string,
) {
prepareMailTemplates("issue/default", tplSubject, tplBody)
prepareMailTemplates("repo/issue/default", tplSubject, tplBody)
recipients := []*user_model.User{{Name: "Test", Email: "test@gitea.com"}}
msg := testComposeIssueCommentMessage(t, &mailComment{
Issue: issue, Doer: doer, ActionType: actionType,
@@ -523,7 +523,7 @@ func TestEmbedBase64Images(t *testing.T) {
att2ImgBase64 := fmt.Sprintf(`<img src="%s"/>`, att2Base64)
t.Run("ComposeMessage", func(t *testing.T) {
prepareMailTemplates("issue/new", subjectTpl, bodyTpl)
prepareMailTemplates("repo/issue/new", subjectTpl, bodyTpl)
issue.Content = fmt.Sprintf(`MSG-BEFORE <image src="attachments/%s"> MSG-AFTER`, att1.UUID)
require.NoError(t, issues_model.UpdateIssueCols(t.Context(), issue, "content"))

View File

@@ -7,7 +7,6 @@ import (
"bytes"
"fmt"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
@@ -18,11 +17,10 @@ import (
)
const (
mailAuthActivate templates.TplName = "auth/activate"
mailAuthActivateEmail templates.TplName = "auth/activate_email"
mailAuthResetPassword templates.TplName = "auth/reset_passwd"
mailAuthRegisterNotify templates.TplName = "auth/register_notify"
mailNotifyCollaborator templates.TplName = "notify/collaborator"
mailAuthActivate templates.TplName = "user/auth/activate"
mailAuthActivateEmail templates.TplName = "user/auth/activate_email"
mailAuthResetPassword templates.TplName = "user/auth/reset_passwd"
mailAuthRegisterNotify templates.TplName = "user/auth/register_notify"
)
// sendUserMail sends a mail to the user
@@ -128,34 +126,3 @@ func SendRegisterNotifyMail(u *user_model.User) {
SendAsync(msg)
}
// SendCollaboratorMail sends mail notification to new collaborator.
func SendCollaboratorMail(u, doer *user_model.User, repo *repo_model.Repository) {
if setting.MailService == nil || !u.IsActive {
// No mail service configured OR the user is inactive
return
}
locale := translation.NewLocale(u.Language)
repoName := repo.FullName()
subject := locale.TrString("mail.repo.collaborator.added.subject", doer.DisplayName(), repoName)
data := map[string]any{
"locale": locale,
"Subject": subject,
"RepoName": repoName,
"Link": repo.HTMLURL(),
"Language": locale.Language(),
}
var content bytes.Buffer
if err := LoadedTemplates().BodyTemplates.ExecuteTemplate(&content, string(mailNotifyCollaborator), data); err != nil {
log.Error("Template: %v", err)
return
}
msg := sender_service.NewMessage(u.EmailTo(), subject, content.String())
msg.Info = fmt.Sprintf("UID: %d, add collaborator", u.ID)
SendAsync(msg)
}

View File

@@ -8,6 +8,7 @@ import (
"context"
"fmt"
"sort"
"time"
actions_model "code.gitea.io/gitea/models/actions"
repo_model "code.gitea.io/gitea/models/repo"
@@ -15,18 +16,20 @@ import (
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/translation"
"code.gitea.io/gitea/services/convert"
sender_service "code.gitea.io/gitea/services/mailer/sender"
)
const tplWorkflowRun = "notify/workflow_run"
const tplWorkflowRun templates.TplName = "repo/actions/workflow_run"
type convertedWorkflowJob struct {
HTMLURL string
Status actions_model.Status
Name string
Attempt int64
HTMLURL string
Name string
Status actions_model.Status
Attempt int64
Duration time.Duration
}
func generateMessageIDForActionsWorkflowRunStatusEmail(repo *repo_model.Repository, run *actions_model.ActionRun) string {
@@ -45,16 +48,15 @@ func composeAndSendActionsWorkflowRunStatusEmail(ctx context.Context, repo *repo
}
}
subject := "Run"
var subjectTrString string
switch run.Status {
case actions_model.StatusFailure:
subject += " failed"
subjectTrString = "mail.repo.actions.run.failed"
case actions_model.StatusCancelled:
subject += " cancelled"
subjectTrString = "mail.repo.actions.run.cancelled"
case actions_model.StatusSuccess:
subject += " succeeded"
subjectTrString = "mail.repo.actions.run.succeeded"
}
subject = fmt.Sprintf("%s: %s (%s)", subject, run.WorkflowID, base.ShortSha(run.CommitSHA))
displayName := fromDisplayName(sender)
messageID := generateMessageIDForActionsWorkflowRunStatusEmail(repo, run)
metadataHeaders := generateMetadataHeaders(repo)
@@ -80,10 +82,11 @@ func composeAndSendActionsWorkflowRunStatusEmail(ctx context.Context, repo *repo
continue
}
convertedJobs = append(convertedJobs, convertedWorkflowJob{
HTMLURL: converted0.HTMLURL,
Name: converted0.Name,
Status: job.Status,
Attempt: converted0.RunAttempt,
HTMLURL: converted0.HTMLURL,
Name: converted0.Name,
Status: job.Status,
Attempt: converted0.RunAttempt,
Duration: job.Duration(),
})
}
@@ -93,27 +96,28 @@ func composeAndSendActionsWorkflowRunStatusEmail(ctx context.Context, repo *repo
}
for lang, tos := range langMap {
locale := translation.NewLocale(lang)
var runStatusText string
var runStatusTrString string
switch run.Status {
case actions_model.StatusSuccess:
runStatusText = "All jobs have succeeded"
runStatusTrString = "mail.repo.actions.jobs.all_succeeded"
case actions_model.StatusFailure:
runStatusText = "All jobs have failed"
runStatusTrString = "mail.repo.actions.jobs.all_failed"
for _, job := range jobs {
if !job.Status.IsFailure() {
runStatusText = "Some jobs were not successful"
runStatusTrString = "mail.repo.actions.jobs.some_not_successful"
break
}
}
case actions_model.StatusCancelled:
runStatusText = "All jobs have been cancelled"
runStatusTrString = "mail.repo.actions.jobs.all_cancelled"
}
subject := fmt.Sprintf("%s: %s (%s)", locale.TrString(subjectTrString), run.WorkflowID, base.ShortSha(run.CommitSHA))
var mailBody bytes.Buffer
if err := LoadedTemplates().BodyTemplates.ExecuteTemplate(&mailBody, tplWorkflowRun, map[string]any{
if err := LoadedTemplates().BodyTemplates.ExecuteTemplate(&mailBody, string(tplWorkflowRun), map[string]any{
"Subject": subject,
"Repo": repo,
"Run": run,
"RunStatusText": runStatusText,
"RunStatusText": locale.TrString(runStatusTrString),
"Jobs": convertedJobs,
"locale": locale,
}); err != nil {