aboutsummaryrefslogtreecommitdiffstats
path: root/services
diff options
context:
space:
mode:
authorLunny Xiao <xiaolunwen@gmail.com>2019-09-24 13:02:49 +0800
committerGitHub <noreply@github.com>2019-09-24 13:02:49 +0800
commit5a438ee3c0303efcb9d1935ff521917fe8a109e8 (patch)
tree8f36452631fc8a6d077addbbf857da6d50024a0a /services
parent730065a3dc72683460b95ba1486dba8ec355a373 (diff)
downloadgitea-5a438ee3c0303efcb9d1935ff521917fe8a109e8.tar.gz
gitea-5a438ee3c0303efcb9d1935ff521917fe8a109e8.zip
Move all mail related codes from models to services/mailer (#7200)
* move all mail related codes from models to modules/mailer * fix lint * use DBContext instead Engine * use WithContext not WithEngine * Use DBContext instead of Engine * don't use defer when sess.Close() * move DBContext to context.go and add some methods * move mailer from modules/ to services * fix lint * fix tests * fix fmt * add gitea copyright * fix tests * don't expose db functions * make code clear * add DefaultDBContext * fix build * fix bug
Diffstat (limited to 'services')
-rw-r--r--services/mailer/mail.go222
-rw-r--r--services/mailer/mail_comment.go47
-rw-r--r--services/mailer/mail_issue.go159
-rw-r--r--services/mailer/mail_test.go91
-rw-r--r--services/mailer/mailer.go303
-rw-r--r--services/mailer/main_test.go16
6 files changed, 838 insertions, 0 deletions
diff --git a/services/mailer/mail.go b/services/mailer/mail.go
new file mode 100644
index 0000000000..3f0a789dc4
--- /dev/null
+++ b/services/mailer/mail.go
@@ -0,0 +1,222 @@
+// Copyright 2016 The Gogs Authors. All rights reserved.
+// 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 mailer
+
+import (
+ "bytes"
+ "fmt"
+ "html/template"
+ "path"
+
+ "code.gitea.io/gitea/models"
+ "code.gitea.io/gitea/modules/base"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/markup"
+ "code.gitea.io/gitea/modules/markup/markdown"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/timeutil"
+
+ "gopkg.in/gomail.v2"
+)
+
+const (
+ mailAuthActivate base.TplName = "auth/activate"
+ mailAuthActivateEmail base.TplName = "auth/activate_email"
+ mailAuthResetPassword base.TplName = "auth/reset_passwd"
+ mailAuthRegisterNotify base.TplName = "auth/register_notify"
+
+ mailIssueComment base.TplName = "issue/comment"
+ mailIssueMention base.TplName = "issue/mention"
+
+ mailNotifyCollaborator base.TplName = "notify/collaborator"
+)
+
+var templates *template.Template
+
+// InitMailRender initializes the mail renderer
+func InitMailRender(tmpls *template.Template) {
+ templates = tmpls
+}
+
+// SendTestMail sends a test mail
+func SendTestMail(email string) error {
+ return gomail.Send(Sender, NewMessage([]string{email}, "Gitea Test Email!", "Gitea Test Email!").Message)
+}
+
+// SendUserMail sends a mail to the user
+func SendUserMail(language string, u *models.User, tpl base.TplName, code, subject, info string) {
+ data := map[string]interface{}{
+ "DisplayName": u.DisplayName(),
+ "ActiveCodeLives": timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, language),
+ "ResetPwdCodeLives": timeutil.MinutesToFriendly(setting.Service.ResetPwdCodeLives, language),
+ "Code": code,
+ }
+
+ var content bytes.Buffer
+
+ if err := templates.ExecuteTemplate(&content, string(tpl), data); err != nil {
+ log.Error("Template: %v", err)
+ return
+ }
+
+ msg := NewMessage([]string{u.Email}, subject, content.String())
+ msg.Info = fmt.Sprintf("UID: %d, %s", u.ID, info)
+
+ SendAsync(msg)
+}
+
+// Locale represents an interface to translation
+type Locale interface {
+ Language() string
+ Tr(string, ...interface{}) string
+}
+
+// SendActivateAccountMail sends an activation mail to the user (new user registration)
+func SendActivateAccountMail(locale Locale, u *models.User) {
+ SendUserMail(locale.Language(), u, mailAuthActivate, u.GenerateActivateCode(), locale.Tr("mail.activate_account"), "activate account")
+}
+
+// SendResetPasswordMail sends a password reset mail to the user
+func SendResetPasswordMail(locale Locale, u *models.User) {
+ SendUserMail(locale.Language(), u, mailAuthResetPassword, u.GenerateActivateCode(), locale.Tr("mail.reset_password"), "recover account")
+}
+
+// SendActivateEmailMail sends confirmation email to confirm new email address
+func SendActivateEmailMail(locale Locale, u *models.User, email *models.EmailAddress) {
+ data := map[string]interface{}{
+ "DisplayName": u.DisplayName(),
+ "ActiveCodeLives": timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, locale.Language()),
+ "Code": u.GenerateEmailActivateCode(email.Email),
+ "Email": email.Email,
+ }
+
+ var content bytes.Buffer
+
+ if err := templates.ExecuteTemplate(&content, string(mailAuthActivateEmail), data); err != nil {
+ log.Error("Template: %v", err)
+ return
+ }
+
+ msg := NewMessage([]string{email.Email}, locale.Tr("mail.activate_email"), content.String())
+ msg.Info = fmt.Sprintf("UID: %d, activate email", u.ID)
+
+ SendAsync(msg)
+}
+
+// SendRegisterNotifyMail triggers a notify e-mail by admin created a account.
+func SendRegisterNotifyMail(locale Locale, u *models.User) {
+ if setting.MailService == nil {
+ log.Warn("SendRegisterNotifyMail is being invoked but mail service hasn't been initialized")
+ return
+ }
+
+ data := map[string]interface{}{
+ "DisplayName": u.DisplayName(),
+ "Username": u.Name,
+ }
+
+ var content bytes.Buffer
+
+ if err := templates.ExecuteTemplate(&content, string(mailAuthRegisterNotify), data); err != nil {
+ log.Error("Template: %v", err)
+ return
+ }
+
+ msg := NewMessage([]string{u.Email}, locale.Tr("mail.register_notify"), content.String())
+ msg.Info = fmt.Sprintf("UID: %d, registration notify", u.ID)
+
+ SendAsync(msg)
+}
+
+// SendCollaboratorMail sends mail notification to new collaborator.
+func SendCollaboratorMail(u, doer *models.User, repo *models.Repository) {
+ repoName := path.Join(repo.Owner.Name, repo.Name)
+ subject := fmt.Sprintf("%s added you to %s", doer.DisplayName(), repoName)
+
+ data := map[string]interface{}{
+ "Subject": subject,
+ "RepoName": repoName,
+ "Link": repo.HTMLURL(),
+ }
+
+ var content bytes.Buffer
+
+ if err := templates.ExecuteTemplate(&content, string(mailNotifyCollaborator), data); err != nil {
+ log.Error("Template: %v", err)
+ return
+ }
+
+ msg := NewMessage([]string{u.Email}, subject, content.String())
+ msg.Info = fmt.Sprintf("UID: %d, add collaborator", u.ID)
+
+ 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, content string, comment *models.Comment, tplName base.TplName, tos []string, info string) *Message {
+ var subject string
+ if comment != nil {
+ subject = "Re: " + mailSubject(issue)
+ } else {
+ subject = mailSubject(issue)
+ }
+ err := issue.LoadRepo()
+ if err != nil {
+ log.Error("LoadRepo: %v", err)
+ }
+ 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())
+ }
+ data["Doer"] = doer
+
+ var mailBody bytes.Buffer
+
+ if err := templates.ExecuteTemplate(&mailBody, string(tplName), data); err != nil {
+ log.Error("Template: %v", err)
+ }
+
+ msg := NewMessageFrom(tos, doer.DisplayName(), setting.MailService.FromEmail, subject, mailBody.String())
+ msg.Info = fmt.Sprintf("Subject: %s, %s", subject, info)
+
+ // Set Message-ID on first message so replies know what to reference
+ if comment == nil {
+ msg.SetHeader("Message-ID", "<"+issue.ReplyReference()+">")
+ } else {
+ msg.SetHeader("In-Reply-To", "<"+issue.ReplyReference()+">")
+ msg.SetHeader("References", "<"+issue.ReplyReference()+">")
+ }
+
+ return msg
+}
+
+// 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) {
+ if len(tos) == 0 {
+ return
+ }
+
+ SendAsync(composeIssueCommentMessage(issue, doer, content, comment, mailIssueComment, 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) {
+ if len(tos) == 0 {
+ return
+ }
+ SendAsync(composeIssueCommentMessage(issue, doer, content, comment, mailIssueMention, tos, "issue mention"))
+}
diff --git a/services/mailer/mail_comment.go b/services/mailer/mail_comment.go
new file mode 100644
index 0000000000..cb477f887b
--- /dev/null
+++ b/services/mailer/mail_comment.go
@@ -0,0 +1,47 @@
+// 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 mailer
+
+import (
+ "fmt"
+
+ "code.gitea.io/gitea/models"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/markup"
+)
+
+// MailParticipantsComment sends new comment emails to repository watchers
+// and mentioned people.
+func MailParticipantsComment(c *models.Comment, opType models.ActionType, issue *models.Issue) error {
+ return mailParticipantsComment(models.DefaultDBContext(), c, opType, issue)
+}
+
+func mailParticipantsComment(ctx models.DBContext, c *models.Comment, opType models.ActionType, issue *models.Issue) (err error) {
+ mentions := markup.FindAllMentions(c.Content)
+ if err = models.UpdateIssueMentions(ctx, c.IssueID, mentions); err != nil {
+ return fmt.Errorf("UpdateIssueMentions [%d]: %v", c.IssueID, err)
+ }
+
+ if len(c.Content) > 0 {
+ if err = mailIssueCommentToParticipants(issue, c.Poster, 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
new file mode 100644
index 0000000000..92d2c5a879
--- /dev/null
+++ b/services/mailer/mail_issue.go
@@ -0,0 +1,159 @@
+// 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 mailer
+
+import (
+ "fmt"
+
+ "code.gitea.io/gitea/models"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/markup"
+ "code.gitea.io/gitea/modules/setting"
+
+ "github.com/unknwon/com"
+)
+
+func mailSubject(issue *models.Issue) string {
+ return fmt.Sprintf("[%s] %s (#%d)", issue.Repo.FullName(), issue.Title, issue.Index)
+}
+
+// mailIssueCommentToParticipants can be used for both new issue creation and comment.
+// 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 {
+ if !setting.Service.EnableNotifyMail {
+ return nil
+ }
+
+ watchers, err := models.GetWatchers(issue.RepoID)
+ if err != nil {
+ return fmt.Errorf("getWatchers [repo_id: %d]: %v", issue.RepoID, err)
+ }
+ participants, err := models.GetParticipantsByIssueID(issue.ID)
+ if err != nil {
+ return fmt.Errorf("getParticipantsByIssueID [issue_id: %d]: %v", issue.ID, err)
+ }
+
+ // In case the issue poster is not watching the repository and is active,
+ // even if we have duplicated in watchers, can be safely filtered out.
+ err = issue.LoadPoster()
+ if err != nil {
+ return fmt.Errorf("GetUserByID [%d]: %v", issue.PosterID, err)
+ }
+ if issue.PosterID != doer.ID && issue.Poster.IsActive && !issue.Poster.ProhibitLogin {
+ participants = append(participants, issue.Poster)
+ }
+
+ // Assignees must receive any communications
+ assignees, err := models.GetAssigneesByIssue(issue)
+ if err != nil {
+ return err
+ }
+
+ for _, assignee := range assignees {
+ if assignee.ID != doer.ID {
+ participants = append(participants, assignee)
+ }
+ }
+
+ tos := make([]string, 0, len(watchers)) // List of email addresses.
+ names := make([]string, 0, len(watchers))
+ for i := range watchers {
+ if watchers[i].UserID == doer.ID {
+ continue
+ }
+
+ to, err := models.GetUserByID(watchers[i].UserID)
+ if err != nil {
+ return fmt.Errorf("GetUserByID [%d]: %v", watchers[i].UserID, err)
+ }
+ if to.IsOrganization() || to.EmailNotifications() != models.EmailNotificationsEnabled {
+ continue
+ }
+
+ tos = append(tos, to.Email)
+ names = append(names, to.Name)
+ }
+ for i := range participants {
+ if participants[i].ID == doer.ID ||
+ com.IsSliceContainsStr(names, participants[i].Name) ||
+ participants[i].EmailNotifications() != models.EmailNotificationsEnabled {
+ continue
+ }
+
+ tos = append(tos, participants[i].Email)
+ names = append(names, participants[i].Name)
+ }
+
+ if err := issue.LoadRepo(); err != nil {
+ return err
+ }
+
+ for _, to := range tos {
+ SendIssueCommentMail(issue, doer, content, comment, []string{to})
+ }
+
+ // Mail mentioned people and exclude watchers.
+ names = append(names, doer.Name)
+ tos = make([]string, 0, len(mentions)) // list of user names.
+ for i := range mentions {
+ if com.IsSliceContainsStr(names, mentions[i]) {
+ continue
+ }
+
+ tos = append(tos, mentions[i])
+ }
+
+ emails := models.GetUserEmailsByNames(tos)
+
+ for _, to := range emails {
+ SendIssueMentionMail(issue, doer, content, comment, []string{to})
+ }
+
+ return nil
+}
+
+// MailParticipants sends new issue thread created emails to repository watchers
+// and mentioned people.
+func MailParticipants(issue *models.Issue, doer *models.User, opType models.ActionType) error {
+ return mailParticipants(models.DefaultDBContext(), issue, doer, opType)
+}
+
+func mailParticipants(ctx models.DBContext, issue *models.Issue, doer *models.User, opType models.ActionType) (err error) {
+ mentions := markup.FindAllMentions(issue.Content)
+
+ if err = models.UpdateIssueMentions(ctx, issue.ID, mentions); err != nil {
+ return fmt.Errorf("UpdateIssueMentions [%d]: %v", issue.ID, err)
+ }
+
+ if len(issue.Content) > 0 {
+ if err = mailIssueCommentToParticipants(issue, doer, 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
new file mode 100644
index 0000000000..c7a84d6b33
--- /dev/null
+++ b/services/mailer/mail_test.go
@@ -0,0 +1,91 @@
+// 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 mailer
+
+import (
+ "html/template"
+ "testing"
+
+ "code.gitea.io/gitea/models"
+ "code.gitea.io/gitea/modules/setting"
+
+ "github.com/stretchr/testify/assert"
+)
+
+const tmpl = `
+<!DOCTYPE html>
+<html>
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+ <title>{{.Subject}}</title>
+</head>
+
+<body>
+ <p>{{.Body}}</p>
+ <p>
+ ---
+ <br>
+ <a href="{{.Link}}">View it on Gitea</a>.
+ </p>
+</body>
+</html>
+`
+
+func TestComposeIssueCommentMessage(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)
+
+ email := template.Must(template.New("issue/comment").Parse(tmpl))
+ InitMailRender(email)
+
+ tos := []string{"test@gitea.com", "test2@gitea.com"}
+ msg := composeIssueCommentMessage(issue, doer, "test body", comment, mailIssueComment, 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, 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")
+}
+
+func TestComposeIssueMessage(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)
+
+ email := template.Must(template.New("issue/comment").Parse(tmpl))
+ InitMailRender(email)
+
+ tos := []string{"test@gitea.com", "test2@gitea.com"}
+ msg := composeIssueCommentMessage(issue, doer, "test body", nil, mailIssueComment, 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.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")
+}
diff --git a/services/mailer/mailer.go b/services/mailer/mailer.go
new file mode 100644
index 0000000000..d19ae7b2f4
--- /dev/null
+++ b/services/mailer/mailer.go
@@ -0,0 +1,303 @@
+// Copyright 2014 The Gogs Authors. All rights reserved.
+// Copyright 2017 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 mailer
+
+import (
+ "bytes"
+ "crypto/tls"
+ "fmt"
+ "io"
+ "net"
+ "net/smtp"
+ "os"
+ "os/exec"
+ "strings"
+ "time"
+
+ "code.gitea.io/gitea/modules/base"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/setting"
+
+ "github.com/jaytaylor/html2text"
+ "gopkg.in/gomail.v2"
+)
+
+// Message mail body and log info
+type Message struct {
+ Info string // Message information for log purpose.
+ *gomail.Message
+}
+
+// NewMessageFrom creates new mail message object with custom From header.
+func NewMessageFrom(to []string, fromDisplayName, fromAddress, subject, body string) *Message {
+ log.Trace("NewMessageFrom (body):\n%s", body)
+
+ msg := gomail.NewMessage()
+ msg.SetAddressHeader("From", fromAddress, fromDisplayName)
+ msg.SetHeader("To", to...)
+ if len(setting.MailService.SubjectPrefix) > 0 {
+ msg.SetHeader("Subject", setting.MailService.SubjectPrefix+" "+subject)
+ } else {
+ msg.SetHeader("Subject", subject)
+ }
+ msg.SetDateHeader("Date", time.Now())
+ msg.SetHeader("X-Auto-Response-Suppress", "All")
+
+ plainBody, err := html2text.FromString(body)
+ if err != nil || setting.MailService.SendAsPlainText {
+ if strings.Contains(base.TruncateString(body, 100), "<html>") {
+ log.Warn("Mail contains HTML but configured to send as plain text.")
+ }
+ msg.SetBody("text/plain", plainBody)
+ } else {
+ msg.SetBody("text/plain", plainBody)
+ msg.AddAlternative("text/html", body)
+ }
+
+ return &Message{
+ Message: msg,
+ }
+}
+
+// NewMessage creates new mail message object with default From header.
+func NewMessage(to []string, subject, body string) *Message {
+ return NewMessageFrom(to, setting.MailService.FromName, setting.MailService.FromEmail, subject, body)
+}
+
+type loginAuth struct {
+ username, password string
+}
+
+// LoginAuth SMTP AUTH LOGIN Auth Handler
+func LoginAuth(username, password string) smtp.Auth {
+ return &loginAuth{username, password}
+}
+
+// Start start SMTP login auth
+func (a *loginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) {
+ return "LOGIN", []byte{}, nil
+}
+
+// Next next step of SMTP login auth
+func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
+ if more {
+ switch string(fromServer) {
+ case "Username:":
+ return []byte(a.username), nil
+ case "Password:":
+ return []byte(a.password), nil
+ default:
+ return nil, fmt.Errorf("unknown fromServer: %s", string(fromServer))
+ }
+ }
+ return nil, nil
+}
+
+// Sender SMTP mail sender
+type smtpSender struct {
+}
+
+// Send send email
+func (s *smtpSender) Send(from string, to []string, msg io.WriterTo) error {
+ opts := setting.MailService
+
+ host, port, err := net.SplitHostPort(opts.Host)
+ if err != nil {
+ return err
+ }
+
+ tlsconfig := &tls.Config{
+ InsecureSkipVerify: opts.SkipVerify,
+ ServerName: host,
+ }
+
+ if opts.UseCertificate {
+ cert, err := tls.LoadX509KeyPair(opts.CertFile, opts.KeyFile)
+ if err != nil {
+ return err
+ }
+ tlsconfig.Certificates = []tls.Certificate{cert}
+ }
+
+ conn, err := net.Dial("tcp", net.JoinHostPort(host, port))
+ if err != nil {
+ return err
+ }
+ defer conn.Close()
+
+ isSecureConn := opts.IsTLSEnabled || (strings.HasSuffix(port, "465"))
+ // Start TLS directly if the port ends with 465 (SMTPS protocol)
+ if isSecureConn {
+ conn = tls.Client(conn, tlsconfig)
+ }
+
+ client, err := smtp.NewClient(conn, host)
+ if err != nil {
+ return fmt.Errorf("NewClient: %v", err)
+ }
+
+ if !opts.DisableHelo {
+ hostname := opts.HeloHostname
+ if len(hostname) == 0 {
+ hostname, err = os.Hostname()
+ if err != nil {
+ return err
+ }
+ }
+
+ if err = client.Hello(hostname); err != nil {
+ return fmt.Errorf("Hello: %v", err)
+ }
+ }
+
+ // If not using SMTPS, always use STARTTLS if available
+ hasStartTLS, _ := client.Extension("STARTTLS")
+ if !isSecureConn && hasStartTLS {
+ if err = client.StartTLS(tlsconfig); err != nil {
+ return fmt.Errorf("StartTLS: %v", err)
+ }
+ }
+
+ canAuth, options := client.Extension("AUTH")
+ if canAuth && len(opts.User) > 0 {
+ var auth smtp.Auth
+
+ if strings.Contains(options, "CRAM-MD5") {
+ auth = smtp.CRAMMD5Auth(opts.User, opts.Passwd)
+ } else if strings.Contains(options, "PLAIN") {
+ auth = smtp.PlainAuth("", opts.User, opts.Passwd, host)
+ } else if strings.Contains(options, "LOGIN") {
+ // Patch for AUTH LOGIN
+ auth = LoginAuth(opts.User, opts.Passwd)
+ }
+
+ if auth != nil {
+ if err = client.Auth(auth); err != nil {
+ return fmt.Errorf("Auth: %v", err)
+ }
+ }
+ }
+
+ if err = client.Mail(from); err != nil {
+ return fmt.Errorf("Mail: %v", err)
+ }
+
+ for _, rec := range to {
+ if err = client.Rcpt(rec); err != nil {
+ return fmt.Errorf("Rcpt: %v", err)
+ }
+ }
+
+ w, err := client.Data()
+ if err != nil {
+ return fmt.Errorf("Data: %v", err)
+ } else if _, err = msg.WriteTo(w); err != nil {
+ return fmt.Errorf("WriteTo: %v", err)
+ } else if err = w.Close(); err != nil {
+ return fmt.Errorf("Close: %v", err)
+ }
+
+ return client.Quit()
+}
+
+// Sender sendmail mail sender
+type sendmailSender struct {
+}
+
+// Send send email
+func (s *sendmailSender) Send(from string, to []string, msg io.WriterTo) error {
+ var err error
+ var closeError error
+ var waitError error
+
+ args := []string{"-F", from, "-i"}
+ args = append(args, setting.MailService.SendmailArgs...)
+ args = append(args, to...)
+ log.Trace("Sending with: %s %v", setting.MailService.SendmailPath, args)
+ cmd := exec.Command(setting.MailService.SendmailPath, args...)
+ pipe, err := cmd.StdinPipe()
+
+ if err != nil {
+ return err
+ }
+
+ if err = cmd.Start(); err != nil {
+ return err
+ }
+
+ _, err = msg.WriteTo(pipe)
+
+ // we MUST close the pipe or sendmail will hang waiting for more of the message
+ // Also we should wait on our sendmail command even if something fails
+ closeError = pipe.Close()
+ waitError = cmd.Wait()
+ if err != nil {
+ return err
+ } else if closeError != nil {
+ return closeError
+ } else {
+ return waitError
+ }
+}
+
+// Sender sendmail mail sender
+type dummySender struct {
+}
+
+// Send send email
+func (s *dummySender) Send(from string, to []string, msg io.WriterTo) error {
+ buf := bytes.Buffer{}
+ if _, err := msg.WriteTo(&buf); err != nil {
+ return err
+ }
+ log.Info("Mail From: %s To: %v Body: %s", from, to, buf.String())
+ return nil
+}
+
+func processMailQueue() {
+ for msg := range mailQueue {
+ log.Trace("New e-mail sending request %s: %s", msg.GetHeader("To"), msg.Info)
+ if err := gomail.Send(Sender, msg.Message); err != nil {
+ log.Error("Failed to send emails %s: %s - %v", msg.GetHeader("To"), msg.Info, err)
+ } else {
+ log.Trace("E-mails sent %s: %s", msg.GetHeader("To"), msg.Info)
+ }
+ }
+}
+
+var mailQueue chan *Message
+
+// Sender sender for sending mail synchronously
+var Sender gomail.Sender
+
+// NewContext start mail queue service
+func NewContext() {
+ // Need to check if mailQueue is nil because in during reinstall (user had installed
+ // before but swithed install lock off), this function will be called again
+ // while mail queue is already processing tasks, and produces a race condition.
+ if setting.MailService == nil || mailQueue != nil {
+ return
+ }
+
+ switch setting.MailService.MailerType {
+ case "smtp":
+ Sender = &smtpSender{}
+ case "sendmail":
+ Sender = &sendmailSender{}
+ case "dummy":
+ Sender = &dummySender{}
+ }
+
+ mailQueue = make(chan *Message, setting.MailService.QueueLength)
+ go processMailQueue()
+}
+
+// SendAsync send mail asynchronous
+func SendAsync(msg *Message) {
+ go func() {
+ mailQueue <- msg
+ }()
+}
diff --git a/services/mailer/main_test.go b/services/mailer/main_test.go
new file mode 100644
index 0000000000..bbd121024e
--- /dev/null
+++ b/services/mailer/main_test.go
@@ -0,0 +1,16 @@
+// 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 mailer
+
+import (
+ "path/filepath"
+ "testing"
+
+ "code.gitea.io/gitea/models"
+)
+
+func TestMain(m *testing.M) {
+ models.MainTest(m, filepath.Join("..", ".."))
+}