summaryrefslogtreecommitdiffstats
path: root/services
diff options
context:
space:
mode:
authorguillep2k <18600385+guillep2k@users.noreply.github.com>2019-11-18 05:08:20 -0300
committerzeripath <art27@cantab.net>2019-11-18 08:08:20 +0000
commit08ae6bb7edb9582c38edb8a0dba1b1be10fb00fc (patch)
treec64e9a1c9cfaeb6cd0c2ee012d3ad32c38e78ce3 /services
parent9ff63126274b0df6e035541eafd48970c402e61e (diff)
downloadgitea-08ae6bb7edb9582c38edb8a0dba1b1be10fb00fc.tar.gz
gitea-08ae6bb7edb9582c38edb8a0dba1b1be10fb00fc.zip
Rewrite delivery of issue and comment mails (#9009)
* Mail issue subscribers, rework the function * Simplify a little more * Fix unused variable * Refactor mail delivery to avoid heavy load on server * Avoid splitting into too many goroutines * Fix comments and optimize GetMaileableUsersByIDs() * Fix return on errors
Diffstat (limited to 'services')
-rw-r--r--services/mailer/mail.go96
-rw-r--r--services/mailer/mail_comment.go13
-rw-r--r--services/mailer/mail_issue.go162
-rw-r--r--services/mailer/mail_test.go49
-rw-r--r--services/mailer/mailer.go11
5 files changed, 187 insertions, 144 deletions
diff --git a/services/mailer/mail.go b/services/mailer/mail.go
index 27767be688..7d26487a07 100644
--- a/services/mailer/mail.go
+++ b/services/mailer/mail.go
@@ -164,13 +164,7 @@ func SendCollaboratorMail(u, doer *models.User, repo *models.Repository) {
SendAsync(msg)
}
-func composeIssueCommentMessage(issue *models.Issue, doer *models.User, actionType models.ActionType, fromMention bool,
- content string, comment *models.Comment, tos []string, info string) *Message {
-
- if err := issue.LoadPullRequest(); err != nil {
- log.Error("LoadPullRequest: %v", err)
- return nil
- }
+func composeIssueCommentMessages(ctx *mailCommentContext, tos []string, fromMention bool, info string) []*Message {
var (
subject string
@@ -182,29 +176,29 @@ func composeIssueCommentMessage(issue *models.Issue, doer *models.User, actionTy
)
commentType := models.CommentTypeComment
- if comment != nil {
+ if ctx.Comment != nil {
prefix = "Re: "
- commentType = comment.Type
- link = issue.HTMLURL() + "#" + comment.HashTag()
+ commentType = ctx.Comment.Type
+ link = ctx.Issue.HTMLURL() + "#" + ctx.Comment.HashTag()
} else {
- link = issue.HTMLURL()
+ link = ctx.Issue.HTMLURL()
}
reviewType := models.ReviewTypeComment
- if comment != nil && comment.Review != nil {
- reviewType = comment.Review.Type
+ if ctx.Comment != nil && ctx.Comment.Review != nil {
+ reviewType = ctx.Comment.Review.Type
}
- fallback = prefix + fallbackMailSubject(issue)
+ fallback = prefix + fallbackMailSubject(ctx.Issue)
// This is the body of the new issue or comment, not the mail body
- body := string(markup.RenderByType(markdown.MarkupName, []byte(content), issue.Repo.HTMLURL(), issue.Repo.ComposeMetas()))
+ body := string(markup.RenderByType(markdown.MarkupName, []byte(ctx.Content), ctx.Issue.Repo.HTMLURL(), ctx.Issue.Repo.ComposeMetas()))
- actType, actName, tplName := actionToTemplate(issue, actionType, commentType, reviewType)
+ actType, actName, tplName := actionToTemplate(ctx.Issue, ctx.ActionType, commentType, reviewType)
- if comment != nil && comment.Review != nil {
+ if ctx.Comment != nil && ctx.Comment.Review != nil {
reviewComments = make([]*models.Comment, 0, 10)
- for _, lines := range comment.Review.CodeComments {
+ for _, lines := range ctx.Comment.Review.CodeComments {
for _, comments := range lines {
reviewComments = append(reviewComments, comments...)
}
@@ -215,12 +209,12 @@ func composeIssueCommentMessage(issue *models.Issue, doer *models.User, actionTy
"FallbackSubject": fallback,
"Body": body,
"Link": link,
- "Issue": issue,
- "Comment": comment,
- "IsPull": issue.IsPull,
- "User": issue.Repo.MustOwner(),
- "Repo": issue.Repo.FullName(),
- "Doer": doer,
+ "Issue": ctx.Issue,
+ "Comment": ctx.Comment,
+ "IsPull": ctx.Issue.IsPull,
+ "User": ctx.Issue.Repo.MustOwner(),
+ "Repo": ctx.Issue.Repo.FullName(),
+ "Doer": ctx.Doer,
"IsMention": fromMention,
"SubjectPrefix": prefix,
"ActionType": actType,
@@ -246,18 +240,23 @@ func composeIssueCommentMessage(issue *models.Issue, doer *models.User, actionTy
log.Error("ExecuteTemplate [%s]: %v", string(tplName)+"/body", 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()+">")
+ // Make sure to compose independent messages to avoid leaking user emails
+ msgs := make([]*Message, 0, len(tos))
+ for _, to := range tos {
+ msg := NewMessageFrom([]string{to}, ctx.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 ctx.Comment == nil {
+ msg.SetHeader("Message-ID", "<"+ctx.Issue.ReplyReference()+">")
+ } else {
+ msg.SetHeader("In-Reply-To", "<"+ctx.Issue.ReplyReference()+">")
+ msg.SetHeader("References", "<"+ctx.Issue.ReplyReference()+">")
+ }
+ msgs = append(msgs, msg)
}
- return msg
+ return msgs
}
func sanitizeSubject(subject string) string {
@@ -269,21 +268,15 @@ func sanitizeSubject(subject string) string {
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, actionType models.ActionType, content string, comment *models.Comment, tos []string) {
- if len(tos) == 0 {
- return
- }
-
- 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, actionType models.ActionType, content string, comment *models.Comment, tos []string) {
- if len(tos) == 0 {
- return
- }
- SendAsync(composeIssueCommentMessage(issue, doer, actionType, true, content, comment, tos, "issue mention"))
+// SendIssueAssignedMail composes and sends issue assigned email
+func SendIssueAssignedMail(issue *models.Issue, doer *models.User, content string, comment *models.Comment, tos []string) {
+ SendAsyncs(composeIssueCommentMessages(&mailCommentContext{
+ Issue: issue,
+ Doer: doer,
+ ActionType: models.ActionType(0),
+ Content: content,
+ Comment: comment,
+ }, tos, false, "issue assigned"))
}
// actionToTemplate returns the type and name of the action facing the user
@@ -341,8 +334,3 @@ func actionToTemplate(issue *models.Issue, actionType models.ActionType,
}
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, models.ActionType(0), false, content, comment, tos, "issue assigned"))
-}
diff --git a/services/mailer/mail_comment.go b/services/mailer/mail_comment.go
index 6469eb1fa1..9edbfabd48 100644
--- a/services/mailer/mail_comment.go
+++ b/services/mailer/mail_comment.go
@@ -27,11 +27,18 @@ func mailParticipantsComment(ctx models.DBContext, c *models.Comment, opType mod
if err = models.UpdateIssueMentions(ctx, c.IssueID, userMentions); err != nil {
return fmt.Errorf("UpdateIssueMentions [%d]: %v", c.IssueID, err)
}
- mentions := make([]string, len(userMentions))
+ mentions := make([]int64, len(userMentions))
for i, u := range userMentions {
- mentions[i] = u.LowerName
+ mentions[i] = u.ID
}
- if err = mailIssueCommentToParticipants(issue, c.Poster, opType, c.Content, c, mentions); err != nil {
+ if err = mailIssueCommentToParticipants(
+ &mailCommentContext{
+ Issue: issue,
+ Doer: c.Poster,
+ ActionType: opType,
+ Content: c.Content,
+ Comment: 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 32b21b1324..696adfadda 100644
--- a/services/mailer/mail_issue.go
+++ b/services/mailer/mail_issue.go
@@ -10,105 +10,118 @@ import (
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/references"
-
- "github.com/unknwon/com"
)
func fallbackMailSubject(issue *models.Issue) string {
return fmt.Sprintf("[%s] %s (#%d)", issue.Repo.FullName(), issue.Title, issue.Index)
}
+type mailCommentContext struct {
+ Issue *models.Issue
+ Doer *models.User
+ ActionType models.ActionType
+ Content string
+ Comment *models.Comment
+}
+
// 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, actionType models.ActionType, content string, comment *models.Comment, mentions []string) error {
+func mailIssueCommentToParticipants(ctx *mailCommentContext, mentions []int64) error {
- watchers, err := models.GetWatchers(issue.RepoID)
- if err != nil {
- return fmt.Errorf("getWatchers [repo_id: %d]: %v", issue.RepoID, err)
+ // Required by the mail composer; make sure to load these before calling the async function
+ if err := ctx.Issue.LoadRepo(); err != nil {
+ return fmt.Errorf("LoadRepo(): %v", err)
}
- participants, err := models.GetParticipantsByIssueID(issue.ID)
- if err != nil {
- return fmt.Errorf("getParticipantsByIssueID [issue_id: %d]: %v", issue.ID, err)
+ if err := ctx.Issue.LoadPoster(); err != nil {
+ return fmt.Errorf("LoadPoster(): %v", err)
+ }
+ if err := ctx.Issue.LoadPullRequest(); err != nil {
+ return fmt.Errorf("LoadPullRequest(): %v", 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()
+ // Enough room to avoid reallocations
+ unfiltered := make([]int64, 1, 64)
+
+ // =========== Original poster ===========
+ unfiltered[0] = ctx.Issue.PosterID
+
+ // =========== Assignees ===========
+ ids, err := models.GetAssigneeIDsByIssue(ctx.Issue.ID)
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)
+ return fmt.Errorf("GetAssigneeIDsByIssue(%d): %v", ctx.Issue.ID, err)
}
+ unfiltered = append(unfiltered, ids...)
- // Assignees must receive any communications
- assignees, err := models.GetAssigneesByIssue(issue)
+ // =========== Participants (i.e. commenters, reviewers) ===========
+ ids, err = models.GetParticipantsIDsByIssueID(ctx.Issue.ID)
if err != nil {
- return err
+ return fmt.Errorf("GetParticipantsIDsByIssueID(%d): %v", ctx.Issue.ID, err)
}
+ unfiltered = append(unfiltered, ids...)
- for _, assignee := range assignees {
- if assignee.ID != doer.ID {
- participants = append(participants, assignee)
- }
+ // =========== Issue watchers ===========
+ ids, err = models.GetIssueWatchersIDs(ctx.Issue.ID)
+ if err != nil {
+ return fmt.Errorf("GetIssueWatchersIDs(%d): %v", ctx.Issue.ID, err)
}
+ unfiltered = append(unfiltered, ids...)
- 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)
+ // =========== Repo watchers ===========
+ // Make repo watchers last, since it's likely the list with the most users
+ ids, err = models.GetRepoWatchersIDs(ctx.Issue.RepoID)
+ if err != nil {
+ return fmt.Errorf("GetRepoWatchersIDs(%d): %v", ctx.Issue.RepoID, err)
}
- for i := range participants {
- if participants[i].ID == doer.ID ||
- com.IsSliceContainsStr(names, participants[i].Name) ||
- participants[i].EmailNotifications() != models.EmailNotificationsEnabled {
- continue
- }
+ unfiltered = append(ids, unfiltered...)
- tos = append(tos, participants[i].Email)
- names = append(names, participants[i].Name)
- }
+ visited := make(map[int64]bool, len(unfiltered)+len(mentions)+1)
- if err := issue.LoadRepo(); err != nil {
- return err
- }
+ // Avoid mailing the doer
+ visited[ctx.Doer.ID] = true
- for _, to := range tos {
- SendIssueCommentMail(issue, doer, actionType, content, comment, []string{to})
+ if err = mailIssueCommentBatch(ctx, unfiltered, visited, false); err != nil {
+ return fmt.Errorf("mailIssueCommentBatch(): %v", err)
}
- // 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])
+ // =========== Mentions ===========
+ if err = mailIssueCommentBatch(ctx, mentions, visited, true); err != nil {
+ return fmt.Errorf("mailIssueCommentBatch() mentions: %v", err)
}
- emails := models.GetUserEmailsByNames(tos)
+ return nil
+}
- for _, to := range emails {
- SendIssueMentionMail(issue, doer, actionType, content, comment, []string{to})
+func mailIssueCommentBatch(ctx *mailCommentContext, ids []int64, visited map[int64]bool, fromMention bool) error {
+ const batchSize = 100
+ for i := 0; i < len(ids); i += batchSize {
+ var last int
+ if i+batchSize < len(ids) {
+ last = i + batchSize
+ } else {
+ last = len(ids)
+ }
+ unique := make([]int64, 0, last-i)
+ for j := i; j < last; j++ {
+ id := ids[j]
+ if _, ok := visited[id]; !ok {
+ unique = append(unique, id)
+ visited[id] = true
+ }
+ }
+ recipients, err := models.GetMaileableUsersByIDs(unique)
+ if err != nil {
+ return err
+ }
+ // TODO: Check issue visibility for each user
+ // TODO: Separate recipients by language for i18n mail templates
+ tos := make([]string, len(recipients))
+ for i := range recipients {
+ tos[i] = recipients[i].Email
+ }
+ SendAsyncs(composeIssueCommentMessages(ctx, tos, fromMention, "issue comments"))
}
-
return nil
}
@@ -127,11 +140,18 @@ func mailParticipants(ctx models.DBContext, issue *models.Issue, doer *models.Us
if err = models.UpdateIssueMentions(ctx, issue.ID, userMentions); err != nil {
return fmt.Errorf("UpdateIssueMentions [%d]: %v", issue.ID, err)
}
- mentions := make([]string, len(userMentions))
+ mentions := make([]int64, len(userMentions))
for i, u := range userMentions {
- mentions[i] = u.LowerName
- }
- if err = mailIssueCommentToParticipants(issue, doer, opType, issue.Content, nil, mentions); err != nil {
+ mentions[i] = u.ID
+ }
+ if err = mailIssueCommentToParticipants(
+ &mailCommentContext{
+ Issue: issue,
+ Doer: doer,
+ ActionType: opType,
+ Content: issue.Content,
+ Comment: 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 a10507e0e4..fd87a157b2 100644
--- a/services/mailer/mail_test.go
+++ b/services/mailer/mail_test.go
@@ -58,12 +58,16 @@ func TestComposeIssueCommentMessage(t *testing.T) {
InitMailRender(stpl, btpl)
tos := []string{"test@gitea.com", "test2@gitea.com"}
- msg := composeIssueCommentMessage(issue, doer, models.ActionCommentIssue, false, "test body", comment, tos, "issue comment")
+ msgs := composeIssueCommentMessages(&mailCommentContext{Issue: issue, Doer: doer, ActionType: models.ActionCommentIssue,
+ Content: "test body", Comment: comment}, tos, false, "issue comment")
+ assert.Len(t, msgs, 2)
- subject := msg.GetHeader("Subject")
- inreplyTo := msg.GetHeader("In-Reply-To")
- references := msg.GetHeader("References")
+ mailto := msgs[0].GetHeader("To")
+ subject := msgs[0].GetHeader("Subject")
+ inreplyTo := msgs[0].GetHeader("In-Reply-To")
+ references := msgs[0].GetHeader("References")
+ assert.Len(t, mailto, 1, "exactly one recipient is expected in the To field")
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")
@@ -88,14 +92,18 @@ func TestComposeIssueMessage(t *testing.T) {
InitMailRender(stpl, btpl)
tos := []string{"test@gitea.com", "test2@gitea.com"}
- msg := composeIssueCommentMessage(issue, doer, models.ActionCreateIssue, false, "test body", nil, tos, "issue create")
+ msgs := composeIssueCommentMessages(&mailCommentContext{Issue: issue, Doer: doer, ActionType: models.ActionCreateIssue,
+ Content: "test body"}, tos, false, "issue create")
+ assert.Len(t, msgs, 2)
- subject := msg.GetHeader("Subject")
- messageID := msg.GetHeader("Message-ID")
+ mailto := msgs[0].GetHeader("To")
+ subject := msgs[0].GetHeader("Subject")
+ messageID := msgs[0].GetHeader("Message-ID")
+ assert.Len(t, mailto, 1, "exactly one recipient is expected in the To field")
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.Nil(t, msgs[0].GetHeader("In-Reply-To"))
+ assert.Nil(t, msgs[0].GetHeader("References"))
assert.Equal(t, messageID[0], "<user2/repo1/issues/1@localhost>", "Message-ID header doesn't match")
}
@@ -134,20 +142,24 @@ func TestTemplateSelection(t *testing.T) {
assert.Contains(t, wholemsg, expBody)
}
- msg := composeIssueCommentMessage(issue, doer, models.ActionCreateIssue, false, "test body", nil, tos, "TestTemplateSelection")
+ msg := testComposeIssueCommentMessage(t, &mailCommentContext{Issue: issue, Doer: doer, ActionType: models.ActionCreateIssue,
+ Content: "test body"}, tos, false, "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")
+ msg = testComposeIssueCommentMessage(t, &mailCommentContext{Issue: issue, Doer: doer, ActionType: models.ActionCommentIssue,
+ Content: "test body", Comment: comment}, tos, false, "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")
+ msg = testComposeIssueCommentMessage(t, &mailCommentContext{Issue: pull, Doer: doer, ActionType: models.ActionCommentIssue,
+ Content: "test body", Comment: comment}, tos, false, "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")
+ msg = testComposeIssueCommentMessage(t, &mailCommentContext{Issue: issue, Doer: doer, ActionType: models.ActionCloseIssue,
+ Content: "test body", Comment: comment}, tos, false, "TestTemplateSelection")
+ expect(t, msg, "Re: [user2/repo1] issue1 (#1)", "issue/close/body")
}
func TestTemplateServices(t *testing.T) {
@@ -173,7 +185,8 @@ func TestTemplateServices(t *testing.T) {
InitMailRender(stpl, btpl)
tos := []string{"test@gitea.com"}
- msg := composeIssueCommentMessage(issue, doer, actionType, fromMention, "test body", comment, tos, "TestTemplateServices")
+ msg := testComposeIssueCommentMessage(t, &mailCommentContext{Issue: issue, Doer: doer, ActionType: actionType,
+ Content: "test body", Comment: comment}, tos, fromMention, "TestTemplateServices")
subject := msg.GetHeader("Subject")
msgbuf := new(bytes.Buffer)
@@ -202,3 +215,9 @@ func TestTemplateServices(t *testing.T) {
"Re: [user2/repo1] issue1 (#1)",
"//Re: //")
}
+
+func testComposeIssueCommentMessage(t *testing.T, ctx *mailCommentContext, tos []string, fromMention bool, info string) *Message {
+ msgs := composeIssueCommentMessages(ctx, tos, fromMention, info)
+ assert.Len(t, msgs, 1)
+ return msgs[0]
+}
diff --git a/services/mailer/mailer.go b/services/mailer/mailer.go
index d19ae7b2f4..2e4aa8d71b 100644
--- a/services/mailer/mailer.go
+++ b/services/mailer/mailer.go
@@ -295,9 +295,18 @@ func NewContext() {
go processMailQueue()
}
-// SendAsync send mail asynchronous
+// SendAsync send mail asynchronously
func SendAsync(msg *Message) {
go func() {
mailQueue <- msg
}()
}
+
+// SendAsyncs send mails asynchronously
+func SendAsyncs(msgs []*Message) {
+ go func() {
+ for _, msg := range msgs {
+ mailQueue <- msg
+ }
+ }()
+}