diff --git a/app/email.go b/app/email.go index 223b0437b2..5499f18053 100644 --- a/app/email.go +++ b/app/email.go @@ -4,7 +4,10 @@ package app import ( + "bytes" "fmt" + "io" + "net/mail" "net/url" "path" "strings" @@ -367,6 +370,17 @@ func (a *App) SendGuestInviteEmails(team *model.Team, channels []*model.Channel, return } + sender, appErr := a.GetUser(senderUserId) + if appErr != nil { + a.Log.Error("Email invite not sent, unable to find the sender user.", mlog.String("user_id", senderUserId), mlog.String("team_id", team.Id), mlog.Err(appErr)) + return + } + + senderProfileImage, _, appErr := a.GetProfileImage(sender) + if appErr != nil { + a.Log.Warn("Unable to get the sender user profile image.", mlog.String("user_id", senderUserId), mlog.String("team_id", team.Id), mlog.Err(appErr)) + } + if rateLimited { a.Log.Error("Invite emails rate limited.", mlog.String("user_id", senderUserId), @@ -430,7 +444,14 @@ func (a *App) SendGuestInviteEmails(team *model.Team, channels []*model.Channel, mlog.Info(fmt.Sprintf("sending invitation to %v %v", invite, bodyPage.Props["Link"])) } - if err := a.SendMail(invite, subject, bodyPage.Render()); err != nil { + embeddedFiles := make(map[string]io.Reader) + if senderProfileImage != nil { + embeddedFiles = map[string]io.Reader{ + "user-avatar.png": bytes.NewReader(senderProfileImage), + } + } + + if err := a.SendMailWithEmbeddedFiles(invite, subject, bodyPage.Render(), embeddedFiles); err != nil { mlog.Error(fmt.Sprintf("Failed to send invite email successfully err=%v", err)) } } @@ -498,3 +519,12 @@ func (a *App) SendMail(to, subject, htmlBody string) *model.AppError { license := a.License() return mailservice.SendMailUsingConfig(to, subject, htmlBody, a.Config(), license != nil && *license.Features.Compliance) } + +func (a *App) SendMailWithEmbeddedFiles(to, subject, htmlBody string, embeddedFiles map[string]io.Reader) *model.AppError { + license := a.License() + config := a.Config() + fromMail := mail.Address{Name: *config.EmailSettings.FeedbackName, Address: *config.EmailSettings.FeedbackEmail} + replyTo := mail.Address{Name: *config.EmailSettings.FeedbackName, Address: *config.EmailSettings.ReplyToAddress} + + return mailservice.SendMailUsingConfigAdvanced(to, to, fromMail, replyTo, subject, htmlBody, nil, embeddedFiles, nil, config, license != nil && *license.Features.Compliance) +} diff --git a/services/mailservice/inbucket.go b/services/mailservice/inbucket.go index 5ae72711ab..b2a9a3bd80 100644 --- a/services/mailservice/inbucket.go +++ b/services/mailservice/inbucket.go @@ -196,7 +196,7 @@ func getInbucketHost() (host string) { inbucket_port := os.Getenv("CI_INBUCKET_PORT") if inbucket_port == "" { - inbucket_port = "9000" + inbucket_port = "10080" } return fmt.Sprintf("http://%s:%s", inbucket_host, inbucket_port) } diff --git a/services/mailservice/mail.go b/services/mailservice/mail.go index 6384257e4b..c02594f89f 100644 --- a/services/mailservice/mail.go +++ b/services/mailservice/mail.go @@ -208,11 +208,11 @@ func SendMailUsingConfig(to, subject, htmlBody string, config *model.Config, ena fromMail := mail.Address{Name: *config.EmailSettings.FeedbackName, Address: *config.EmailSettings.FeedbackEmail} replyTo := mail.Address{Name: *config.EmailSettings.FeedbackName, Address: *config.EmailSettings.ReplyToAddress} - return SendMailUsingConfigAdvanced(to, to, fromMail, replyTo, subject, htmlBody, nil, nil, config, enableComplianceFeatures) + return SendMailUsingConfigAdvanced(to, to, fromMail, replyTo, subject, htmlBody, nil, nil, nil, config, enableComplianceFeatures) } // allows for sending an email with attachments and differing MIME/SMTP recipients -func SendMailUsingConfigAdvanced(mimeTo, smtpTo string, from, replyTo mail.Address, subject, htmlBody string, attachments []*model.FileInfo, mimeHeaders map[string]string, config *model.Config, enableComplianceFeatures bool) *model.AppError { +func SendMailUsingConfigAdvanced(mimeTo, smtpTo string, from, replyTo mail.Address, subject, htmlBody string, attachments []*model.FileInfo, embeddedFiles map[string]io.Reader, mimeHeaders map[string]string, config *model.Config, enableComplianceFeatures bool) *model.AppError { if len(*config.EmailSettings.SMTPServer) == 0 { return nil } @@ -235,10 +235,10 @@ func SendMailUsingConfigAdvanced(mimeTo, smtpTo string, from, replyTo mail.Addre return err } - return SendMail(c, mimeTo, smtpTo, from, replyTo, subject, htmlBody, attachments, mimeHeaders, fileBackend, time.Now()) + return SendMail(c, mimeTo, smtpTo, from, replyTo, subject, htmlBody, attachments, embeddedFiles, mimeHeaders, fileBackend, time.Now()) } -func SendMail(c smtpClient, mimeTo, smtpTo string, from, replyTo mail.Address, subject, htmlBody string, attachments []*model.FileInfo, mimeHeaders map[string]string, fileBackend filesstore.FileBackend, date time.Time) *model.AppError { +func SendMail(c smtpClient, mimeTo, smtpTo string, from, replyTo mail.Address, subject, htmlBody string, attachments []*model.FileInfo, embeddedFiles map[string]io.Reader, mimeHeaders map[string]string, fileBackend filesstore.FileBackend, date time.Time) *model.AppError { mlog.Debug("sending mail", mlog.String("to", smtpTo), mlog.String("subject", subject)) htmlMessage := "\r\n
" + htmlBody + "" @@ -272,6 +272,10 @@ func SendMail(c smtpClient, mimeTo, smtpTo string, from, replyTo mail.Address, s m.SetBody("text/plain", txtBody) m.AddAlternative("text/html", htmlMessage) + for name, reader := range embeddedFiles { + m.EmbedReader(name, reader) + } + for _, fileInfo := range attachments { bytes, err := fileBackend.ReadFile(fileInfo.Path) if err != nil { diff --git a/services/mailservice/mail_test.go b/services/mailservice/mail_test.go index bdcfc8cd34..0b62fcab15 100644 --- a/services/mailservice/mail_test.go +++ b/services/mailservice/mail_test.go @@ -168,9 +168,8 @@ func TestSendMailUsingConfigAdvanced(t *testing.T) { assert.Nil(t, err) // create two files with the same name that will both be attached to the email - fileName := "file.txt" - filePath1 := fmt.Sprintf("test1/%s", fileName) - filePath2 := fmt.Sprintf("test2/%s", fileName) + filePath1 := fmt.Sprintf("test1/%s", "file1.txt") + filePath2 := fmt.Sprintf("test2/%s", "file2.txt") fileContents1 := []byte("hello world") fileContents2 := []byte("foo bar") _, err = fileBackend.WriteFile(bytes.NewReader(fileContents1), filePath1) @@ -182,68 +181,70 @@ func TestSendMailUsingConfigAdvanced(t *testing.T) { attachments := make([]*model.FileInfo, 2) attachments[0] = &model.FileInfo{ - Name: fileName, + Name: "file1.txt", Path: filePath1, } attachments[1] = &model.FileInfo{ - Name: fileName, + Name: "file2.txt", Path: filePath2, } + embeddedFiles := map[string]io.Reader{ + "test": bytes.NewReader([]byte("test data")), + } + headers := make(map[string]string) headers["TestHeader"] = "TestValue" - if err := SendMailUsingConfigAdvanced(mimeTo, smtpTo, from, replyTo, emailSubject, emailBody, attachments, headers, cfg, true); err != nil { - t.Log(err) - t.Fatal("Should connect to the STMP Server") - } else { - //Check if the email was send to the right email address - var resultsMailbox JSONMessageHeaderInbucket - err := RetryInbucket(5, func() error { - var err error - resultsMailbox, err = GetMailBox(smtpTo) - return err - }) - if err != nil { - t.Log(err) - t.Fatal("No emails found for address " + smtpTo) - } - if err == nil && len(resultsMailbox) > 0 { - if !strings.ContainsAny(resultsMailbox[0].To[0], smtpTo) { - t.Fatal("Wrong To recipient") - } else { - if resultsEmail, err := GetMessageFromMailbox(smtpTo, resultsMailbox[0].ID); err == nil { - if !strings.Contains(resultsEmail.Body.Text, emailBody) { - t.Log(resultsEmail.Body.Text) - t.Fatal("Received message") - } + err = SendMailUsingConfigAdvanced(mimeTo, smtpTo, from, replyTo, emailSubject, emailBody, attachments, embeddedFiles, headers, cfg, true) + require.Nil(t, err, "Should connect to the STMP Server: %v", err) - // verify that the To header of the email message is set to the MIME recipient, even though we got it out of the SMTP recipient's email inbox - assert.Equal(t, mimeTo, resultsEmail.Header["To"][0]) + //Check if the email was send to the right email address + var resultsMailbox JSONMessageHeaderInbucket + err = RetryInbucket(5, func() error { + var mailErr error + resultsMailbox, mailErr = GetMailBox(smtpTo) + return mailErr + }) + require.Nil(t, err, "No emails found for address %s. error: %v", smtpTo, err) + require.NotEqual(t, len(resultsMailbox), 0) - // verify that the MIME from address is correct - unfortunately, we can't verify the SMTP from address - assert.Equal(t, from.String(), resultsEmail.Header["From"][0]) + require.Contains(t, resultsMailbox[0].To[0], mimeTo, "Wrong To recipient") - // check that the custom mime headers came through - header case seems to get mutated - assert.Equal(t, "TestValue", resultsEmail.Header["Testheader"][0]) + resultsEmail, err := GetMessageFromMailbox(smtpTo, resultsMailbox[0].ID) + require.Nil(t, err) - // ensure that the attachments were successfully sent - assert.Len(t, resultsEmail.Attachments, 2) - assert.Equal(t, fileName, resultsEmail.Attachments[0].Filename) - assert.Equal(t, fileName, resultsEmail.Attachments[1].Filename) - attachment1 := string(resultsEmail.Attachments[0].Bytes) - attachment2 := string(resultsEmail.Attachments[1].Bytes) - if attachment1 == string(fileContents1) { - assert.Equal(t, attachment2, string(fileContents2)) - } else if attachment1 == string(fileContents2) { - assert.Equal(t, attachment2, string(fileContents1)) - } else { - assert.Fail(t, "Unrecognized attachment contents") - } - } - } - } + require.Contains(t, emailBody, resultsEmail.Body.Text, "Wrong received message") + + // verify that the To header of the email message is set to the MIME recipient, even though we got it out of the SMTP recipient's email inbox + assert.Equal(t, mimeTo, resultsEmail.Header["To"][0]) + + // verify that the MIME from address is correct - unfortunately, we can't verify the SMTP from address + assert.Equal(t, from.String(), resultsEmail.Header["From"][0]) + + // check that the custom mime headers came through - header case seems to get mutated + assert.Equal(t, "TestValue", resultsEmail.Header["Testheader"][0]) + + // ensure that the attachments were successfully sent + assert.Len(t, resultsEmail.Attachments, 3) + + attachmentsFilenames := []string{ + resultsEmail.Attachments[0].Filename, + resultsEmail.Attachments[1].Filename, + resultsEmail.Attachments[2].Filename, } + assert.Contains(t, attachmentsFilenames, "file1.txt") + assert.Contains(t, attachmentsFilenames, "file2.txt") + assert.Contains(t, attachmentsFilenames, "test") + + attachment1 := string(resultsEmail.Attachments[0].Bytes) + attachment2 := string(resultsEmail.Attachments[1].Bytes) + attachment3 := string(resultsEmail.Attachments[2].Bytes) + attachmentsData := []string{attachment1, attachment2, attachment3} + + assert.Contains(t, attachmentsData, string(fileContents1)) + assert.Contains(t, attachmentsData, string(fileContents2)) + assert.Contains(t, attachmentsData, "test data") } func TestAuthMethods(t *testing.T) { @@ -349,7 +350,7 @@ func TestSendMail(t *testing.T) { for testName, tc := range testCases { t.Run(testName, func(t *testing.T) { - appErr = SendMail(mocm, "", "", mail.Address{}, tc.replyTo, "", "", nil, nil, mockBackend, time.Now()) + appErr = SendMail(mocm, "", "", mail.Address{}, tc.replyTo, "", "", nil, nil, nil, mockBackend, time.Now()) require.Nil(t, appErr) if len(tc.contains) > 0 { require.Contains(t, string(mocm.data), tc.contains) diff --git a/templates/invite_body.html b/templates/invite_body.html index 0887b472ab..0f451fa3cb 100644 --- a/templates/invite_body.html +++ b/templates/invite_body.html @@ -26,11 +26,11 @@|
- |
- + |
{{.Props.SenderName}} -{{.Props.Message}} +{{.Props.Message}} |