Skip to content

Commit

Permalink
feat(GODT-2201): IMAP ID Command
Browse files Browse the repository at this point in the history
  • Loading branch information
LBeernaertProton committed Feb 15, 2023
1 parent 7a8be0b commit 362c741
Show file tree
Hide file tree
Showing 7 changed files with 250 additions and 0 deletions.
9 changes: 9 additions & 0 deletions imap/command/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package command

import (
"encoding/base64"
"fmt"
"github.com/ProtonMail/gluon/internal/hash"
)

Expand All @@ -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())
}
96 changes: 96 additions & 0 deletions imap/command/id.go
Original file line number Diff line number Diff line change
@@ -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
}
80 changes: 80 additions & 0 deletions imap/command/id_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
19 changes: 19 additions & 0 deletions imap/command/nstring.go
Original file line number Diff line number Diff line change
@@ -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
}
35 changes: 35 additions & 0 deletions imap/command/nstring_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
1 change: 1 addition & 0 deletions imap/command/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ func NewParserWithLiteralContinuationCb(s *rfcparser.Scanner, cb func() error) *
"copy": &CopyCommandParser{},
"move": &MoveCommandParser{},
"uid": NewUIDCommandParser(),
"id": &IDCommandParser{},
},
}
}
Expand Down
10 changes: 10 additions & 0 deletions rfcparser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
/*
Expand Down

0 comments on commit 362c741

Please sign in to comment.