1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170
|
package command
import (
"fmt"
"strings"
"github.com/ProtonMail/gluon/rfcparser"
)
type Builder interface {
FromParser(p *rfcparser.Parser) (Payload, error)
}
// Parser parses IMAP Commands.
type Parser struct {
parser *rfcparser.Parser
scanner *rfcparser.Scanner
commands map[string]Builder
lastTag string
lastCmd string
}
func NewParser(s *rfcparser.Scanner) *Parser {
return NewParserWithLiteralContinuationCb(s, nil)
}
func NewParserWithLiteralContinuationCb(s *rfcparser.Scanner, cb func() error) *Parser {
return &Parser{
scanner: s,
parser: rfcparser.NewParserWithLiteralContinuationCb(s, cb),
commands: map[string]Builder{
"list": &ListCommandParser{},
"append": &AppendCommandParser{},
"search": &SearchCommandParser{},
"fetch": &FetchCommandParser{},
"capability": &CapabilityCommandParser{},
"idle": &IdleCommandParser{},
"noop": &NoopCommandParser{},
"logout": &LogoutCommandParser{},
"check": &CheckCommandParser{},
"close": &CloseCommandParser{},
"expunge": &ExpungeCommandParser{},
"unselect": &UnselectCommandParser{},
"starttls": &StartTLSCommandParser{},
"status": &StatusCommandParser{},
"select": &SelectCommandParser{},
"examine": &ExamineCommandParser{},
"create": &CreateCommandParser{},
"delete": &DeleteCommandParser{},
"subscribe": &SubscribeCommandParser{},
"unsubscribe": &UnsubscribeCommandParser{},
"rename": &RenameCommandParser{},
"lsub": &LSubCommandParser{},
"login": &LoginCommandParser{},
"store": &StoreCommandParser{},
"copy": &CopyCommandParser{},
"move": &MoveCommandParser{},
"uid": NewUIDCommandParser(),
"id": &IDCommandParser{},
},
}
}
func (p *Parser) LastParsedTag() string {
return p.lastTag
}
func (p *Parser) LastParsedCommand() string {
return p.lastCmd
}
// ConsumeInvalidInput will consume all remaining scanner input until a new line has been reached.
func (p *Parser) ConsumeInvalidInput() error {
_, err := p.scanner.ConsumeUntilNewLine()
return err
}
func (p *Parser) Parse() (Command, error) {
result := Command{}
p.lastTag = ""
p.lastCmd = ""
p.parser.ResetOffsetCounter()
if err := p.parser.Advance(); err != nil {
return result, err
}
tag, err := p.parseTag()
if err != nil {
return result, err
}
// Done command does not have a tag.
if strings.ToLower(tag.Value) == "done" {
p.lastCmd = "done"
result.Tag = ""
result.Payload = &Done{}
} else {
result.Tag = tag.Value
p.lastTag = tag.Value
if err := p.parser.Consume(rfcparser.TokenTypeSP, "Expected space after tag"); err != nil {
return result, err
}
payload, err := p.parseCommand()
if err != nil {
return result, err
}
result.Payload = payload
}
if err := p.parser.Consume(rfcparser.TokenTypeCR, "expected CR"); err != nil {
return Command{}, err
}
// Can't fully consume the last new line here or we will hang forever as the clients don't send the next token.
// In the next loop, the call to advance will ensure the next token in the stream gets loaded properly.
if !p.parser.Check(rfcparser.TokenTypeLF) {
return Command{}, p.parser.MakeError("expected LF after CR")
}
return result, nil
}
func (p *Parser) parseCommand() (Payload, error) {
var commandBytes []byte
commandOffset := p.parser.CurrentToken().Offset
for {
if ok, err := p.parser.Matches(rfcparser.TokenTypeChar); err != nil {
return nil, err
} else if ok {
commandBytes = append(commandBytes, rfcparser.ByteToLower(p.parser.PreviousToken().Value))
} else {
break
}
}
p.lastCmd = string(commandBytes)
builder, ok := p.commands[p.lastCmd]
if !ok {
return nil, p.parser.MakeErrorAtOffset(fmt.Sprintf("unknown command '%v'", p.lastCmd), commandOffset)
}
return builder.FromParser(p.parser)
}
func (p *Parser) parseTag() (rfcparser.String, error) {
// tag = 1*<any ASTRING-CHAR except "+">
isTagChar := func(tt rfcparser.TokenType) bool {
return rfcparser.IsAStringChar(tt) && tt != rfcparser.TokenTypePlus
}
if err := p.parser.ConsumeWith(isTagChar, "Invalid tag char detected"); err != nil {
return rfcparser.String{}, err
}
tag, err := p.parser.CollectBytesWhileMatchesWithPrevWith(isTagChar)
if err != nil {
return rfcparser.String{}, err
}
return tag.IntoString(), err
}
|