Skip to content

Commit

Permalink
feat(GODT-2201): Native GO IMAP Parser Foundations
Browse files Browse the repository at this point in the history
Lay the foundations for native parsers in Go so we can move away from
the C++ libraries.

This patch sets up the foundations to convert the remaining commands to
this new format. At the moment the List and Append command have been
ported and should serve as an example for the remaining commands.

More commands will follow in future patches.
  • Loading branch information
LBeernaertProton committed Feb 13, 2023
1 parent 9f98ae4 commit b62de94
Show file tree
Hide file tree
Showing 12 changed files with 1,732 additions and 0 deletions.
92 changes: 92 additions & 0 deletions imap/command/append.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package command

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

type AppendCommand struct {
Mailbox string
Flags []string
DateTime time.Time
Literal []byte
}

func (l AppendCommand) String() string {
return fmt.Sprintf("APPEND '%v' Flags='%v' DateTime='%v' Literal=%v",
l.Mailbox,
l.Flags,
l.DateTime,
l.Literal,
)
}

func (l AppendCommand) SanitizedString() string {
return fmt.Sprintf("APPEND '%v' Flags='%v' DateTime='%v'",
sanitizeString(l.Mailbox),
l.Flags,
l.DateTime,
)
}

func (l AppendCommand) HasDateTime() bool {
return l.DateTime != time.Time{}
}

type AppendCommandParser struct{}

func (AppendCommandParser) FromParser(p *parser.Parser) (Payload, error) {
mailbox, err := p.ParseMailbox()
if err != nil {
return nil, err
}

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

var appendFlags []string

// check if we have flags.
flagList, hasFlagList, err := p.TryParseFlagList()
if err != nil {
return nil, err
} else if hasFlagList {
appendFlags = flagList
}

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

var dateTime time.Time
// check date time.
if !p.Check(parser.TokenTypeLCurly) {
dt, err := ParseDateTime(p)
if err != nil {
return nil, err
}

dateTime = dt

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

// read literal.
literal, err := p.ParseLiteral()
if err != nil {
return nil, err
}

return &AppendCommand{
Mailbox: mailbox,
Literal: literal,
Flags: appendFlags,
DateTime: dateTime,
}, nil
}
138 changes: 138 additions & 0 deletions imap/command/append_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package command

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

func buildAppendDateTime(year int, month time.Month, day int, hour int, min int, sec int, zoneHour int, zoneMinutes int, negativeZone bool) time.Time {
zoneMultiplier := 1
if negativeZone {
zoneMultiplier = -1
}

zone := (zoneHour*3600 + zoneMinutes*60) * zoneMultiplier

location := time.FixedZone("zone", zone)

return time.Date(year, month, day, hour, min, sec, 0, location)
}

func TestParser_AppendCommandWithAllFields(t *testing.T) {
input := toIMAPLine(`A003 APPEND saved-messages (\Seen) "15-Nov-1984 13:37:01 +0730" {23}`, `My message body is here`)
s := parser.NewScanner(bytes.NewReader(input))
p := NewParser(s)

expected := Command{Tag: "A003", Payload: &AppendCommand{
Mailbox: "saved-messages",
Flags: []string{`\Seen`},
Literal: []byte("My message body is here"),
DateTime: buildAppendDateTime(1984, time.November, 15, 13, 37, 1, 07, 30, false),
}}

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

func TestParser_AppendCommandWithLiteralOnly(t *testing.T) {
input := toIMAPLine(`A003 APPEND saved-messages {23}`, `My message body is here`)
s := parser.NewScanner(bytes.NewReader(input))
p := NewParser(s)

expected := Command{Tag: "A003", Payload: &AppendCommand{
Mailbox: "saved-messages",
Literal: []byte("My message body is here"),
}}

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

func TestParser_AppendCommandWithFlagAndLiteral(t *testing.T) {
input := toIMAPLine(`A003 APPEND saved-messages (\Seen) {23}`, `My message body is here`)
s := parser.NewScanner(bytes.NewReader(input))
p := NewParser(s)

expected := Command{Tag: "A003", Payload: &AppendCommand{
Mailbox: "saved-messages",
Flags: []string{`\Seen`},
Literal: []byte("My message body is here"),
}}

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

func TestParser_AppendCommandWithDateTimeAndLiteral(t *testing.T) {
input := toIMAPLine(`A003 APPEND saved-messages "15-Nov-1984 13:37:01 +0730" {23}`, `My message body is here`)
s := parser.NewScanner(bytes.NewReader(input))
p := NewParser(s)

expected := Command{Tag: "A003", Payload: &AppendCommand{
Mailbox: "saved-messages",
Literal: []byte("My message body is here"),
DateTime: buildAppendDateTime(1984, time.November, 15, 13, 37, 1, 07, 30, false),
}}

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

func TestParser_AppendWithUTF8Literal(t *testing.T) {
const literal = `割ゃちとた紀別チノホコ隠面ノネシ披畑つ筋型菊ラ済百チユネ報能げほべえ一王ユサ禁未シムカ学康ほル退今ずはぞゃ宿理古えべにあ。民さぱをだ意宇りう医6通海ヘクヲ点71丈2社つぴげわ中知多ッ場親られ見要クラ著喜栄潟ぼゆラけ。著災ンう三育府能に汁裁ラヤユ哉能ルサレ開30被ちゃ英死オイ教禁能ふてっせ社化選市へす。`
input := toIMAPLine(fmt.Sprintf("A003 APPEND saved-messages (\\Seen) {%v}", len(literal)), literal)

s := parser.NewScanner(bytes.NewReader(input))
p := NewParser(s)

expected := Command{Tag: "A003", Payload: &AppendCommand{
Mailbox: "saved-messages",
Flags: []string{`\Seen`},
Literal: []byte(literal),
}}

cmd, err := p.Parse()
require.NoError(t, err)
require.Equal(t, expected, cmd)
}

func BenchmarkParser_Append(b *testing.B) {
input := toIMAPLine(`A003 APPEND saved-messages (\Seen) {23}`, `My message body is here`)

for i := 0; i < b.N; i++ {
s := parser.NewScanner(bytes.NewReader(input))
p := NewParser(s)

_, err := p.Parse()
require.NoError(b, err)
}
}

func BenchmarkParser_AppendCPP(b *testing.B) {
input := string(toIMAPLine(`A003 APPEND saved-messages (\Seen) {23}`, `04269a34-ad29-472c-9d5d-042a02b7fc0d`))

literalMap := cppParser.NewStringMap()

literalMap.Set("04269a34-ad29-472c-9d5d-042a02b7fc0d", "My message body is here")

for i := 0; i < b.N; i++ {
cppParser.Parse(input, literalMap, "/")
}
}
22 changes: 22 additions & 0 deletions imap/command/command.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package command

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

type Payload interface {
String() string

// SanitizedString should return the command payload with all the sensitive information stripped out.
SanitizedString() string
}

func sanitizeString(s string) string {
return base64.StdEncoding.EncodeToString(hash.SHA256([]byte(s)))
}

type Command struct {
Tag string
Payload Payload
}
Loading

0 comments on commit b62de94

Please sign in to comment.