Basic system autuomation, mail processing
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
package mail
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/mail"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/cortezaproject/corteza-server/pkg/automation"
|
||||
"github.com/cortezaproject/corteza-server/system/types"
|
||||
)
|
||||
|
||||
// Trigger condition:
|
||||
// Matcher for mail headers
|
||||
|
||||
type (
|
||||
Condition struct {
|
||||
MatchAll bool `json:"matchAll"`
|
||||
Headers []HeaderMatcher `json:"headers"`
|
||||
}
|
||||
|
||||
HeaderMatcher struct {
|
||||
Name HMName `json:"name"`
|
||||
Op HMOp `json:"op"`
|
||||
Match string `json:"match"`
|
||||
|
||||
// Compiled regexp
|
||||
re *regexp.Regexp
|
||||
}
|
||||
|
||||
HMName string
|
||||
HMOp string
|
||||
|
||||
userExistanceVerifier func(string) bool
|
||||
)
|
||||
|
||||
const (
|
||||
HeaderMatchNameFrom HMName = "from"
|
||||
HeaderMatchNameTo = "to"
|
||||
HeaderMatchNameCC = "cc"
|
||||
HeaderMatchNameBCC = "bcc"
|
||||
HeaderMatchNameReplyTo = "reply-to"
|
||||
HeaderMatchNameSubject = "subject"
|
||||
|
||||
HMOpEqualCi HMOp = ""
|
||||
HMOpSuffixCi = "suffix-ci"
|
||||
HMOpPrefixCi = "prefix-ci"
|
||||
HMOpRegex = "regex"
|
||||
HMOpUser = "user"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrUnknownHeaderMatcherName = errors.New("unknown header matcher field")
|
||||
ErrUnknownHeaderMatcherOperator = errors.New("unknown header matcher operator")
|
||||
ErrInvalidHeaderMatcherValue = errors.New("invalid header matcher value")
|
||||
)
|
||||
|
||||
func (c *Condition) Prepare() (err error) {
|
||||
for i := range c.Headers {
|
||||
err = c.Headers[i].prepare()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// IsValid verifies if header matcher is valid
|
||||
func (m *HeaderMatcher) prepare() (err error) {
|
||||
switch m.Name {
|
||||
case HeaderMatchNameFrom,
|
||||
HeaderMatchNameTo,
|
||||
HeaderMatchNameCC,
|
||||
HeaderMatchNameBCC,
|
||||
HeaderMatchNameReplyTo,
|
||||
HeaderMatchNameSubject:
|
||||
// ok fields
|
||||
default:
|
||||
return ErrUnknownHeaderMatcherName
|
||||
}
|
||||
|
||||
switch m.Op {
|
||||
case HMOpRegex:
|
||||
// Try to compile given regex
|
||||
m.re, err = regexp.Compile(m.Match)
|
||||
if err != nil {
|
||||
return ErrInvalidHeaderMatcherValue
|
||||
}
|
||||
case HMOpUser:
|
||||
// When matching against existing user,
|
||||
// there should be no value set
|
||||
if m.Match != "" {
|
||||
return ErrInvalidHeaderMatcherValue
|
||||
}
|
||||
case HMOpEqualCi, HMOpSuffixCi, HMOpPrefixCi:
|
||||
// no special validation here
|
||||
m.Match = strings.ToLower(m.Match)
|
||||
default:
|
||||
return ErrUnknownHeaderMatcherOperator
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (n HMName) match(name string) bool {
|
||||
return string(n) == strings.ToLower(name)
|
||||
}
|
||||
|
||||
// IsMatch checks if header matcher matches against given headers
|
||||
func (m *HeaderMatcher) isMatch(header mail.Header, exists userExistanceVerifier, matchAll bool) (match bool) {
|
||||
var lcHeader string
|
||||
|
||||
for name, vv := range header {
|
||||
if !m.Name.match(name) {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, v := range vv {
|
||||
lcHeader = strings.ToLower(v)
|
||||
|
||||
switch m.Op {
|
||||
case HMOpEqualCi:
|
||||
match = m.Match == lcHeader
|
||||
// spew.Dump("doing simple string match", match, m, v)
|
||||
case HMOpSuffixCi:
|
||||
match = strings.HasSuffix(lcHeader, m.Match)
|
||||
case HMOpPrefixCi:
|
||||
match = strings.HasPrefix(lcHeader, m.Match)
|
||||
case HMOpRegex:
|
||||
match = m.re.MatchString(v)
|
||||
case HMOpUser:
|
||||
match = exists(v)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
||||
if !match && matchAll {
|
||||
// fail in first non-match
|
||||
return false
|
||||
} else if match && !matchAll {
|
||||
// match in first
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return match
|
||||
}
|
||||
|
||||
func (c *Condition) CheckHeader(header mail.Header, uev userExistanceVerifier) (match bool) {
|
||||
_ = c.Prepare()
|
||||
|
||||
// Pre-process & simplify header values: parse all addresses,
|
||||
// extract emails and toss away names, we do not need them
|
||||
for name := range header {
|
||||
for i, v := range header[name] {
|
||||
switch name {
|
||||
case "from",
|
||||
"to",
|
||||
"cc",
|
||||
"bcc",
|
||||
"reply-to":
|
||||
addr, _ := mail.ParseAddress(v)
|
||||
header[name][i] = addr.Address
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, h := range c.Headers {
|
||||
match = h.isMatch(header, uev, c.MatchAll)
|
||||
|
||||
if !match && c.MatchAll {
|
||||
// fail in first non-match
|
||||
return false
|
||||
} else if match && !c.MatchAll {
|
||||
// match in first
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return match
|
||||
}
|
||||
|
||||
func MakeChecker(headers types.MailMessageHeader, uev userExistanceVerifier) automation.TriggerConditionChecker {
|
||||
return func(c string) bool {
|
||||
var (
|
||||
tc = Condition{}
|
||||
)
|
||||
|
||||
if err := json.Unmarshal([]byte(c), &tc); err == nil {
|
||||
return tc.CheckHeader(headers.Raw, uev)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package mail
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/cortezaproject/corteza-server/system/types"
|
||||
)
|
||||
|
||||
func Test_makeMailHeaderChecker(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
mh types.MailMessageHeader
|
||||
tc Condition
|
||||
expecting bool
|
||||
}{
|
||||
{
|
||||
name: "empty should not match",
|
||||
mh: types.MailMessageHeader{},
|
||||
tc: Condition{},
|
||||
},
|
||||
{
|
||||
name: "simple check",
|
||||
mh: types.MailMessageHeader{Raw: map[string][]string{"Subject": []string{"SIMPLE"}}},
|
||||
tc: Condition{Headers: []HeaderMatcher{{Name: HeaderMatchNameSubject, Match: "SIMPLE"}}},
|
||||
expecting: true,
|
||||
},
|
||||
{
|
||||
name: "simple check - no match",
|
||||
mh: types.MailMessageHeader{Raw: map[string][]string{"Subject": []string{"SIMPLE"}}},
|
||||
tc: Condition{Headers: []HeaderMatcher{{Name: HeaderMatchNameSubject, Match: "complex"}}},
|
||||
expecting: false,
|
||||
},
|
||||
{
|
||||
name: "simple check - no match",
|
||||
mh: types.MailMessageHeader{Raw: map[string][]string{"Subject": []string{"SIMPLE"}}},
|
||||
tc: Condition{Headers: []HeaderMatcher{{Name: HeaderMatchNameFrom, Match: "SIMPLE"}}},
|
||||
expecting: false,
|
||||
},
|
||||
{
|
||||
name: "simple check - name-case",
|
||||
mh: types.MailMessageHeader{Raw: map[string][]string{"SUBJECT": []string{"SIMPLE"}}},
|
||||
tc: Condition{Headers: []HeaderMatcher{{Name: HeaderMatchNameSubject, Match: "SIMPLE"}}},
|
||||
expecting: true,
|
||||
},
|
||||
{
|
||||
name: "two matchers, one matches",
|
||||
mh: types.MailMessageHeader{Raw: map[string][]string{"Subject": []string{"SIMPLE"}}},
|
||||
tc: Condition{
|
||||
Headers: []HeaderMatcher{
|
||||
{Name: HeaderMatchNameSubject, Match: "SIMPLE"},
|
||||
{Name: HeaderMatchNameSubject, Match: "complex"},
|
||||
},
|
||||
},
|
||||
expecting: true,
|
||||
},
|
||||
{
|
||||
name: "two matchers, one matches, match-all=true",
|
||||
mh: types.MailMessageHeader{Raw: map[string][]string{"Subject": []string{"SIMPLE"}}},
|
||||
tc: Condition{
|
||||
MatchAll: true,
|
||||
Headers: []HeaderMatcher{
|
||||
{Name: HeaderMatchNameSubject, Match: "SIMPLE"},
|
||||
{Name: HeaderMatchNameSubject, Match: "complex"},
|
||||
},
|
||||
},
|
||||
expecting: false,
|
||||
},
|
||||
{
|
||||
name: "regex check",
|
||||
mh: types.MailMessageHeader{Raw: map[string][]string{"Subject": []string{"SIMPLE"}}},
|
||||
tc: Condition{Headers: []HeaderMatcher{{Name: HeaderMatchNameSubject, Match: "^S.+$", Op: HMOpRegex}}},
|
||||
expecting: true,
|
||||
},
|
||||
{
|
||||
name: "case-insensitive check",
|
||||
mh: types.MailMessageHeader{Raw: map[string][]string{"Subject": []string{"SIMPLE"}}},
|
||||
tc: Condition{Headers: []HeaderMatcher{{Name: HeaderMatchNameSubject, Match: "simple", Op: HMOpEqualCi}}},
|
||||
expecting: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := tt.tc.Prepare(); err != nil {
|
||||
t.Errorf("unable to prepare header matcher: %v", err)
|
||||
}
|
||||
|
||||
checker := MakeChecker(tt.mh, nil)
|
||||
|
||||
j, _ := json.Marshal(tt.tc)
|
||||
if checker(string(j)) != tt.expecting {
|
||||
t.Errorf("did not match (expecting: %v)", tt.expecting)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user