Skip to content

Commit

Permalink
feat(GODT-2201): IMAP Store Command
Browse files Browse the repository at this point in the history
  • Loading branch information
LBeernaertProton committed Feb 14, 2023
1 parent 2238d80 commit 6aac246
Show file tree
Hide file tree
Showing 4 changed files with 297 additions and 0 deletions.
1 change: 1 addition & 0 deletions imap/command/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ func NewParserWithLiteralContinuationCb(s *parser.Scanner, cb func() error) *Par
"rename": &RenameCommandParser{},
"lsub": &LSubCommandParser{},
"login": &LoginCommandParser{},
"store": &StoreCommandParser{},
},
}
}
Expand Down
156 changes: 156 additions & 0 deletions imap/command/store.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package command

import (
"fmt"
"github.com/ProtonMail/gluon/imap/parser"
)

type StoreAction int

const (
StoreActionAddFlags StoreAction = iota
StoreActionRemFlags
StoreActionSetFlags
)

func (s StoreAction) String() string {
switch s {
case StoreActionAddFlags:
return "+FLAGS"
case StoreActionRemFlags:
return "+FLAGS"
case StoreActionSetFlags:
return "FLAGS"
default:
return "UNKNOWN"
}
}

type StoreCommand struct {
SeqSet []SeqRange
Action StoreAction
Flags []string
Silent bool
}

func (s StoreCommand) String() string {
silentStr := ""
if s.Silent {
silentStr = ".SILENT"
}

return fmt.Sprintf("STORE %v%v %v", s.Action.String(), silentStr, s.Flags)
}

func (s StoreCommand) SanitizedString() string {
return s.String()
}

type StoreCommandParser struct{}

func (StoreCommandParser) FromParser(p *parser.Parser) (Payload, error) {
//nolint:dupword
// store = "STORE" SP sequence-set SP store-att-flags
// store-att-flags = (["+" / "-"] "FLAGS" [".SILENT"]) SP
// (flag-list / (flag *(SP flag)))
if err := p.Consume(parser.TokenTypeSP, "expected space after command"); err != nil {
return nil, err
}

seqSet, err := ParseSeqSet(p)
if err != nil {
return nil, err
}

if err := p.Consume(parser.TokenTypeSP, "expected space after sequence set"); err != nil {
return nil, err
}

var action StoreAction

if ok, err := p.Matches(parser.TokenTypePlus); err != nil {
return nil, err
} else if !ok {
if ok, err := p.Matches(parser.TokenTypeMinus); err != nil {
return nil, err
} else if ok {
action = StoreActionRemFlags
} else {
action = StoreActionSetFlags
}
} else {
action = StoreActionAddFlags
}

if err := p.ConsumeBytesFold([]byte{'F', 'L', 'A', 'G', 'S'}); err != nil {
return nil, err
}

var silent bool

if ok, err := p.Matches(parser.TokenTypePeriod); err != nil {
return nil, err
} else if ok {
if err := p.ConsumeBytesFold([]byte{'S', 'I', 'L', 'E', 'N', 'T'}); err != nil {
return nil, err
}

silent = true
}

if err := p.Consume(parser.TokenTypeSP, "expected space after FLAGS"); err != nil {
return nil, err
}

flags, err := parseStoreFlags(p)
if err != nil {
return nil, err
}

return &StoreCommand{
SeqSet: seqSet,
Action: action,
Flags: flags,
Silent: silent,
}, nil
}

func parseStoreFlags(p *parser.Parser) ([]string, error) {
// (flag-list / (flag *(SP flag)))
fl, ok, err := p.TryParseFlagList()
if err != nil {
return nil, err
} else if ok {
return fl, nil
}

var flags []string

// first flag.
{
f, err := p.ParseFlag()
if err != nil {
return nil, err
}

flags = append(flags, f)
}

// remaining.
for {
if ok, err := p.Matches(parser.TokenTypeSP); err != nil {
return nil, err
} else if !ok {
break
}

f, err := p.ParseFlag()
if err != nil {
return nil, err
}

flags = append(flags, f)
}

return flags, nil
}
110 changes: 110 additions & 0 deletions imap/command/store_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package command

import (
"bytes"
"github.com/ProtonMail/gluon/imap/parser"
"github.com/stretchr/testify/require"
"testing"
)

func TestParser_StoreCommandSetFlags(t *testing.T) {
input := toIMAPLine(`tag STORE 1 FLAGS Foo`)
s := parser.NewScanner(bytes.NewReader(input))
p := NewParser(s)

expected := Command{Tag: "tag", Payload: &StoreCommand{
SeqSet: []SeqRange{{
Begin: 1,
End: 1,
}},
Action: StoreActionSetFlags,
Flags: []string{"Foo"},
Silent: false,
}}

cmd, err := p.Parse()
require.NoError(t, err)
require.Equal(t, expected, cmd)
require.Equal(t, "store", p.LastParsedCommand())
require.Equal(t, "tag", p.LastParsedTag())
}

func TestParser_StoreCommandAddFlags(t *testing.T) {
expected := Command{Tag: "tag", Payload: &StoreCommand{
SeqSet: []SeqRange{{
Begin: 1,
End: 1,
}},
Action: StoreActionAddFlags,
Flags: []string{"Foo"},
Silent: false,
}}

cmd, err := testParseCommand(`tag STORE 1 +FLAGS Foo`)
require.NoError(t, err)
require.Equal(t, expected, cmd)
}

func TestParser_StoreCommandRemoveFlags(t *testing.T) {
expected := Command{Tag: "tag", Payload: &StoreCommand{
SeqSet: []SeqRange{{
Begin: 1,
End: 1,
}},
Action: StoreActionRemFlags,
Flags: []string{"Foo"},
Silent: false,
}}

cmd, err := testParseCommand(`tag STORE 1 -FLAGS Foo`)
require.NoError(t, err)
require.Equal(t, expected, cmd)
}

func TestParser_StoreCommandSilent(t *testing.T) {
expected := Command{Tag: "tag", Payload: &StoreCommand{
SeqSet: []SeqRange{{
Begin: 1,
End: 1,
}},
Action: StoreActionAddFlags,
Flags: []string{"Foo"},
Silent: true,
}}

cmd, err := testParseCommand(`tag STORE 1 +FLAGS.SILENT Foo`)
require.NoError(t, err)
require.Equal(t, expected, cmd)
}

func TestParser_StoreCommandMultipleFlags(t *testing.T) {
expected := Command{Tag: "tag", Payload: &StoreCommand{
SeqSet: []SeqRange{{
Begin: 1,
End: 1,
}},
Action: StoreActionAddFlags,
Flags: []string{"Foo", "Bar"},
Silent: true,
}}

cmd, err := testParseCommand(`tag STORE 1 +FLAGS.SILENT Foo Bar`)
require.NoError(t, err)
require.Equal(t, expected, cmd)
}

func TestParser_StoreCommandMultipleFlagsWithParen(t *testing.T) {
expected := Command{Tag: "tag", Payload: &StoreCommand{
SeqSet: []SeqRange{{
Begin: 1,
End: 1,
}},
Action: StoreActionAddFlags,
Flags: []string{"Foo", "Bar"},
Silent: true,
}}

cmd, err := testParseCommand(`tag STORE 1 +FLAGS.SILENT (Foo Bar)`)
require.NoError(t, err)
require.Equal(t, expected, cmd)
}
30 changes: 30 additions & 0 deletions imap/parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,36 @@ func (p *Parser) ConsumeWith(f func(token TokenType) bool, message string) error
return p.MakeError(message)
}

// ConsumeBytes will advance if the next token value matches the given sequence.
func (p *Parser) ConsumeBytes(chars []byte) error {
for _, c := range chars {
if p.currentToken.Value != c {
return p.MakeError(fmt.Sprintf("expected byte value %x", c))
}

if err := p.Advance(); err != nil {
return err
}
}

return nil
}

// ConsumeBytesFold behaves the same as ConsumeBytes, but case insensitive for characters.
func (p *Parser) ConsumeBytesFold(chars []byte) error {
for _, c := range chars {
if ByteToLower(p.currentToken.Value) != ByteToLower(c) {
return p.MakeError(fmt.Sprintf("expected byte value %x", c))
}

if err := p.Advance(); err != nil {
return err
}
}

return nil
}

// MatchesWith will advance the scanner to the next token and return true if the current token matches the given
// condition.
func (p *Parser) MatchesWith(f func(tokenType TokenType) bool) (bool, error) {
Expand Down

0 comments on commit 6aac246

Please sign in to comment.