* Move mailer to use a queue * Make sectionMap map[string]bool * Ensure that Message is json encodabletags/v1.21.12.1
| @@ -103,11 +103,11 @@ func NewQueueService() { | |||
| // Now handle the old issue_indexer configuration | |||
| section := Cfg.Section("queue.issue_indexer") | |||
| issueIndexerSectionMap := map[string]string{} | |||
| sectionMap := map[string]bool{} | |||
| for _, key := range section.Keys() { | |||
| issueIndexerSectionMap[key.Name()] = key.Value() | |||
| sectionMap[key.Name()] = true | |||
| } | |||
| if _, ok := issueIndexerSectionMap["TYPE"]; !ok { | |||
| if _, ok := sectionMap["TYPE"]; !ok { | |||
| switch Indexer.IssueQueueType { | |||
| case LevelQueueType: | |||
| section.Key("TYPE").SetValue("level") | |||
| @@ -120,18 +120,28 @@ func NewQueueService() { | |||
| Indexer.IssueQueueType) | |||
| } | |||
| } | |||
| if _, ok := issueIndexerSectionMap["LENGTH"]; !ok { | |||
| if _, ok := sectionMap["LENGTH"]; !ok { | |||
| section.Key("LENGTH").SetValue(fmt.Sprintf("%d", Indexer.UpdateQueueLength)) | |||
| } | |||
| if _, ok := issueIndexerSectionMap["BATCH_LENGTH"]; !ok { | |||
| if _, ok := sectionMap["BATCH_LENGTH"]; !ok { | |||
| section.Key("BATCH_LENGTH").SetValue(fmt.Sprintf("%d", Indexer.IssueQueueBatchNumber)) | |||
| } | |||
| if _, ok := issueIndexerSectionMap["DATADIR"]; !ok { | |||
| if _, ok := sectionMap["DATADIR"]; !ok { | |||
| section.Key("DATADIR").SetValue(Indexer.IssueQueueDir) | |||
| } | |||
| if _, ok := issueIndexerSectionMap["CONN_STR"]; !ok { | |||
| if _, ok := sectionMap["CONN_STR"]; !ok { | |||
| section.Key("CONN_STR").SetValue(Indexer.IssueQueueConnStr) | |||
| } | |||
| // Handle the old mailer configuration | |||
| section = Cfg.Section("queue.mailer") | |||
| sectionMap = map[string]bool{} | |||
| for _, key := range section.Keys() { | |||
| sectionMap[key.Name()] = true | |||
| } | |||
| if _, ok := sectionMap["LENGTH"]; !ok { | |||
| section.Key("LENGTH").SetValue(fmt.Sprintf("%d", Cfg.Section("mailer").Key("SEND_BUFFER_LEN").MustInt(100))) | |||
| } | |||
| } | |||
| // ParseQueueConnStr parses a queue connection string | |||
| @@ -51,7 +51,7 @@ func InitMailRender(subjectTpl *texttmpl.Template, bodyTpl *template.Template) { | |||
| // SendTestMail sends a test mail | |||
| func SendTestMail(email string) error { | |||
| return gomail.Send(Sender, NewMessage([]string{email}, "Gitea Test Email!", "Gitea Test Email!").Message) | |||
| return gomail.Send(Sender, NewMessage([]string{email}, "Gitea Test Email!", "Gitea Test Email!").ToMessage()) | |||
| } | |||
| // SendUserMail sends a mail to the user | |||
| @@ -61,11 +61,11 @@ func TestComposeIssueCommentMessage(t *testing.T) { | |||
| msgs := composeIssueCommentMessages(&mailCommentContext{Issue: issue, Doer: doer, ActionType: models.ActionCommentIssue, | |||
| Content: "test body", Comment: comment}, tos, false, "issue comment") | |||
| assert.Len(t, msgs, 2) | |||
| mailto := msgs[0].GetHeader("To") | |||
| subject := msgs[0].GetHeader("Subject") | |||
| inreplyTo := msgs[0].GetHeader("In-Reply-To") | |||
| references := msgs[0].GetHeader("References") | |||
| gomailMsg := msgs[0].ToMessage() | |||
| mailto := gomailMsg.GetHeader("To") | |||
| subject := gomailMsg.GetHeader("Subject") | |||
| inreplyTo := gomailMsg.GetHeader("In-Reply-To") | |||
| references := gomailMsg.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:") | |||
| @@ -96,14 +96,15 @@ func TestComposeIssueMessage(t *testing.T) { | |||
| Content: "test body"}, tos, false, "issue create") | |||
| assert.Len(t, msgs, 2) | |||
| mailto := msgs[0].GetHeader("To") | |||
| subject := msgs[0].GetHeader("Subject") | |||
| messageID := msgs[0].GetHeader("Message-ID") | |||
| gomailMsg := msgs[0].ToMessage() | |||
| mailto := gomailMsg.GetHeader("To") | |||
| subject := gomailMsg.GetHeader("Subject") | |||
| messageID := gomailMsg.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, msgs[0].GetHeader("In-Reply-To")) | |||
| assert.Nil(t, msgs[0].GetHeader("References")) | |||
| assert.Nil(t, gomailMsg.GetHeader("In-Reply-To")) | |||
| assert.Nil(t, gomailMsg.GetHeader("References")) | |||
| assert.Equal(t, messageID[0], "<user2/repo1/issues/1@localhost>", "Message-ID header doesn't match") | |||
| } | |||
| @@ -134,9 +135,9 @@ func TestTemplateSelection(t *testing.T) { | |||
| InitMailRender(stpl, btpl) | |||
| expect := func(t *testing.T, msg *Message, expSubject, expBody string) { | |||
| subject := msg.GetHeader("Subject") | |||
| subject := msg.ToMessage().GetHeader("Subject") | |||
| msgbuf := new(bytes.Buffer) | |||
| _, _ = msg.WriteTo(msgbuf) | |||
| _, _ = msg.ToMessage().WriteTo(msgbuf) | |||
| wholemsg := msgbuf.String() | |||
| assert.Equal(t, []string{expSubject}, subject) | |||
| assert.Contains(t, wholemsg, expBody) | |||
| @@ -188,9 +189,9 @@ func TestTemplateServices(t *testing.T) { | |||
| msg := testComposeIssueCommentMessage(t, &mailCommentContext{Issue: issue, Doer: doer, ActionType: actionType, | |||
| Content: "test body", Comment: comment}, tos, fromMention, "TestTemplateServices") | |||
| subject := msg.GetHeader("Subject") | |||
| subject := msg.ToMessage().GetHeader("Subject") | |||
| msgbuf := new(bytes.Buffer) | |||
| _, _ = msg.WriteTo(msgbuf) | |||
| _, _ = msg.ToMessage().WriteTo(msgbuf) | |||
| wholemsg := msgbuf.String() | |||
| assert.Equal(t, []string{expSubject}, subject) | |||
| @@ -18,7 +18,9 @@ import ( | |||
| "time" | |||
| "code.gitea.io/gitea/modules/base" | |||
| "code.gitea.io/gitea/modules/graceful" | |||
| "code.gitea.io/gitea/modules/log" | |||
| "code.gitea.io/gitea/modules/queue" | |||
| "code.gitea.io/gitea/modules/setting" | |||
| "github.com/jaytaylor/html2text" | |||
| @@ -27,38 +29,63 @@ import ( | |||
| // Message mail body and log info | |||
| type Message struct { | |||
| Info string // Message information for log purpose. | |||
| *gomail.Message | |||
| Info string // Message information for log purpose. | |||
| FromAddress string | |||
| FromDisplayName string | |||
| To []string | |||
| Subject string | |||
| Date time.Time | |||
| Body string | |||
| Headers map[string][]string | |||
| } | |||
| // 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) | |||
| // ToMessage converts a Message to gomail.Message | |||
| func (m *Message) ToMessage() *gomail.Message { | |||
| msg := gomail.NewMessage() | |||
| msg.SetAddressHeader("From", fromAddress, fromDisplayName) | |||
| msg.SetHeader("To", to...) | |||
| msg.SetAddressHeader("From", m.FromAddress, m.FromDisplayName) | |||
| msg.SetHeader("To", m.To...) | |||
| for header := range m.Headers { | |||
| msg.SetHeader(header, m.Headers[header]...) | |||
| } | |||
| if len(setting.MailService.SubjectPrefix) > 0 { | |||
| msg.SetHeader("Subject", setting.MailService.SubjectPrefix+" "+subject) | |||
| msg.SetHeader("Subject", setting.MailService.SubjectPrefix+" "+m.Subject) | |||
| } else { | |||
| msg.SetHeader("Subject", subject) | |||
| msg.SetHeader("Subject", m.Subject) | |||
| } | |||
| msg.SetDateHeader("Date", time.Now()) | |||
| msg.SetDateHeader("Date", m.Date) | |||
| msg.SetHeader("X-Auto-Response-Suppress", "All") | |||
| plainBody, err := html2text.FromString(body) | |||
| plainBody, err := html2text.FromString(m.Body) | |||
| if err != nil || setting.MailService.SendAsPlainText { | |||
| if strings.Contains(base.TruncateString(body, 100), "<html>") { | |||
| if strings.Contains(base.TruncateString(m.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) | |||
| msg.AddAlternative("text/html", m.Body) | |||
| } | |||
| return msg | |||
| } | |||
| // SetHeader adds additional headers to a message | |||
| func (m *Message) SetHeader(field string, value ...string) { | |||
| m.Headers[field] = value | |||
| } | |||
| // 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) | |||
| return &Message{ | |||
| Message: msg, | |||
| FromAddress: fromAddress, | |||
| FromDisplayName: fromDisplayName, | |||
| To: to, | |||
| Subject: subject, | |||
| Date: time.Now(), | |||
| Body: body, | |||
| Headers: map[string][]string{}, | |||
| } | |||
| } | |||
| @@ -257,18 +284,7 @@ func (s *dummySender) Send(from string, to []string, msg io.WriterTo) error { | |||
| 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 | |||
| var mailQueue queue.Queue | |||
| // Sender sender for sending mail synchronously | |||
| var Sender gomail.Sender | |||
| @@ -291,14 +307,26 @@ func NewContext() { | |||
| Sender = &dummySender{} | |||
| } | |||
| mailQueue = make(chan *Message, setting.MailService.QueueLength) | |||
| go processMailQueue() | |||
| mailQueue = queue.CreateQueue("mail", func(data ...queue.Data) { | |||
| for _, datum := range data { | |||
| msg := datum.(*Message) | |||
| gomailMsg := msg.ToMessage() | |||
| log.Trace("New e-mail sending request %s: %s", gomailMsg.GetHeader("To"), msg.Info) | |||
| if err := gomail.Send(Sender, gomailMsg); err != nil { | |||
| log.Error("Failed to send emails %s: %s - %v", gomailMsg.GetHeader("To"), msg.Info, err) | |||
| } else { | |||
| log.Trace("E-mails sent %s: %s", gomailMsg.GetHeader("To"), msg.Info) | |||
| } | |||
| } | |||
| }, &Message{}) | |||
| go graceful.GetManager().RunWithShutdownFns(mailQueue.Run) | |||
| } | |||
| // SendAsync send mail asynchronously | |||
| func SendAsync(msg *Message) { | |||
| go func() { | |||
| mailQueue <- msg | |||
| _ = mailQueue.Push(msg) | |||
| }() | |||
| } | |||
| @@ -306,7 +334,7 @@ func SendAsync(msg *Message) { | |||
| func SendAsyncs(msgs []*Message) { | |||
| go func() { | |||
| for _, msg := range msgs { | |||
| mailQueue <- msg | |||
| _ = mailQueue.Push(msg) | |||
| } | |||
| }() | |||
| } | |||