diff --git a/docs/content/doc/advanced/mail-templates-us.md b/docs/content/doc/advanced/mail-templates-us.md
new file mode 100644
index 0000000000..ffe2d4a27b
--- /dev/null
+++ b/docs/content/doc/advanced/mail-templates-us.md
@@ -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}}
+```
diff --git a/modules/templates/dynamic.go b/modules/templates/dynamic.go
index 6217f1c3b0..6153e8d027 100644
--- a/modules/templates/dynamic.go
+++ b/modules/templates/dynamic.go
@@ -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
 }
diff --git a/modules/templates/helper.go b/modules/templates/helper.go
index 2d7a1aee9b..1347835b80 100644
--- a/modules/templates/helper.go
+++ b/modules/templates/helper.go
@@ -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)
+	}
+}
diff --git a/modules/templates/helper_test.go b/modules/templates/helper_test.go
new file mode 100644
index 0000000000..e2997cb853
--- /dev/null
+++ b/modules/templates/helper_test.go
@@ -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")
+}
diff --git a/modules/templates/static.go b/modules/templates/static.go
index f7e53ce887..435ccb1f95 100644
--- a/modules/templates/static.go
+++ b/modules/templates/static.go
@@ -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) {
diff --git a/services/mailer/mail.go b/services/mailer/mail.go
index bc2aff7314..fc892f6076 100644
--- a/services/mailer/mail.go
+++ b/services/mailer/mail.go
@@ -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"))
 }
diff --git a/services/mailer/mail_comment.go b/services/mailer/mail_comment.go
index d306c14f42..6469eb1fa1 100644
--- a/services/mailer/mail_comment.go
+++ b/services/mailer/mail_comment.go
@@ -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
 }
diff --git a/services/mailer/mail_issue.go b/services/mailer/mail_issue.go
index a5f3251807..32b21b1324 100644
--- a/services/mailer/mail_issue.go
+++ b/services/mailer/mail_issue.go
@@ -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
 }
diff --git a/services/mailer/mail_test.go b/services/mailer/mail_test.go
index c7a84d6b33..a10507e0e4 100644
--- a/services/mailer/mail_test.go
+++ b/services/mailer/mail_test.go
@@ -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: //")
+}
diff --git a/templates/mail/issue/assigned.tmpl b/templates/mail/issue/assigned.tmpl
index ab06ade1f4..997e2447fc 100644
--- a/templates/mail/issue/assigned.tmpl
+++ b/templates/mail/issue/assigned.tmpl
@@ -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>
diff --git a/templates/mail/issue/comment.tmpl b/templates/mail/issue/comment.tmpl
deleted file mode 100644
index cc86addaf0..0000000000
--- a/templates/mail/issue/comment.tmpl
+++ /dev/null
@@ -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>
diff --git a/templates/mail/issue/default.tmpl b/templates/mail/issue/default.tmpl
new file mode 100644
index 0000000000..ee15d6d8e1
--- /dev/null
+++ b/templates/mail/issue/default.tmpl
@@ -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>
diff --git a/templates/mail/issue/mention.tmpl b/templates/mail/issue/mention.tmpl
deleted file mode 100644
index 032eea053d..0000000000
--- a/templates/mail/issue/mention.tmpl
+++ /dev/null
@@ -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>