diff --git a/cmd/sieve-run/main.go b/cmd/sieve-run/main.go index c3f68fa..b3543af 100644 --- a/cmd/sieve-run/main.go +++ b/cmd/sieve-run/main.go @@ -8,6 +8,7 @@ import ( "log" "net/textproto" "os" + "strings" "time" "github.com/foxcpp/go-sieve" @@ -76,4 +77,5 @@ func main() { fmt.Println("redirect:", data.RedirectAddr) fmt.Println("fileinfo:", data.Mailboxes) fmt.Println("keep:", data.ImplicitKeep || data.Keep) + fmt.Printf("flags: %s\n", strings.Join(data.Flags, " ")) } diff --git a/execute_test.go b/execute_test.go new file mode 100644 index 0000000..a0c971c --- /dev/null +++ b/execute_test.go @@ -0,0 +1,126 @@ +package sieve + +import ( + "bufio" + "context" + "net/textproto" + "reflect" + "strings" + "testing" + + "github.com/foxcpp/go-sieve/interp" +) + +var eml string = `Date: Tue, 1 Apr 1997 09:06:31 -0800 (PST) +From: coyote@desert.example.org +To: roadrunner@acme.example.com +Subject: I have a present for you + +Look, I'm sorry about the whole anvil thing, and I really +didn't mean to try and drop it on you from the top of the +cliff. I want to try to make it up to you. I've got some +great birdseed over here at my place--top of the line +stuff--and if you come by, I'll have it all wrapped up +for you. I'm really sorry for all the problems I've caused +for you over the years, but I know we can work this out. +-- +Wile E. Coyote "Super Genius" coyote@desert.example.org +` + +type result struct { + redirect []string + fileinto []string + implicitKeep bool + keep bool + flags []string +} + +func testExecute(t *testing.T, in string, eml string, intendedResult result) { + t.Run("case", func(t *testing.T) { + + msgHdr, err := textproto.NewReader(bufio.NewReader(strings.NewReader(eml))).ReadMIMEHeader() + if err != nil { + t.Fatal(err) + } + + script := bufio.NewReader(strings.NewReader(in)) + + loadedScript, err := Load(script, DefaultOptions()) + if err != nil { + t.Fatal(err) + } + data := interp.NewRuntimeData(loadedScript, interp.Callback{ + RedirectAllowed: func(ctx context.Context, d *interp.RuntimeData, addr string) (bool, error) { + return true, nil + }, + HeaderGet: func(key string) (string, bool, error) { + vals, ok := msgHdr[key] + if !ok { + return "", false, nil + } + return vals[0], true, nil + }, + }) + data.MessageSize = len(eml) + data.SMTP.From = "from@test.com" + data.SMTP.To = "to@test.com" + + ctx := context.Background() + if err := loadedScript.Execute(ctx, data); err != nil { + t.Fatal(err) + } + + r := result{ + redirect: data.RedirectAddr, + fileinto: data.Mailboxes, + keep: data.Keep, + implicitKeep: data.ImplicitKeep, + flags: data.Flags, + } + + if !reflect.DeepEqual(r, intendedResult) { + t.Log("Wrong Execute output") + t.Log("Actual: ", r) + t.Log("Expected:", intendedResult) + t.Fail() + } + }) +} + +func TestFileinto(t *testing.T) { + testExecute(t, `require ["fileinto"]; + fileinto "test"; +`, eml, + result{ + fileinto: []string{"test"}, + }) + testExecute(t, `require ["fileinto"]; + fileinto "test"; + fileinto "test2"; + `, eml, + result{ + fileinto: []string{"test", "test2"}, + }) +} + +func TestFlags(t *testing.T) { + testExecute(t, `require ["fileinto", "imap4flags"]; + setflag ["flag1", "flag2"]; + addflag ["flag2", "flag3"]; + removeflag ["flag1"]; + fileinto "test"; +`, eml, + result{ + fileinto: []string{"test"}, + flags: []string{"flag2", "flag3"}, + }) + testExecute(t, `require ["fileinto", "imap4flags"]; + addflag ["flag2", "flag3"]; + removeflag ["flag3", "flag4"]; + fileinto "test"; + `, eml, + result{ + fileinto: []string{"test"}, + flags: []string{"flag2"}, + }) +} diff --git a/interp/action.go b/interp/action.go index 061d70e..bb5a74d 100644 --- a/interp/action.go +++ b/interp/action.go @@ -13,6 +13,7 @@ func (c CmdStop) Execute(ctx context.Context, d *RuntimeData) error { type CmdFileInto struct { Mailbox string + Flags *Flags } func (c CmdFileInto) Execute(ctx context.Context, d *RuntimeData) error { @@ -27,6 +28,9 @@ func (c CmdFileInto) Execute(ctx context.Context, d *RuntimeData) error { } d.Mailboxes = append(d.Mailboxes, c.Mailbox) d.ImplicitKeep = false + if c.Flags != nil { + d.Flags = *canonicalFlags(make([]string, len(*c.Flags)), nil, d.FlagAliases) + } return nil } @@ -53,10 +57,15 @@ func (c CmdRedirect) Execute(ctx context.Context, d *RuntimeData) error { return nil } -type CmdKeep struct{} +type CmdKeep struct { + Flags *Flags +} func (c CmdKeep) Execute(_ context.Context, d *RuntimeData) error { d.Keep = true + if c.Flags != nil { + d.Flags = *canonicalFlags(make([]string, len(*c.Flags)), nil, d.FlagAliases) + } return nil } @@ -64,5 +73,46 @@ type CmdDiscard struct{} func (c CmdDiscard) Execute(_ context.Context, d *RuntimeData) error { d.ImplicitKeep = false + d.Flags = make([]string, 0) + return nil +} + +type CmdSetFlag struct { + Flags *Flags +} + +func (c CmdSetFlag) Execute(_ context.Context, d *RuntimeData) error { + if c.Flags != nil { + d.Flags = *canonicalFlags(*c.Flags, nil, d.FlagAliases) + } + return nil +} + +type CmdAddFlag struct { + Flags *Flags +} + +func (c CmdAddFlag) Execute(_ context.Context, d *RuntimeData) error { + if c.Flags != nil { + if d.Flags == nil { + d.Flags = make([]string, len(*c.Flags)) + copy(d.Flags, *c.Flags) + } else { + // Use canonicalFlags to remove duplicates + d.Flags = *canonicalFlags(append(d.Flags, *c.Flags...), nil, d.FlagAliases) + } + } + return nil +} + +type CmdRemoveFlag struct { + Flags *Flags +} + +func (c CmdRemoveFlag) Execute(_ context.Context, d *RuntimeData) error { + if c.Flags != nil { + // Use canonicalFlags to remove duplicates + d.Flags = *canonicalFlags(d.Flags, c.Flags, d.FlagAliases) + } return nil } diff --git a/interp/load.go b/interp/load.go index 6bef4eb..522beef 100644 --- a/interp/load.go +++ b/interp/load.go @@ -8,8 +8,9 @@ import ( ) var supportedExtensions = map[string]struct{}{ - "fileinto": {}, - "envelope": {}, + "fileinto": {}, + "envelope": {}, + "imap4flags": {}, } var ( @@ -32,6 +33,10 @@ func init() { "redirect": loadRedirect, "keep": loadKeep, "discard": loadDiscard, + // RFC 5232 Actions + "setflag": loadSetFlag, // imap4flags extension + "addflag": loadAddFlag, // imap4flags extension + "removeflag": loadRemoveFlag, // imap4flags extension } tests = map[string]func(*Script, parser.Test) (Test, error){ // RFC 5228 Tests diff --git a/interp/load_action.go b/interp/load_action.go index e236a62..7094281 100644 --- a/interp/load_action.go +++ b/interp/load_action.go @@ -2,16 +2,65 @@ package interp import ( "fmt" + "sort" + "strings" "github.com/foxcpp/go-sieve/parser" ) +type Flags []string + +func canonicalFlags(src []string, remove *Flags, aliases map[string]string) *Flags { + // This does four things + // * Translate space delimited lists of flags into separate flags + // * Handle flag aliases + // * Deduplicate + // * Sort + // * (optionally) remove flags + c := make(Flags, 0, len(src)) + fm := make(map[string]struct{}) + for _, fl := range src { + for _, f := range strings.Split(fl, " ") { + if fc, ok := aliases[f]; ok { + fm[fc] = struct{}{} + } else { + fm[f] = struct{}{} + } + } + } + if remove != nil { + for _, fl := range *remove { + for _, f := range strings.Split(fl, " ") { + if fc, ok := aliases[f]; ok { + delete(fm, fc) + } else { + delete(fm, f) + } + } + } + } + for f, _ := range fm { + c = append(c, f) + } + sort.Strings(c) + return &c +} + func loadFileInto(s *Script, pcmd parser.Cmd) (Cmd, error) { if !s.RequiresExtension("fileinto") { return nil, fmt.Errorf("require fileinto to use it") } cmd := CmdFileInto{} err := LoadSpec(s, &Spec{ + Tags: map[string]SpecTag{ + "flags": { + NeedsValue: true, + MinStrCount: 1, + MatchStr: func(val []string) { + cmd.Flags = canonicalFlags(val, nil, nil) + }, + }, + }, Pos: []SpecPosArg{ { MinStrCount: 1, @@ -22,6 +71,9 @@ func loadFileInto(s *Script, pcmd parser.Cmd) (Cmd, error) { }, }, }, pcmd.Position, pcmd.Args, pcmd.Tests, pcmd.Block) + if !s.RequiresExtension("imap4flags") && cmd.Flags != nil { + return nil, fmt.Errorf("require imap4flags to use it") + } return cmd, err } @@ -43,7 +95,20 @@ func loadRedirect(s *Script, pcmd parser.Cmd) (Cmd, error) { func loadKeep(s *Script, pcmd parser.Cmd) (Cmd, error) { cmd := CmdKeep{} - err := LoadSpec(s, &Spec{}, pcmd.Position, pcmd.Args, pcmd.Tests, pcmd.Block) + err := LoadSpec(s, &Spec{ + Tags: map[string]SpecTag{ + "flags": { + NeedsValue: true, + MinStrCount: 1, + MatchStr: func(val []string) { + cmd.Flags = canonicalFlags(val, nil, nil) + }, + }, + }, + }, pcmd.Position, pcmd.Args, pcmd.Tests, pcmd.Block) + if !s.RequiresExtension("imap4flags") && cmd.Flags != nil { + return nil, fmt.Errorf("require imap4flags to use it") + } return cmd, err } @@ -52,3 +117,57 @@ func loadDiscard(s *Script, pcmd parser.Cmd) (Cmd, error) { err := LoadSpec(s, &Spec{}, pcmd.Position, pcmd.Args, pcmd.Tests, pcmd.Block) return cmd, err } + +func loadSetFlag(s *Script, pcmd parser.Cmd) (Cmd, error) { + if !s.RequiresExtension("imap4flags") { + return nil, fmt.Errorf("require impa4flags to use it") + } + cmd := CmdSetFlag{} + err := LoadSpec(s, &Spec{ + Pos: []SpecPosArg{ + { + MinStrCount: 1, + MatchStr: func(val []string) { + cmd.Flags = canonicalFlags(val, nil, nil) + }, + }, + }, + }, pcmd.Position, pcmd.Args, pcmd.Tests, pcmd.Block) + return cmd, err +} + +func loadAddFlag(s *Script, pcmd parser.Cmd) (Cmd, error) { + if !s.RequiresExtension("imap4flags") { + return nil, fmt.Errorf("require impa4flags to use it") + } + cmd := CmdAddFlag{} + err := LoadSpec(s, &Spec{ + Pos: []SpecPosArg{ + { + MinStrCount: 1, + MatchStr: func(val []string) { + cmd.Flags = canonicalFlags(val, nil, nil) + }, + }, + }, + }, pcmd.Position, pcmd.Args, pcmd.Tests, pcmd.Block) + return cmd, err +} + +func loadRemoveFlag(s *Script, pcmd parser.Cmd) (Cmd, error) { + if !s.RequiresExtension("imap4flags") { + return nil, fmt.Errorf("require impa4flags to use it") + } + cmd := CmdRemoveFlag{} + err := LoadSpec(s, &Spec{ + Pos: []SpecPosArg{ + { + MinStrCount: 1, + MatchStr: func(val []string) { + cmd.Flags = canonicalFlags(val, nil, nil) + }, + }, + }, + }, pcmd.Position, pcmd.Args, pcmd.Tests, pcmd.Block) + return cmd, err +} diff --git a/interp/load_test.go b/interp/load_test.go index 0575288..1e647cc 100644 --- a/interp/load_test.go +++ b/interp/load_test.go @@ -74,4 +74,29 @@ if envelope :is "from" "test@example.org" { }, }, }) + testCmdLoader(t, s, `require "imap4flags"; +require "fileinto"; +fileinto :flags "flag1 flag2" "hell"; +keep :flags ["flag1", "flag2"]; +setflag ["flag2", "flag1", "flag2"]; +addflag ["flag2", "flag1"]; +removeflag "flag2"; +`, []Cmd{ + CmdFileInto{ + Mailbox: "hell", + Flags: &Flags{"flag1", "flag2"}, + }, + CmdKeep{ + Flags: &Flags{"flag1", "flag2"}, + }, + CmdSetFlag{ + Flags: &Flags{"flag1", "flag2"}, + }, + CmdAddFlag{ + Flags: &Flags{"flag1", "flag2"}, + }, + CmdRemoveFlag{ + Flags: &Flags{"flag2"}, + }, + }) } diff --git a/interp/runtime.go b/interp/runtime.go index c050e41..b3f2661 100644 --- a/interp/runtime.go +++ b/interp/runtime.go @@ -22,8 +22,11 @@ type RuntimeData struct { RedirectAddr []string Mailboxes []string + Flags []string Keep bool ImplicitKeep bool + + FlagAliases map[string]string } func NewRuntimeData(s *Script, p Callback) *RuntimeData { @@ -31,5 +34,6 @@ func NewRuntimeData(s *Script, p Callback) *RuntimeData { Script: s, Callback: p, ImplicitKeep: true, + FlagAliases: make(map[string]string), } }