mirror of
				https://gitee.com/gitea/gitea
				synced 2025-11-04 16:40:24 +08: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:
		
							
								
								
									
										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"
 | 
						"io/ioutil"
 | 
				
			||||||
	"path"
 | 
						"path"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
 | 
						texttmpl "text/template"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"code.gitea.io/gitea/modules/log"
 | 
						"code.gitea.io/gitea/modules/log"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/setting"
 | 
						"code.gitea.io/gitea/modules/setting"
 | 
				
			||||||
@@ -20,7 +21,8 @@ import (
 | 
				
			|||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var (
 | 
					var (
 | 
				
			||||||
	templates = template.New("")
 | 
						subjectTemplates = texttmpl.New("")
 | 
				
			||||||
 | 
						bodyTemplates    = template.New("")
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// HTMLRenderer implements the macaron handler for serving HTML templates.
 | 
					// 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.
 | 
					// 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() {
 | 
						for _, funcs := range NewFuncMap() {
 | 
				
			||||||
		templates.Funcs(funcs)
 | 
							bodyTemplates.Funcs(funcs)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	staticDir := path.Join(setting.StaticRootPath, "templates", "mail")
 | 
						staticDir := path.Join(setting.StaticRootPath, "templates", "mail")
 | 
				
			||||||
@@ -84,15 +89,7 @@ func Mailer() *template.Template {
 | 
				
			|||||||
					continue
 | 
										continue
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				_, err = templates.New(
 | 
									buildSubjectBodyTemplate(subjectTemplates, bodyTemplates, strings.TrimSuffix(filePath, ".tmpl"), content)
 | 
				
			||||||
					strings.TrimSuffix(
 | 
					 | 
				
			||||||
						filePath,
 | 
					 | 
				
			||||||
						".tmpl",
 | 
					 | 
				
			||||||
					),
 | 
					 | 
				
			||||||
				).Parse(string(content))
 | 
					 | 
				
			||||||
				if err != nil {
 | 
					 | 
				
			||||||
					log.Warn("Failed to parse template %v", err)
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -117,18 +114,10 @@ func Mailer() *template.Template {
 | 
				
			|||||||
					continue
 | 
										continue
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				_, err = templates.New(
 | 
									buildSubjectBodyTemplate(subjectTemplates, bodyTemplates, strings.TrimSuffix(filePath, ".tmpl"), content)
 | 
				
			||||||
					strings.TrimSuffix(
 | 
					 | 
				
			||||||
						filePath,
 | 
					 | 
				
			||||||
						".tmpl",
 | 
					 | 
				
			||||||
					),
 | 
					 | 
				
			||||||
				).Parse(string(content))
 | 
					 | 
				
			||||||
				if err != nil {
 | 
					 | 
				
			||||||
					log.Warn("Failed to parse template %v", err)
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return templates
 | 
						return subjectTemplates, bodyTemplates
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -16,8 +16,10 @@ import (
 | 
				
			|||||||
	"mime"
 | 
						"mime"
 | 
				
			||||||
	"net/url"
 | 
						"net/url"
 | 
				
			||||||
	"path/filepath"
 | 
						"path/filepath"
 | 
				
			||||||
 | 
						"regexp"
 | 
				
			||||||
	"runtime"
 | 
						"runtime"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
 | 
						texttmpl "text/template"
 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
	"unicode"
 | 
						"unicode"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -34,6 +36,9 @@ import (
 | 
				
			|||||||
	"github.com/editorconfig/editorconfig-core-go/v2"
 | 
						"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
 | 
					// NewFuncMap returns functions for injecting to templates
 | 
				
			||||||
func NewFuncMap() []template.FuncMap {
 | 
					func NewFuncMap() []template.FuncMap {
 | 
				
			||||||
	return []template.FuncMap{map[string]interface{}{
 | 
						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
 | 
					// Safe render raw as HTML
 | 
				
			||||||
func Safe(raw string) template.HTML {
 | 
					func Safe(raw string) template.HTML {
 | 
				
			||||||
	return template.HTML(raw)
 | 
						return template.HTML(raw)
 | 
				
			||||||
@@ -551,3 +662,22 @@ func MigrationIcon(hostname string) string {
 | 
				
			|||||||
		return "fa-git-alt"
 | 
							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"
 | 
						"io/ioutil"
 | 
				
			||||||
	"path"
 | 
						"path"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
 | 
						texttmpl "text/template"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"code.gitea.io/gitea/modules/log"
 | 
						"code.gitea.io/gitea/modules/log"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/setting"
 | 
						"code.gitea.io/gitea/modules/setting"
 | 
				
			||||||
@@ -23,7 +24,8 @@ import (
 | 
				
			|||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var (
 | 
					var (
 | 
				
			||||||
	templates = template.New("")
 | 
						subjectTemplates = texttmpl.New("")
 | 
				
			||||||
 | 
						bodyTemplates    = template.New("")
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type templateFileSystem struct {
 | 
					type templateFileSystem struct {
 | 
				
			||||||
@@ -140,9 +142,12 @@ func JSRenderer() macaron.Handler {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Mailer provides the templates required for sending notification mails.
 | 
					// 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() {
 | 
						for _, funcs := range NewFuncMap() {
 | 
				
			||||||
		templates.Funcs(funcs)
 | 
							bodyTemplates.Funcs(funcs)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	for _, assetPath := range AssetNames() {
 | 
						for _, assetPath := range AssetNames() {
 | 
				
			||||||
@@ -161,7 +166,8 @@ func Mailer() *template.Template {
 | 
				
			|||||||
			continue
 | 
								continue
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		templates.New(
 | 
							buildSubjectBodyTemplate(subjectTemplates,
 | 
				
			||||||
 | 
								bodyTemplates,
 | 
				
			||||||
			strings.TrimPrefix(
 | 
								strings.TrimPrefix(
 | 
				
			||||||
				strings.TrimSuffix(
 | 
									strings.TrimSuffix(
 | 
				
			||||||
					assetPath,
 | 
										assetPath,
 | 
				
			||||||
@@ -169,7 +175,7 @@ func Mailer() *template.Template {
 | 
				
			|||||||
				),
 | 
									),
 | 
				
			||||||
				"mail/",
 | 
									"mail/",
 | 
				
			||||||
			),
 | 
								),
 | 
				
			||||||
		).Parse(string(content))
 | 
								content)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	customDir := path.Join(setting.CustomPath, "templates", "mail")
 | 
						customDir := path.Join(setting.CustomPath, "templates", "mail")
 | 
				
			||||||
@@ -192,17 +198,18 @@ func Mailer() *template.Template {
 | 
				
			|||||||
					continue
 | 
										continue
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				templates.New(
 | 
									buildSubjectBodyTemplate(subjectTemplates,
 | 
				
			||||||
 | 
										bodyTemplates,
 | 
				
			||||||
					strings.TrimSuffix(
 | 
										strings.TrimSuffix(
 | 
				
			||||||
						filePath,
 | 
											filePath,
 | 
				
			||||||
						".tmpl",
 | 
											".tmpl",
 | 
				
			||||||
					),
 | 
										),
 | 
				
			||||||
				).Parse(string(content))
 | 
										content)
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return templates
 | 
						return subjectTemplates, bodyTemplates
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func Asset(name string) ([]byte, error) {
 | 
					func Asset(name string) ([]byte, error) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,7 +9,11 @@ import (
 | 
				
			|||||||
	"bytes"
 | 
						"bytes"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"html/template"
 | 
						"html/template"
 | 
				
			||||||
 | 
						"mime"
 | 
				
			||||||
	"path"
 | 
						"path"
 | 
				
			||||||
 | 
						"regexp"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						texttmpl "text/template"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"code.gitea.io/gitea/models"
 | 
						"code.gitea.io/gitea/models"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/base"
 | 
						"code.gitea.io/gitea/modules/base"
 | 
				
			||||||
@@ -28,18 +32,22 @@ const (
 | 
				
			|||||||
	mailAuthResetPassword  base.TplName = "auth/reset_passwd"
 | 
						mailAuthResetPassword  base.TplName = "auth/reset_passwd"
 | 
				
			||||||
	mailAuthRegisterNotify base.TplName = "auth/register_notify"
 | 
						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"
 | 
						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
 | 
					// InitMailRender initializes the mail renderer
 | 
				
			||||||
func InitMailRender(tmpls *template.Template) {
 | 
					func InitMailRender(subjectTpl *texttmpl.Template, bodyTpl *template.Template) {
 | 
				
			||||||
	templates = tmpls
 | 
						subjectTemplates = subjectTpl
 | 
				
			||||||
 | 
						bodyTemplates = bodyTpl
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// SendTestMail sends a test mail
 | 
					// 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
 | 
						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)
 | 
							log.Error("Template: %v", err)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -96,7 +104,7 @@ func SendActivateEmailMail(locale Locale, u *models.User, email *models.EmailAdd
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	var content bytes.Buffer
 | 
						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)
 | 
							log.Error("Template: %v", err)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -121,7 +129,7 @@ func SendRegisterNotifyMail(locale Locale, u *models.User) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	var content bytes.Buffer
 | 
						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)
 | 
							log.Error("Template: %v", err)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -145,7 +153,7 @@ func SendCollaboratorMail(u, doer *models.User, repo *models.Repository) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	var content bytes.Buffer
 | 
						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)
 | 
							log.Error("Template: %v", err)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -156,40 +164,70 @@ func SendCollaboratorMail(u, doer *models.User, repo *models.Repository) {
 | 
				
			|||||||
	SendAsync(msg)
 | 
						SendAsync(msg)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func composeTplData(subject, body, link string) map[string]interface{} {
 | 
					func composeIssueCommentMessage(issue *models.Issue, doer *models.User, actionType models.ActionType, fromMention bool,
 | 
				
			||||||
	data := make(map[string]interface{}, 10)
 | 
						content string, comment *models.Comment, tos []string, info string) *Message {
 | 
				
			||||||
	data["Subject"] = subject
 | 
					 | 
				
			||||||
	data["Body"] = body
 | 
					 | 
				
			||||||
	data["Link"] = link
 | 
					 | 
				
			||||||
	return data
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
func composeIssueCommentMessage(issue *models.Issue, doer *models.User, content string, comment *models.Comment, tplName base.TplName, tos []string, info string) *Message {
 | 
						if err := issue.LoadPullRequest(); err != nil {
 | 
				
			||||||
	var subject string
 | 
							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 {
 | 
						if comment != nil {
 | 
				
			||||||
		subject = "Re: " + mailSubject(issue)
 | 
							prefix = "Re: "
 | 
				
			||||||
 | 
							commentType = comment.Type
 | 
				
			||||||
 | 
							link = issue.HTMLURL() + "#" + comment.HashTag()
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
		subject = mailSubject(issue)
 | 
							link = issue.HTMLURL()
 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	err := issue.LoadRepo()
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		log.Error("LoadRepo: %v", err)
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						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()))
 | 
						body := string(markup.RenderByType(markdown.MarkupName, []byte(content), issue.Repo.HTMLURL(), issue.Repo.ComposeMetas()))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	var data = make(map[string]interface{}, 10)
 | 
						actType, actName, tplName := actionToTemplate(issue, actionType, commentType)
 | 
				
			||||||
	if comment != nil {
 | 
					
 | 
				
			||||||
		data = composeTplData(subject, body, issue.HTMLURL()+"#"+comment.HashTag())
 | 
						mailMeta := map[string]interface{}{
 | 
				
			||||||
	} else {
 | 
							"FallbackSubject": fallback,
 | 
				
			||||||
		data = composeTplData(subject, body, issue.HTMLURL())
 | 
							"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
 | 
						var mailBody bytes.Buffer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err := templates.ExecuteTemplate(&mailBody, string(tplName), data); err != nil {
 | 
						if err := bodyTemplates.ExecuteTemplate(&mailBody, string(tplName), mailMeta); err != nil {
 | 
				
			||||||
		log.Error("Template: %v", err)
 | 
							log.Error("ExecuteTemplate [%s]: %v", string(tplName)+"/body", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	msg := NewMessageFrom(tos, doer.DisplayName(), setting.MailService.FromEmail, subject, mailBody.String())
 | 
						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
 | 
						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.
 | 
					// 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 {
 | 
						if len(tos) == 0 {
 | 
				
			||||||
		return
 | 
							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.
 | 
					// 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 {
 | 
						if len(tos) == 0 {
 | 
				
			||||||
		return
 | 
							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
 | 
					// SendIssueAssignedMail composes and sends issue assigned email
 | 
				
			||||||
func SendIssueAssignedMail(issue *models.Issue, doer *models.User, content string, comment *models.Comment, tos []string) {
 | 
					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 {
 | 
						for i, u := range userMentions {
 | 
				
			||||||
		mentions[i] = u.LowerName
 | 
							mentions[i] = u.LowerName
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if len(c.Content) > 0 {
 | 
						if err = mailIssueCommentToParticipants(issue, c.Poster, opType, c.Content, c, mentions); err != nil {
 | 
				
			||||||
		if err = mailIssueCommentToParticipants(issue, c.Poster, c.Content, c, mentions); err != nil {
 | 
					 | 
				
			||||||
		log.Error("mailIssueCommentToParticipants: %v", err)
 | 
							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
 | 
						return nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,7 +14,7 @@ import (
 | 
				
			|||||||
	"github.com/unknwon/com"
 | 
						"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)
 | 
						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:
 | 
					// This function sends two list of emails:
 | 
				
			||||||
// 1. Repository watchers and users who are participated in comments.
 | 
					// 1. Repository watchers and users who are participated in comments.
 | 
				
			||||||
// 2. Users who are not in 1. but get mentioned in current issue/comment.
 | 
					// 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)
 | 
						watchers, err := models.GetWatchers(issue.RepoID)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
@@ -89,7 +89,7 @@ func mailIssueCommentToParticipants(issue *models.Issue, doer *models.User, cont
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	for _, to := range tos {
 | 
						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.
 | 
						// Mail mentioned people and exclude watchers.
 | 
				
			||||||
@@ -106,7 +106,7 @@ func mailIssueCommentToParticipants(issue *models.Issue, doer *models.User, cont
 | 
				
			|||||||
	emails := models.GetUserEmailsByNames(tos)
 | 
						emails := models.GetUserEmailsByNames(tos)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	for _, to := range emails {
 | 
						for _, to := range emails {
 | 
				
			||||||
		SendIssueMentionMail(issue, doer, content, comment, []string{to})
 | 
							SendIssueMentionMail(issue, doer, actionType, content, comment, []string{to})
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return nil
 | 
						return nil
 | 
				
			||||||
@@ -131,32 +131,8 @@ func mailParticipants(ctx models.DBContext, issue *models.Issue, doer *models.Us
 | 
				
			|||||||
	for i, u := range userMentions {
 | 
						for i, u := range userMentions {
 | 
				
			||||||
		mentions[i] = u.LowerName
 | 
							mentions[i] = u.LowerName
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						if err = mailIssueCommentToParticipants(issue, doer, opType, issue.Content, nil, mentions); err != nil {
 | 
				
			||||||
	if len(issue.Content) > 0 {
 | 
					 | 
				
			||||||
		if err = mailIssueCommentToParticipants(issue, doer, issue.Content, nil, mentions); err != nil {
 | 
					 | 
				
			||||||
		log.Error("mailIssueCommentToParticipants: %v", err)
 | 
							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
 | 
						return nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,8 +5,10 @@
 | 
				
			|||||||
package mailer
 | 
					package mailer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"bytes"
 | 
				
			||||||
	"html/template"
 | 
						"html/template"
 | 
				
			||||||
	"testing"
 | 
						"testing"
 | 
				
			||||||
 | 
						texttmpl "text/template"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"code.gitea.io/gitea/models"
 | 
						"code.gitea.io/gitea/models"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/setting"
 | 
						"code.gitea.io/gitea/modules/setting"
 | 
				
			||||||
@@ -14,7 +16,11 @@ import (
 | 
				
			|||||||
	"github.com/stretchr/testify/assert"
 | 
						"github.com/stretchr/testify/assert"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const tmpl = `
 | 
					const subjectTpl = `
 | 
				
			||||||
 | 
					{{.SubjectPrefix}}[{{.Repo}}] @{{.Doer.Name}} #{{.Issue.Index}} - {{.Issue.Title}}
 | 
				
			||||||
 | 
					`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const bodyTpl = `
 | 
				
			||||||
<!DOCTYPE html>
 | 
					<!DOCTYPE html>
 | 
				
			||||||
<html>
 | 
					<html>
 | 
				
			||||||
<head>
 | 
					<head>
 | 
				
			||||||
@@ -47,17 +53,19 @@ func TestComposeIssueCommentMessage(t *testing.T) {
 | 
				
			|||||||
	issue := models.AssertExistsAndLoadBean(t, &models.Issue{ID: 1, Repo: repo, Poster: doer}).(*models.Issue)
 | 
						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)
 | 
						comment := models.AssertExistsAndLoadBean(t, &models.Comment{ID: 2, Issue: issue}).(*models.Comment)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	email := template.Must(template.New("issue/comment").Parse(tmpl))
 | 
						stpl := texttmpl.Must(texttmpl.New("issue/comment").Parse(subjectTpl))
 | 
				
			||||||
	InitMailRender(email)
 | 
						btpl := template.Must(template.New("issue/comment").Parse(bodyTpl))
 | 
				
			||||||
 | 
						InitMailRender(stpl, btpl)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	tos := []string{"test@gitea.com", "test2@gitea.com"}
 | 
						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")
 | 
						subject := msg.GetHeader("Subject")
 | 
				
			||||||
	inreplyTo := msg.GetHeader("In-Reply-To")
 | 
						inreplyTo := msg.GetHeader("In-Reply-To")
 | 
				
			||||||
	references := msg.GetHeader("References")
 | 
						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, 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")
 | 
						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)
 | 
						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)
 | 
						issue := models.AssertExistsAndLoadBean(t, &models.Issue{ID: 1, Repo: repo, Poster: doer}).(*models.Issue)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	email := template.Must(template.New("issue/comment").Parse(tmpl))
 | 
						stpl := texttmpl.Must(texttmpl.New("issue/new").Parse(subjectTpl))
 | 
				
			||||||
	InitMailRender(email)
 | 
						btpl := template.Must(template.New("issue/new").Parse(bodyTpl))
 | 
				
			||||||
 | 
						InitMailRender(stpl, btpl)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	tos := []string{"test@gitea.com", "test2@gitea.com"}
 | 
						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")
 | 
						subject := msg.GetHeader("Subject")
 | 
				
			||||||
	messageID := msg.GetHeader("Message-ID")
 | 
						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("In-Reply-To"))
 | 
				
			||||||
	assert.Nil(t, msg.GetHeader("References"))
 | 
						assert.Nil(t, msg.GetHeader("References"))
 | 
				
			||||||
	assert.Equal(t, messageID[0], "<user2/repo1/issues/1@localhost>", "Message-ID header doesn't match")
 | 
						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>
 | 
					</head>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<body>
 | 
					<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>
 | 
					    <p>
 | 
				
			||||||
        ---
 | 
					        ---
 | 
				
			||||||
        <br>
 | 
					        <br>
 | 
				
			||||||
        <a href="{{.Link}}">View it on Gitea</a>.
 | 
					        <a href="{{.Link}}">View it on {{AppName}}</a>.
 | 
				
			||||||
    </p>
 | 
					    </p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
</body>
 | 
					</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>
 | 
					 | 
				
			||||||
		Reference in New Issue
	
	Block a user