Basic system autuomation, mail processing

This commit is contained in:
Denis Arh
2019-09-03 09:09:44 +02:00
parent 78660c47ec
commit 6b5ef8aaae
19 changed files with 2807 additions and 122 deletions
+198
View File
@@ -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
}
}
+96
View File
@@ -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)
}
})
}
}