2016-07-15 19:36:39 +03:00
// Copyright 2016 The Gogs Authors. All rights reserved.
2019-09-24 08:02:49 +03:00
// Copyright 2019 The Gitea Authors. All rights reserved.
2016-07-15 19:36:39 +03:00
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
2019-09-24 08:02:49 +03:00
package mailer
2016-07-15 19:36:39 +03:00
import (
2016-12-06 20:58:31 +03:00
"bytes"
2016-07-15 19:36:39 +03:00
"fmt"
"html/template"
2019-11-07 16:34:28 +03:00
"mime"
"regexp"
2021-05-22 09:47:16 +03:00
"strconv"
2019-11-07 16:34:28 +03:00
"strings"
texttmpl "text/template"
2016-07-15 19:36:39 +03:00
2019-09-24 08:02:49 +03:00
"code.gitea.io/gitea/models"
2016-11-10 19:24:48 +03:00
"code.gitea.io/gitea/modules/base"
2020-04-28 21:05:39 +03:00
"code.gitea.io/gitea/modules/emoji"
2016-11-10 19:24:48 +03:00
"code.gitea.io/gitea/modules/log"
2017-09-16 20:17:57 +03:00
"code.gitea.io/gitea/modules/markup"
2017-09-21 08:20:14 +03:00
"code.gitea.io/gitea/modules/markup/markdown"
2016-11-10 19:24:48 +03:00
"code.gitea.io/gitea/modules/setting"
2019-08-15 17:46:21 +03:00
"code.gitea.io/gitea/modules/timeutil"
2021-04-02 13:25:13 +03:00
"code.gitea.io/gitea/modules/translation"
2019-08-23 19:40:30 +03:00
2016-12-06 20:58:31 +03:00
"gopkg.in/gomail.v2"
2016-07-15 19:36:39 +03:00
)
const (
2016-11-25 11:11:12 +03:00
mailAuthActivate base . TplName = "auth/activate"
mailAuthActivateEmail base . TplName = "auth/activate_email"
mailAuthResetPassword base . TplName = "auth/reset_passwd"
mailAuthRegisterNotify base . TplName = "auth/register_notify"
2016-07-15 19:36:39 +03:00
2016-11-25 11:11:12 +03:00
mailNotifyCollaborator base . TplName = "notify/collaborator"
2019-11-07 16:34:28 +03:00
2021-03-01 03:47:30 +03:00
mailRepoTransferNotify base . TplName = "notify/repo_transfer"
2019-11-07 16:34:28 +03:00
// There's no actual limit for subject in RFC 5322
mailMaxSubjectRunes = 256
2016-07-15 19:36:39 +03:00
)
2019-11-07 16:34:28 +03:00
var (
bodyTemplates * template . Template
subjectTemplates * texttmpl . Template
subjectRemoveSpaces = regexp . MustCompile ( ` [\s]+ ` )
)
2016-07-15 19:36:39 +03:00
2019-05-14 16:52:18 +03:00
// InitMailRender initializes the mail renderer
2019-11-07 16:34:28 +03:00
func InitMailRender ( subjectTpl * texttmpl . Template , bodyTpl * template . Template ) {
subjectTemplates = subjectTpl
bodyTemplates = bodyTpl
2016-07-15 19:36:39 +03:00
}
2016-11-25 11:11:12 +03:00
// SendTestMail sends a test mail
2016-07-15 19:36:39 +03:00
func SendTestMail ( email string ) error {
2020-01-16 20:55:36 +03:00
return gomail . Send ( Sender , NewMessage ( [ ] string { email } , "Gitea Test Email!" , "Gitea Test Email!" ) . ToMessage ( ) )
2016-07-15 19:36:39 +03:00
}
2021-04-02 13:25:13 +03:00
// sendUserMail sends a mail to the user
func sendUserMail ( language string , u * models . User , tpl base . TplName , code , subject , info string ) {
locale := translation . NewLocale ( language )
2016-07-15 19:36:39 +03:00
data := map [ string ] interface { } {
2019-04-04 10:52:48 +03:00
"DisplayName" : u . DisplayName ( ) ,
2019-08-15 17:46:21 +03:00
"ActiveCodeLives" : timeutil . MinutesToFriendly ( setting . Service . ActiveCodeLives , language ) ,
"ResetPwdCodeLives" : timeutil . MinutesToFriendly ( setting . Service . ResetPwdCodeLives , language ) ,
2016-07-15 19:36:39 +03:00
"Code" : code ,
2021-04-02 13:25:13 +03:00
"i18n" : locale ,
"Language" : locale . Language ( ) ,
2016-07-15 19:36:39 +03:00
}
2016-12-06 20:58:31 +03:00
var content bytes . Buffer
2021-04-02 13:25:13 +03:00
// TODO: i18n templates?
2019-11-07 16:34:28 +03:00
if err := bodyTemplates . ExecuteTemplate ( & content , string ( tpl ) , data ) ; err != nil {
2019-04-02 10:48:31 +03:00
log . Error ( "Template: %v" , err )
2016-07-15 19:36:39 +03:00
return
}
2019-09-24 08:02:49 +03:00
msg := NewMessage ( [ ] string { u . Email } , subject , content . String ( ) )
2016-07-23 20:08:22 +03:00
msg . Info = fmt . Sprintf ( "UID: %d, %s" , u . ID , info )
2016-07-15 19:36:39 +03:00
2019-09-24 08:02:49 +03:00
SendAsync ( msg )
2016-07-15 19:36:39 +03:00
}
2017-05-14 05:38:30 +03:00
// SendActivateAccountMail sends an activation mail to the user (new user registration)
2021-04-02 13:25:13 +03:00
func SendActivateAccountMail ( locale translation . Locale , u * models . User ) {
sendUserMail ( locale . Language ( ) , u , mailAuthActivate , u . GenerateEmailActivateCode ( u . Email ) , locale . Tr ( "mail.activate_account" ) , "activate account" )
2016-07-15 19:36:39 +03:00
}
2016-11-25 11:11:12 +03:00
// SendResetPasswordMail sends a password reset mail to the user
2021-04-02 13:25:13 +03:00
func SendResetPasswordMail ( u * models . User ) {
locale := translation . NewLocale ( u . Language )
sendUserMail ( u . Language , u , mailAuthResetPassword , u . GenerateEmailActivateCode ( u . Email ) , locale . Tr ( "mail.reset_password" ) , "recover account" )
2016-07-15 19:36:39 +03:00
}
2017-05-14 05:38:30 +03:00
// SendActivateEmailMail sends confirmation email to confirm new email address
2021-04-02 13:25:13 +03:00
func SendActivateEmailMail ( u * models . User , email * models . EmailAddress ) {
locale := translation . NewLocale ( u . Language )
2016-07-15 19:36:39 +03:00
data := map [ string ] interface { } {
2019-04-04 10:52:48 +03:00
"DisplayName" : u . DisplayName ( ) ,
2019-08-15 17:46:21 +03:00
"ActiveCodeLives" : timeutil . MinutesToFriendly ( setting . Service . ActiveCodeLives , locale . Language ( ) ) ,
2016-07-15 19:36:39 +03:00
"Code" : u . GenerateEmailActivateCode ( email . Email ) ,
"Email" : email . Email ,
2021-04-02 13:25:13 +03:00
"i18n" : locale ,
"Language" : locale . Language ( ) ,
2016-07-15 19:36:39 +03:00
}
2016-12-06 20:58:31 +03:00
var content bytes . Buffer
2021-04-02 13:25:13 +03:00
// TODO: i18n templates?
2019-11-07 16:34:28 +03:00
if err := bodyTemplates . ExecuteTemplate ( & content , string ( mailAuthActivateEmail ) , data ) ; err != nil {
2019-04-02 10:48:31 +03:00
log . Error ( "Template: %v" , err )
2016-07-15 19:36:39 +03:00
return
}
2019-09-24 08:02:49 +03:00
msg := NewMessage ( [ ] string { email . Email } , locale . Tr ( "mail.activate_email" ) , content . String ( ) )
2016-07-23 20:08:22 +03:00
msg . Info = fmt . Sprintf ( "UID: %d, activate email" , u . ID )
2016-07-15 19:36:39 +03:00
2019-09-24 08:02:49 +03:00
SendAsync ( msg )
2016-07-15 19:36:39 +03:00
}
// SendRegisterNotifyMail triggers a notify e-mail by admin created a account.
2021-04-02 13:25:13 +03:00
func SendRegisterNotifyMail ( u * models . User ) {
locale := translation . NewLocale ( u . Language )
2019-09-24 08:02:49 +03:00
2016-07-15 19:36:39 +03:00
data := map [ string ] interface { } {
2019-04-04 10:52:48 +03:00
"DisplayName" : u . DisplayName ( ) ,
"Username" : u . Name ,
2021-04-02 13:25:13 +03:00
"i18n" : locale ,
"Language" : locale . Language ( ) ,
2016-07-15 19:36:39 +03:00
}
2016-12-06 20:58:31 +03:00
var content bytes . Buffer
2021-04-02 13:25:13 +03:00
// TODO: i18n templates?
2019-11-07 16:34:28 +03:00
if err := bodyTemplates . ExecuteTemplate ( & content , string ( mailAuthRegisterNotify ) , data ) ; err != nil {
2019-04-02 10:48:31 +03:00
log . Error ( "Template: %v" , err )
2016-07-15 19:36:39 +03:00
return
}
2019-09-24 08:02:49 +03:00
msg := NewMessage ( [ ] string { u . Email } , locale . Tr ( "mail.register_notify" ) , content . String ( ) )
2016-07-23 20:08:22 +03:00
msg . Info = fmt . Sprintf ( "UID: %d, registration notify" , u . ID )
2016-07-15 19:36:39 +03:00
2019-09-24 08:02:49 +03:00
SendAsync ( msg )
2016-07-15 19:36:39 +03:00
}
// SendCollaboratorMail sends mail notification to new collaborator.
2019-09-24 08:02:49 +03:00
func SendCollaboratorMail ( u , doer * models . User , repo * models . Repository ) {
2021-04-02 13:25:13 +03:00
locale := translation . NewLocale ( u . Language )
2020-01-12 12:36:21 +03:00
repoName := repo . FullName ( )
2016-07-15 19:36:39 +03:00
2021-04-02 13:25:13 +03:00
subject := locale . Tr ( "mail.repo.collaborator.added.subject" , doer . DisplayName ( ) , repoName )
2016-07-15 19:36:39 +03:00
data := map [ string ] interface { } {
"Subject" : subject ,
"RepoName" : repoName ,
2016-08-16 20:19:09 +03:00
"Link" : repo . HTMLURL ( ) ,
2021-04-02 13:25:13 +03:00
"i18n" : locale ,
"Language" : locale . Language ( ) ,
2016-07-15 19:36:39 +03:00
}
2016-12-06 20:58:31 +03:00
var content bytes . Buffer
2021-04-02 13:25:13 +03:00
// TODO: i18n templates?
2019-11-07 16:34:28 +03:00
if err := bodyTemplates . ExecuteTemplate ( & content , string ( mailNotifyCollaborator ) , data ) ; err != nil {
2019-04-02 10:48:31 +03:00
log . Error ( "Template: %v" , err )
2016-07-15 19:36:39 +03:00
return
}
2019-09-24 08:02:49 +03:00
msg := NewMessage ( [ ] string { u . Email } , subject , content . String ( ) )
2016-07-23 20:08:22 +03:00
msg . Info = fmt . Sprintf ( "UID: %d, add collaborator" , u . ID )
2016-07-15 19:36:39 +03:00
2019-09-24 08:02:49 +03:00
SendAsync ( msg )
2016-07-15 19:36:39 +03:00
}
2021-05-22 09:47:16 +03:00
func composeIssueCommentMessages ( ctx * mailCommentContext , lang string , recipients [ ] * models . User , fromMention bool , info string ) ( [ ] * Message , error ) {
2019-11-07 16:34:28 +03:00
var (
subject string
link string
prefix string
// Fall back subject for bad templates, make sure subject is never empty
2019-11-15 15:59:21 +03:00
fallback string
reviewComments [ ] * models . Comment
2019-11-07 16:34:28 +03:00
)
2016-07-15 19:36:39 +03:00
2019-11-07 16:34:28 +03:00
commentType := models . CommentTypeComment
2019-11-18 11:08:20 +03:00
if ctx . Comment != nil {
commentType = ctx . Comment . Type
link = ctx . Issue . HTMLURL ( ) + "#" + ctx . Comment . HashTag ( )
2019-07-17 22:02:42 +03:00
} else {
2019-11-18 11:08:20 +03:00
link = ctx . Issue . HTMLURL ( )
2019-06-12 22:41:28 +03:00
}
2019-11-07 16:34:28 +03:00
2019-11-15 15:59:21 +03:00
reviewType := models . ReviewTypeComment
2019-11-18 11:08:20 +03:00
if ctx . Comment != nil && ctx . Comment . Review != nil {
reviewType = ctx . Comment . Review . Type
2019-11-15 15:59:21 +03:00
}
2019-11-07 16:34:28 +03:00
// This is the body of the new issue or comment, not the mail body
2021-04-20 01:25:08 +03:00
body , err := markdown . RenderString ( & markup . RenderContext {
URLPrefix : ctx . Issue . Repo . HTMLURL ( ) ,
Metas : ctx . Issue . Repo . ComposeMetas ( ) ,
} , ctx . Content )
if err != nil {
return nil , err
}
2019-11-18 11:08:20 +03:00
actType , actName , tplName := actionToTemplate ( ctx . Issue , ctx . ActionType , commentType , reviewType )
2019-11-15 15:59:21 +03:00
2020-01-03 20:13:22 +03:00
if actName != "new" {
prefix = "Re: "
}
fallback = prefix + fallbackMailSubject ( ctx . Issue )
2019-11-18 11:08:20 +03:00
if ctx . Comment != nil && ctx . Comment . Review != nil {
2019-11-15 15:59:21 +03:00
reviewComments = make ( [ ] * models . Comment , 0 , 10 )
2019-11-18 11:08:20 +03:00
for _ , lines := range ctx . Comment . Review . CodeComments {
2019-11-15 15:59:21 +03:00
for _ , comments := range lines {
reviewComments = append ( reviewComments , comments ... )
}
}
}
2021-04-02 13:25:13 +03:00
locale := translation . NewLocale ( lang )
2019-11-07 16:34:28 +03:00
mailMeta := map [ string ] interface { } {
"FallbackSubject" : fallback ,
"Body" : body ,
"Link" : link ,
2019-11-18 11:08:20 +03:00
"Issue" : ctx . Issue ,
"Comment" : ctx . Comment ,
"IsPull" : ctx . Issue . IsPull ,
"User" : ctx . Issue . Repo . MustOwner ( ) ,
"Repo" : ctx . Issue . Repo . FullName ( ) ,
"Doer" : ctx . Doer ,
2019-11-07 16:34:28 +03:00
"IsMention" : fromMention ,
"SubjectPrefix" : prefix ,
"ActionType" : actType ,
"ActionName" : actName ,
2019-11-15 15:59:21 +03:00
"ReviewComments" : reviewComments ,
2021-04-02 13:25:13 +03:00
"i18n" : locale ,
"Language" : locale . Language ( ) ,
2019-11-07 16:34:28 +03:00
}
var mailSubject bytes . Buffer
2021-04-02 13:25:13 +03:00
// TODO: i18n templates?
2019-11-07 16:34:28 +03:00
if err := subjectTemplates . ExecuteTemplate ( & mailSubject , string ( tplName ) , mailMeta ) ; err == nil {
subject = sanitizeSubject ( mailSubject . String ( ) )
2021-04-20 01:25:08 +03:00
if subject == "" {
subject = fallback
}
2017-05-25 05:38:56 +03:00
} else {
2021-04-02 13:25:13 +03:00
log . Error ( "ExecuteTemplate [%s]: %v" , tplName + "/subject" , err )
2019-11-07 16:34:28 +03:00
}
2020-04-28 21:05:39 +03:00
subject = emoji . ReplaceAliases ( subject )
2019-11-07 16:34:28 +03:00
mailMeta [ "Subject" ] = subject
2016-12-06 20:58:31 +03:00
2017-11-03 12:23:17 +03:00
var mailBody bytes . Buffer
2016-12-06 20:58:31 +03:00
2021-04-02 13:25:13 +03:00
// TODO: i18n templates?
2019-11-07 16:34:28 +03:00
if err := bodyTemplates . ExecuteTemplate ( & mailBody , string ( tplName ) , mailMeta ) ; err != nil {
log . Error ( "ExecuteTemplate [%s]: %v" , string ( tplName ) + "/body" , err )
2016-07-15 19:36:39 +03:00
}
2016-12-06 20:58:31 +03:00
2019-11-18 11:08:20 +03:00
// Make sure to compose independent messages to avoid leaking user emails
2021-05-22 09:47:16 +03:00
msgs := make ( [ ] * Message , 0 , len ( recipients ) )
for _ , recipient := range recipients {
msg := NewMessageFrom ( [ ] string { recipient . Email } , ctx . Doer . DisplayName ( ) , setting . MailService . FromEmail , subject , mailBody . String ( ) )
2019-11-18 11:08:20 +03:00
msg . Info = fmt . Sprintf ( "Subject: %s, %s" , subject , info )
// Set Message-ID on first message so replies know what to reference
2020-01-03 20:13:22 +03:00
if actName == "new" {
2019-11-18 11:08:20 +03:00
msg . SetHeader ( "Message-ID" , "<" + ctx . Issue . ReplyReference ( ) + ">" )
} else {
msg . SetHeader ( "In-Reply-To" , "<" + ctx . Issue . ReplyReference ( ) + ">" )
msg . SetHeader ( "References" , "<" + ctx . Issue . ReplyReference ( ) + ">" )
}
2021-05-22 09:47:16 +03:00
for key , value := range generateAdditionalHeaders ( ctx , actType , recipient ) {
msg . SetHeader ( key , value )
}
2019-11-18 11:08:20 +03:00
msgs = append ( msgs , msg )
2019-07-17 22:02:42 +03:00
}
2021-04-20 01:25:08 +03:00
return msgs , nil
2016-07-15 19:36:39 +03:00
}
2021-05-22 09:47:16 +03:00
func generateAdditionalHeaders ( ctx * mailCommentContext , reason string , recipient * models . User ) map [ string ] string {
repo := ctx . Issue . Repo
return map [ string ] string {
// https://datatracker.ietf.org/doc/html/rfc2919
"List-ID" : fmt . Sprintf ( "%s <%s.%s.%s>" , repo . FullName ( ) , repo . Name , repo . OwnerName , setting . Domain ) ,
// https://datatracker.ietf.org/doc/html/rfc2369
"List-Archive" : fmt . Sprintf ( "<%s>" , repo . HTMLURL ( ) ) ,
//"List-Post": https://github.com/go-gitea/gitea/pull/13585
//"List-Unsubscribe": https://github.com/go-gitea/gitea/issues/10808, https://github.com/go-gitea/gitea/issues/13283
"X-Gitea-Reason" : reason ,
"X-Gitea-Sender" : ctx . Doer . DisplayName ( ) ,
"X-Gitea-Recipient" : recipient . DisplayName ( ) ,
"X-Gitea-Recipient-Address" : recipient . Email ,
"X-Gitea-Repository" : repo . Name ,
"X-Gitea-Repository-Path" : repo . FullName ( ) ,
"X-Gitea-Repository-Link" : repo . HTMLURL ( ) ,
"X-Gitea-Issue-ID" : strconv . FormatInt ( ctx . Issue . Index , 10 ) ,
"X-Gitea-Issue-Link" : ctx . Issue . HTMLURL ( ) ,
"X-GitHub-Reason" : reason ,
"X-GitHub-Sender" : ctx . Doer . DisplayName ( ) ,
"X-GitHub-Recipient" : recipient . DisplayName ( ) ,
"X-GitHub-Recipient-Address" : recipient . Email ,
"X-GitLab-NotificationReason" : reason ,
"X-GitLab-Project" : repo . Name ,
"X-GitLab-Project-Path" : repo . FullName ( ) ,
"X-GitLab-Issue-IID" : strconv . FormatInt ( ctx . Issue . Index , 10 ) ,
}
}
2019-11-07 16:34:28 +03:00
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 ) )
}
2019-11-18 11:08:20 +03:00
// SendIssueAssignedMail composes and sends issue assigned email
2021-04-20 01:25:08 +03:00
func SendIssueAssignedMail ( issue * models . Issue , doer * models . User , content string , comment * models . Comment , recipients [ ] * models . User ) error {
2021-05-22 09:47:16 +03:00
langMap := make ( map [ string ] [ ] * models . User )
2021-04-02 13:25:13 +03:00
for _ , user := range recipients {
2021-05-22 09:47:16 +03:00
langMap [ user . Language ] = append ( langMap [ user . Language ] , user )
2021-04-02 13:25:13 +03:00
}
for lang , tos := range langMap {
2021-04-20 01:25:08 +03:00
msgs , err := composeIssueCommentMessages ( & mailCommentContext {
2021-04-02 13:25:13 +03:00
Issue : issue ,
Doer : doer ,
ActionType : models . ActionType ( 0 ) ,
Content : content ,
Comment : comment ,
2021-04-20 01:25:08 +03:00
} , lang , tos , false , "issue assigned" )
if err != nil {
return err
}
SendAsyncs ( msgs )
2021-04-02 13:25:13 +03:00
}
2021-04-20 01:25:08 +03:00
return nil
2019-11-07 16:34:28 +03:00
}
// 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)
2019-11-15 15:59:21 +03:00
func actionToTemplate ( issue * models . Issue , actionType models . ActionType ,
commentType models . CommentType , reviewType models . ReviewType ) ( typeName , name , template string ) {
2019-11-07 16:34:28 +03:00
if issue . IsPull {
typeName = "pull"
} else {
typeName = "issue"
}
switch actionType {
case models . ActionCreateIssue , models . ActionCreatePullRequest :
name = "new"
2019-12-22 11:29:26 +03:00
case models . ActionCommentIssue , models . ActionCommentPull :
2019-11-07 16:34:28 +03:00
name = "comment"
case models . ActionCloseIssue , models . ActionClosePullRequest :
name = "close"
case models . ActionReopenIssue , models . ActionReopenPullRequest :
name = "reopen"
case models . ActionMergePullRequest :
name = "merge"
2021-02-11 20:32:25 +03:00
case models . ActionPullReviewDismissed :
name = "review_dismissed"
2019-11-07 16:34:28 +03:00
default :
switch commentType {
case models . CommentTypeReview :
2019-11-15 15:59:21 +03:00
switch reviewType {
case models . ReviewTypeApprove :
name = "approve"
case models . ReviewTypeReject :
name = "reject"
default :
name = "review"
}
2019-11-07 16:34:28 +03:00
case models . CommentTypeCode :
name = "code"
case models . CommentTypeAssignees :
name = "assigned"
2020-05-20 15:47:24 +03:00
case models . CommentTypePullPush :
name = "push"
2019-11-07 16:34:28 +03:00
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
2016-07-15 19:36:39 +03:00
}