Implement actions & errors for notifications
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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:
|
||||
// - <valid email>
|
||||
// - <valid email><space><name...>
|
||||
// - <userID>
|
||||
// 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 <user ID>
|
||||
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: <email> <name> ("foo@bar.baz foo baz")
|
||||
// proc <email> <name> ("foo@bar.baz foo baz")
|
||||
email, name = rcpt[:spaceAt], strings.TrimSpace(rcpt[spaceAt+1:])
|
||||
} else {
|
||||
// handle: <email>
|
||||
// proc <email>
|
||||
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()
|
||||
|
||||
612
compose/service/notifications_actions.gen.go
Normal file
612
compose/service/notifications_actions.gen.go
Normal file
@@ -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
|
||||
}
|
||||
44
compose/service/notifications_actions.yaml
Normal file
44
compose/service/notifications_actions.yaml
Normal file
@@ -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}"
|
||||
13
compose/types/notification.go
Normal file
13
compose/types/notification.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package types
|
||||
|
||||
type (
|
||||
EmailNotification struct {
|
||||
To []string
|
||||
Cc []string
|
||||
ReplyTo string
|
||||
Subject string
|
||||
ContentPlain string
|
||||
ContentHTML string
|
||||
RemoteAttachments []string
|
||||
}
|
||||
)
|
||||
Reference in New Issue
Block a user