diff --git a/cmd/send.go b/cmd/send.go index d133ab0..185789f 100644 --- a/cmd/send.go +++ b/cmd/send.go @@ -9,6 +9,7 @@ import ( func sendCmd() *cobra.Command { var serverURL string + var recipientsFilter string cmd := &cobra.Command{ Use: "send [content] [list]", @@ -24,7 +25,7 @@ func sendCmd() *cobra.Command { } if u := serverURL; u == "" { - return mail.LoadAndSendCampaign(cfg, args[0], args[1]) + return mail.LoadAndSendCampaign(cfg, args[0], args[1], recipientsFilter) } else { return client.New(cmd.Context(), u).Send(client.SendArgs{ ProjectPath: ".", // TODO: configurable @@ -38,6 +39,7 @@ func sendCmd() *cobra.Command { // Server to specify remote server cmd.Flags().StringVar(&serverURL, "server", "", "URL of server") + cmd.Flags().StringVar(&recipientsFilter, "filter", "", "Recipients filter") return cmd } diff --git a/go.mod b/go.mod index a5189b1..861c7bb 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.24.1 require ( github.com/PuerkitoBio/goquery v1.10.3 github.com/bep/inflect v0.0.0-20160408190323-b896c45f5af9 + github.com/casbin/govaluate v1.10.0 github.com/cenkalti/backoff/v5 v5.0.3 github.com/charmbracelet/glamour v0.10.0 github.com/chris-ramon/douceur v0.2.0 diff --git a/go.sum b/go.sum index 451dbbb..c5748f6 100644 --- a/go.sum +++ b/go.sum @@ -16,6 +16,8 @@ github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuP github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/bep/inflect v0.0.0-20160408190323-b896c45f5af9 h1:2ZyfRr6MKtNow0D0AbbVlzrS3OI6a+svlOHrtFYGI9Q= github.com/bep/inflect v0.0.0-20160408190323-b896c45f5af9/go.mod h1:/fmCHLLmoBKSfptXUFVJZb7MMt7JCS4vm0vqQmAo3xE= +github.com/casbin/govaluate v1.10.0 h1:ffGw51/hYH3w3rZcxO/KcaUIDOLP84w7nsidMVgaDG0= +github.com/casbin/govaluate v1.10.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/charmbracelet/colorprofile v0.3.2 h1:9J27WdztfJQVAQKX2WOlSSRB+5gaKqqITmrvb1uTIiI= diff --git a/mail/campaign.go b/mail/campaign.go index 965bf4b..52b0be7 100644 --- a/mail/campaign.go +++ b/mail/campaign.go @@ -39,7 +39,7 @@ type tmplContext struct { } type Campaign struct { - Recipients []*ctxRecipient + Recipients CtxRecipients EmailMeta *ctxCampaign Email parser.Email diff --git a/mail/context.go b/mail/context.go index 84d4fa5..d306aab 100644 --- a/mail/context.go +++ b/mail/context.go @@ -44,7 +44,9 @@ func newRecipient(data map[string]any) ctxRecipient { // Campaign variable type ctxCampaign struct { - From string + From string + // Filter recipients + Filter string Params map[string]interface{} // Original subject from frontmatter @@ -73,6 +75,7 @@ func newCampaign(cfg *config.AConfig, data map[string]interface{}) ctxCampaign { if c.From == "" { c.From = cfg.From } + c.Filter, _ = c.Params["filter"].(string) // This will cast either an array or an invidivual string into an array. // We remove blanks because an empty string will become []string{""} @@ -86,6 +89,7 @@ func newCampaign(cfg *config.AConfig, data map[string]interface{}) ctxCampaign { delete(c.Params, "subject") delete(c.Params, "from") delete(c.Params, "to") + delete(c.Params, "filter") return c } diff --git a/mail/ctx_recipients.go b/mail/ctx_recipients.go new file mode 100644 index 0000000..94e5f1f --- /dev/null +++ b/mail/ctx_recipients.go @@ -0,0 +1,41 @@ +package mail + +import ( + "github.com/casbin/govaluate" +) + +type CtxRecipients []*ctxRecipient + +// Filter filters the recipients based on the provided filter expression. +// It evaluates the filter expression against each recipient's parameters +// and returns a slice of recipients that match the criteria. +// +// Parameters: +// +// filter: A string representing the filter expression to evaluate. +// The expression should be in a format compatible with +// the govaluate library. +// +// Returns: +// +// A slice of pointers to ctxRecipient that match the filter criteria, +// or an error if the evaluation of the expression fails or if any +// other error occurs during the filtering process. +func (cr CtxRecipients) Filter(filter string) ([]*ctxRecipient, error) { + expression, err := govaluate.NewEvaluableExpression(filter) + if err != nil { + return nil, err + } + + var filteredRecipients []*ctxRecipient + for _, r := range cr { + result, err := expression.Evaluate(r.Params) + if err != nil { + return nil, err + } + if result == true { + filteredRecipients = append(filteredRecipients, r) + } + } + return filteredRecipients, nil +} diff --git a/mail/ctx_recipients_test.go b/mail/ctx_recipients_test.go new file mode 100644 index 0000000..87bb60a --- /dev/null +++ b/mail/ctx_recipients_test.go @@ -0,0 +1,29 @@ +package mail + +import "testing" + +func TestCtxRecipientsFilter(t *testing.T) { + var recipients CtxRecipients = []*ctxRecipient{ + &ctxRecipient{ + Name: "Name1", + Email: "name1@example.com", + Params: map[string]interface{}{ + "class": "1", + }, + }, + &ctxRecipient{ + Name: "Name2", + Email: "name2@example.com", + Params: map[string]interface{}{ + "class": "2", + }, + }, + } + filtered, err := recipients.Filter("class == '1'") + if err != nil { + t.Errorf("Failed: %s", err) + } + if len(filtered) != 1 { + t.Errorf("Got %d", len(filtered)) + } +} diff --git a/mail/sender.go b/mail/sender.go index cd2c069..b14122c 100644 --- a/mail/sender.go +++ b/mail/sender.go @@ -28,13 +28,26 @@ type sendConn interface { Close() error } -func LoadAndSendCampaign(cfg *config.AConfig, tmplFile, recipientFile string) error { +func LoadAndSendCampaign(cfg *config.AConfig, tmplFile, recipientFile string, filter string) error { // Load up template and recipientswith frontmatter c, err := LoadCampaign(cfg, tmplFile, recipientFile) if err != nil { return err } + if filter == "" { + // Argument specified: use the possibly declared in Campaign + filter = c.EmailMeta.Filter + } + if filter != "" { + // Filter the recipients + filteredRecipients, err := c.Recipients.Filter(filter) + if err != nil { + return err + } + c.Recipients = filteredRecipients + } + return SendCampaign(cfg, c) } diff --git a/paperboy b/paperboy new file mode 100755 index 0000000..30634d8 Binary files /dev/null and b/paperboy differ diff --git a/server/send.go b/server/send.go index e64dd8f..fa578fe 100644 --- a/server/send.go +++ b/server/send.go @@ -106,6 +106,6 @@ func (r *Resolver) SendCampaign(ctx context.Context, args SendCampaignArgs) (boo } // Load campaign and recipient list, and send it 🚀 - err = mail.LoadAndSendCampaign(cfg, args.Campaign, args.List) + err = mail.LoadAndSendCampaign(cfg, args.Campaign, args.List, "") return err == nil, err }