mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-05-05 22:53:34 -04:00
* The email HTML templates added #6146 * use a single palne text email template. use fs.FS * Update services/notifications/README.md Co-authored-by: Martin <github@diemattels.at> * Update services/notifications/README.md Co-authored-by: Martin <github@diemattels.at> * fix md --------- Co-authored-by: Roman Perekhod <rperekhod@owncloud.com> Co-authored-by: Martin <github@diemattels.at>
This commit is contained in:
6
changelog/unreleased/add-html-email-templates.md
Normal file
6
changelog/unreleased/add-html-email-templates.md
Normal file
@@ -0,0 +1,6 @@
|
||||
Enhancement: Add the email HTML templates
|
||||
|
||||
Add the email HTML templates
|
||||
|
||||
https://github.com/owncloud/ocis/pull/6147
|
||||
https://github.com/owncloud/ocis/issues/6146
|
||||
@@ -4,37 +4,43 @@ The notification service is responsible for sending emails to users informing th
|
||||
|
||||
## Email Notification Templates
|
||||
|
||||
The `notifications` service has embedded email body templates. Email templates can use the placeholders `{{ .Greeting }}`, `{{ .MessageBody }}` and `{{ .CallToAction }}` which are replaced with translations when sent, see the [Translations](#translations) section for more details. Depending on the email purpose, placeholders will contain different strings. An individual translatable string is available for each purpose, finally resolved by the placeholder. Though the email subject is also part of translations, it has no placeholder as it is a mandatory email component. The embedded templates are available for all deployment scenarios.
|
||||
The `notifications` service has embedded email text and html body templates. Email templates can use the placeholders `{{ .Greeting }}`, `{{ .MessageBody }}` and `{{ .CallToAction }}` which are replaced with translations when sent, see the [Translations](#translations) section for more details. Depending on the email purpose, placeholders will contain different strings. An individual translatable string is available for each purpose, finally resolved by the placeholder. Though the email subject is also part of translations, it has no placeholder as it is a mandatory email component. The embedded templates are available for all deployment scenarios.
|
||||
|
||||
```text
|
||||
template
|
||||
template
|
||||
placeholders
|
||||
translated strings <-- source strings <-- purpose
|
||||
final output
|
||||
```
|
||||
|
||||
In addition, the notifications service supports custom templates. Custom email templates take precedence over the embedded ones. If a custom email template exists, the embedded templates are not used. To configure custom email templates, the `NOTIFICATIONS_EMAIL_TEMPLATE_PATH` environment variable needs to point to a base folder that will contain the email templates. This path must be available from all instances of the notifications service, a shared storage is recommended. The source templates provided by ocis you can derive from are located in following base folder [https://github.com/owncloud/ocis/tree/master/services/notifications/pkg/email/templates](https://github.com/owncloud/ocis/tree/master/services/notifications/pkg/email/templates) with subfolders `shares` and `spaces`.
|
||||
In addition, the notifications service supports custom templates. Custom email templates take precedence over the embedded ones. If a custom email template exists, the embedded templates are not used. To configure custom email templates, the `NOTIFICATIONS_EMAIL_TEMPLATE_PATH` environment variable needs to point to a base folder that will contain the email templates and follow the [templates subfolder hierarchy](#templates-subfolder-hierarchy).This path must be available from all instances of the notifications service, a shared storage is recommended.
|
||||
```text
|
||||
{NOTIFICATIONS_EMAIL_TEMPLATE_PATH}/templates/text/email.text.tmpl
|
||||
{NOTIFICATIONS_EMAIL_TEMPLATE_PATH}/templates/html/email.html.tmpl
|
||||
{NOTIFICATIONS_EMAIL_TEMPLATE_PATH}/templates/html/img/
|
||||
```
|
||||
The source templates provided by ocis you can derive from are located in the following base folder [https://github.com/owncloud/ocis/tree/master/services/notifications/pkg/email/templates](https://github.com/owncloud/ocis/tree/master/services/notifications/pkg/email/templates) with subfolders `templates/text` and `templates/html`.
|
||||
|
||||
- [shares/shareCreated.email.body.tmpl](https://github.com/owncloud/ocis/blob/master/services/notifications/pkg/email/templates/shares/shareCreated.email.body.tmpl)
|
||||
- [shares/shareExpired.email.body.tmpl](https://github.com/owncloud/ocis/blob/master/services/notifications/pkg/email/templates/shares/shareExpired.email.body.tmpl)
|
||||
- [spaces/membershipExpired.email.body.tmpl](https://github.com/owncloud/ocis/blob/master/services/notifications/pkg/email/templates/spaces/membershipExpired.email.body.tmpl)
|
||||
- [spaces/sharedSpace.email.body.tmpl](https://github.com/owncloud/ocis/blob/master/services/notifications/pkg/email/templates/spaces/sharedSpace.email.body.tmpl)
|
||||
- [spaces/unsharedSpace.email.body.tmpl](https://github.com/owncloud/ocis/blob/master/services/notifications/pkg/email/templates/spaces/unsharedSpace.email.body.tmpl)
|
||||
- [text/email.text.tmpl](https://github.com/owncloud/ocis/blob/master/services/notifications/pkg/email/templates/text/email.text.tmpl)
|
||||
- [html/email.html.tmpl](https://github.com/owncloud/ocis/blob/master/services/notifications/pkg/email/templates/html/email.html.tmpl)
|
||||
|
||||
### Templates subfolder hierarchy
|
||||
```text
|
||||
templates
|
||||
│
|
||||
└───shares
|
||||
│ │ shareCreated.email.body.tmpl
|
||||
│ │ shareExpired.email.body.tmpl
|
||||
└───html
|
||||
│ │ email.html.tmpl
|
||||
│ │
|
||||
│ └───img
|
||||
│ │ logo-mail.gif
|
||||
│
|
||||
└───spaces
|
||||
│ membershipExpired.email.body.tmpl
|
||||
│ sharedSpace.email.body.tmpl
|
||||
│ unsharedSpace.email.body.tmpl
|
||||
└───text
|
||||
│ email.text.tmpl
|
||||
```
|
||||
|
||||
Custom email templates referenced via `NOTIFICATIONS_EMAIL_TEMPLATE_PATH` must also be located in subfolders `shares` and `spaces` and must have the same names as the embedded templates. It is important that the names of these files and folders match the embedded ones.
|
||||
Custom email templates referenced via `NOTIFICATIONS_EMAIL_TEMPLATE_PATH` must also be located in subfolder `templates/text` and `templates/html` and must have the same names as the embedded templates. It is important that the names of these files and folders match the embedded ones.
|
||||
The `templates/html` subfolder contains a default HTML template provided by ocis. When using a custom HTML template, hosted images can either be linked with standard HTML code like ```<img src="https://raw.githubusercontent.com/owncloud/core/master/core/img/logo-mail.gif" alt="logo-mail"/>``` or embedded as a CID source ```<img src="cid:logo-mail.gif" alt="logo-mail"/>```. In the latter case, image files must be located in the `templates/html/img` subfolder. Supported embedded image types are png, jpeg, and gif.
|
||||
Consider that embedding images via a CID resource may not be fully supported in all email web clients.
|
||||
|
||||
## Translations
|
||||
|
||||
|
||||
@@ -7,10 +7,6 @@ import (
|
||||
"fmt"
|
||||
"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,39 +16,31 @@ 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
|
||||
}
|
||||
|
||||
// Mail is the communication channel for email.
|
||||
type Mail struct {
|
||||
gatewayClient gateway.GatewayAPIClient
|
||||
conf config.Config
|
||||
logger log.Logger
|
||||
conf config.Config
|
||||
logger log.Logger
|
||||
}
|
||||
|
||||
func (m Mail) getMailClient() (*mail.SMTPClient, error) {
|
||||
@@ -111,73 +99,31 @@ 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 == "" {
|
||||
m.logger.Info().Str("mail", "SendMessage").Msg("failed to send a message. SMTP host is not set")
|
||||
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.SetFrom(m.conf.Notifications.SMTP.Sender).AddTo(message.Recipient...)
|
||||
}
|
||||
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})
|
||||
}
|
||||
}
|
||||
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")
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
addresses = append(addresses, res.User.Mail)
|
||||
}
|
||||
|
||||
return addresses, nil
|
||||
}
|
||||
|
||||
@@ -1,36 +1,70 @@
|
||||
package email
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"io/fs"
|
||||
"bytes"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/owncloud/ocis/v2/services/notifications/pkg/email/l10n"
|
||||
)
|
||||
|
||||
var (
|
||||
//go:embed l10n/locale
|
||||
_translationFS embed.FS
|
||||
_domain = "notifications"
|
||||
)
|
||||
|
||||
// ComposeMessage renders the message based on template
|
||||
func ComposeMessage(template, locale string, path string) string {
|
||||
raw := loadTemplate(template, locale, path)
|
||||
return replacePlaceholders(raw)
|
||||
// NewTextTemplate replace the body message template placeholders with the translated template
|
||||
func NewTextTemplate(mt MessageTemplate, locale string, translationPath string, vars map[string]interface{}) (MessageTemplate, error) {
|
||||
var err error
|
||||
t := l10n.NewTranslator(locale, translationPath)
|
||||
mt.Subject, err = composeMessage(t.Translate(mt.Subject), vars)
|
||||
if err != nil {
|
||||
return mt, err
|
||||
}
|
||||
mt.Greeting, err = composeMessage(t.Translate(mt.Greeting), vars)
|
||||
if err != nil {
|
||||
return mt, err
|
||||
}
|
||||
mt.MessageBody, err = composeMessage(t.Translate(mt.MessageBody), vars)
|
||||
if err != nil {
|
||||
return mt, err
|
||||
}
|
||||
mt.CallToAction, err = composeMessage(t.Translate(mt.CallToAction), vars)
|
||||
if err != nil {
|
||||
return mt, err
|
||||
}
|
||||
return mt, nil
|
||||
}
|
||||
|
||||
func loadTemplate(template, locale string, path string) string {
|
||||
// Create Locale with library path and language code and load default domain
|
||||
var l *gotext.Locale
|
||||
if path == "" {
|
||||
filesystem, _ := fs.Sub(_translationFS, "l10n/locale")
|
||||
l = gotext.NewLocaleFS(locale, filesystem)
|
||||
} else { // use custom path instead
|
||||
l = gotext.NewLocale(path, locale)
|
||||
// NewHTMLTemplate replace the body message template placeholders with the translated template
|
||||
func NewHTMLTemplate(mt MessageTemplate, locale string, translationPath string, vars map[string]interface{}) (MessageTemplate, error) {
|
||||
var err error
|
||||
t := l10n.NewTranslator(locale, translationPath)
|
||||
mt.Subject, err = composeMessage(t.Translate(mt.Subject), vars)
|
||||
if err != nil {
|
||||
return mt, err
|
||||
}
|
||||
l.AddDomain(_domain) // make domain configurable only if needed
|
||||
return l.Get(template)
|
||||
mt.Greeting, err = composeMessage(newlineToBr(t.Translate(mt.Greeting)), vars)
|
||||
if err != nil {
|
||||
return mt, err
|
||||
}
|
||||
mt.MessageBody, err = composeMessage(newlineToBr(t.Translate(mt.MessageBody)), vars)
|
||||
if err != nil {
|
||||
return mt, err
|
||||
}
|
||||
mt.CallToAction, err = composeMessage(callToActionToHTML(t.Translate(mt.CallToAction)), vars)
|
||||
if err != nil {
|
||||
return mt, err
|
||||
}
|
||||
return mt, nil
|
||||
}
|
||||
|
||||
// composeMessage renders the message based on template
|
||||
func composeMessage(tmpl string, vars map[string]interface{}) (string, error) {
|
||||
tpl, err := template.New("").Parse(replacePlaceholders(tmpl))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var writer bytes.Buffer
|
||||
if err := tpl.Execute(&writer, vars); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return writer.String(), nil
|
||||
}
|
||||
|
||||
func replacePlaceholders(raw string) string {
|
||||
@@ -39,3 +73,15 @@ func replacePlaceholders(raw string) string {
|
||||
}
|
||||
return raw
|
||||
}
|
||||
|
||||
func newlineToBr(s string) string {
|
||||
return strings.Replace(s, "\n", "<br>", -1)
|
||||
}
|
||||
|
||||
func callToActionToHTML(s string) string {
|
||||
if strings.TrimSpace(s) == "" {
|
||||
return ""
|
||||
}
|
||||
s = strings.TrimSuffix(s, "{{ .ShareLink }}")
|
||||
return `<a href="{{ .ShareLink }}">` + s + `</a>`
|
||||
}
|
||||
|
||||
@@ -6,68 +6,87 @@ package email
|
||||
import (
|
||||
"bytes"
|
||||
"embed"
|
||||
"html"
|
||||
"html/template"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/owncloud/ocis/v2/services/notifications/pkg/channels"
|
||||
)
|
||||
|
||||
var (
|
||||
//go:embed templates
|
||||
templatesFS embed.FS
|
||||
|
||||
imgDir = filepath.Join("templates", "html", "img")
|
||||
)
|
||||
|
||||
// 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 "", "", err
|
||||
return nil, err
|
||||
}
|
||||
tpl, err := parseTemplate(emailTemplatePath, mt.textTemplate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
textBody, err := emailTemplate(tpl, textMt)
|
||||
if err != nil {
|
||||
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)
|
||||
htmlTpl, err := parseTemplate(emailTemplatePath, mt.htmlTemplate)
|
||||
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))
|
||||
htmlBody, err := emailTemplate(htmlTpl, htmlMt)
|
||||
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))
|
||||
return nil, err
|
||||
}
|
||||
var data map[string][]byte
|
||||
if emailTemplatePath != "" {
|
||||
data, err = readImages(emailTemplatePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
data, err = readImagesFs()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
str, err := executeTemplate(tpl, mt)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return html.UnescapeString(str), err
|
||||
|
||||
return &channels.Message{
|
||||
Subject: textMt.Subject,
|
||||
TextBody: textBody,
|
||||
HTMLBody: htmlBody,
|
||||
AttachInline: data,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func executeRaw(raw string, vars map[string]interface{}) (string, error) {
|
||||
tpl, err := template.New("").Parse(raw)
|
||||
func emailTemplate(tpl *template.Template, mt MessageTemplate) (string, error) {
|
||||
str, err := executeTemplate(tpl, map[string]interface{}{
|
||||
"Greeting": template.HTML(strings.TrimSpace(mt.Greeting)),
|
||||
"MessageBody": template.HTML(strings.TrimSpace(mt.MessageBody)),
|
||||
"CallToAction": template.HTML(strings.TrimSpace(mt.CallToAction)),
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return executeTemplate(tpl, vars)
|
||||
return str, err
|
||||
}
|
||||
|
||||
func parseTemplate(emailTemplatePath string, file string) (*template.Template, error) {
|
||||
if emailTemplatePath != "" {
|
||||
return template.ParseFiles(filepath.Join(emailTemplatePath, file))
|
||||
}
|
||||
return template.ParseFS(templatesFS, filepath.Join(file))
|
||||
}
|
||||
|
||||
func executeTemplate(tpl *template.Template, vars any) (string, error) {
|
||||
@@ -77,3 +96,56 @@ func executeTemplate(tpl *template.Template, vars any) (string, error) {
|
||||
}
|
||||
return writer.String(), nil
|
||||
}
|
||||
|
||||
func readImagesFs() (map[string][]byte, error) {
|
||||
dir := filepath.Join(imgDir)
|
||||
entries, err := templatesFS.ReadDir(dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return read(entries, templatesFS)
|
||||
}
|
||||
|
||||
func readImages(emailTemplatePath string) (map[string][]byte, error) {
|
||||
dir := filepath.Join(emailTemplatePath, imgDir)
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return read(entries, os.DirFS(emailTemplatePath))
|
||||
}
|
||||
|
||||
func read(entries []fs.DirEntry, fsys fs.FS) (map[string][]byte, error) {
|
||||
list := make(map[string][]byte)
|
||||
for _, e := range entries {
|
||||
if !e.IsDir() {
|
||||
file, err := fs.ReadFile(fsys, filepath.Join(imgDir, 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
|
||||
}
|
||||
|
||||
43
services/notifications/pkg/email/l10n/locate.go
Normal file
43
services/notifications/pkg/email/l10n/locate.go
Normal file
@@ -0,0 +1,43 @@
|
||||
// Package l10n implements utility for translation the text templates.
|
||||
//
|
||||
// The l10n package use transifex translation for text templates.
|
||||
package l10n
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"io/fs"
|
||||
|
||||
"github.com/leonelquinteros/gotext"
|
||||
)
|
||||
|
||||
var (
|
||||
//go:embed locale
|
||||
_translationFS embed.FS
|
||||
_domain = "notifications"
|
||||
)
|
||||
|
||||
// Translator is the interface to the translation
|
||||
type Translator interface {
|
||||
Translate(str string) string
|
||||
}
|
||||
|
||||
type translator struct {
|
||||
l *gotext.Locale
|
||||
}
|
||||
|
||||
// NewTranslator Create Translator with library path and language code and load default domain
|
||||
func NewTranslator(local string, path string) Translator {
|
||||
var l *gotext.Locale
|
||||
if path == "" {
|
||||
filesystem, _ := fs.Sub(_translationFS, "locale")
|
||||
l = gotext.NewLocaleFS(local, filesystem)
|
||||
} else { // use custom path instead
|
||||
l = gotext.NewLocale(path, local)
|
||||
}
|
||||
l.AddDomain(_domain) // make domain configurable only if needed
|
||||
return &translator{l: l}
|
||||
}
|
||||
|
||||
func (t *translator) Translate(str string) string {
|
||||
return t.l.Get(str)
|
||||
}
|
||||
@@ -7,7 +7,8 @@ func Template(s string) string { return s }
|
||||
var (
|
||||
// Shares
|
||||
ShareCreated = MessageTemplate{
|
||||
bodyTemplate: "shares/shareCreated.email.body.tmpl",
|
||||
textTemplate: "templates/text/email.text.tmpl",
|
||||
htmlTemplate: "templates/html/email.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: "templates/text/email.text.tmpl",
|
||||
htmlTemplate: "templates/html/email.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: "templates/text/email.text.tmpl",
|
||||
htmlTemplate: "templates/html/email.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: "templates/text/email.text.tmpl",
|
||||
htmlTemplate: "templates/html/email.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: "templates/text/email.text.tmpl",
|
||||
htmlTemplate: "templates/html/email.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
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<table cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||
<tr>
|
||||
<td>
|
||||
<table cellspacing="0" cellpadding="0" border="0" width="600px">
|
||||
<tr>
|
||||
<td bgcolor="#041e41" width="20px"> </td>
|
||||
<td bgcolor="#041e41">
|
||||
<img src="cid:logo-mail.gif" alt="logo-mail"/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2"> </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="20px"> </td>
|
||||
<td style="font-weight:normal; font-size:0.8em; line-height:1.2em; font-family:verdana,'arial',sans;">
|
||||
{{ .Greeting }}
|
||||
<br><br>
|
||||
{{ .MessageBody }}
|
||||
{{if ne .CallToAction "" }}
|
||||
<br><br>{{ .CallToAction }}
|
||||
{{end}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2"> </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="20px"> </td>
|
||||
<td style="font-weight:normal; font-size:0.8em; line-height:1.2em; font-family:verdana,'arial',sans;">
|
||||
<footer>
|
||||
<br>
|
||||
<br>
|
||||
--- <br>
|
||||
ownCloud - Store. Share. Work.<br>
|
||||
<a href="https://owncloud.com">https://owncloud.com</a>
|
||||
</footer>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2"> </td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.2 KiB |
@@ -1,8 +0,0 @@
|
||||
{{ .Greeting }}
|
||||
|
||||
{{ .MessageBody }}
|
||||
|
||||
|
||||
---
|
||||
ownCloud - Store. Share. Work.
|
||||
https://owncloud.com
|
||||
@@ -1,8 +0,0 @@
|
||||
{{ .Greeting }}
|
||||
|
||||
{{ .MessageBody }}
|
||||
|
||||
|
||||
---
|
||||
ownCloud - Store. Share. Work.
|
||||
https://owncloud.com
|
||||
@@ -1,10 +0,0 @@
|
||||
{{ .Greeting }}
|
||||
|
||||
{{ .MessageBody }}
|
||||
|
||||
{{ .CallToAction }}
|
||||
|
||||
|
||||
---
|
||||
ownCloud - Store. Share. Work.
|
||||
https://owncloud.com
|
||||
@@ -1,10 +0,0 @@
|
||||
{{ .Greeting }}
|
||||
|
||||
{{ .MessageBody }}
|
||||
|
||||
{{ .CallToAction }}
|
||||
|
||||
|
||||
---
|
||||
ownCloud - Store. Share. Work.
|
||||
https://owncloud.com
|
||||
@@ -1,9 +1,9 @@
|
||||
{{ .Greeting }}
|
||||
|
||||
{{ .MessageBody }}
|
||||
|
||||
{{if ne .CallToAction "" }}
|
||||
{{ .CallToAction }}
|
||||
|
||||
{{end}}
|
||||
|
||||
---
|
||||
ownCloud - Store. Share. Work.
|
||||
@@ -97,98 +97,95 @@ 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 := s.getUserLang(ctx, userID)
|
||||
grantee, err := s.getUserName(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fields[granteeFieldName] = grantee
|
||||
messageList := make([]*channels.Message, len(granteeList))
|
||||
for i, usr := range granteeList {
|
||||
locale := s.getUserLang(ctx, usr.GetId())
|
||||
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, executant, u *user.UserId, g *group.GroupId) ([]*user.UserId, error) {
|
||||
func (s eventsNotifier) getGranteeList(ctx context.Context, executant, u *user.UserId, g *group.GroupId) ([]*user.User, error) {
|
||||
switch {
|
||||
case u != nil:
|
||||
if s.disableEmails(ctx, u) {
|
||||
return []*user.UserId{}, nil
|
||||
return nil, 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")
|
||||
}
|
||||
|
||||
var grantees []*user.UserId
|
||||
userList := make([]*user.User, 0, len(res.GetGroup().GetMembers()))
|
||||
for _, userID := range res.GetGroup().GetMembers() {
|
||||
// don't add the executant
|
||||
if userID.GetOpaqueId() == executant.GetOpaqueId() {
|
||||
continue
|
||||
}
|
||||
|
||||
// don't add users who opted out
|
||||
if s.disableEmails(ctx, userID) {
|
||||
continue
|
||||
}
|
||||
|
||||
grantees = append(grantees, userID)
|
||||
usr, err := s.getUser(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
userList = append(userList, usr)
|
||||
}
|
||||
return grantees, 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) 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 {
|
||||
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 {
|
||||
@@ -226,8 +223,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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user