From 362c7416923f8d25ff01a1266e39a7f6573d5543 Mon Sep 17 00:00:00 2001 From: Leander Beernaert Date: Wed, 15 Feb 2023 10:20:37 +0100 Subject: [PATCH] feat(GODT-2201): IMAP ID Command --- imap/command/command.go | 9 ++++ imap/command/id.go | 96 ++++++++++++++++++++++++++++++++++++ imap/command/id_test.go | 80 ++++++++++++++++++++++++++++++ imap/command/nstring.go | 19 +++++++ imap/command/nstring_test.go | 35 +++++++++++++ imap/command/parser.go | 1 + rfcparser/parser.go | 10 ++++ 7 files changed, 250 insertions(+) create mode 100644 imap/command/id.go create mode 100644 imap/command/id_test.go create mode 100644 imap/command/nstring.go create mode 100644 imap/command/nstring_test.go diff --git a/imap/command/command.go b/imap/command/command.go index ebc9c447..a0d5d396 100644 --- a/imap/command/command.go +++ b/imap/command/command.go @@ -2,6 +2,7 @@ package command import ( "encoding/base64" + "fmt" "github.com/ProtonMail/gluon/internal/hash" ) @@ -20,3 +21,11 @@ type Command struct { Tag string Payload Payload } + +func (c Command) String() string { + return fmt.Sprintf("%v %v", c.Tag, c.Payload.String()) +} + +func (c Command) SaniztedString() string { + return fmt.Sprintf("%v %v", c.Tag, c.Payload.SanitizedString()) +} diff --git a/imap/command/id.go b/imap/command/id.go new file mode 100644 index 00000000..f519f0c0 --- /dev/null +++ b/imap/command/id.go @@ -0,0 +1,96 @@ +package command + +import ( + "fmt" + "github.com/ProtonMail/gluon/rfcparser" +) + +type IDGet struct{} + +func (l IDGet) String() string { + return fmt.Sprintf("ID") +} + +func (l IDGet) SanitizedString() string { + return l.String() +} + +type IDSet struct { + Values map[string]string +} + +func (l IDSet) String() string { + if len(l.Values) == 0 { + return "ID" + } + + return fmt.Sprintf("ID %v", l.Values) +} + +func (l IDSet) SanitizedString() string { + return l.String() +} + +type IDCommandParser struct{} + +func (IDCommandParser) FromParser(p *rfcparser.Parser) (Payload, error) { + // nolint:dupword + // id ::= "ID" SPACE id_params_list + // id_params_list ::= "(" #(string SPACE nstring) ")" / nil + // ;; list of field value pairs + if err := p.Consume(rfcparser.TokenTypeSP, "expected space after command"); err != nil { + return nil, err + } + + if p.Check(rfcparser.TokenTypeChar) { + if err := p.ConsumeBytesFold('N', 'I', 'L'); err != nil { + return nil, err + } + + return &IDGet{}, nil + } + + values := make(map[string]string) + + if err := p.Consume(rfcparser.TokenTypeLParen, "expected ( for id values start"); err != nil { + return nil, err + } + + for { + key, ok, err := p.TryParseString() + if err != nil { + return nil, err + } else if !ok { + break + } + + if err := p.Consume(rfcparser.TokenTypeSP, "expected space after ID key"); err != nil { + return nil, err + } + + value, isNil, err := ParseNString(p) + if err != nil { + return nil, err + } + + if !isNil { + values[key.Value] = value.Value + } else { + values[key.Value] = "" + } + + if !p.Check(rfcparser.TokenTypeRParen) { + if err := p.Consume(rfcparser.TokenTypeSP, "expected space after ID value"); err != nil { + return nil, err + } + } + } + + if err := p.Consume(rfcparser.TokenTypeRParen, "expected ) for id values end"); err != nil { + return nil, err + } + + return &IDSet{ + Values: values, + }, nil +} diff --git a/imap/command/id_test.go b/imap/command/id_test.go new file mode 100644 index 00000000..67217729 --- /dev/null +++ b/imap/command/id_test.go @@ -0,0 +1,80 @@ +package command + +import ( + "bytes" + "github.com/ProtonMail/gluon/rfcparser" + "github.com/stretchr/testify/require" + "testing" +) + +func TestParser_IDCommandGet(t *testing.T) { + input := toIMAPLine(`tag ID NIL`) + s := rfcparser.NewScanner(bytes.NewReader(input)) + p := NewParser(s) + + expected := Command{Tag: "tag", Payload: &IDGet{}} + + cmd, err := p.Parse() + require.NoError(t, err) + require.Equal(t, expected, cmd) + require.Equal(t, "id", p.LastParsedCommand()) + require.Equal(t, "tag", p.LastParsedTag()) +} + +func TestParser_IDCommandSetOne(t *testing.T) { + input := toIMAPLine(`tag ID ("foo" "bar")`) + s := rfcparser.NewScanner(bytes.NewReader(input)) + p := NewParser(s) + + expected := Command{Tag: "tag", Payload: &IDSet{Values: map[string]string{"foo": "bar"}}} + + cmd, err := p.Parse() + require.NoError(t, err) + require.Equal(t, expected, cmd) + require.Equal(t, "id", p.LastParsedCommand()) + require.Equal(t, "tag", p.LastParsedTag()) +} + +func TestParser_IDCommandSetEmpty(t *testing.T) { + expected := Command{Tag: "tag", Payload: &IDSet{ + Values: map[string]string{}, + }} + + cmd, err := testParseCommand(`tag ID ()`) + require.NoError(t, err) + require.Equal(t, expected, cmd) +} + +func TestParser_IDCommandSetMany(t *testing.T) { + expected := Command{Tag: "tag", Payload: &IDSet{ + Values: map[string]string{ + "foo": "bar", + "a": "", + "c": "d", + }, + }} + + cmd, err := testParseCommand(`tag ID ("foo" "bar" "a" NIL "c" "d")`) + require.NoError(t, err) + require.Equal(t, expected, cmd) +} + +func TestParser_IDCommandFailures(t *testing.T) { + inputs := []string{ + "tag ID", + "tag ID ", + "tag ID N", + "tag ID (", + `tag ID ("foo")`, + `tag ID ("foo" )`, + `tag ID ("foo""bar")`, + `tag ID (nil nil)`, + `tag ID ("foo" "bar"`, + `tag ID ("foo" "bar" "z")`, + } + + for _, i := range inputs { + _, err := testParseCommand(i) + require.Error(t, err) + } +} diff --git a/imap/command/nstring.go b/imap/command/nstring.go new file mode 100644 index 00000000..74ee373e --- /dev/null +++ b/imap/command/nstring.go @@ -0,0 +1,19 @@ +package command + +import "github.com/ProtonMail/gluon/rfcparser" + +// ParseNString pareses a string or NIL. If NIL was parsed the boolean return is set to false. +func ParseNString(p *rfcparser.Parser) (rfcparser.String, bool, error) { + // nstring = string / nil + if s, ok, err := p.TryParseString(); err != nil { + return rfcparser.String{}, false, err + } else if ok { + return s, false, nil + } + + if err := p.ConsumeBytesFold('N', 'I', 'L'); err != nil { + return rfcparser.String{}, false, err + } + + return rfcparser.String{}, true, nil +} diff --git a/imap/command/nstring_test.go b/imap/command/nstring_test.go new file mode 100644 index 00000000..e6bb169a --- /dev/null +++ b/imap/command/nstring_test.go @@ -0,0 +1,35 @@ +package command + +import ( + "bytes" + "github.com/ProtonMail/gluon/rfcparser" + "github.com/stretchr/testify/require" + "testing" +) + +func TestParseNStringString(t *testing.T) { + input := []byte(`"foo"`) + + p := rfcparser.NewParser(rfcparser.NewScanner(bytes.NewReader(input))) + // Advance at least once to prepare first token. + err := p.Advance() + require.NoError(t, err) + + v, isNil, err := ParseNString(p) + require.NoError(t, err) + require.Equal(t, "foo", v.Value) + require.False(t, isNil) +} + +func TestParseNStringNIL(t *testing.T) { + input := []byte(`NIL`) + + p := rfcparser.NewParser(rfcparser.NewScanner(bytes.NewReader(input))) + // Advance at least once to prepare first token. + err := p.Advance() + require.NoError(t, err) + + _, isNil, err := ParseNString(p) + require.NoError(t, err) + require.True(t, isNil) +} diff --git a/imap/command/parser.go b/imap/command/parser.go index b06985bf..cf4d0aa8 100644 --- a/imap/command/parser.go +++ b/imap/command/parser.go @@ -53,6 +53,7 @@ func NewParserWithLiteralContinuationCb(s *rfcparser.Scanner, cb func() error) * "copy": &CopyCommandParser{}, "move": &MoveCommandParser{}, "uid": NewUIDCommandParser(), + "id": &IDCommandParser{}, }, } } diff --git a/rfcparser/parser.go b/rfcparser/parser.go index 5053250b..ad2a5ff8 100644 --- a/rfcparser/parser.go +++ b/rfcparser/parser.go @@ -92,6 +92,16 @@ func (p *Parser) ParseAString() (String, error) { return astring.IntoString(), nil } +func (p *Parser) TryParseString() (String, bool, error) { + if !p.Check(TokenTypeDQuote) && !p.Check(TokenTypeLCurly) { + return String{}, false, nil + } + + v, err := p.ParseString() + + return v, true, err +} + // ParseString parses a string according to RFC3501. func (p *Parser) ParseString() (String, error) { /*