diff --git a/services/notifications/pkg/channels/channels.go b/services/notifications/pkg/channels/channels.go index eaf51e4e70b..a535882a751 100644 --- a/services/notifications/pkg/channels/channels.go +++ b/services/notifications/pkg/channels/channels.go @@ -8,9 +8,6 @@ import ( "strings" gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" - groups "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1" - rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" - "github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool" "github.com/owncloud/ocis/v2/ocis-pkg/log" "github.com/owncloud/ocis/v2/services/notifications/pkg/config" "github.com/pkg/errors" @@ -20,31 +17,24 @@ import ( // Channel defines the methods of a communication channel. type Channel interface { // SendMessage sends a message to users. - SendMessage(ctx context.Context, userIDs []string, msg, subject, senderDisplayName string) error - // SendMessageToGroup sends a message to a group. - SendMessageToGroup(ctx context.Context, groupdID *groups.GroupId, msg, subject, senderDisplayName string) error + SendMessage(ctx context.Context, message *Message) error +} + +// Message represent the already rendered message including the user id opaqueID +type Message struct { + Sender string + Recipient []string + Subject string + TextBody string + HtmlBody string + AttachInline map[string][]byte } // NewMailChannel instantiates a new mail communication channel. func NewMailChannel(cfg config.Config, logger log.Logger) (Channel, error) { - tm, err := pool.StringToTLSMode(cfg.Notifications.GRPCClientTLS.Mode) - if err != nil { - logger.Error().Err(err).Msg("could not get gateway client tls mode") - return nil, err - } - gc, err := pool.GetGatewayServiceClient(cfg.Notifications.RevaGateway, - pool.WithTLSCACert(cfg.Notifications.GRPCClientTLS.CACert), - pool.WithTLSMode(tm), - ) - if err != nil { - logger.Error().Err(err).Msg("could not get gateway client") - return nil, err - } - return Mail{ - gatewayClient: gc, - conf: cfg, - logger: logger, + conf: cfg, + logger: logger, }, nil } @@ -111,73 +101,30 @@ func (m Mail) getMailClient() (*mail.SMTPClient, error) { } // SendMessage sends a message to all given users. -func (m Mail) SendMessage(ctx context.Context, userIDs []string, msg, subject, senderDisplayName string) error { +func (m Mail) SendMessage(ctx context.Context, message *Message) error { if m.conf.Notifications.SMTP.Host == "" { return nil } - to, err := m.getReceiverAddresses(ctx, userIDs) - if err != nil { - return err - } - smtpClient, err := m.getMailClient() if err != nil { return err } email := mail.NewMSG() - if senderDisplayName != "" { - email.SetFrom(fmt.Sprintf("%s via %s", senderDisplayName, m.conf.Notifications.SMTP.Sender)).AddTo(to...) + if message.Sender != "" { + email.SetFrom(fmt.Sprintf("%s via %s", message.Sender, m.conf.Notifications.SMTP.Sender)).AddTo(message.Recipient...) } else { - email.SetFrom(m.conf.Notifications.SMTP.Sender).AddTo(to...) - } - email.SetBody(mail.TextPlain, msg) - email.SetSubject(subject) - - return email.Send(smtpClient) -} - -// SendMessageToGroup sends a message to all members of the given group. -func (m Mail) SendMessageToGroup(ctx context.Context, groupID *groups.GroupId, msg, subject, senderDisplayName string) error { - res, err := m.gatewayClient.GetGroup(ctx, &groups.GetGroupRequest{GroupId: groupID}) - if err != nil { - return err - } - if res.Status.Code != rpc.Code_CODE_OK { - return errors.New("could not get group") + email.SetFrom(m.conf.Notifications.SMTP.Sender).AddTo(message.Recipient...) } - - members := make([]string, 0, len(res.Group.Members)) - for _, id := range res.Group.Members { - members = append(members, id.OpaqueId) - } - - return m.SendMessage(ctx, members, msg, subject, senderDisplayName) -} - -func (m Mail) getReceiverAddresses(ctx context.Context, receivers []string) ([]string, error) { - addresses := make([]string, 0, len(receivers)) - for _, id := range receivers { - // Authenticate is too costly but at the moment our only option to get the user. - // We don't have an authenticated context so calling `GetUser` doesn't work. - res, err := m.gatewayClient.Authenticate(ctx, &gateway.AuthenticateRequest{ - Type: "machine", - ClientId: "userid:" + id, - ClientSecret: m.conf.Notifications.MachineAuthAPIKey, - }) - if err != nil { - return nil, err - } - if res.Status.Code != rpc.Code_CODE_OK { - m.logger.Error(). - Interface("status", res.Status). - Str("receiver_id", id). - Msg("could not get user") - continue + email.SetSubject(message.Subject) + email.SetBody(mail.TextPlain, message.TextBody) + if message.HtmlBody != "" { + email.AddAlternative(mail.TextHTML, message.HtmlBody) + for filename, data := range message.AttachInline { + email.Attach(&mail.File{Data: data, Name: filename, Inline: true}) } - addresses = append(addresses, res.User.Mail) } - return addresses, nil + return email.Send(smtpClient) } diff --git a/services/notifications/pkg/email/composer.go b/services/notifications/pkg/email/composer.go index ece92e0a21c..8a33c20cf42 100644 --- a/services/notifications/pkg/email/composer.go +++ b/services/notifications/pkg/email/composer.go @@ -14,10 +14,48 @@ var ( _domain = "notifications" ) +func NewTextTemplate(mt MessageTemplate, locale string, translationPath string, vars map[string]interface{}) (MessageTemplate, error) { + var err error + mt.Subject, err = ComposeMessage(mt.Subject, locale, translationPath, vars) + if err != nil { + return mt, err + } + mt.Greeting, err = ComposeMessage(mt.Greeting, locale, translationPath, vars) + if err != nil { + return mt, err + } + mt.MessageBody, err = ComposeMessage(mt.MessageBody, locale, translationPath, vars) + if err != nil { + return mt, err + } + mt.CallToAction, err = ComposeMessage(mt.CallToAction, locale, translationPath, vars) + if err != nil { + return mt, err + } + return mt, nil +} + +func NewHtmlTemplate(mt MessageTemplate, locale string, translationPath string, vars map[string]interface{}) (MessageTemplate, error) { + var err error + mt.Greeting, err = ComposeMessage(newlineToBr(mt.Greeting), locale, translationPath, vars) + if err != nil { + return mt, err + } + mt.MessageBody, err = ComposeMessage(newlineToBr(mt.MessageBody), locale, translationPath, vars) + if err != nil { + return mt, err + } + mt.CallToAction, err = ComposeMessage(callToActionToHtml(mt.CallToAction), locale, translationPath, vars) + if err != nil { + return mt, err + } + return mt, nil +} + // ComposeMessage renders the message based on template -func ComposeMessage(template, locale string, path string) string { +func ComposeMessage(template, locale string, path string, vars map[string]interface{}) (string, error) { raw := loadTemplate(template, locale, path) - return replacePlaceholders(raw) + return executeRaw(replacePlaceholders(raw), vars) } func loadTemplate(template, locale string, path string) string { @@ -39,3 +77,15 @@ func replacePlaceholders(raw string) string { } return raw } + +func newlineToBr(s string) string { + return strings.Replace(s, "\n", "
", -1) +} + +func callToActionToHtml(s string) string { + if strings.TrimSpace(s) == "" { + return "" + } + s = strings.TrimSpace(strings.TrimRight(s, "{{ .ShareLink }}")) + return `` + s + `` +} diff --git a/services/notifications/pkg/email/email.go b/services/notifications/pkg/email/email.go index 283559b9c5f..4a13e8d9bc5 100644 --- a/services/notifications/pkg/email/email.go +++ b/services/notifications/pkg/email/email.go @@ -8,7 +8,11 @@ import ( "embed" "html" "html/template" + "os" "path/filepath" + "strings" + + "github.com/owncloud/ocis/v2/services/notifications/pkg/channels" ) var ( @@ -17,44 +21,46 @@ var ( ) // RenderEmailTemplate renders the email template for a new share -func RenderEmailTemplate(mt MessageTemplate, locale string, emailTemplatePath string, translationPath string, vars map[string]interface{}) (string, string, error) { - // translate a message - mt.Subject = ComposeMessage(mt.Subject, locale, translationPath) - mt.Greeting = ComposeMessage(mt.Greeting, locale, translationPath) - mt.MessageBody = ComposeMessage(mt.MessageBody, locale, translationPath) - mt.CallToAction = ComposeMessage(mt.CallToAction, locale, translationPath) - - // replace the body email placeholders with the values - subject, err := executeRaw(mt.Subject, vars) +func RenderEmailTemplate(mt MessageTemplate, locale string, emailTemplatePath string, translationPath string, vars map[string]interface{}) (*channels.Message, error) { + textMt, err := NewTextTemplate(mt, locale, translationPath, vars) + if err != nil { + return nil, err + } + textBody, err := emailTemplate(emailTemplatePath, textMt) if err != nil { - return "", "", err + return nil, err } - // replace the body email template placeholders with the translated template - rawBody, err := executeEmailTemplate(emailTemplatePath, mt) + htmlMt, err := NewHtmlTemplate(mt, locale, translationPath, vars) if err != nil { - return "", "", err + return nil, err } - // replace the body email placeholders with the values - body, err := executeRaw(rawBody, vars) + htmlBody, err := htmlEmailTemplate(emailTemplatePath, htmlMt) if err != nil { - return "", "", err + return nil, err } - return subject, body, nil -} -func executeEmailTemplate(emailTemplatePath string, mt MessageTemplate) (string, error) { - var err error - var tpl *template.Template - // try to lookup the files in the filesystem - tpl, err = template.ParseFiles(filepath.Join(emailTemplatePath, mt.bodyTemplate)) + var data map[string][]byte + data, err = readImages(emailTemplatePath) if err != nil { - // template has not been found in the fs, or path has not been specified => use embed templates - tpl, err = template.ParseFS(templatesFS, filepath.Join("templates/", mt.bodyTemplate)) + data, err = readFs() if err != nil { - return "", err + return nil, err } } + return &channels.Message{ + Subject: textMt.Subject, + TextBody: textBody, + HtmlBody: htmlBody, + AttachInline: data, + }, nil +} + +func emailTemplate(emailTemplatePath string, mt MessageTemplate) (string, error) { + tpl, err := parseTemplate(emailTemplatePath, mt.textTemplate) + if err != nil { + return "", err + } str, err := executeTemplate(tpl, mt) if err != nil { return "", err @@ -62,6 +68,37 @@ func executeEmailTemplate(emailTemplatePath string, mt MessageTemplate) (string, return html.UnescapeString(str), err } +func htmlEmailTemplate(emailTemplatePath string, mt MessageTemplate) (string, error) { + mailTpl, err := parseTemplate(emailTemplatePath, filepath.Join(emailTemplatePath, "html", "email.html.tmpl")) + if err != nil { + return "", err + } + str, err := executeTemplate(mailTpl, map[string]interface{}{ + "Greeting": template.HTML(html.UnescapeString(strings.TrimSpace(mt.Greeting))), + "MessageBody": template.HTML(html.UnescapeString(strings.TrimSpace(mt.MessageBody))), + "CallToAction": template.HTML(html.UnescapeString(strings.TrimSpace(mt.CallToAction))), + }) + if err != nil { + return "", err + } + return strings.TrimSpace(str), err +} + +func parseTemplate(emailTemplatePath string, file string) (*template.Template, error) { + var err error + var tpl *template.Template + // try to lookup the files in the filesystem + tpl, err = template.ParseFiles(filepath.Join(emailTemplatePath, file)) + if err != nil { + // template has not been found in the fs, or path has not been specified => use embed templates + tpl, err = template.ParseFS(templatesFS, filepath.Join("templates", file)) + if err != nil { + return nil, err + } + } + return tpl, err +} + func executeRaw(raw string, vars map[string]interface{}) (string, error) { tpl, err := template.New("").Parse(raw) if err != nil { @@ -77,3 +114,66 @@ func executeTemplate(tpl *template.Template, vars any) (string, error) { } return writer.String(), nil } + +func readFs() (map[string][]byte, error) { + dir := filepath.Join("templates", "html", "img") + entries, err := templatesFS.ReadDir(dir) + if err != nil { + return nil, err + } + + list := make(map[string][]byte) + for _, e := range entries { + if !e.IsDir() { + file, err := templatesFS.ReadFile(filepath.Join(dir, e.Name())) + if err != nil { + return nil, err + } + if !validateMime(file) { + continue + } + list[e.Name()] = file + } + } + return list, nil +} + +func readImages(emailTemplatePath string) (map[string][]byte, error) { + dir := filepath.Join(emailTemplatePath, "html", "img") + entries, err := os.ReadDir(dir) + if err != nil { + return nil, err + } + list := make(map[string][]byte) + for _, e := range entries { + if !e.IsDir() { + file, err := os.ReadFile(filepath.Join(dir, e.Name())) + if err != nil { + return nil, err + } + if !validateMime(file) { + continue + } + list[e.Name()] = file + } + } + return list, nil +} + +// signature image formats signature https://go.dev/src/net/http/sniff.go #L:118 +var signature = map[string]string{ + "\xff\xd8\xff": "image/jpeg", + "\x89PNG\r\n\x1a\n": "image/png", + "GIF87a": "image/gif", + "GIF89a": "image/gif", +} + +// validateMime validate the mime type of image file from its first few bytes +func validateMime(incipit []byte) bool { + for s := range signature { + if strings.HasPrefix(string(incipit), s) { + return true + } + } + return false +} diff --git a/services/notifications/pkg/email/templates.go b/services/notifications/pkg/email/templates.go index f8161521764..bb0723d6933 100644 --- a/services/notifications/pkg/email/templates.go +++ b/services/notifications/pkg/email/templates.go @@ -7,7 +7,8 @@ func Template(s string) string { return s } var ( // Shares ShareCreated = MessageTemplate{ - bodyTemplate: "shares/shareCreated.email.body.tmpl", + textTemplate: "shares/shareCreated.email.body.tmpl", + htmlTemplate: "shares/shareCreated.email.body.html.tmpl", // ShareCreated email template, Subject field (resolves directly) Subject: Template(`{ShareSharer} shared '{ShareFolder}' with you`), // ShareCreated email template, resolves via {{ .Greeting }} @@ -19,7 +20,8 @@ var ( } ShareExpired = MessageTemplate{ - bodyTemplate: "shares/shareExpired.email.body.tmpl", + textTemplate: "shares/shareExpired.email.body.tmpl", + htmlTemplate: "shares/shareExpired.email.body.html.tmpl", // ShareExpired email template, Subject field (resolves directly) Subject: Template(`Share to '{ShareFolder}' expired at {ExpiredAt}`), // ShareExpired email template, resolves via {{ .Greeting }} @@ -32,7 +34,8 @@ Even though this share has been revoked you still might have access through othe // Spaces templates SharedSpace = MessageTemplate{ - bodyTemplate: "spaces/sharedSpace.email.body.tmpl", + textTemplate: "spaces/sharedSpace.email.body.tmpl", + htmlTemplate: "spaces/sharedSpace.email.body.html.tmpl", // SharedSpace email template, Subject field (resolves directly) Subject: Template("{SpaceSharer} invited you to join {SpaceName}"), // SharedSpace email template, resolves via {{ .Greeting }} @@ -44,7 +47,8 @@ Even though this share has been revoked you still might have access through othe } UnsharedSpace = MessageTemplate{ - bodyTemplate: "spaces/unsharedSpace.email.body.tmpl", + textTemplate: "spaces/unsharedSpace.email.body.tmpl", + htmlTemplate: "spaces/unsharedSpace.email.body.html.tmpl", // UnsharedSpace email template, Subject field (resolves directly) Subject: Template(`{SpaceSharer} removed you from {SpaceName}`), // UnsharedSpace email template, resolves via {{ .Greeting }} @@ -58,7 +62,8 @@ You might still have access through your other groups or direct membership.`), } MembershipExpired = MessageTemplate{ - bodyTemplate: "spaces/membershipExpired.email.body.tmpl", + textTemplate: "spaces/membershipExpired.email.body.tmpl", + htmlTemplate: "spaces/membershipExpired.email.body.html.tmpl", // MembershipExpired email template, Subject field (resolves directly) Subject: Template(`Membership of '{SpaceName}' expired at {ExpiredAt}`), // MembershipExpired email template, resolves via {{ .Greeting }} @@ -84,8 +89,11 @@ var _placeholders = map[string]string{ // MessageTemplate is the data structure for the email type MessageTemplate struct { - // bodyTemplate represent the path to .tmpl file - bodyTemplate string + // textTemplate represent the path to text plain .tmpl file + textTemplate string + // htmlTemplate represent the path to html .tmpl file + htmlTemplate string + // The fields below represent the placeholders for the translatable templates Subject string Greeting string MessageBody string diff --git a/services/notifications/pkg/email/templates/html/email.html.tmpl b/services/notifications/pkg/email/templates/html/email.html.tmpl new file mode 100644 index 00000000000..5ba3adda1a2 --- /dev/null +++ b/services/notifications/pkg/email/templates/html/email.html.tmpl @@ -0,0 +1,51 @@ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
  + logo-mail +
 
  + {{ .Greeting }} +

+ {{ .MessageBody }} + {{if ne .CallToAction "" }} +

{{ .CallToAction }} + {{end}} +
 
  + +
 
+
+ + diff --git a/services/notifications/pkg/email/templates/html/img/logo-mail.gif b/services/notifications/pkg/email/templates/html/img/logo-mail.gif new file mode 100644 index 00000000000..c8bb0c7e51a Binary files /dev/null and b/services/notifications/pkg/email/templates/html/img/logo-mail.gif differ diff --git a/services/notifications/pkg/service/service.go b/services/notifications/pkg/service/service.go index e20b917c283..b518887cc4d 100644 --- a/services/notifications/pkg/service/service.go +++ b/services/notifications/pkg/service/service.go @@ -97,90 +97,91 @@ func (s eventsNotifier) Run() error { } } -// recipient represent the already rendered message including the user id opaqueID -type recipient struct { - opaqueID string - subject string - msg string -} - func (s eventsNotifier) render(ctx context.Context, template email.MessageTemplate, - granteeFieldName string, fields map[string]interface{}, granteeList []*user.UserId) ([]recipient, error) { + granteeFieldName string, fields map[string]interface{}, granteeList []*user.User, sender string) ([]*channels.Message, error) { // Render the Email Template for each user - recipientList := make([]recipient, len(granteeList)) - for i, userID := range granteeList { - locale, err := s.getUserLang(ctx, userID) + messageList := make([]*channels.Message, len(granteeList)) + for i, usr := range granteeList { + locale, err := s.getUserLang(ctx, usr.GetId()) if err != nil { return nil, err } - grantee, err := s.getUserName(ctx, userID) - if err != nil { - return nil, err - } - fields[granteeFieldName] = grantee + fields[granteeFieldName] = usr.GetDisplayName() - subj, msg, err := email.RenderEmailTemplate(template, locale, s.emailTemplatePath, s.translationPath, fields) + rendered, err := email.RenderEmailTemplate(template, locale, s.emailTemplatePath, s.translationPath, fields) if err != nil { return nil, err } - recipientList[i] = recipient{opaqueID: userID.GetOpaqueId(), subject: subj, msg: msg} + rendered.Sender = sender + rendered.Recipient = []string{usr.GetMail()} + messageList[i] = rendered } - return recipientList, nil + return messageList, nil } -func (s eventsNotifier) send(ctx context.Context, recipientList []recipient, sender string) { +func (s eventsNotifier) send(ctx context.Context, recipientList []*channels.Message) { for _, r := range recipientList { - err := s.channel.SendMessage(ctx, []string{r.opaqueID}, r.msg, r.subject, sender) + err := s.channel.SendMessage(ctx, r) if err != nil { s.logger.Error().Err(err).Str("event", "SendEmail").Msg("failed to send a message") } } } -func (s eventsNotifier) getGranteeList(ctx context.Context, owner, u *user.UserId, g *group.GroupId) ([]*user.UserId, error) { +func (s eventsNotifier) getGranteeList(ctx context.Context, owner, u *user.UserId, g *group.GroupId) ([]*user.User, error) { switch { case u != nil: - return []*user.UserId{u}, nil + usr, err := s.getUser(ctx, u) + if err != nil { + return nil, err + } + return []*user.User{usr}, nil case g != nil: res, err := s.gwClient.GetGroup(ctx, &group.GetGroupRequest{GroupId: g}) if err != nil { return nil, err } - if res.Status.Code != rpc.Code_CODE_OK { + if res.GetStatus().GetCode() != rpc.Code_CODE_OK { return nil, errors.New("could not get group") } - for i, userID := range res.GetGroup().GetMembers() { + userList := make([]*user.User, 0, len(res.GetGroup().GetMembers())) + for _, userID := range res.GetGroup().GetMembers() { // remove an executant from a list if userID.GetOpaqueId() == owner.GetOpaqueId() { - res.Group.Members[i] = res.Group.Members[len(res.Group.Members)-1] - return res.Group.Members[:len(res.Group.Members)-1], nil + continue + } + usr, err := s.getUser(ctx, userID) + if err != nil { + return nil, err } + userList = append(userList, usr) } - return res.Group.Members, nil + return userList, nil default: return nil, errors.New("need at least one non-nil grantee") } } -func (s eventsNotifier) getUserName(ctx context.Context, u *user.UserId) (string, error) { +// func (s eventsNotifier) getUserName(ctx context.Context, u *user.UserId) (string, error) { +func (s eventsNotifier) getUser(ctx context.Context, u *user.UserId) (*user.User, error) { if u == nil { - return "", errors.New("need at least one non-nil grantee") + return nil, errors.New("need at least one non-nil grantee") } r, err := s.gwClient.GetUser(ctx, &user.GetUserRequest{UserId: u}) if err != nil { - return "", err + return nil, err } - if r.Status.Code != rpc.Code_CODE_OK { - return "", fmt.Errorf("unexpected status code from gateway client: %d", r.GetStatus().GetCode()) + if r.GetStatus().GetCode() != rpc.Code_CODE_OK { + return nil, fmt.Errorf("unexpected status code from gateway client: %d", r.GetStatus().GetCode()) } - return r.GetUser().GetDisplayName(), nil + return r.GetUser(), nil } func (s eventsNotifier) getUserLang(ctx context.Context, u *user.UserId) (string, error) { - granteeCtx := metadata.Set(ctx, middleware.AccountID, u.OpaqueId) + granteeCtx := metadata.Set(ctx, middleware.AccountID, u.GetOpaqueId()) if resp, err := s.valueService.GetValueByUniqueIdentifiers(granteeCtx, &settingssvc.GetValueByUniqueIdentifiersRequest{ - AccountUuid: u.OpaqueId, + AccountUuid: u.GetOpaqueId(), SettingId: defaults.SettingUUIDProfileLanguage, }); err == nil { if resp == nil { @@ -206,8 +207,7 @@ func (s eventsNotifier) getResourceInfo(ctx context.Context, resourceID *provide if err != nil { return nil, err } - - if md.Status.Code != rpc.Code_CODE_OK { + if md.GetStatus().GetCode() != rpc.Code_CODE_OK { return nil, fmt.Errorf("could not resource info: %s", md.Status.Message) } return md.GetInfo(), nil diff --git a/services/notifications/pkg/service/service_test.go b/services/notifications/pkg/service/service_test.go index 4f6017eded5..fdc64410c9f 100644 --- a/services/notifications/pkg/service/service_test.go +++ b/services/notifications/pkg/service/service_test.go @@ -5,7 +5,6 @@ import ( "time" gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" - group "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1" user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" @@ -19,6 +18,7 @@ import ( "github.com/owncloud/ocis/v2/ocis-pkg/shared" settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0" "github.com/owncloud/ocis/v2/services/graph/pkg/config/defaults" + "github.com/owncloud/ocis/v2/services/notifications/pkg/channels" "github.com/owncloud/ocis/v2/services/notifications/pkg/service" "github.com/test-go/testify/mock" "go-micro.dev/v4/client" @@ -32,12 +32,14 @@ var _ = Describe("Notifications", func() { Id: &user.UserId{ OpaqueId: "sharer", }, + Mail: "sharer@owncloud.com", DisplayName: "Dr. S. Harer", } sharee = &user.User{ Id: &user.UserId{ OpaqueId: "sharee", }, + Mail: "sharee@owncloud.com", DisplayName: "Eric Expireling", } resourceid = &provider.ResourceId{ @@ -78,7 +80,7 @@ var _ = Describe("Notifications", func() { }, Entry("Share Created", testChannel{ - expectedReceipients: map[string]bool{sharee.GetId().GetOpaqueId(): true}, + expectedReceipients: []string{sharee.GetMail()}, expectedSubject: "Dr. S. Harer shared 'secrets of the board' with you", expectedMessage: `Hello Eric Expireling @@ -103,7 +105,7 @@ https://owncloud.com }), Entry("Share Expired", testChannel{ - expectedReceipients: map[string]bool{sharee.GetId().GetOpaqueId(): true}, + expectedReceipients: []string{sharee.GetMail()}, expectedSubject: "Share to 'secrets of the board' expired at 2023-04-17 16:42:00", expectedMessage: `Hello Eric Expireling, @@ -128,7 +130,7 @@ https://owncloud.com }), Entry("Added to Space", testChannel{ - expectedReceipients: map[string]bool{sharee.GetId().GetOpaqueId(): true}, + expectedReceipients: []string{sharee.GetMail()}, expectedSubject: "Dr. S. Harer invited you to join secret space", expectedMessage: `Hello Eric Expireling, @@ -153,7 +155,7 @@ https://owncloud.com }), Entry("Removed from Space", testChannel{ - expectedReceipients: map[string]bool{sharee.GetId().GetOpaqueId(): true}, + expectedReceipients: []string{sharee.GetMail()}, expectedSubject: "Dr. S. Harer removed you from secret space", expectedMessage: `Hello Eric Expireling, @@ -179,7 +181,7 @@ https://owncloud.com }), Entry("Space Expired", testChannel{ - expectedReceipients: map[string]bool{sharee.GetId().GetOpaqueId(): true}, + expectedReceipients: []string{sharee.GetMail()}, expectedSubject: "Membership of 'secret space' expired at 2023-04-17 16:42:00", expectedMessage: `Hello Eric Expireling, @@ -208,27 +210,20 @@ https://owncloud.com // NOTE: This is explictitly not testing the message itself. Should we? type testChannel struct { - expectedReceipients map[string]bool + expectedReceipients []string expectedSubject string expectedMessage string expectedSender string done chan struct{} } -func (tc testChannel) SendMessage(ctx context.Context, userIDs []string, msg, subject, senderDisplayName string) error { +func (tc testChannel) SendMessage(ctx context.Context, m *channels.Message) error { defer GinkgoRecover() - for _, u := range userIDs { - Expect(tc.expectedReceipients[u]).To(Equal(true)) - } - - Expect(msg).To(Equal(tc.expectedMessage)) - Expect(subject).To(Equal(tc.expectedSubject)) - Expect(senderDisplayName).To(Equal(tc.expectedSender)) + Expect(m.Recipient).To(Equal(tc.expectedReceipients)) + Expect(m.Subject).To(Equal(tc.expectedSubject)) + Expect(m.TextBody).To(Equal(tc.expectedMessage)) + Expect(m.Sender).To(Equal(tc.expectedSender)) tc.done <- struct{}{} return nil } - -func (tc testChannel) SendMessageToGroup(ctx context.Context, groupID *group.GroupId, msg, subject, senderDisplayName string) error { - return tc.SendMessage(ctx, []string{groupID.GetOpaqueId()}, msg, subject, senderDisplayName) -} diff --git a/services/notifications/pkg/service/shares.go b/services/notifications/pkg/service/shares.go index 4c8d8444cf5..1182ea38ee5 100644 --- a/services/notifications/pkg/service/shares.go +++ b/services/notifications/pkg/service/shares.go @@ -48,12 +48,12 @@ func (s eventsNotifier) handleShareCreated(e events.ShareCreated) { "ShareSharer": sharerDisplayName, "ShareFolder": resourceInfo.Name, "ShareLink": shareLink, - }, granteeList) + }, granteeList, sharerDisplayName) if err != nil { s.logger.Error().Err(err).Str("event", "ShareCreated").Msg("could not get render the email") return } - s.send(ownerCtx, recipientList, sharerDisplayName) + s.send(ownerCtx, recipientList) } func (s eventsNotifier) handleShareExpired(e events.ShareExpired) { @@ -87,10 +87,10 @@ func (s eventsNotifier) handleShareExpired(e events.ShareExpired) { map[string]interface{}{ "ShareFolder": resourceInfo.GetName(), "ExpiredAt": e.ExpiredAt.Format("2006-01-02 15:04:05"), - }, granteeList) + }, granteeList, owner.GetDisplayName()) if err != nil { s.logger.Error().Err(err).Str("event", "ShareExpired").Msg("could not get render the email") return } - s.send(ownerCtx, recipientList, owner.GetDisplayName()) + s.send(ownerCtx, recipientList) } diff --git a/services/notifications/pkg/service/spaces.go b/services/notifications/pkg/service/spaces.go index 78f6e59359d..37af8171330 100644 --- a/services/notifications/pkg/service/spaces.go +++ b/services/notifications/pkg/service/spaces.go @@ -61,12 +61,12 @@ func (s eventsNotifier) handleSpaceShared(e events.SpaceShared) { "SpaceSharer": sharerDisplayName, "SpaceName": resourceInfo.GetSpace().GetName(), "ShareLink": shareLink, - }, spaceGrantee) + }, spaceGrantee, sharerDisplayName) if err != nil { s.logger.Error().Err(err).Str("event", "SharedSpace").Msg("could not get render the email") return } - s.send(executantCtx, recipientList, sharerDisplayName) + s.send(executantCtx, recipientList) } func (s eventsNotifier) handleSpaceUnshared(e events.SpaceUnshared) { @@ -121,12 +121,12 @@ func (s eventsNotifier) handleSpaceUnshared(e events.SpaceUnshared) { "SpaceSharer": sharerDisplayName, "SpaceName": resourceInfo.GetSpace().Name, "ShareLink": shareLink, - }, spaceGrantee) + }, spaceGrantee, sharerDisplayName) if err != nil { s.logger.Error().Err(err).Str("event", "UnsharedSpace").Msg("Could not get render the email") return } - s.send(executantCtx, recipientList, sharerDisplayName) + s.send(executantCtx, recipientList) } func (s eventsNotifier) handleSpaceMembershipExpired(e events.SpaceMembershipExpired) { @@ -152,10 +152,10 @@ func (s eventsNotifier) handleSpaceMembershipExpired(e events.SpaceMembershipExp map[string]interface{}{ "SpaceName": e.SpaceName, "ExpiredAt": e.ExpiredAt.Format("2006-01-02 15:04:05"), - }, granteeList) + }, granteeList, owner.GetDisplayName()) if err != nil { s.logger.Error().Err(err).Str("event", "SpaceUnshared").Msg("could not get render the email") return } - s.send(ownerCtx, recipientList, owner.GetDisplayName()) + s.send(ownerCtx, recipientList) }