Embedding avatar in guest invite emails (#12328)
* Embedding avatar in guest invite emails * Fixing tests * Improving mailservice tests * Fixing shadow variables * Fixing styles for gmail * Keep spacing on invite messages * Fixing spacing
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
8cea561ba6
Коммит
6ba961a480
32
app/email.go
32
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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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<html><body>" + htmlBody + "</body></html>"
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -26,11 +26,11 @@
|
||||
<table border="0" cellpadding="0" cellspacing="0" style="padding: 20px; margin: 35px auto 0 auto; border-radius: 5px; background: #f2f2f2; min-width: 550px">
|
||||
<tr>
|
||||
<td style="border: 0; padding: 0; width: 36px; vertical-align: top">
|
||||
<img src="{{.Props.SiteURL}}/api/v4/users/{{.Props.SenderId}}/image" style="height: 36px; width=36px; border-radius: 18px;">
|
||||
<img width="36" height="36" style="border-radius: 18px;" src="cid:user-avatar.png">
|
||||
</td>
|
||||
<td style="border: 0; padding: 0 0 0 20px;">
|
||||
<td style="border: 0; padding: 0 0 0 20px; text-align: left;">
|
||||
<p style="font-weight: bold; margin-top: 0;">{{.Props.SenderName}}</p>
|
||||
<p>{{.Props.Message}}</p>
|
||||
<p style="white-space: break-spaces">{{.Props.Message}}</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
Ссылка в новой задаче
Block a user