From ebff2fd6a6308d0387bbd6c2292139413e23ba2a Mon Sep 17 00:00:00 2001 From: Denis Arh Date: Sun, 31 May 2020 13:49:49 +0200 Subject: [PATCH] Implement actions & errors for notifications --- compose/rest/notification.go | 52 +- compose/service/notification.go | 169 +++-- compose/service/notifications_actions.gen.go | 612 +++++++++++++++++++ compose/service/notifications_actions.yaml | 44 ++ compose/types/notification.go | 13 + 5 files changed, 798 insertions(+), 92 deletions(-) create mode 100644 compose/service/notifications_actions.gen.go create mode 100644 compose/service/notifications_actions.yaml create mode 100644 compose/types/notification.go diff --git a/compose/rest/notification.go b/compose/rest/notification.go index d8aaecb55..0d0ce72d8 100644 --- a/compose/rest/notification.go +++ b/compose/rest/notification.go @@ -2,77 +2,59 @@ package rest import ( "context" + "fmt" "github.com/cortezaproject/corteza-server/compose/rest/request" "github.com/cortezaproject/corteza-server/compose/service" - "github.com/cortezaproject/corteza-server/pkg/mail" - - "github.com/pkg/errors" - gomail "gopkg.in/mail.v2" + "github.com/cortezaproject/corteza-server/compose/types" ) -var _ = errors.Wrap - type ( Notification struct { - notification notificationService + svc notificationService } contentPayload struct { Plain string `json:"plain"` - Html string `json:"html"` + HTML string `json:"html"` } notificationService interface { - SendEmail(context.Context, *gomail.Message) error - AttachEmailRecipients(context.Context, *gomail.Message, string, ...string) error - AttachRemoteFiles(context.Context, *gomail.Message, ...string) error + SendEmail(context.Context, *types.EmailNotification) error } ) func (Notification) New() *Notification { return &Notification{ - notification: service.DefaultNotification, + svc: service.DefaultNotification, } } // EmailSend assembles Email Message and pushes message to notification service func (ctrl *Notification) EmailSend(ctx context.Context, r *request.NotificationEmailSend) (interface{}, error) { - ntf := ctrl.notification - - msg := mail.New() - if err := ntf.AttachEmailRecipients(ctx, msg, "To", r.To...); err != nil { - return false, err - } - - if err := ntf.AttachEmailRecipients(ctx, msg, "Cc", r.Cc...); err != nil { - return false, err + ntf := &types.EmailNotification{ + To: r.To, + Cc: r.Cc, + ReplyTo: r.ReplyTo, + Subject: r.Subject, + RemoteAttachments: r.RemoteAttachments, } var cp = contentPayload{} if err := r.Content.Unmarshal(&cp); err != nil { - return false, errors.Wrap(err, "Could not unmarshal content") + return false, fmt.Errorf("could not unmarshal content: %w", err) } else { - if len(cp.Html) > 0 { - msg.SetBody("text/html", cp.Html) + if len(cp.HTML) > 0 { + ntf.ContentHTML = cp.HTML } if len(cp.Plain) > 0 { - msg.SetBody("text/plain", cp.Plain) + ntf.ContentPlain = cp.Plain } } - msg.SetHeader("Subject", r.Subject) - - if len(r.RemoteAttachments) > 0 { - err := ntf.AttachRemoteFiles(ctx, msg, r.RemoteAttachments...) - if err != nil { - return nil, err - } - } - - if err := ntf.SendEmail(ctx, msg); err != nil { + if err := ctrl.svc.SendEmail(ctx, ntf); err != nil { return false, err } else { return true, nil diff --git a/compose/service/notification.go b/compose/service/notification.go index ae12e6ac3..c1fefbda1 100644 --- a/compose/service/notification.go +++ b/compose/service/notification.go @@ -2,152 +2,207 @@ package service import ( "context" + "fmt" "net/http" "path" "strconv" "strings" "sync" - "github.com/pkg/errors" - "go.uber.org/zap" - "go.uber.org/zap/zapcore" gomail "gopkg.in/mail.v2" + "github.com/cortezaproject/corteza-server/compose/types" + "github.com/cortezaproject/corteza-server/pkg/actionlog" httpClient "github.com/cortezaproject/corteza-server/pkg/http" - "github.com/cortezaproject/corteza-server/pkg/logger" "github.com/cortezaproject/corteza-server/pkg/mail" - "github.com/cortezaproject/corteza-server/system/types" + systemTypes "github.com/cortezaproject/corteza-server/system/types" ) type ( + // notification is a service that relays notification to receipients + // currently, we only support email notifications + // + // @todo due to initial architectural decisions, this service landed under compose + // but should be moved to system + // Warning: API endpoints on compose should be kept so that we do not break backward compatibility) notification struct { - logger *zap.Logger - users notificationUserFinder + actionlog actionlog.Recorder + users notificationUserFinder } notificationUserFinder interface { - FindByID(context.Context, uint64) (*types.User, error) + FindByID(context.Context, uint64) (*systemTypes.User, error) } ) func Notification() *notification { return ¬ification{ - logger: DefaultLogger.Named("notification"), - users: DefaultSystemUser, + actionlog: DefaultActionlog, + users: DefaultSystemUser, } } -// log() returns zap's logger with requestID from current context and fields. -func (svc notification) log(ctx context.Context, fields ...zapcore.Field) *zap.Logger { - return logger.AddRequestID(ctx, svc.logger).With(fields...) +// SendEmail sends email notification +func (svc notification) SendEmail(ctx context.Context, n *types.EmailNotification) (err error) { + var ( + aProps = ¬ificationActionProps{mail: n} + ) + + err = func() error { + msg := mail.New() + + if len(n.To) == 0 { + return NotificationErrNoRecipients() + } + + if err = svc.procEmailRecipients(ctx, msg, "To", n.To...); err != nil { + return err + } + + if err = svc.procEmailRecipients(ctx, msg, "Cc", n.Cc...); err != nil { + return err + } + + if len(n.ReplyTo) > 0 { + // extra check for length because ReplyTo can only hold 1 address! + if err = svc.procEmailRecipients(ctx, msg, "ReplyTo", n.ReplyTo); err != nil { + return err + } + } + + msg.SetHeader("Subject", n.Subject) + + if len(n.ContentHTML) > 0 { + msg.SetBody("text/html", n.ContentHTML) + + } + + if len(n.ContentPlain) > 0 || len(n.ContentHTML) == 0 { + // Make sure plain body is always set, even if empty + msg.SetBody("text/plain", n.ContentPlain) + } + + if err = svc.procEmailAttachments(ctx, msg, n.RemoteAttachments...); err != nil { + + return err + } + + return mail.Send(msg) + }() + + return svc.recordAction(ctx, aProps, NotificationActionSend, err) } -func (svc notification) SendEmail(ctx context.Context, message *gomail.Message) error { - return mail.Send(message) -} - -// AttachEmailRecipients validates, resolves, formats and attaches set of recipients to message +// procEmailRecipients validates, resolves, formats and attaches set of recipients to message // // Supports 3 input formats: // - // - // - // Last one is then translated into valid email + name (when/if possible) -func (svc notification) AttachEmailRecipients(ctx context.Context, message *gomail.Message, field string, recipients ...string) (err error) { +func (svc notification) procEmailRecipients(ctx context.Context, m *gomail.Message, field string, rr ...string) (err error) { var ( email string name string ) - if len(recipients) == 0 { + if len(rr) == 0 { return } - for r, rcpt := range recipients { + for r, rcpt := range rr { + aProps := ¬ificationActionProps{recipient: rcpt} + name, email = "", "" rcpt = strings.TrimSpace(rcpt) if userID, err := strconv.ParseUint(rcpt, 10, 64); err == nil && userID > 0 { - // handle user ID + // proc if user, err := svc.users.FindByID(ctx, userID); err != nil { - return errors.Wrap(err, "could not get notification address") + return NotificationErrFailedToLoadUser(aProps).Wrap(err) } else { email = user.Email name = user.Name } } else if spaceAt := strings.Index(rcpt, " "); spaceAt > -1 { - // handle: ("foo@bar.baz foo baz") + // proc ("foo@bar.baz foo baz") email, name = rcpt[:spaceAt], strings.TrimSpace(rcpt[spaceAt+1:]) } else { - // handle: + // proc email = rcpt } // Validate email here if !mail.IsValidAddress(email) { - return errors.New("Invalid recipient email format") + return NotificationErrInvalidReceipientFormat(aProps) } - recipients[r] = message.FormatAddress(email, name) + rr[r] = m.FormatAddress(email, name) } - message.SetHeader(field, recipients...) - return + m.SetHeader(field, rr...) + return nil } -func (svc notification) AttachRemoteFiles(ctx context.Context, message *gomail.Message, rr ...string) error { +// procEmailAttachments treats given strings (URLs) as remote files, downloads and attaches them to +// the message +// +// This could/should be easily extended to support data URLs as well +// see https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs +func (svc notification) procEmailAttachments(ctx context.Context, message *gomail.Message, aa ...string) error { var ( - wg = &sync.WaitGroup{} + // threading safely (when multiple files are to be attached) l = &sync.Mutex{} + wg = &sync.WaitGroup{} client, err = httpClient.New(&httpClient.Config{ Timeout: 10, }) - - log = svc.logger ) - log.Debug("attaching files to mail notification", zap.Strings("urls", rr)) - if err != nil { - return errors.WithStack(err) + return err } - get := func(log *zap.Logger, req *http.Request) { - defer wg.Done() + get := func(url string) error { + aProps := ¬ificationActionProps{attachmentURL: url} - resp, err := client.Do(req) + req, err := client.Get(url) if err != nil { - log.Error("could not send request to download remote attachment", zap.Error(err)) - return + return err } + resp, err := client.Do(req.WithContext(ctx)) + if err != nil { + return NotificationErrFailedToDownloadAttachment(aProps).Wrap(err) + } + + aProps.setAttachmentType(resp.Header.Get("Content-Type")) + aProps.setAttachmentSize(resp.ContentLength) + if resp.StatusCode != http.StatusOK { - log.Error("could not download remote attachment", zap.String("status", resp.Status)) - return + return NotificationErrFailedToDownloadAttachment(aProps). + Wrap(fmt.Errorf("unexpected HTTP status: %s", resp.Status)) } - log.Info("download successful", - zap.Int64("content-length", resp.ContentLength), - zap.String("content-type", resp.Header.Get("Content-Type")), - ) - l.Lock() defer l.Unlock() message.AttachReader(path.Base(req.URL.Path), resp.Body) + return nil } - for _, url := range rr { - log := log.With(zap.String("remote-file", url)) - - req, err := client.Get(url) - if err != nil { - return errors.WithStack(err) - } - + for _, url := range aa { wg.Add(1) - go get(log, req) + go func() { + defer wg.Done() + _ = svc.recordAction( + ctx, + ¬ificationActionProps{attachmentURL: url}, + NotificationActionAttachmentDownload, + get(url), + ) + }() } wg.Wait() diff --git a/compose/service/notifications_actions.gen.go b/compose/service/notifications_actions.gen.go new file mode 100644 index 000000000..b0a71a44d --- /dev/null +++ b/compose/service/notifications_actions.gen.go @@ -0,0 +1,612 @@ +package service + +// This file is auto-generated from compose/service/notifications_actions.yaml +// + +import ( + "context" + "errors" + "fmt" + "strings" + "time" + + "github.com/cortezaproject/corteza-server/compose/types" + "github.com/cortezaproject/corteza-server/pkg/actionlog" +) + +type ( + notificationActionProps struct { + mail *types.EmailNotification + recipient string + attachmentURL string + attachmentSize int64 + attachmentType string + } + + notificationAction struct { + timestamp time.Time + resource string + action string + log string + severity actionlog.Severity + + // prefix for error when action fails + errorMessage string + + props *notificationActionProps + } + + notificationError struct { + timestamp time.Time + error string + resource string + action string + message string + log string + severity actionlog.Severity + + wrap error + + props *notificationActionProps + } +) + +var ( + // just a placeholder to cover template cases w/o fmt package use + _ = fmt.Println +) + +// ********************************************************************************************************************* +// ********************************************************************************************************************* +// Props methods +// setMail updates notificationActionProps's mail +// +// Allows method chaining +// +// This function is auto-generated. +// +func (p *notificationActionProps) setMail(mail *types.EmailNotification) *notificationActionProps { + p.mail = mail + return p +} + +// setRecipient updates notificationActionProps's recipient +// +// Allows method chaining +// +// This function is auto-generated. +// +func (p *notificationActionProps) setRecipient(recipient string) *notificationActionProps { + p.recipient = recipient + return p +} + +// setAttachmentURL updates notificationActionProps's attachmentURL +// +// Allows method chaining +// +// This function is auto-generated. +// +func (p *notificationActionProps) setAttachmentURL(attachmentURL string) *notificationActionProps { + p.attachmentURL = attachmentURL + return p +} + +// setAttachmentSize updates notificationActionProps's attachmentSize +// +// Allows method chaining +// +// This function is auto-generated. +// +func (p *notificationActionProps) setAttachmentSize(attachmentSize int64) *notificationActionProps { + p.attachmentSize = attachmentSize + return p +} + +// setAttachmentType updates notificationActionProps's attachmentType +// +// Allows method chaining +// +// This function is auto-generated. +// +func (p *notificationActionProps) setAttachmentType(attachmentType string) *notificationActionProps { + p.attachmentType = attachmentType + return p +} + +// serialize converts notificationActionProps to actionlog.Meta +// +// This function is auto-generated. +// +func (p notificationActionProps) serialize() actionlog.Meta { + var ( + m = make(actionlog.Meta) + ) + + if p.mail != nil { + m.Set("mail.subject", p.mail.Subject, true) + m.Set("mail.to", p.mail.To, true) + m.Set("mail.cc", p.mail.Cc, true) + m.Set("mail.replyTo", p.mail.ReplyTo, true) + m.Set("mail.contentPlain", p.mail.ContentPlain, true) + m.Set("mail.contentHTML", p.mail.ContentHTML, true) + m.Set("mail.remoteAttachments", p.mail.RemoteAttachments, true) + } + m.Set("recipient", p.recipient, true) + m.Set("attachmentURL", p.attachmentURL, true) + m.Set("attachmentSize", p.attachmentSize, true) + m.Set("attachmentType", p.attachmentType, true) + + return m +} + +// tr translates string and replaces meta value placeholder with values +// +// This function is auto-generated. +// +func (p notificationActionProps) tr(in string, err error) string { + var ( + pairs = []string{"{err}"} + // first non-empty string + fns = func(ii ...interface{}) string { + for _, i := range ii { + if s := fmt.Sprintf("%v", i); len(s) > 0 { + return s + } + } + + return "" + } + ) + + if err != nil { + for { + // Unwrap errors + ue := errors.Unwrap(err) + if ue == nil { + break + } + + err = ue + } + + pairs = append(pairs, err.Error()) + } else { + pairs = append(pairs, "nil") + } + + if p.mail != nil { + // replacement for "{mail}" (in order how fields are defined) + pairs = append( + pairs, + "{mail}", + fns( + p.mail.Subject, + p.mail.To, + p.mail.Cc, + p.mail.ReplyTo, + p.mail.ContentPlain, + p.mail.ContentHTML, + p.mail.RemoteAttachments, + ), + ) + pairs = append(pairs, "{mail.subject}", fns(p.mail.Subject)) + pairs = append(pairs, "{mail.to}", fns(p.mail.To)) + pairs = append(pairs, "{mail.cc}", fns(p.mail.Cc)) + pairs = append(pairs, "{mail.replyTo}", fns(p.mail.ReplyTo)) + pairs = append(pairs, "{mail.contentPlain}", fns(p.mail.ContentPlain)) + pairs = append(pairs, "{mail.contentHTML}", fns(p.mail.ContentHTML)) + pairs = append(pairs, "{mail.remoteAttachments}", fns(p.mail.RemoteAttachments)) + } + pairs = append(pairs, "{recipient}", fns(p.recipient)) + pairs = append(pairs, "{attachmentURL}", fns(p.attachmentURL)) + pairs = append(pairs, "{attachmentSize}", fns(p.attachmentSize)) + pairs = append(pairs, "{attachmentType}", fns(p.attachmentType)) + return strings.NewReplacer(pairs...).Replace(in) +} + +// ********************************************************************************************************************* +// ********************************************************************************************************************* +// Action methods + +// String returns loggable description as string +// +// This function is auto-generated. +// +func (a *notificationAction) String() string { + var props = ¬ificationActionProps{} + + if a.props != nil { + props = a.props + } + + return props.tr(a.log, nil) +} + +func (e *notificationAction) LoggableAction() *actionlog.Action { + return &actionlog.Action{ + Timestamp: e.timestamp, + Resource: e.resource, + Action: e.action, + Severity: e.severity, + Description: e.String(), + Meta: e.props.serialize(), + } +} + +// ********************************************************************************************************************* +// ********************************************************************************************************************* +// Error methods + +// String returns loggable description as string +// +// It falls back to message if log is not set +// +// This function is auto-generated. +// +func (e *notificationError) String() string { + var props = ¬ificationActionProps{} + + if e.props != nil { + props = e.props + } + + if e.wrap != nil && !strings.Contains(e.log, "{err}") { + // Suffix error log with {err} to ensure + // we log the cause for this error + e.log += ": {err}" + } + + return props.tr(e.log, e.wrap) +} + +// Error satisfies +// +// This function is auto-generated. +// +func (e *notificationError) Error() string { + var props = ¬ificationActionProps{} + + if e.props != nil { + props = e.props + } + + return props.tr(e.message, e.wrap) +} + +// Is fn for error equality check +// +// This function is auto-generated. +// +func (e *notificationError) Is(Resource error) bool { + t, ok := Resource.(*notificationError) + if !ok { + return false + } + + return t.resource == e.resource && t.error == e.error +} + +// Wrap wraps notificationError around another error +// +// This function is auto-generated. +// +func (e *notificationError) Wrap(err error) *notificationError { + e.wrap = err + return e +} + +// Unwrap returns wrapped error +// +// This function is auto-generated. +// +func (e *notificationError) Unwrap() error { + return e.wrap +} + +func (e *notificationError) LoggableAction() *actionlog.Action { + return &actionlog.Action{ + Timestamp: e.timestamp, + Resource: e.resource, + Action: e.action, + Severity: e.severity, + Description: e.String(), + Error: e.Error(), + Meta: e.props.serialize(), + } +} + +// ********************************************************************************************************************* +// ********************************************************************************************************************* +// Action constructors + +// NotificationActionSend returns "compose:notification.send" error +// +// This function is auto-generated. +// +func NotificationActionSend(props ...*notificationActionProps) *notificationAction { + a := ¬ificationAction{ + timestamp: time.Now(), + resource: "compose:notification", + action: "send", + log: "notification sent", + severity: actionlog.Info, + } + + if len(props) > 0 { + a.props = props[0] + } + + return a +} + +// NotificationActionAttachmentDownload returns "compose:notification.attachmentDownload" error +// +// This function is auto-generated. +// +func NotificationActionAttachmentDownload(props ...*notificationActionProps) *notificationAction { + a := ¬ificationAction{ + timestamp: time.Now(), + resource: "compose:notification", + action: "attachmentDownload", + log: "attachment downloaded", + severity: actionlog.Debug, + } + + if len(props) > 0 { + a.props = props[0] + } + + return a +} + +// ********************************************************************************************************************* +// ********************************************************************************************************************* +// Error constructors + +// NotificationErrGeneric returns "compose:notification.generic" audit event as actionlog.Error +// +// +// This function is auto-generated. +// +func NotificationErrGeneric(props ...*notificationActionProps) *notificationError { + var e = ¬ificationError{ + timestamp: time.Now(), + resource: "compose:notification", + error: "generic", + action: "error", + message: "failed to complete request due to internal error", + log: "{err}", + severity: actionlog.Error, + props: func() *notificationActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// NotificationErrFailedToLoadUser returns "compose:notification.failedToLoadUser" audit event as actionlog.Error +// +// +// This function is auto-generated. +// +func NotificationErrFailedToLoadUser(props ...*notificationActionProps) *notificationError { + var e = ¬ificationError{ + timestamp: time.Now(), + resource: "compose:notification", + error: "failedToLoadUser", + action: "error", + message: "could not load user for {recipient}", + log: "could not load user for {recipient}", + severity: actionlog.Error, + props: func() *notificationActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// NotificationErrInvalidReceipientFormat returns "compose:notification.invalidReceipientFormat" audit event as actionlog.Error +// +// +// This function is auto-generated. +// +func NotificationErrInvalidReceipientFormat(props ...*notificationActionProps) *notificationError { + var e = ¬ificationError{ + timestamp: time.Now(), + resource: "compose:notification", + error: "invalidReceipientFormat", + action: "error", + message: "invalid recipient format ({recipient})", + log: "invalid recipient format ({recipient})", + severity: actionlog.Error, + props: func() *notificationActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// NotificationErrNoRecipients returns "compose:notification.noRecipients" audit event as actionlog.Error +// +// +// This function is auto-generated. +// +func NotificationErrNoRecipients(props ...*notificationActionProps) *notificationError { + var e = ¬ificationError{ + timestamp: time.Now(), + resource: "compose:notification", + error: "noRecipients", + action: "error", + message: "can not send email message without recipients", + log: "can not send email message without recipients", + severity: actionlog.Error, + props: func() *notificationActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// NotificationErrFailedToDownloadAttachment returns "compose:notification.failedToDownloadAttachment" audit event as actionlog.Error +// +// +// This function is auto-generated. +// +func NotificationErrFailedToDownloadAttachment(props ...*notificationActionProps) *notificationError { + var e = ¬ificationError{ + timestamp: time.Now(), + resource: "compose:notification", + error: "failedToDownloadAttachment", + action: "error", + message: "could not download attachment from {attachmentURL}: {err}", + log: "could not download attachment from {attachmentURL}: {err}", + severity: actionlog.Error, + props: func() *notificationActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// ********************************************************************************************************************* +// ********************************************************************************************************************* + +// recordAction is a service helper function wraps function that can return error +// +// context is used to enrich audit log entry with current user info, request ID, IP address... +// props are collected action/error properties +// action (optional) fn will be used to construct notificationAction struct from given props (and error) +// err is any error that occurred while action was happening +// +// Action has success and fail (error) state: +// - when recorded without an error (4th param), action is recorded as successful. +// - when an additional error is given (4th param), action is used to wrap +// the additional error +// +// This function is auto-generated. +// +func (svc notification) recordAction(ctx context.Context, props *notificationActionProps, action func(...*notificationActionProps) *notificationAction, err error) error { + var ( + ok bool + + // Return error + retError *notificationError + + // Recorder error + recError *notificationError + ) + + if err != nil { + if retError, ok = err.(*notificationError); !ok { + // got non-notification error, wrap it with NotificationErrGeneric + retError = NotificationErrGeneric(props).Wrap(err) + + if action != nil { + // copy action to returning and recording error + retError.action = action().action + } + + // we'll use NotificationErrGeneric for recording too + // because it can hold more info + recError = retError + } else if retError != nil { + if action != nil { + // copy action to returning and recording error + retError.action = action().action + } + // start with copy of return error for recording + // this will be updated with tha root cause as we try and + // unwrap the error + recError = retError + + // find the original recError for this error + // for the purpose of logging + var unwrappedError error = retError + for { + if unwrappedError = errors.Unwrap(unwrappedError); unwrappedError == nil { + // nothing wrapped + break + } + + // update recError ONLY of wrapped error is of type notificationError + if unwrappedSinkError, ok := unwrappedError.(*notificationError); ok { + recError = unwrappedSinkError + } + } + + if retError.props == nil { + // set props on returning error if empty + retError.props = props + } + + if recError.props == nil { + // set props on recording error if empty + recError.props = props + } + } + } + + if svc.actionlog != nil { + if retError != nil { + // failed action, log error + svc.actionlog.Record(ctx, recError) + } else if action != nil { + // successful + svc.actionlog.Record(ctx, action(props)) + } + } + + if err == nil { + // retError not an interface and that WILL (!!) cause issues + // with nil check (== nil) when it is not explicitly returned + return nil + } + + return retError +} diff --git a/compose/service/notifications_actions.yaml b/compose/service/notifications_actions.yaml new file mode 100644 index 000000000..786799f8b --- /dev/null +++ b/compose/service/notifications_actions.yaml @@ -0,0 +1,44 @@ +# List of loggable service actions + +resource: compose:notification +service: notification + +# Default sensitivity for actions +defaultActionSeverity: info + +# default severity for errors +defaultErrorSeverity: error + +import: + - github.com/cortezaproject/corteza-server/compose/types + +props: + - name: mail + type: "*types.EmailNotification" + fields: [ subject, to, cc, replyTo, contentPlain, contentHTML, remoteAttachments ] + - name: recipient + - name: attachmentURL + - name: attachmentSize + type: int64 + - name: attachmentType + +actions: + - action: send + log: "notification sent" + + - action: attachmentDownload + log: "attachment downloaded" + severity: debug + +errors: + - error: failedToLoadUser + message: "could not load user for {recipient}" + + - error: invalidReceipientFormat + message: "invalid recipient format ({recipient})" + + - error: noRecipients + message: "can not send email message without recipients" + + - error: failedToDownloadAttachment + message: "could not download attachment from {attachmentURL}: {err}" diff --git a/compose/types/notification.go b/compose/types/notification.go new file mode 100644 index 000000000..b58a917d6 --- /dev/null +++ b/compose/types/notification.go @@ -0,0 +1,13 @@ +package types + +type ( + EmailNotification struct { + To []string + Cc []string + ReplyTo string + Subject string + ContentPlain string + ContentHTML string + RemoteAttachments []string + } +)