mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-12-23 14:53:34 +01:00
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
272
docs/content/doc/advanced/mail-templates-us.md
Normal file
272
docs/content/doc/advanced/mail-templates-us.md
Normal file
|
@ -0,0 +1,272 @@
|
|||
---
|
||||
date: "2019-10-23T17:00:00-03:00"
|
||||
title: "Mail templates"
|
||||
slug: "mail-templates"
|
||||
weight: 45
|
||||
toc: true
|
||||
draft: false
|
||||
menu:
|
||||
sidebar:
|
||||
parent: "advanced"
|
||||
name: "Mail templates"
|
||||
weight: 45
|
||||
identifier: "mail-templates"
|
||||
---
|
||||
|
||||
# Mail templates
|
||||
|
||||
To craft the e-mail subject and contents for certain operations, Gitea can be customized by using templates. The templates
|
||||
for these functions are located under the [`custom` directory](https://docs.gitea.io/en-us/customizing-gitea/).
|
||||
Gitea has an internal template that serves as default in case there's no custom alternative.
|
||||
|
||||
Custom templates are loaded when Gitea starts. Changes made to them are not recognized until Gitea is restarted again.
|
||||
|
||||
## Mail notifications supporting templates
|
||||
|
||||
Currently, the following notification events make use of templates:
|
||||
|
||||
| Action name | Usage |
|
||||
|---------------|--------------------------------------------------------------------------------------------------------------|
|
||||
| `new` | A new issue or pull request was created. |
|
||||
| `comment` | A new comment was created in an existing issue or pull request. |
|
||||
| `close` | An issue or pull request was closed. |
|
||||
| `reopen` | An issue or pull request was reopened. |
|
||||
| `review` | The head comment of a review in a pull request. |
|
||||
| `code` | A single comment on the code of a pull request. |
|
||||
| `assigned` | Used was assigned to an issue or pull request. |
|
||||
| `default` | Any action not included in the above categories, or when the corresponding category template is not present. |
|
||||
|
||||
The path for the template of a particular message type is:
|
||||
|
||||
```
|
||||
custom/templates/mail/{action type}/{action name}.tmpl
|
||||
```
|
||||
|
||||
Where `{action type}` is one of `issue` or `pull` (for pull requests), and `{action name}` is one of the names listed above.
|
||||
|
||||
For example, the specific template for a mail regarding a comment in a pull request is:
|
||||
```
|
||||
custom/templates/mail/pull/comment.tmpl
|
||||
```
|
||||
|
||||
However, creating templates for each and every action type/name combination is not required.
|
||||
A fallback system is used to choose the appropriate template for an event. The _first existing_
|
||||
template on this list is used:
|
||||
|
||||
* The specific template for the desired **action type** and **action name**.
|
||||
* The template for action type `issue` and the desired **action name**.
|
||||
* The template for the desired **action type**, action name `default`.
|
||||
* The template for action type `issue`, action name `default`.
|
||||
|
||||
The only mandatory template is action type `issue`, action name `default`, which is already embedded in Gitea
|
||||
unless it's overridden by the user in the `custom` directory.
|
||||
|
||||
## Template syntax
|
||||
|
||||
Mail templates are UTF-8 encoded text files that need to follow one of the following formats:
|
||||
|
||||
```
|
||||
Text and macros for the subject line
|
||||
------------
|
||||
Text and macros for the mail body
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```
|
||||
Text and macros for the mail body
|
||||
```
|
||||
|
||||
Specifying a _subject_ section is optional (and therefore also the dash line separator). When used, the separator between
|
||||
_subject_ and _mail body_ templates requires at least three dashes; no other characters are allowed in the separator line.
|
||||
|
||||
|
||||
_Subject_ and _mail body_ are parsed by [Golang's template engine](https://golang.org/pkg/text/template/) and
|
||||
are provided with a _metadata context_ assembled for each notification. The context contains the following elements:
|
||||
|
||||
| Name | Type | Available | Usage |
|
||||
|--------------------|----------------|---------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `.FallbackSubject` | string | Always | A default subject line. See Below. |
|
||||
| `.Subject` | string | Only in body | The _subject_, once resolved. |
|
||||
| `.Body` | string | Always | The message of the issue, pull request or comment, parsed from Markdown into HTML and sanitized. Do not confuse with the _mail body_ |
|
||||
| `.Link` | string | Always | The address of the originating issue, pull request or comment. |
|
||||
| `.Issue` | models.Issue | Always | The issue (or pull request) originating the notification. To get data specific to a pull request (e.g. `HasMerged`), `.Issue.PullRequest` can be used, but care should be taken as this field will be `nil` if the issue is *not* a pull request. |
|
||||
| `.Comment` | models.Comment | If applicable | If the notification is from a comment added to an issue or pull request, this will contain the information about the comment. |
|
||||
| `.IsPull` | bool | Always | `true` if the mail notification is associated with a pull request (i.e. `.Issue.PullRequest` is not `nil`). |
|
||||
| `.Repo` | string | Always | Name of the repository, including owner name (e.g. `mike/stuff`) |
|
||||
| `.User` | models.User | Always | Owner of the repository from which the event originated. To get the user name (e.g. `mike`),`.User.Name` can be used. |
|
||||
| `.Doer` | models.User | Always | User that executed the action triggering the notification event. To get the user name (e.g. `rhonda`), `.Doer.Name` can be used. |
|
||||
| `.IsMention` | bool | Always | `true` if this notification was only generated because the user was mentioned in the comment, while not being subscribed to the source. It will be `false` if the recipient was subscribed to the issue or repository. |
|
||||
| `.SubjectPrefix` | string | Always | `Re: ` if the notification is about other than issue or pull request creation; otherwise an empty string. |
|
||||
| `.ActionType` | string | Always | `"issue"` or `"pull"`. Will correspond to the actual _action type_ independently of which template was selected. |
|
||||
| `.ActionName` | string | Always | It will be one of the action types described above (`new`, `comment`, etc.), and will correspond to the actual _action name_ independently of which template was selected. |
|
||||
|
||||
All names are case sensitive.
|
||||
|
||||
### The _subject_ part of the template
|
||||
|
||||
The template engine used for the mail _subject_ is golang's [`text/template`](https://golang.org/pkg/text/template/).
|
||||
Please refer to the linked documentation for details about its syntax.
|
||||
|
||||
The _subject_ is built using the following steps:
|
||||
|
||||
* A template is selected according to the type of notification and to what templates are present.
|
||||
* The template is parsed and resolved (e.g. `{{.Issue.Index}}` is converted to the number of the issue
|
||||
or pull request).
|
||||
* All space-like characters (e.g. `TAB`, `LF`, etc.) are converted to normal spaces.
|
||||
* All leading, trailing and redundant spaces are removed.
|
||||
* The string is truncated to its first 256 runes (characters).
|
||||
|
||||
If the end result is an empty string, **or** no subject template was available (i.e. the selected template
|
||||
did not include a subject part), Gitea's **internal default** will be used.
|
||||
|
||||
The internal default (fallback) subject is the equivalent of:
|
||||
|
||||
```
|
||||
{{.SubjectPrefix}}[{{.Repo}}] {{.Issue.Title}} (#.Issue.Index)
|
||||
```
|
||||
|
||||
For example: `Re: [mike/stuff] New color palette (#38)`
|
||||
|
||||
Gitea's default subject can also be found in the template _metadata_ as `.FallbackSubject` from any of
|
||||
the two templates, even if a valid subject template is present.
|
||||
|
||||
### The _mail body_ part of the template
|
||||
|
||||
The template engine used for the _mail body_ is golang's [`html/template`](https://golang.org/pkg/html/template/).
|
||||
Please refer to the linked documentation for details about its syntax.
|
||||
|
||||
The _mail body_ is parsed after the mail subject, so there is an additional _metadata_ field which is
|
||||
the actual rendered subject, after all considerations.
|
||||
|
||||
The expected result is HTML (including structural elements like`<html>`, `<body>`, etc.). Styling
|
||||
through `<style>` blocks, `class` and `style` attributes is possible. However, `html/template`
|
||||
does some [automatic escaping](https://golang.org/pkg/html/template/#hdr-Contexts) that should be considered.
|
||||
|
||||
Attachments (such as images or external style sheets) are not supported. However, other templates can
|
||||
be referenced too, for example to provide the contents of a `<style>` element in a centralized fashion.
|
||||
The external template must be placed under `custom/mail` and referenced relative to that directory.
|
||||
For example, `custom/mail/styles/base.tmpl` can be included using `{{template styles/base}}`.
|
||||
|
||||
The mail is sent with `Content-Type: multipart/alternative`, so the body is sent in both HTML
|
||||
and text formats. The latter is obtained by stripping the HTML markup.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
How a mail is rendered is directly dependent on the capabilities of the mail application. Many mail
|
||||
clients don't even support HTML, so they show the text version included in the generated mail.
|
||||
|
||||
If the template fails to render, it will be noticed only at the moment the mail is sent.
|
||||
A default subject is used if the subject template fails, and whatever was rendered successfully
|
||||
from the the _mail body_ is used, disregarding the rest.
|
||||
|
||||
Please check [Gitea's logs](https://docs.gitea.io/en-us/logging-configuration/) for error messages in case of trouble.
|
||||
|
||||
## Example
|
||||
|
||||
`custom/templates/mail/issue/default.tmpl`:
|
||||
|
||||
```
|
||||
[{{.Repo}}] @{{.Doer.Name}}
|
||||
{{if eq .ActionName "new"}}
|
||||
created
|
||||
{{else if eq .ActionName "comment"}}
|
||||
commented on
|
||||
{{else if eq .ActionName "close"}}
|
||||
closed
|
||||
{{else if eq .ActionName "reopen"}}
|
||||
reopened
|
||||
{{else}}
|
||||
updated
|
||||
{{end}}
|
||||
{{if eq .ActionType "issue"}}
|
||||
issue
|
||||
{{else}}
|
||||
pull request
|
||||
{{end}}
|
||||
#{{.Issue.Index}}: {{.Issue.Title}}
|
||||
------------
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<title>{{.Subject}}</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
{{if .IsMention}}
|
||||
<p>
|
||||
You are receiving this because @{{.Doer.Name}} mentioned you.
|
||||
</p>
|
||||
{{end}}
|
||||
<p>
|
||||
<p>
|
||||
<a href="{{AppURL}}/{{.Doer.LowerName}}">@{{.Doer.Name}}</a>
|
||||
{{if not (eq .Doer.FullName "")}}
|
||||
({{.Doer.FullName}})
|
||||
{{end}}
|
||||
{{if eq .ActionName "new"}}
|
||||
created
|
||||
{{else if eq .ActionName "close"}}
|
||||
closed
|
||||
{{else if eq .ActionName "reopen"}}
|
||||
reopened
|
||||
{{else}}
|
||||
updated
|
||||
{{end}}
|
||||
<a href="{{.Link}}">{{.Repo}}#{{.Issue.Index}}</a>.
|
||||
</p>
|
||||
{{if not (eq .Body "")}}
|
||||
<h3>Message content:</h3>
|
||||
<hr>
|
||||
{{.Body | Str2html}}
|
||||
{{end}}
|
||||
</p>
|
||||
<hr>
|
||||
<p>
|
||||
<a href="{{.Link}}">View it on Gitea</a>.
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
This template produces something along these lines:
|
||||
|
||||
#### Subject
|
||||
|
||||
> [mike/stuff] @rhonda commented on pull request #38: New color palette
|
||||
|
||||
#### Mail body
|
||||
|
||||
> [@rhonda](#) (Rhonda Myers) updated [mike/stuff#38](#).
|
||||
>
|
||||
> #### Message content:
|
||||
>
|
||||
> \__________________________________________________________________
|
||||
>
|
||||
> Mike, I think we should tone down the blues a little.
|
||||
> \__________________________________________________________________
|
||||
>
|
||||
> [View it on Gitea](#).
|
||||
|
||||
## Advanced
|
||||
|
||||
The template system contains several functions that can be used to further process and format
|
||||
the messages. Here's a list of some of them:
|
||||
|
||||
| Name | Parameters | Available | Usage |
|
||||
|----------------------|-------------|-----------|---------------------------------------------------------------------|
|
||||
| `AppUrl` | - | Any | Gitea's URL |
|
||||
| `AppName` | - | Any | Set from `app.ini`, usually "Gitea" |
|
||||
| `AppDomain` | - | Any | Gitea's host name |
|
||||
| `EllipsisString` | string, int | Any | Truncates a string to the specified length; adds ellipsis as needed |
|
||||
| `Str2html` | string | Body only | Sanitizes text by removing any HTML tags from it. |
|
||||
|
||||
These are _functions_, not metadata, so they have to be used:
|
||||
|
||||
```
|
||||
Like this: {{Str2html "Escape<my>text"}}
|
||||
Or this: {{"Escape<my>text" | Str2html}}
|
||||
Or this: {{AppUrl}}
|
||||
But not like this: {{.AppUrl}}
|
||||
```
|
|
@ -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) {
|
||||
|
|
|
@ -9,7 +9,11 @@ import (
|
|||
"bytes"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"mime"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
texttmpl "text/template"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
|
@ -28,18 +32,22 @@ const (
|
|||
mailAuthResetPassword base.TplName = "auth/reset_passwd"
|
||||
mailAuthRegisterNotify base.TplName = "auth/register_notify"
|
||||
|
||||
mailIssueComment base.TplName = "issue/comment"
|
||||
mailIssueMention base.TplName = "issue/mention"
|
||||
mailIssueAssigned base.TplName = "issue/assigned"
|
||||
|
||||
mailNotifyCollaborator base.TplName = "notify/collaborator"
|
||||
|
||||
// There's no actual limit for subject in RFC 5322
|
||||
mailMaxSubjectRunes = 256
|
||||
)
|
||||
|
||||
var templates *template.Template
|
||||
var (
|
||||
bodyTemplates *template.Template
|
||||
subjectTemplates *texttmpl.Template
|
||||
subjectRemoveSpaces = regexp.MustCompile(`[\s]+`)
|
||||
)
|
||||
|
||||
// InitMailRender initializes the mail renderer
|
||||
func InitMailRender(tmpls *template.Template) {
|
||||
templates = tmpls
|
||||
func InitMailRender(subjectTpl *texttmpl.Template, bodyTpl *template.Template) {
|
||||
subjectTemplates = subjectTpl
|
||||
bodyTemplates = bodyTpl
|
||||
}
|
||||
|
||||
// SendTestMail sends a test mail
|
||||
|
@ -58,7 +66,7 @@ func SendUserMail(language string, u *models.User, tpl base.TplName, code, subje
|
|||
|
||||
var content bytes.Buffer
|
||||
|
||||
if err := templates.ExecuteTemplate(&content, string(tpl), data); err != nil {
|
||||
if err := bodyTemplates.ExecuteTemplate(&content, string(tpl), data); err != nil {
|
||||
log.Error("Template: %v", err)
|
||||
return
|
||||
}
|
||||
|
@ -96,7 +104,7 @@ func SendActivateEmailMail(locale Locale, u *models.User, email *models.EmailAdd
|
|||
|
||||
var content bytes.Buffer
|
||||
|
||||
if err := templates.ExecuteTemplate(&content, string(mailAuthActivateEmail), data); err != nil {
|
||||
if err := bodyTemplates.ExecuteTemplate(&content, string(mailAuthActivateEmail), data); err != nil {
|
||||
log.Error("Template: %v", err)
|
||||
return
|
||||
}
|
||||
|
@ -121,7 +129,7 @@ func SendRegisterNotifyMail(locale Locale, u *models.User) {
|
|||
|
||||
var content bytes.Buffer
|
||||
|
||||
if err := templates.ExecuteTemplate(&content, string(mailAuthRegisterNotify), data); err != nil {
|
||||
if err := bodyTemplates.ExecuteTemplate(&content, string(mailAuthRegisterNotify), data); err != nil {
|
||||
log.Error("Template: %v", err)
|
||||
return
|
||||
}
|
||||
|
@ -145,7 +153,7 @@ func SendCollaboratorMail(u, doer *models.User, repo *models.Repository) {
|
|||
|
||||
var content bytes.Buffer
|
||||
|
||||
if err := templates.ExecuteTemplate(&content, string(mailNotifyCollaborator), data); err != nil {
|
||||
if err := bodyTemplates.ExecuteTemplate(&content, string(mailNotifyCollaborator), data); err != nil {
|
||||
log.Error("Template: %v", err)
|
||||
return
|
||||
}
|
||||
|
@ -156,40 +164,70 @@ func SendCollaboratorMail(u, doer *models.User, repo *models.Repository) {
|
|||
SendAsync(msg)
|
||||
}
|
||||
|
||||
func composeTplData(subject, body, link string) map[string]interface{} {
|
||||
data := make(map[string]interface{}, 10)
|
||||
data["Subject"] = subject
|
||||
data["Body"] = body
|
||||
data["Link"] = link
|
||||
return data
|
||||
}
|
||||
func composeIssueCommentMessage(issue *models.Issue, doer *models.User, actionType models.ActionType, fromMention bool,
|
||||
content string, comment *models.Comment, tos []string, info string) *Message {
|
||||
|
||||
func composeIssueCommentMessage(issue *models.Issue, doer *models.User, content string, comment *models.Comment, tplName base.TplName, tos []string, info string) *Message {
|
||||
var subject string
|
||||
if err := issue.LoadPullRequest(); err != nil {
|
||||
log.Error("LoadPullRequest: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
subject string
|
||||
link string
|
||||
prefix string
|
||||
// Fall back subject for bad templates, make sure subject is never empty
|
||||
fallback string
|
||||
)
|
||||
|
||||
commentType := models.CommentTypeComment
|
||||
if comment != nil {
|
||||
subject = "Re: " + mailSubject(issue)
|
||||
prefix = "Re: "
|
||||
commentType = comment.Type
|
||||
link = issue.HTMLURL() + "#" + comment.HashTag()
|
||||
} else {
|
||||
subject = mailSubject(issue)
|
||||
}
|
||||
err := issue.LoadRepo()
|
||||
if err != nil {
|
||||
log.Error("LoadRepo: %v", err)
|
||||
link = issue.HTMLURL()
|
||||
}
|
||||
|
||||
fallback = prefix + fallbackMailSubject(issue)
|
||||
|
||||
// This is the body of the new issue or comment, not the mail body
|
||||
body := string(markup.RenderByType(markdown.MarkupName, []byte(content), issue.Repo.HTMLURL(), issue.Repo.ComposeMetas()))
|
||||
|
||||
var data = make(map[string]interface{}, 10)
|
||||
if comment != nil {
|
||||
data = composeTplData(subject, body, issue.HTMLURL()+"#"+comment.HashTag())
|
||||
} else {
|
||||
data = composeTplData(subject, body, issue.HTMLURL())
|
||||
actType, actName, tplName := actionToTemplate(issue, actionType, commentType)
|
||||
|
||||
mailMeta := map[string]interface{}{
|
||||
"FallbackSubject": fallback,
|
||||
"Body": body,
|
||||
"Link": link,
|
||||
"Issue": issue,
|
||||
"Comment": comment,
|
||||
"IsPull": issue.IsPull,
|
||||
"User": issue.Repo.MustOwner(),
|
||||
"Repo": issue.Repo.FullName(),
|
||||
"Doer": doer,
|
||||
"IsMention": fromMention,
|
||||
"SubjectPrefix": prefix,
|
||||
"ActionType": actType,
|
||||
"ActionName": actName,
|
||||
}
|
||||
data["Doer"] = doer
|
||||
data["Issue"] = issue
|
||||
|
||||
var mailSubject bytes.Buffer
|
||||
if err := subjectTemplates.ExecuteTemplate(&mailSubject, string(tplName), mailMeta); err == nil {
|
||||
subject = sanitizeSubject(mailSubject.String())
|
||||
} else {
|
||||
log.Error("ExecuteTemplate [%s]: %v", string(tplName)+"/subject", err)
|
||||
}
|
||||
|
||||
if subject == "" {
|
||||
subject = fallback
|
||||
}
|
||||
mailMeta["Subject"] = subject
|
||||
|
||||
var mailBody bytes.Buffer
|
||||
|
||||
if err := templates.ExecuteTemplate(&mailBody, string(tplName), data); err != nil {
|
||||
log.Error("Template: %v", err)
|
||||
if err := bodyTemplates.ExecuteTemplate(&mailBody, string(tplName), mailMeta); err != nil {
|
||||
log.Error("ExecuteTemplate [%s]: %v", string(tplName)+"/body", err)
|
||||
}
|
||||
|
||||
msg := NewMessageFrom(tos, doer.DisplayName(), setting.MailService.FromEmail, subject, mailBody.String())
|
||||
|
@ -206,24 +244,81 @@ func composeIssueCommentMessage(issue *models.Issue, doer *models.User, content
|
|||
return msg
|
||||
}
|
||||
|
||||
func sanitizeSubject(subject string) string {
|
||||
runes := []rune(strings.TrimSpace(subjectRemoveSpaces.ReplaceAllLiteralString(subject, " ")))
|
||||
if len(runes) > mailMaxSubjectRunes {
|
||||
runes = runes[:mailMaxSubjectRunes]
|
||||
}
|
||||
// Encode non-ASCII characters
|
||||
return mime.QEncoding.Encode("utf-8", string(runes))
|
||||
}
|
||||
|
||||
// SendIssueCommentMail composes and sends issue comment emails to target receivers.
|
||||
func SendIssueCommentMail(issue *models.Issue, doer *models.User, content string, comment *models.Comment, tos []string) {
|
||||
func SendIssueCommentMail(issue *models.Issue, doer *models.User, actionType models.ActionType, content string, comment *models.Comment, tos []string) {
|
||||
if len(tos) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
SendAsync(composeIssueCommentMessage(issue, doer, content, comment, mailIssueComment, tos, "issue comment"))
|
||||
SendAsync(composeIssueCommentMessage(issue, doer, actionType, false, content, comment, tos, "issue comment"))
|
||||
}
|
||||
|
||||
// SendIssueMentionMail composes and sends issue mention emails to target receivers.
|
||||
func SendIssueMentionMail(issue *models.Issue, doer *models.User, content string, comment *models.Comment, tos []string) {
|
||||
func SendIssueMentionMail(issue *models.Issue, doer *models.User, actionType models.ActionType, content string, comment *models.Comment, tos []string) {
|
||||
if len(tos) == 0 {
|
||||
return
|
||||
}
|
||||
SendAsync(composeIssueCommentMessage(issue, doer, content, comment, mailIssueMention, tos, "issue mention"))
|
||||
SendAsync(composeIssueCommentMessage(issue, doer, actionType, true, content, comment, tos, "issue mention"))
|
||||
}
|
||||
|
||||
// actionToTemplate returns the type and name of the action facing the user
|
||||
// (slightly different from models.ActionType) and the name of the template to use (based on availability)
|
||||
func actionToTemplate(issue *models.Issue, actionType models.ActionType, commentType models.CommentType) (typeName, name, template string) {
|
||||
if issue.IsPull {
|
||||
typeName = "pull"
|
||||
} else {
|
||||
typeName = "issue"
|
||||
}
|
||||
switch actionType {
|
||||
case models.ActionCreateIssue, models.ActionCreatePullRequest:
|
||||
name = "new"
|
||||
case models.ActionCommentIssue:
|
||||
name = "comment"
|
||||
case models.ActionCloseIssue, models.ActionClosePullRequest:
|
||||
name = "close"
|
||||
case models.ActionReopenIssue, models.ActionReopenPullRequest:
|
||||
name = "reopen"
|
||||
case models.ActionMergePullRequest:
|
||||
name = "merge"
|
||||
default:
|
||||
switch commentType {
|
||||
case models.CommentTypeReview:
|
||||
name = "review"
|
||||
case models.CommentTypeCode:
|
||||
name = "code"
|
||||
case models.CommentTypeAssignees:
|
||||
name = "assigned"
|
||||
default:
|
||||
name = "default"
|
||||
}
|
||||
}
|
||||
|
||||
template = typeName + "/" + name
|
||||
ok := bodyTemplates.Lookup(template) != nil
|
||||
if !ok && typeName != "issue" {
|
||||
template = "issue/" + name
|
||||
ok = bodyTemplates.Lookup(template) != nil
|
||||
}
|
||||
if !ok {
|
||||
template = typeName + "/default"
|
||||
ok = bodyTemplates.Lookup(template) != nil
|
||||
}
|
||||
if !ok {
|
||||
template = "issue/default"
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// SendIssueAssignedMail composes and sends issue assigned email
|
||||
func SendIssueAssignedMail(issue *models.Issue, doer *models.User, content string, comment *models.Comment, tos []string) {
|
||||
SendAsync(composeIssueCommentMessage(issue, doer, content, comment, mailIssueAssigned, tos, "issue assigned"))
|
||||
SendAsync(composeIssueCommentMessage(issue, doer, models.ActionType(0), false, content, comment, tos, "issue assigned"))
|
||||
}
|
||||
|
|
|
@ -31,24 +31,8 @@ func mailParticipantsComment(ctx models.DBContext, c *models.Comment, opType mod
|
|||
for i, u := range userMentions {
|
||||
mentions[i] = u.LowerName
|
||||
}
|
||||
if len(c.Content) > 0 {
|
||||
if err = mailIssueCommentToParticipants(issue, c.Poster, c.Content, c, mentions); err != nil {
|
||||
log.Error("mailIssueCommentToParticipants: %v", err)
|
||||
}
|
||||
if err = mailIssueCommentToParticipants(issue, c.Poster, opType, c.Content, c, mentions); err != nil {
|
||||
log.Error("mailIssueCommentToParticipants: %v", err)
|
||||
}
|
||||
|
||||
switch opType {
|
||||
case models.ActionCloseIssue:
|
||||
ct := fmt.Sprintf("Closed #%d.", issue.Index)
|
||||
if err = mailIssueCommentToParticipants(issue, c.Poster, ct, c, mentions); err != nil {
|
||||
log.Error("mailIssueCommentToParticipants: %v", err)
|
||||
}
|
||||
case models.ActionReopenIssue:
|
||||
ct := fmt.Sprintf("Reopened #%d.", issue.Index)
|
||||
if err = mailIssueCommentToParticipants(issue, c.Poster, ct, c, mentions); err != nil {
|
||||
log.Error("mailIssueCommentToParticipants: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ import (
|
|||
"github.com/unknwon/com"
|
||||
)
|
||||
|
||||
func mailSubject(issue *models.Issue) string {
|
||||
func fallbackMailSubject(issue *models.Issue) string {
|
||||
return fmt.Sprintf("[%s] %s (#%d)", issue.Repo.FullName(), issue.Title, issue.Index)
|
||||
}
|
||||
|
||||
|
@ -22,7 +22,7 @@ func mailSubject(issue *models.Issue) string {
|
|||
// This function sends two list of emails:
|
||||
// 1. Repository watchers and users who are participated in comments.
|
||||
// 2. Users who are not in 1. but get mentioned in current issue/comment.
|
||||
func mailIssueCommentToParticipants(issue *models.Issue, doer *models.User, content string, comment *models.Comment, mentions []string) error {
|
||||
func mailIssueCommentToParticipants(issue *models.Issue, doer *models.User, actionType models.ActionType, content string, comment *models.Comment, mentions []string) error {
|
||||
|
||||
watchers, err := models.GetWatchers(issue.RepoID)
|
||||
if err != nil {
|
||||
|
@ -89,7 +89,7 @@ func mailIssueCommentToParticipants(issue *models.Issue, doer *models.User, cont
|
|||
}
|
||||
|
||||
for _, to := range tos {
|
||||
SendIssueCommentMail(issue, doer, content, comment, []string{to})
|
||||
SendIssueCommentMail(issue, doer, actionType, content, comment, []string{to})
|
||||
}
|
||||
|
||||
// Mail mentioned people and exclude watchers.
|
||||
|
@ -106,7 +106,7 @@ func mailIssueCommentToParticipants(issue *models.Issue, doer *models.User, cont
|
|||
emails := models.GetUserEmailsByNames(tos)
|
||||
|
||||
for _, to := range emails {
|
||||
SendIssueMentionMail(issue, doer, content, comment, []string{to})
|
||||
SendIssueMentionMail(issue, doer, actionType, content, comment, []string{to})
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -131,32 +131,8 @@ func mailParticipants(ctx models.DBContext, issue *models.Issue, doer *models.Us
|
|||
for i, u := range userMentions {
|
||||
mentions[i] = u.LowerName
|
||||
}
|
||||
|
||||
if len(issue.Content) > 0 {
|
||||
if err = mailIssueCommentToParticipants(issue, doer, issue.Content, nil, mentions); err != nil {
|
||||
log.Error("mailIssueCommentToParticipants: %v", err)
|
||||
}
|
||||
if err = mailIssueCommentToParticipants(issue, doer, opType, issue.Content, nil, mentions); err != nil {
|
||||
log.Error("mailIssueCommentToParticipants: %v", err)
|
||||
}
|
||||
|
||||
switch opType {
|
||||
case models.ActionCreateIssue, models.ActionCreatePullRequest:
|
||||
if len(issue.Content) == 0 {
|
||||
ct := fmt.Sprintf("Created #%d.", issue.Index)
|
||||
if err = mailIssueCommentToParticipants(issue, doer, ct, nil, mentions); err != nil {
|
||||
log.Error("mailIssueCommentToParticipants: %v", err)
|
||||
}
|
||||
}
|
||||
case models.ActionCloseIssue, models.ActionClosePullRequest:
|
||||
ct := fmt.Sprintf("Closed #%d.", issue.Index)
|
||||
if err = mailIssueCommentToParticipants(issue, doer, ct, nil, mentions); err != nil {
|
||||
log.Error("mailIssueCommentToParticipants: %v", err)
|
||||
}
|
||||
case models.ActionReopenIssue, models.ActionReopenPullRequest:
|
||||
ct := fmt.Sprintf("Reopened #%d.", issue.Index)
|
||||
if err = mailIssueCommentToParticipants(issue, doer, ct, nil, mentions); err != nil {
|
||||
log.Error("mailIssueCommentToParticipants: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -5,8 +5,10 @@
|
|||
package mailer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"html/template"
|
||||
"testing"
|
||||
texttmpl "text/template"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
@ -14,7 +16,11 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const tmpl = `
|
||||
const subjectTpl = `
|
||||
{{.SubjectPrefix}}[{{.Repo}}] @{{.Doer.Name}} #{{.Issue.Index}} - {{.Issue.Title}}
|
||||
`
|
||||
|
||||
const bodyTpl = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
|
@ -47,17 +53,19 @@ func TestComposeIssueCommentMessage(t *testing.T) {
|
|||
issue := models.AssertExistsAndLoadBean(t, &models.Issue{ID: 1, Repo: repo, Poster: doer}).(*models.Issue)
|
||||
comment := models.AssertExistsAndLoadBean(t, &models.Comment{ID: 2, Issue: issue}).(*models.Comment)
|
||||
|
||||
email := template.Must(template.New("issue/comment").Parse(tmpl))
|
||||
InitMailRender(email)
|
||||
stpl := texttmpl.Must(texttmpl.New("issue/comment").Parse(subjectTpl))
|
||||
btpl := template.Must(template.New("issue/comment").Parse(bodyTpl))
|
||||
InitMailRender(stpl, btpl)
|
||||
|
||||
tos := []string{"test@gitea.com", "test2@gitea.com"}
|
||||
msg := composeIssueCommentMessage(issue, doer, "test body", comment, mailIssueComment, tos, "issue comment")
|
||||
msg := composeIssueCommentMessage(issue, doer, models.ActionCommentIssue, false, "test body", comment, tos, "issue comment")
|
||||
|
||||
subject := msg.GetHeader("Subject")
|
||||
inreplyTo := msg.GetHeader("In-Reply-To")
|
||||
references := msg.GetHeader("References")
|
||||
|
||||
assert.Equal(t, subject[0], "Re: "+mailSubject(issue), "Comment reply subject should contain Re:")
|
||||
assert.Equal(t, "Re: ", subject[0][:4], "Comment reply subject should contain Re:")
|
||||
assert.Equal(t, "Re: [user2/repo1] @user2 #1 - issue1", subject[0])
|
||||
assert.Equal(t, inreplyTo[0], "<user2/repo1/issues/1@localhost>", "In-Reply-To header doesn't match")
|
||||
assert.Equal(t, references[0], "<user2/repo1/issues/1@localhost>", "References header doesn't match")
|
||||
}
|
||||
|
@ -75,17 +83,122 @@ func TestComposeIssueMessage(t *testing.T) {
|
|||
repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1, Owner: doer}).(*models.Repository)
|
||||
issue := models.AssertExistsAndLoadBean(t, &models.Issue{ID: 1, Repo: repo, Poster: doer}).(*models.Issue)
|
||||
|
||||
email := template.Must(template.New("issue/comment").Parse(tmpl))
|
||||
InitMailRender(email)
|
||||
stpl := texttmpl.Must(texttmpl.New("issue/new").Parse(subjectTpl))
|
||||
btpl := template.Must(template.New("issue/new").Parse(bodyTpl))
|
||||
InitMailRender(stpl, btpl)
|
||||
|
||||
tos := []string{"test@gitea.com", "test2@gitea.com"}
|
||||
msg := composeIssueCommentMessage(issue, doer, "test body", nil, mailIssueComment, tos, "issue create")
|
||||
msg := composeIssueCommentMessage(issue, doer, models.ActionCreateIssue, false, "test body", nil, tos, "issue create")
|
||||
|
||||
subject := msg.GetHeader("Subject")
|
||||
messageID := msg.GetHeader("Message-ID")
|
||||
|
||||
assert.Equal(t, subject[0], mailSubject(issue), "Subject not equal to issue.mailSubject()")
|
||||
assert.Equal(t, "[user2/repo1] @user2 #1 - issue1", subject[0])
|
||||
assert.Nil(t, msg.GetHeader("In-Reply-To"))
|
||||
assert.Nil(t, msg.GetHeader("References"))
|
||||
assert.Equal(t, messageID[0], "<user2/repo1/issues/1@localhost>", "Message-ID header doesn't match")
|
||||
}
|
||||
|
||||
func TestTemplateSelection(t *testing.T) {
|
||||
assert.NoError(t, models.PrepareTestDatabase())
|
||||
var mailService = setting.Mailer{
|
||||
From: "test@gitea.com",
|
||||
}
|
||||
|
||||
setting.MailService = &mailService
|
||||
setting.Domain = "localhost"
|
||||
|
||||
doer := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User)
|
||||
repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1, Owner: doer}).(*models.Repository)
|
||||
issue := models.AssertExistsAndLoadBean(t, &models.Issue{ID: 1, Repo: repo, Poster: doer}).(*models.Issue)
|
||||
tos := []string{"test@gitea.com"}
|
||||
|
||||
stpl := texttmpl.Must(texttmpl.New("issue/default").Parse("issue/default/subject"))
|
||||
texttmpl.Must(stpl.New("issue/new").Parse("issue/new/subject"))
|
||||
texttmpl.Must(stpl.New("pull/comment").Parse("pull/comment/subject"))
|
||||
texttmpl.Must(stpl.New("issue/close").Parse("")) // Must default to fallback subject
|
||||
|
||||
btpl := template.Must(template.New("issue/default").Parse("issue/default/body"))
|
||||
template.Must(btpl.New("issue/new").Parse("issue/new/body"))
|
||||
template.Must(btpl.New("pull/comment").Parse("pull/comment/body"))
|
||||
template.Must(btpl.New("issue/close").Parse("issue/close/body"))
|
||||
|
||||
InitMailRender(stpl, btpl)
|
||||
|
||||
expect := func(t *testing.T, msg *Message, expSubject, expBody string) {
|
||||
subject := msg.GetHeader("Subject")
|
||||
msgbuf := new(bytes.Buffer)
|
||||
_, _ = msg.WriteTo(msgbuf)
|
||||
wholemsg := msgbuf.String()
|
||||
assert.Equal(t, []string{expSubject}, subject)
|
||||
assert.Contains(t, wholemsg, expBody)
|
||||
}
|
||||
|
||||
msg := composeIssueCommentMessage(issue, doer, models.ActionCreateIssue, false, "test body", nil, tos, "TestTemplateSelection")
|
||||
expect(t, msg, "issue/new/subject", "issue/new/body")
|
||||
|
||||
comment := models.AssertExistsAndLoadBean(t, &models.Comment{ID: 2, Issue: issue}).(*models.Comment)
|
||||
msg = composeIssueCommentMessage(issue, doer, models.ActionCommentIssue, false, "test body", comment, tos, "TestTemplateSelection")
|
||||
expect(t, msg, "issue/default/subject", "issue/default/body")
|
||||
|
||||
pull := models.AssertExistsAndLoadBean(t, &models.Issue{ID: 2, Repo: repo, Poster: doer}).(*models.Issue)
|
||||
comment = models.AssertExistsAndLoadBean(t, &models.Comment{ID: 4, Issue: pull}).(*models.Comment)
|
||||
msg = composeIssueCommentMessage(pull, doer, models.ActionCommentIssue, false, "test body", comment, tos, "TestTemplateSelection")
|
||||
expect(t, msg, "pull/comment/subject", "pull/comment/body")
|
||||
|
||||
msg = composeIssueCommentMessage(issue, doer, models.ActionCloseIssue, false, "test body", nil, tos, "TestTemplateSelection")
|
||||
expect(t, msg, "[user2/repo1] issue1 (#1)", "issue/close/body")
|
||||
}
|
||||
|
||||
func TestTemplateServices(t *testing.T) {
|
||||
assert.NoError(t, models.PrepareTestDatabase())
|
||||
var mailService = setting.Mailer{
|
||||
From: "test@gitea.com",
|
||||
}
|
||||
|
||||
setting.MailService = &mailService
|
||||
setting.Domain = "localhost"
|
||||
|
||||
doer := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User)
|
||||
repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1, Owner: doer}).(*models.Repository)
|
||||
issue := models.AssertExistsAndLoadBean(t, &models.Issue{ID: 1, Repo: repo, Poster: doer}).(*models.Issue)
|
||||
comment := models.AssertExistsAndLoadBean(t, &models.Comment{ID: 2, Issue: issue}).(*models.Comment)
|
||||
assert.NoError(t, issue.LoadRepo())
|
||||
|
||||
expect := func(t *testing.T, issue *models.Issue, comment *models.Comment, doer *models.User,
|
||||
actionType models.ActionType, fromMention bool, tplSubject, tplBody, expSubject, expBody string) {
|
||||
|
||||
stpl := texttmpl.Must(texttmpl.New("issue/default").Parse(tplSubject))
|
||||
btpl := template.Must(template.New("issue/default").Parse(tplBody))
|
||||
InitMailRender(stpl, btpl)
|
||||
|
||||
tos := []string{"test@gitea.com"}
|
||||
msg := composeIssueCommentMessage(issue, doer, actionType, fromMention, "test body", comment, tos, "TestTemplateServices")
|
||||
|
||||
subject := msg.GetHeader("Subject")
|
||||
msgbuf := new(bytes.Buffer)
|
||||
_, _ = msg.WriteTo(msgbuf)
|
||||
wholemsg := msgbuf.String()
|
||||
|
||||
assert.Equal(t, []string{expSubject}, subject)
|
||||
assert.Contains(t, wholemsg, "\r\n"+expBody+"\r\n")
|
||||
}
|
||||
|
||||
expect(t, issue, comment, doer, models.ActionCommentIssue, false,
|
||||
"{{.SubjectPrefix}}[{{.Repo}}]: @{{.Doer.Name}} commented on #{{.Issue.Index}} - {{.Issue.Title}}",
|
||||
"//{{.ActionType}},{{.ActionName}},{{if .IsMention}}norender{{end}}//",
|
||||
"Re: [user2/repo1]: @user2 commented on #1 - issue1",
|
||||
"//issue,comment,//")
|
||||
|
||||
expect(t, issue, comment, doer, models.ActionCommentIssue, true,
|
||||
"{{if .IsMention}}must render{{end}}",
|
||||
"//subject is: {{.Subject}}//",
|
||||
"must render",
|
||||
"//subject is: must render//")
|
||||
|
||||
expect(t, issue, comment, doer, models.ActionCommentIssue, true,
|
||||
"{{.FallbackSubject}}",
|
||||
"//{{.SubjectPrefix}}//",
|
||||
"Re: [user2/repo1] issue1 (#1)",
|
||||
"//Re: //")
|
||||
}
|
||||
|
|
|
@ -6,11 +6,11 @@
|
|||
</head>
|
||||
|
||||
<body>
|
||||
<p>@{{.Doer.Name}} assigned you to the {{if eq .Issue.IsPull true}}pull request{{else}}issue{{end}} <a href="{{.Link}}">#{{.Issue.Index}}</a> in repository {{.Issue.Repo.FullName}}.</p>
|
||||
<p>@{{.Doer.Name}} assigned you to the {{if .IsPull}}pull request{{else}}issue{{end}} <a href="{{.Link}}">#{{.Issue.Index}}</a> in repository {{.Repo}}.</p>
|
||||
<p>
|
||||
---
|
||||
<br>
|
||||
<a href="{{.Link}}">View it on Gitea</a>.
|
||||
<a href="{{.Link}}">View it on {{AppName}}</a>.
|
||||
</p>
|
||||
|
||||
</body>
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<title>{{.Subject}}</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<p>{{.Body | Str2html}}</p>
|
||||
<p>
|
||||
---
|
||||
<br>
|
||||
<a href="{{.Link}}">View it on Gitea</a>.
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
31
templates/mail/issue/default.tmpl
Normal file
31
templates/mail/issue/default.tmpl
Normal file
|
@ -0,0 +1,31 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<title>{{.Subject}}</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
{{if .IsMention}}<p>@{{.Doer.Name}} mentioned you:</p>{{end}}
|
||||
<p>
|
||||
{{- if eq .Body ""}}
|
||||
{{if eq .ActionName "new"}}
|
||||
Created #{{.Issue.Index}}.
|
||||
{{else if eq .ActionName "close"}}
|
||||
Closed #{{.Issue.Index}}.
|
||||
{{else if eq .ActionName "reopen"}}
|
||||
Reopened #{{.Issue.Index}}.
|
||||
{{else}}
|
||||
Empty comment on #{{.Issue.Index}}.
|
||||
{{end}}
|
||||
{{else}}
|
||||
{{.Body | Str2html}}
|
||||
{{end -}}
|
||||
</p>
|
||||
<p>
|
||||
---
|
||||
<br>
|
||||
<a href="{{.Link}}">View it on {{AppName}}</a>.
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
|
@ -1,17 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<title>{{.Subject}}</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<p>@{{.Doer.Name}} mentioned you:</p>
|
||||
<p>{{.Body | Str2html}}</p>
|
||||
<p>
|
||||
---
|
||||
<br>
|
||||
<a href="{{.Link}}">View it on Gitea</a>.
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in a new issue