Use templates for issue e-mail subject and body (#8329)
* Add template capability for issue mail subject * Remove test string * Fix trim subject length * Add comment to template and run make fmt * Add information for the template * Rename defaultMailSubject() to fallbackMailSubject() * General rewrite of the mail template code * Fix .Doer name * Use text/template for subject instead of html * Fix subject Re: prefix * Fix mail tests * Fix static templates * [skip ci] Updated translations via Crowdin * Expose db.SetMaxOpenConns and allow non MySQL dbs to set conn pool params (#8528) * Expose db.SetMaxOpenConns and allow other dbs to set their connection params * Add note about port exhaustion Co-Authored-By: guillep2k <18600385+guillep2k@users.noreply.github.com> * Prevent .code-view from overriding font on icon fonts (#8614) * Correct some outdated statements in the contributing guidelines (#8612) * More information for drone-cli in CONTRIBUTING.md * Increases the version of drone-cli to 1.2.0 * Adds a note for the Docker Toolbox on Windows Signed-off-by: LukBukkit <luk.bukkit@gmail.com> * Fix the url for the blog repository (now on gitea.com) Signed-off-by: LukBukkit <luk.bukkit@gmail.com> * Remove TrN due to lack of lang context * Redo templates to match previous code * Fix extra character in template * Unify PR & Issue tempaltes, fix format * Remove default subject * Add template tests * Fix template * Remove replaced function * Provide User as models.User for better consistency * Add docs * Fix doc inaccuracies, improve examples * Change mail footer to math AppName * Add test for mail subject/body template separation * Add support for code review comments * Update docs/content/doc/advanced/mail-templates-us.md Co-Authored-By: 6543 <24977596+6543@users.noreply.github.com>
This commit is contained in:
parent
d5b1e6bc51
commit
1f90147f39
13 changed files with 781 additions and 162 deletions
|
@ -11,6 +11,7 @@ import (
|
|||
"io/ioutil"
|
||||
"path"
|
||||
"strings"
|
||||
texttmpl "text/template"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
@ -20,7 +21,8 @@ import (
|
|||
)
|
||||
|
||||
var (
|
||||
templates = template.New("")
|
||||
subjectTemplates = texttmpl.New("")
|
||||
bodyTemplates = template.New("")
|
||||
)
|
||||
|
||||
// HTMLRenderer implements the macaron handler for serving HTML templates.
|
||||
|
@ -59,9 +61,12 @@ func JSRenderer() macaron.Handler {
|
|||
}
|
||||
|
||||
// Mailer provides the templates required for sending notification mails.
|
||||
func Mailer() *template.Template {
|
||||
func Mailer() (*texttmpl.Template, *template.Template) {
|
||||
for _, funcs := range NewTextFuncMap() {
|
||||
subjectTemplates.Funcs(funcs)
|
||||
}
|
||||
for _, funcs := range NewFuncMap() {
|
||||
templates.Funcs(funcs)
|
||||
bodyTemplates.Funcs(funcs)
|
||||
}
|
||||
|
||||
staticDir := path.Join(setting.StaticRootPath, "templates", "mail")
|
||||
|
@ -84,15 +89,7 @@ func Mailer() *template.Template {
|
|||
continue
|
||||
}
|
||||
|
||||
_, err = templates.New(
|
||||
strings.TrimSuffix(
|
||||
filePath,
|
||||
".tmpl",
|
||||
),
|
||||
).Parse(string(content))
|
||||
if err != nil {
|
||||
log.Warn("Failed to parse template %v", err)
|
||||
}
|
||||
buildSubjectBodyTemplate(subjectTemplates, bodyTemplates, strings.TrimSuffix(filePath, ".tmpl"), content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -117,18 +114,10 @@ func Mailer() *template.Template {
|
|||
continue
|
||||
}
|
||||
|
||||
_, err = templates.New(
|
||||
strings.TrimSuffix(
|
||||
filePath,
|
||||
".tmpl",
|
||||
),
|
||||
).Parse(string(content))
|
||||
if err != nil {
|
||||
log.Warn("Failed to parse template %v", err)
|
||||
}
|
||||
buildSubjectBodyTemplate(subjectTemplates, bodyTemplates, strings.TrimSuffix(filePath, ".tmpl"), content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return templates
|
||||
return subjectTemplates, bodyTemplates
|
||||
}
|
||||
|
|
|
@ -16,8 +16,10 @@ import (
|
|||
"mime"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
texttmpl "text/template"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
|
@ -34,6 +36,9 @@ import (
|
|||
"github.com/editorconfig/editorconfig-core-go/v2"
|
||||
)
|
||||
|
||||
// Used from static.go && dynamic.go
|
||||
var mailSubjectSplit = regexp.MustCompile(`(?m)^-{3,}[\s]*$`)
|
||||
|
||||
// NewFuncMap returns functions for injecting to templates
|
||||
func NewFuncMap() []template.FuncMap {
|
||||
return []template.FuncMap{map[string]interface{}{
|
||||
|
@ -261,6 +266,112 @@ func NewFuncMap() []template.FuncMap {
|
|||
}}
|
||||
}
|
||||
|
||||
// NewTextFuncMap returns functions for injecting to text templates
|
||||
// It's a subset of those used for HTML and other templates
|
||||
func NewTextFuncMap() []texttmpl.FuncMap {
|
||||
return []texttmpl.FuncMap{map[string]interface{}{
|
||||
"GoVer": func() string {
|
||||
return strings.Title(runtime.Version())
|
||||
},
|
||||
"AppName": func() string {
|
||||
return setting.AppName
|
||||
},
|
||||
"AppSubUrl": func() string {
|
||||
return setting.AppSubURL
|
||||
},
|
||||
"AppUrl": func() string {
|
||||
return setting.AppURL
|
||||
},
|
||||
"AppVer": func() string {
|
||||
return setting.AppVer
|
||||
},
|
||||
"AppBuiltWith": func() string {
|
||||
return setting.AppBuiltWith
|
||||
},
|
||||
"AppDomain": func() string {
|
||||
return setting.Domain
|
||||
},
|
||||
"TimeSince": timeutil.TimeSince,
|
||||
"TimeSinceUnix": timeutil.TimeSinceUnix,
|
||||
"RawTimeSince": timeutil.RawTimeSince,
|
||||
"DateFmtLong": func(t time.Time) string {
|
||||
return t.Format(time.RFC1123Z)
|
||||
},
|
||||
"DateFmtShort": func(t time.Time) string {
|
||||
return t.Format("Jan 02, 2006")
|
||||
},
|
||||
"List": List,
|
||||
"SubStr": func(str string, start, length int) string {
|
||||
if len(str) == 0 {
|
||||
return ""
|
||||
}
|
||||
end := start + length
|
||||
if length == -1 {
|
||||
end = len(str)
|
||||
}
|
||||
if len(str) < end {
|
||||
return str
|
||||
}
|
||||
return str[start:end]
|
||||
},
|
||||
"EllipsisString": base.EllipsisString,
|
||||
"URLJoin": util.URLJoin,
|
||||
"Dict": func(values ...interface{}) (map[string]interface{}, error) {
|
||||
if len(values)%2 != 0 {
|
||||
return nil, errors.New("invalid dict call")
|
||||
}
|
||||
dict := make(map[string]interface{}, len(values)/2)
|
||||
for i := 0; i < len(values); i += 2 {
|
||||
key, ok := values[i].(string)
|
||||
if !ok {
|
||||
return nil, errors.New("dict keys must be strings")
|
||||
}
|
||||
dict[key] = values[i+1]
|
||||
}
|
||||
return dict, nil
|
||||
},
|
||||
"Printf": fmt.Sprintf,
|
||||
"Escape": Escape,
|
||||
"Sec2Time": models.SecToTime,
|
||||
"ParseDeadline": func(deadline string) []string {
|
||||
return strings.Split(deadline, "|")
|
||||
},
|
||||
"dict": func(values ...interface{}) (map[string]interface{}, error) {
|
||||
if len(values) == 0 {
|
||||
return nil, errors.New("invalid dict call")
|
||||
}
|
||||
|
||||
dict := make(map[string]interface{})
|
||||
|
||||
for i := 0; i < len(values); i++ {
|
||||
switch key := values[i].(type) {
|
||||
case string:
|
||||
i++
|
||||
if i == len(values) {
|
||||
return nil, errors.New("specify the key for non array values")
|
||||
}
|
||||
dict[key] = values[i]
|
||||
case map[string]interface{}:
|
||||
m := values[i].(map[string]interface{})
|
||||
for i, v := range m {
|
||||
dict[i] = v
|
||||
}
|
||||
default:
|
||||
return nil, errors.New("dict values must be maps")
|
||||
}
|
||||
}
|
||||
return dict, nil
|
||||
},
|
||||
"percentage": func(n int, values ...int) float32 {
|
||||
var sum = 0
|
||||
for i := 0; i < len(values); i++ {
|
||||
sum += values[i]
|
||||
}
|
||||
return float32(n) * 100 / float32(sum)
|
||||
},
|
||||
}}
|
||||
}
|
||||
|
||||
// Safe render raw as HTML
|
||||
func Safe(raw string) template.HTML {
|
||||
return template.HTML(raw)
|
||||
|
@ -551,3 +662,22 @@ func MigrationIcon(hostname string) string {
|
|||
return "fa-git-alt"
|
||||
}
|
||||
}
|
||||
|
||||
func buildSubjectBodyTemplate(stpl *texttmpl.Template, btpl *template.Template, name string, content []byte) {
|
||||
// Split template into subject and body
|
||||
var subjectContent []byte
|
||||
bodyContent := content
|
||||
loc := mailSubjectSplit.FindIndex(content)
|
||||
if loc != nil {
|
||||
subjectContent = content[0:loc[0]]
|
||||
bodyContent = content[loc[1]:]
|
||||
}
|
||||
if _, err := stpl.New(name).
|
||||
Parse(string(subjectContent)); err != nil {
|
||||
log.Warn("Failed to parse template [%s/subject]: %v", name, err)
|
||||
}
|
||||
if _, err := btpl.New(name).
|
||||
Parse(string(bodyContent)); err != nil {
|
||||
log.Warn("Failed to parse template [%s/body]: %v", name, err)
|
||||
}
|
||||
}
|
||||
|
|
55
modules/templates/helper_test.go
Normal file
55
modules/templates/helper_test.go
Normal file
|
@ -0,0 +1,55 @@
|
|||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package templates
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSubjectBodySeparator(t *testing.T) {
|
||||
test := func(input, subject, body string) {
|
||||
loc := mailSubjectSplit.FindIndex([]byte(input))
|
||||
if loc == nil {
|
||||
assert.Empty(t, subject, "no subject found, but one expected")
|
||||
assert.Equal(t, body, input)
|
||||
} else {
|
||||
assert.Equal(t, subject, string(input[0:loc[0]]))
|
||||
assert.Equal(t, body, string(input[loc[1]:]))
|
||||
}
|
||||
}
|
||||
|
||||
test("Simple\n---------------\nCase",
|
||||
"Simple\n",
|
||||
"\nCase")
|
||||
test("Only\nBody",
|
||||
"",
|
||||
"Only\nBody")
|
||||
test("Minimal\n---\nseparator",
|
||||
"Minimal\n",
|
||||
"\nseparator")
|
||||
test("False --- separator",
|
||||
"",
|
||||
"False --- separator")
|
||||
test("False\n--- separator",
|
||||
"",
|
||||
"False\n--- separator")
|
||||
test("False ---\nseparator",
|
||||
"",
|
||||
"False ---\nseparator")
|
||||
test("With extra spaces\n----- \t \nBody",
|
||||
"With extra spaces\n",
|
||||
"\nBody")
|
||||
test("With leading spaces\n -------\nOnly body",
|
||||
"",
|
||||
"With leading spaces\n -------\nOnly body")
|
||||
test("Multiple\n---\n-------\n---\nSeparators",
|
||||
"Multiple\n",
|
||||
"\n-------\n---\nSeparators")
|
||||
test("Insuficient\n--\nSeparators",
|
||||
"",
|
||||
"Insuficient\n--\nSeparators")
|
||||
}
|
|
@ -14,6 +14,7 @@ import (
|
|||
"io/ioutil"
|
||||
"path"
|
||||
"strings"
|
||||
texttmpl "text/template"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
@ -23,7 +24,8 @@ import (
|
|||
)
|
||||
|
||||
var (
|
||||
templates = template.New("")
|
||||
subjectTemplates = texttmpl.New("")
|
||||
bodyTemplates = template.New("")
|
||||
)
|
||||
|
||||
type templateFileSystem struct {
|
||||
|
@ -140,9 +142,12 @@ func JSRenderer() macaron.Handler {
|
|||
}
|
||||
|
||||
// Mailer provides the templates required for sending notification mails.
|
||||
func Mailer() *template.Template {
|
||||
func Mailer() (*texttmpl.Template, *template.Template) {
|
||||
for _, funcs := range NewTextFuncMap() {
|
||||
subjectTemplates.Funcs(funcs)
|
||||
}
|
||||
for _, funcs := range NewFuncMap() {
|
||||
templates.Funcs(funcs)
|
||||
bodyTemplates.Funcs(funcs)
|
||||
}
|
||||
|
||||
for _, assetPath := range AssetNames() {
|
||||
|
@ -161,7 +166,8 @@ func Mailer() *template.Template {
|
|||
continue
|
||||
}
|
||||
|
||||
templates.New(
|
||||
buildSubjectBodyTemplate(subjectTemplates,
|
||||
bodyTemplates,
|
||||
strings.TrimPrefix(
|
||||
strings.TrimSuffix(
|
||||
assetPath,
|
||||
|
@ -169,7 +175,7 @@ func Mailer() *template.Template {
|
|||
),
|
||||
"mail/",
|
||||
),
|
||||
).Parse(string(content))
|
||||
content)
|
||||
}
|
||||
|
||||
customDir := path.Join(setting.CustomPath, "templates", "mail")
|
||||
|
@ -192,17 +198,18 @@ func Mailer() *template.Template {
|
|||
continue
|
||||
}
|
||||
|
||||
templates.New(
|
||||
buildSubjectBodyTemplate(subjectTemplates,
|
||||
bodyTemplates,
|
||||
strings.TrimSuffix(
|
||||
filePath,
|
||||
".tmpl",
|
||||
),
|
||||
).Parse(string(content))
|
||||
content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return templates
|
||||
return subjectTemplates, bodyTemplates
|
||||
}
|
||||
|
||||
func Asset(name string) ([]byte, error) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue